본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 5. · 14 Views
플랫폼 물리와 충돌 완벽 가이드
게임 개발에서 가장 기본이 되는 플랫폼 물리와 충돌 처리를 다룹니다. 타일맵 충돌부터 원웨이 플랫폼, 슬로프 처리까지 실무에서 바로 활용할 수 있는 핵심 개념을 배워봅니다.
목차
1. 타일맵 충돌 설정
김개발 씨는 처음으로 플랫포머 게임을 만들어보기로 했습니다. 열심히 타일맵 에디터로 멋진 스테이지를 그렸는데, 캐릭터가 바닥을 그냥 뚫고 지나가버립니다.
"분명히 타일은 있는데 왜 충돌이 안 되는 거지?" 타일맵에서 충돌을 설정하는 방법을 몰랐던 것입니다.
타일맵 충돌 설정은 타일맵의 특정 타일에 물리적 속성을 부여하는 작업입니다. 마치 투명한 유리문에 손잡이를 달아 "여기는 문이에요"라고 알려주는 것과 같습니다.
Phaser에서는 타일 인덱스나 프로퍼티를 기반으로 충돌 영역을 지정할 수 있습니다.
다음 코드를 살펴봅시다.
// 타일맵과 타일셋 로드
const map = this.make.tilemap({ key: 'level1' });
const tileset = map.addTilesetImage('platformTiles', 'tiles');
// 레이어 생성
const groundLayer = map.createLayer('Ground', tileset, 0, 0);
// 방법 1: 특정 타일 인덱스에 충돌 설정
groundLayer.setCollision([1, 2, 3, 4, 5]);
// 방법 2: 타일 범위로 충돌 설정
groundLayer.setCollisionBetween(1, 50);
// 방법 3: Tiled에서 설정한 프로퍼티로 충돌 설정
groundLayer.setCollisionByProperty({ collides: true });
// 플레이어와 레이어 간 충돌 활성화
this.physics.add.collider(this.player, groundLayer);
김개발 씨는 입사 첫 프로젝트로 간단한 플랫포머 게임을 맡게 되었습니다. Tiled라는 도구로 열심히 맵을 그리고, Phaser에서 로드하는 데까지는 성공했습니다.
그런데 이상합니다. 캐릭터가 멋지게 그려진 바닥 타일 위를 유유히 뚫고 지나갑니다.
선배 개발자 박시니어 씨가 화면을 보더니 웃으며 말했습니다. "타일맵은 그냥 그림이에요.
충돌을 원하면 직접 설정해줘야 합니다." 그렇다면 타일맵 충돌 설정이란 정확히 무엇일까요? 쉽게 비유하자면, 타일맵은 마치 연극 무대의 배경 그림과 같습니다.
아무리 벽돌 그림을 그려놔도 배우가 그 앞을 지나가면 그냥 통과해버립니다. 진짜 벽처럼 막으려면 실제 장애물을 설치해야 합니다.
타일맵 충돌 설정이 바로 그 역할을 합니다. Phaser에서 타일맵 충돌을 설정하는 방법은 크게 세 가지가 있습니다.
첫 번째는 setCollision 메서드입니다. 특정 타일 인덱스를 배열로 전달하면 해당 타일들에만 충돌이 적용됩니다.
타일 인덱스는 타일셋에서 각 타일에 부여된 고유 번호입니다. Tiled 에디터에서 타일 위에 마우스를 올리면 확인할 수 있습니다.
두 번째는 setCollisionBetween 메서드입니다. 시작 인덱스와 끝 인덱스를 지정하면 그 범위 안의 모든 타일에 충돌이 적용됩니다.
지형 타일이 연속된 번호를 가질 때 유용합니다. 세 번째이자 가장 권장되는 방법은 setCollisionByProperty입니다.
Tiled에서 타일셋을 편집할 때 각 타일에 커스텀 프로퍼티를 설정할 수 있습니다. 예를 들어 collides라는 불리언 프로퍼티를 만들고 true로 설정해두면, 코드에서 해당 프로퍼티를 기준으로 충돌을 적용할 수 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 make.tilemap으로 타일맵 데이터를 로드합니다.
이 데이터는 preload에서 미리 로드한 JSON 파일입니다. 다음으로 addTilesetImage를 호출하여 타일맵에서 사용할 이미지를 연결합니다.
첫 번째 인자는 Tiled에서 설정한 타일셋 이름이고, 두 번째 인자는 Phaser에서 로드한 이미지 키입니다. createLayer로 실제 레이어를 생성합니다.
레이어 이름은 Tiled에서 지정한 것과 정확히 일치해야 합니다. 그 후 setCollision 계열 메서드로 충돌을 설정하고, 마지막으로 physics.add.collider로 플레이어와 레이어 간의 충돌 감지를 활성화합니다.
실제 현업에서는 어떻게 활용할까요? 대부분의 게임 프로젝트에서는 Tiled의 커스텀 프로퍼티 방식을 선호합니다.
레벨 디자이너가 코드를 수정하지 않고도 어떤 타일에 충돌을 적용할지 결정할 수 있기 때문입니다. 타일 인덱스가 변경되어도 프로퍼티만 제대로 설정되어 있다면 코드 수정이 필요 없습니다.
주의할 점도 있습니다. 충돌 설정 후 반드시 physics.add.collider를 호출해야 합니다.
setCollision만으로는 충돌 속성만 부여될 뿐, 실제 충돌 감지는 이루어지지 않습니다. 이 부분을 빠뜨리는 실수가 초보자에게 흔합니다.
박시니어 씨의 설명을 들은 김개발 씨는 코드를 수정했습니다. 드디어 캐릭터가 바닥 위에 제대로 서 있습니다.
"생각보다 간단하네요!" 타일맵 충돌 설정은 플랫포머 게임의 가장 기초적인 토대입니다.
실전 팁
💡 - Tiled에서 타일셋 편집 시 충돌용 타일에 collides: true 프로퍼티를 미리 설정해두세요
- 디버그 모드에서 충돌 영역을 시각화하려면 this.physics.world.createDebugGraphic()을 사용하세요
- 여러 레이어가 있을 때 각 레이어마다 개별적으로 충돌 설정이 필요합니다
2. 바닥 착지 감지
김개발 씨의 캐릭터가 이제 바닥 위에 서게 되었습니다. 그런데 점프 기능을 추가하자 새로운 문제가 생겼습니다.
스페이스바를 누를 때마다 캐릭터가 공중에서도 무한으로 점프합니다. "어떻게 하면 바닥에 있을 때만 점프하게 할 수 있을까요?" 바닥 착지 감지가 필요한 순간입니다.
바닥 착지 감지는 캐릭터가 현재 지면에 닿아있는지 확인하는 기능입니다. 마치 엘리베이터가 층에 도착했는지 센서로 확인하는 것과 같습니다.
Phaser의 Arcade Physics에서는 body.blocked나 body.touching 속성으로 간단하게 확인할 수 있습니다.
다음 코드를 살펴봅시다.
// update 함수 내에서 착지 감지
function update() {
// 방법 1: blocked 속성 사용 (타일맵 충돌 시)
const onGround = this.player.body.blocked.down;
// 방법 2: touching 속성 사용 (다른 스프라이트 충돌 시)
const touchingGround = this.player.body.touching.down;
// 방법 3: onFloor 메서드 사용 (두 가지 모두 체크)
const isOnFloor = this.player.body.onFloor();
// 점프 입력 처리
if (this.cursors.space.isDown && isOnFloor) {
this.player.setVelocityY(-400);
}
}
김개발 씨는 점프 기능을 구현했습니다. 스페이스바를 누르면 캐릭터에 위쪽으로 속도를 부여하는 간단한 코드였습니다.
그런데 테스트를 해보니 캐릭터가 공중에서도 계속 점프합니다. 마치 무중력 공간에서 발버둥 치는 것처럼요.
박시니어 씨가 말했습니다. "점프하기 전에 땅에 있는지 확인해야 해요.
그게 바로 바닥 착지 감지입니다." 바닥 착지 감지는 마치 우리가 수영장에서 뛰어내리기 전 발이 다이빙대 위에 있는지 확인하는 것과 같습니다. 공중에 떠 있는데 점프하려고 하면 당연히 안 되겠죠.
게임에서도 마찬가지입니다. Phaser의 Arcade Physics에서는 이 감지를 위한 여러 속성을 제공합니다.
첫 번째는 body.blocked 속성입니다. 이 속성은 캐릭터가 타일맵이나 월드 경계와 충돌했을 때 설정됩니다.
blocked.down이 true면 캐릭터 아래에 충돌하는 타일이 있다는 뜻입니다. 타일맵 기반 게임에서 가장 많이 사용됩니다.
두 번째는 body.touching 속성입니다. 다른 게임 오브젝트나 스프라이트와 충돌했을 때 설정됩니다.
움직이는 플랫폼 위에 서 있는지 확인할 때 유용합니다. 세 번째는 body.onFloor() 메서드입니다.
이 메서드는 blocked.down과 touching.down을 모두 확인하여 어느 쪽이든 바닥에 닿아있으면 true를 반환합니다. 가장 범용적으로 사용할 수 있는 방법입니다.
코드를 살펴보면 구조가 매우 단순합니다. update 함수는 게임이 실행되는 동안 매 프레임마다 호출됩니다.
이 안에서 isOnFloor 변수에 착지 상태를 저장합니다. 그리고 점프 키 입력과 착지 상태를 논리 AND(&&) 연산으로 묶어서, 두 조건이 모두 참일 때만 점프가 실행되도록 합니다.
**setVelocityY(-400)**에서 음수 값은 위쪽 방향을 의미합니다. Phaser의 좌표계에서 Y축은 아래쪽이 양수이기 때문입니다.
이 값을 조절하면 점프 높이를 변경할 수 있습니다. 실무에서는 착지 감지를 좀 더 관대하게 처리하기도 합니다.
예를 들어 코요테 타임이라는 기법이 있습니다. 플랫폼 끝에서 떨어지기 시작한 후에도 짧은 시간 동안 점프를 허용하는 것입니다.
플레이어 경험을 부드럽게 만드는 기법인데, 이 역시 착지 감지를 기반으로 구현됩니다. 주의할 점이 있습니다.
blocked와 touching 속성은 매 프레임 초기화됩니다. 따라서 충돌이 발생한 바로 그 프레임에서만 true 값을 가집니다.
이전 프레임의 충돌 정보를 저장해두고 싶다면 별도의 변수에 기록해야 합니다. 김개발 씨는 점프 조건에 착지 감지를 추가했습니다.
이제 캐릭터가 바닥에 있을 때만 점프합니다. 무한 점프 버그가 사라지자 게임이 훨씬 자연스러워졌습니다.
실전 팁
💡 - onFloor()는 blocked.down과 touching.down을 모두 체크하므로 범용적으로 사용하기 좋습니다
- 점프감을 좋게 하려면 코요테 타임(플랫폼을 벗어난 후 짧은 시간 점프 허용)을 구현해보세요
- 디버깅 시 console.log로 blocked, touching 값을 출력하면 충돌 상태를 쉽게 파악할 수 있습니다
3. 벽 충돌 처리
점프 문제를 해결한 김개발 씨에게 다음 과제가 주어졌습니다. 벽을 만들어야 합니다.
단순히 막기만 하면 되는 게 아니라, 벽에 붙어서 미끄러지듯 내려오는 벽 슬라이딩 기능도 필요합니다. "바닥 충돌은 했는데, 벽 충돌은 어떻게 다르게 처리하죠?"
벽 충돌 처리는 캐릭터가 수평 방향 장애물과 부딪혔을 때의 동작을 제어합니다. 바닥 충돌이 중력에 대응한다면, 벽 충돌은 이동에 대응합니다.
blocked.left와 blocked.right 속성으로 벽 접촉을 감지하고, 이를 활용해 벽 점프나 벽 슬라이딩 같은 기능을 구현할 수 있습니다.
다음 코드를 살펴봅시다.
function update() {
const touchingLeft = this.player.body.blocked.left;
const touchingRight = this.player.body.blocked.right;
const touchingWall = touchingLeft || touchingRight;
const inAir = !this.player.body.onFloor();
// 벽 슬라이딩: 공중에서 벽에 붙어있으면 천천히 내려옴
if (touchingWall && inAir) {
// 낙하 속도 제한으로 미끄러지는 효과
this.player.setVelocityY(Math.min(this.player.body.velocity.y, 100));
this.player.isWallSliding = true;
} else {
this.player.isWallSliding = false;
}
// 벽 점프: 벽에서 반대 방향으로 점프
if (this.cursors.space.isDown && this.player.isWallSliding) {
const jumpDirection = touchingLeft ? 1 : -1;
this.player.setVelocity(jumpDirection * 300, -400);
}
}
김개발 씨가 만드는 게임에는 높은 벽을 타고 올라가는 구간이 있습니다. 단순히 벽에 막히기만 해서는 재미가 없습니다.
요즘 플랫포머 게임처럼 벽을 타고 미끄러지다가 반대편으로 점프하는 기능이 필요했습니다. 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.
"바닥 감지와 원리는 같아요. 다만 방향이 다를 뿐이죠." 벽 충돌 처리는 마치 엘리베이터 문이 사람을 감지하는 것과 비슷합니다.
문 양쪽에 센서가 있어서 왼쪽에서 접근하는지, 오른쪽에서 접근하는지 알 수 있습니다. 게임에서도 캐릭터의 왼쪽과 오른쪽에 가상의 센서가 있다고 생각하면 됩니다.
Phaser에서는 body.blocked.left와 body.blocked.right 속성으로 벽 접촉을 감지합니다. 이 속성들은 바닥 감지에 사용한 blocked.down과 같은 원리로 작동합니다.
코드의 첫 부분을 살펴보겠습니다. touchingLeft와 touchingRight 변수에 각각 왼쪽 벽, 오른쪽 벽 접촉 여부를 저장합니다.
touchingWall은 둘 중 하나라도 true면 true가 되는 논리 OR 연산 결과입니다. inAir는 공중에 떠 있는지 확인합니다.
바닥에 서 있으면서 벽에 기대는 것은 벽 슬라이딩이 아니니까요. 벽 슬라이딩 로직의 핵심은 낙하 속도 제한입니다.
일반적으로 캐릭터가 떨어질 때 중력에 의해 속도가 계속 증가합니다. 하지만 벽에 붙어있을 때는 Math.min 함수를 사용해 낙하 속도를 100으로 제한합니다.
이렇게 하면 벽을 타고 천천히 미끄러지는 느낌이 납니다. isWallSliding이라는 커스텀 속성을 추가한 것도 주목해주세요.
Phaser의 스프라이트 객체에는 개발자가 원하는 속성을 자유롭게 추가할 수 있습니다. 이 속성은 벽 점프 조건 확인이나 애니메이션 전환에 활용됩니다.
벽 점프 구현은 좀 더 흥미롭습니다. 점프 키를 눌렀을 때 벽 슬라이딩 상태라면, 벽의 반대 방향으로 튕겨나가야 합니다.
jumpDirection 변수가 이 방향을 결정합니다. 왼쪽 벽에 붙어있으면 오른쪽(양수)으로, 오른쪽 벽에 붙어있으면 왼쪽(음수)으로 이동합니다.
setVelocity 메서드는 X와 Y 속도를 동시에 설정합니다. 수평으로는 벽에서 멀어지는 방향으로, 수직으로는 위쪽으로 속도를 부여하면 대각선으로 튕겨나가는 벽 점프가 완성됩니다.
실무에서는 벽 점프 후 일정 시간 동안 플레이어 입력을 무시하는 처리를 추가하기도 합니다. 그렇지 않으면 벽 점프 직후 이동 키를 눌러 다시 벽에 붙어버리는 현상이 발생합니다.
자연스러운 벽 점프를 위한 세부 조정이 필요합니다. 주의할 점이 있습니다.
벽 슬라이딩 중에는 일반 점프를 막아야 합니다. 그렇지 않으면 벽 점프와 일반 점프가 동시에 발동할 수 있습니다.
점프 조건을 명확하게 분리해야 버그를 방지할 수 있습니다. 김개발 씨는 벽 슬라이딩과 벽 점프를 구현했습니다.
캐릭터가 벽을 타고 능숙하게 이동하는 모습을 보니 뿌듯합니다. 이제 좀 더 게임다워졌습니다.
실전 팁
💡 - 벽 슬라이딩 시 별도의 애니메이션을 재생하면 시각적 피드백이 좋아집니다
- 벽 점프 후 짧은 입력 무시 시간을 두면 더 자연스러운 움직임이 됩니다
- 벽 방향에 따라 캐릭터 스프라이트를 뒤집어주면 더 자연스럽습니다
4. 점프 가능 여부 체크
게임 테스트 중에 기획자가 요청을 해왔습니다. "2단 점프도 넣어주세요.
아, 그리고 벽 점프할 때는 2단 점프 횟수가 리셋되면 좋겠어요." 김개발 씨는 고민에 빠졌습니다. 단순히 바닥에 있는지만 체크해서는 이런 복잡한 점프 로직을 구현할 수 없습니다.
점프 가능 여부 체크는 단순한 바닥 감지를 넘어 게임의 점프 규칙을 구현하는 시스템입니다. 마치 놀이공원 놀이기구의 탑승 조건을 확인하는 것처럼, 현재 점프할 수 있는 상태인지 여러 조건을 종합적으로 판단합니다.
더블 점프, 코요테 타임, 점프 버퍼링 등 다양한 기능의 기반이 됩니다.
다음 코드를 살펴봅시다.
function create() {
this.player.jumpCount = 0;
this.player.maxJumps = 2; // 2단 점프 허용
this.player.coyoteTime = 0;
this.player.jumpBufferTime = 0;
}
function update(time, delta) {
const onGround = this.player.body.onFloor();
// 코요테 타임: 플랫폼을 벗어나도 짧은 시간 점프 허용
if (onGround) {
this.player.coyoteTime = 100; // 100ms 유예
this.player.jumpCount = 0;
} else {
this.player.coyoteTime -= delta;
}
// 점프 버퍼: 착지 직전 점프 입력을 기억
if (Phaser.Input.Keyboard.JustDown(this.cursors.space)) {
this.player.jumpBufferTime = 100;
} else {
this.player.jumpBufferTime -= delta;
}
// 점프 가능 여부 종합 판단
const canJump = (this.player.coyoteTime > 0 || this.player.jumpCount < this.player.maxJumps)
&& this.player.jumpBufferTime > 0;
if (canJump) {
this.player.setVelocityY(-400);
this.player.jumpCount++;
this.player.jumpBufferTime = 0;
this.player.coyoteTime = 0;
}
}
김개발 씨는 간단한 바닥 체크만으로 점프를 구현했었습니다. 하지만 기획자의 요청은 더 복잡했습니다.
2단 점프, 플랫폼 끝에서 떨어질 때 점프 유예, 착지 직전 점프 선입력까지. 점프 하나에 이렇게 많은 요소가 들어갈 줄은 몰랐습니다.
박시니어 씨가 조언했습니다. "좋은 플랫포머의 비결은 점프 필이에요.
그리고 그 핵심이 바로 점프 가능 여부 시스템입니다." 점프 가능 여부 체크는 마치 자격 시험과 같습니다. 단순히 한 가지 조건만 보는 게 아니라 여러 조건을 종합해서 "합격/불합격"을 판정합니다.
시험에 응시 자격, 점수, 출석 등 여러 기준이 있듯이, 점프에도 바닥 상태, 점프 횟수, 타이밍 등 여러 기준이 있습니다. 코드에서 create 함수를 먼저 살펴보겠습니다.
jumpCount는 현재까지 점프한 횟수입니다. 바닥에 닿으면 0으로 리셋됩니다.
maxJumps는 최대 점프 횟수로, 2로 설정하면 2단 점프가 가능합니다. coyoteTime과 jumpBufferTime은 각각 유예 시간을 저장하는 변수입니다.
코요테 타임이라는 용어가 재미있습니다. 만화에서 코요테가 절벽을 벗어나도 잠시 공중에 떠 있다가 떨어지는 장면에서 유래했습니다.
게임에서도 플랫폼 끝을 벗어난 직후 짧은 시간 동안 점프를 허용하면 플레이어에게 관대한 조작감을 줄 수 있습니다. 바닥에 있을 때마다 coyoteTime을 100ms로 설정합니다.
공중에 있으면 delta만큼 감소합니다. delta는 이전 프레임과 현재 프레임 사이의 시간 간격(밀리초)입니다.
이렇게 하면 플랫폼을 막 벗어났을 때 100ms 동안은 점프가 가능합니다. 점프 버퍼링은 그 반대 개념입니다.
플레이어가 착지 직전에 점프 키를 눌렀다면, 실제로 착지하는 순간 점프가 발동되어야 합니다. 점프 입력을 "버퍼"에 저장해두었다가 조건이 맞으면 실행하는 것입니다.
JustDown은 키가 이번 프레임에 새로 눌렸는지 확인합니다. 누르고 있는 동안 계속 true인 isDown과 다릅니다.
점프 가능 여부 판단 조건을 해석해보겠습니다. coyoteTime이 0보다 크거나, jumpCount가 maxJumps보다 작아야 합니다.
첫 번째 조건은 "바닥이거나 막 떨어졌을 때"를 의미하고, 두 번째 조건은 "아직 점프 횟수가 남았을 때"를 의미합니다. 그리고 jumpBufferTime이 0보다 커야 하는데, 이는 "최근에 점프 키를 눌렀다"는 뜻입니다.
모든 조건이 충족되면 점프를 실행하고 관련 변수들을 초기화합니다. 실무에서는 이 시스템을 더 확장하기도 합니다.
예를 들어 벽 점프 후 jumpCount를 리셋하거나, 특정 아이템을 먹으면 maxJumps를 증가시키거나, 스킬에 따라 코요테 타임을 늘리는 등의 변형이 가능합니다. 주의할 점이 있습니다.
코요테 타임과 점프 버퍼 값이 너무 크면 의도치 않은 점프가 발생합니다. 보통 80-150ms 사이가 적당합니다.
너무 작으면 효과가 없고, 너무 크면 부자연스럽습니다. 김개발 씨는 점프 시스템을 완성했습니다.
테스트해보니 확실히 조작감이 달라졌습니다. 미세한 차이지만 게임이 훨씬 쾌적해졌습니다.
실전 팁
💡 - 코요테 타임과 점프 버퍼는 80-150ms가 일반적으로 적당합니다
- JustDown을 사용해야 키를 누르고 있을 때 연속 점프를 방지할 수 있습니다
- 디버그용 UI로 점프 횟수와 유예 시간을 표시하면 테스트가 편합니다
5. 원웨이 플랫폼
기획자가 또 찾아왔습니다. "아래에서 점프해서 플랫폼을 통과하고, 위에 착지할 수 있는 발판 있잖아요.
그것도 필요해요." 김개발 씨는 고개를 끄덕였습니다. 슈퍼 마리오의 그 발판 말이군요.
하지만 생각해보니 까다롭습니다. 같은 타일인데 어떻게 아래에서는 통과하고 위에서는 막을 수 있을까요?
원웨이 플랫폼은 한 방향에서만 충돌이 발생하는 플랫폼입니다. 마치 일방통행 도로처럼 아래에서 위로는 통과하지만, 위에서 아래로는 막힙니다.
Phaser에서는 타일의 충돌 방향을 개별 설정하거나, 아케이드 바디의 checkCollision 속성을 조작하여 구현할 수 있습니다.
다음 코드를 살펴봅시다.
function create() {
const map = this.make.tilemap({ key: 'level1' });
const tileset = map.addTilesetImage('platformTiles', 'tiles');
// 원웨이 플랫폼 레이어 생성
this.oneWayLayer = map.createLayer('OneWay', tileset, 0, 0);
// 방법 1: 타일 충돌 방향 설정
this.oneWayLayer.forEachTile(tile => {
if (tile.index !== -1) {
// 위쪽 충돌만 활성화
tile.setCollision(false, false, true, false); // left, right, up, down
}
});
// 방법 2: 플레이어 상태에 따른 동적 충돌
this.physics.add.collider(this.player, this.oneWayLayer, null, (player, tile) => {
// 플레이어가 위에서 내려오고, 플랫폼보다 위에 있을 때만 충돌
return player.body.velocity.y > 0 && player.body.bottom <= tile.pixelY + 10;
});
}
김개발 씨는 원웨이 플랫폼이라는 개념을 처음 들었을 때 의아했습니다. "충돌이 한쪽에서만 일어난다고요?
그게 물리적으로 말이 되나요?" 박시니어 씨가 웃으며 답했습니다. "게임 물리는 현실 물리가 아니에요.
플레이어 경험을 위해 현실을 왜곡하는 거죠." 원웨이 플랫폼을 이해하려면 자동문을 떠올려보세요. 쇼핑몰 입구의 자동문은 안에서 밖으로는 열리지만, 밖에서 안으로는 열리지 않습니다(물론 실제로는 양방향이지만, 일부 보안 구역은 그렇습니다).
게임의 원웨이 플랫폼도 마찬가지입니다. 아래에서 위로는 통과하지만, 위에서 아래로는 막힙니다.
구현 방법은 크게 두 가지입니다. 첫 번째 방법은 타일 자체의 충돌 방향 설정입니다.
forEachTile 메서드로 레이어의 모든 타일을 순회하면서 setCollision 메서드를 호출합니다. 이 메서드는 네 개의 불리언 값을 받는데, 순서대로 왼쪽, 오른쪽, 위쪽, 아래쪽 충돌 여부입니다.
세 번째 인자만 true로 설정하면 위쪽 충돌만 활성화됩니다. 즉, 플레이어가 플랫폼 위에서 아래로 내려오면 막히고, 아래에서 위로 올라오면 통과합니다.
두 번째 방법은 동적 충돌 판단입니다. collider에 콜백 함수를 전달하여 충돌 여부를 매번 판단합니다.
네 번째 인자로 전달되는 콜백은 processCallback이라고 불리며, true를 반환하면 충돌이 발생하고 false를 반환하면 무시됩니다. 콜백의 조건을 해석해보겠습니다.
player.body.velocity.y > 0은 플레이어가 아래로 떨어지고 있다는 뜻입니다. 위로 점프 중이면 이 값은 음수이므로 충돌이 발생하지 않습니다.
player.body.bottom <= tile.pixelY + 10은 플레이어의 발 위치가 타일 상단 근처에 있다는 뜻입니다. 10픽셀의 여유를 두는 것은 프레임 간 이동 오차를 감안한 것입니다.
두 방법 중 어떤 것을 선택해야 할까요? 첫 번째 방법은 간단하고 성능이 좋습니다.
타일 설정만으로 끝나니까요. 하지만 "아래 키를 누르면 원웨이 플랫폼을 통과해서 내려가기" 같은 기능은 구현하기 어렵습니다.
두 번째 방법은 더 유연합니다. 콜백에서 어떤 조건이든 추가할 수 있습니다.
예를 들어 아래 키를 누르고 있으면 충돌을 무시하게 할 수 있습니다. 다만 매 프레임 콜백이 호출되므로 성능에 약간의 부담이 있습니다.
실무에서는 보통 두 방법을 혼합합니다. 기본적인 원웨이 동작은 타일 설정으로 처리하고, 통과 내려가기 같은 특수 기능만 동적으로 처리합니다.
주의할 점이 있습니다. 원웨이 플랫폼에서 벽 충돌이 발생하면 이상한 동작이 일어날 수 있습니다.
플레이어가 옆에서 플랫폼에 부딪히면 어떻게 될까요? 일반적으로 원웨이 플랫폼은 얇고 좁은 발판 형태로 디자인하여 측면 충돌 자체를 피합니다.
김개발 씨는 원웨이 플랫폼을 구현했습니다. 캐릭터가 발판 아래에서 점프해 올라가 착지하는 모습이 슈퍼 마리오 그 자체입니다.
게임이 점점 완성되어 갑니다.
실전 팁
💡 - 원웨이 플랫폼은 얇은 발판 형태로 디자인하면 측면 충돌 문제를 피할 수 있습니다
- 아래 키로 통과하는 기능은 processCallback에서 키 입력을 체크하여 구현합니다
- Tiled에서 원웨이 플랫폼용 별도 레이어를 만들면 관리가 편합니다
6. 슬로프 처리
게임에 경사로가 필요해졌습니다. 평평한 땅만 있으면 너무 단조롭다는 피드백이 들어온 것입니다.
김개발 씨는 타일맵에 대각선 타일을 추가하고 충돌을 설정했습니다. 그런데 캐릭터가 경사로에서 덜덜 떨리며 이상하게 움직입니다.
"왜 경사로에서만 이상하게 되는 거지?"
슬로프 처리는 경사진 지형에서 캐릭터가 자연스럽게 이동하도록 하는 기술입니다. 평평한 바닥에서는 수평 이동만 고려하면 되지만, 경사로에서는 높이 변화를 함께 처리해야 합니다.
Phaser의 Matter Physics를 사용하거나, Arcade Physics에서 타일 콜리전 보정으로 구현할 수 있습니다.
다음 코드를 살펴봅시다.
// Arcade Physics에서 슬로프 처리 (간단한 방식)
function create() {
// 슬로프 타일에 커스텀 충돌 박스 설정
this.groundLayer.forEachTile(tile => {
if (tile.properties.slope) {
// 타일별 충돌 영역을 경사에 맞게 조정
const slopeData = {
leftUp: { left: 0, right: 16, top: 16, bottom: 0 }, // 왼쪽 위로 올라감
rightUp: { left: 0, right: 16, top: 0, bottom: 16 } // 오른쪽 위로 올라감
};
const data = slopeData[tile.properties.slopeType];
if (data) {
tile.setCollision(true);
}
}
});
}
// 슬로프에서 미끄러짐 방지
function update() {
const onSlope = this.checkSlope();
if (onSlope && this.player.body.velocity.x === 0) {
// 정지 시 중력에 의한 미끄러짐 방지
this.player.body.setAllowGravity(false);
this.player.setVelocityY(0);
} else {
this.player.body.setAllowGravity(true);
}
}
김개발 씨가 경사로를 만들었을 때 처음 마주한 문제는 캐릭터의 떨림이었습니다. 경사면 위에서 캐릭터가 미세하게 위아래로 튕기는 현상이 발생했습니다.
마치 멀미가 날 것 같은 움직임이었습니다. 박시니어 씨가 설명했습니다.
"Arcade Physics는 사각형 충돌 박스를 사용해요. 대각선 지형과 사각형이 만나면 자연스럽게 처리되지 않는 거죠." 슬로프 처리가 어려운 이유를 이해하려면 충돌 감지 원리를 알아야 합니다.
Phaser의 Arcade Physics에서 모든 충돌체는 AABB(Axis-Aligned Bounding Box), 즉 축에 정렬된 사각형입니다. 회전하거나 기울어진 충돌 박스는 지원하지 않습니다.
경사면도 실제로는 여러 개의 작은 사각형 타일로 구성되어 있습니다. 캐릭터가 경사면을 걸어 올라갈 때 무슨 일이 일어날까요?
발밑의 타일 높이가 계속 변합니다. 캐릭터는 한 발짝 이동할 때마다 약간 위로 밀려나고, 중력에 의해 다시 아래로 끌려가고, 다음 타일에 부딪히고.
이 과정이 매우 빠르게 반복되면서 떨림이 발생합니다. 이 문제를 해결하는 첫 번째 방법은 타일별 충돌 영역 조정입니다.
Tiled에서 타일셋을 편집할 때 각 타일의 충돌 영역을 다각형으로 직접 그릴 수 있습니다. 경사 타일의 충돌 영역을 대각선으로 설정하면 더 자연스러운 충돌이 가능합니다.
하지만 Arcade Physics에서는 이 다각형을 완벽하게 지원하지 않아 한계가 있습니다. 두 번째 방법은 코드에서 보여준 것처럼 중력 제어입니다.
경사면에서 정지해 있을 때 중력을 비활성화합니다. **setAllowGravity(false)**를 호출하면 해당 오브젝트에 중력이 적용되지 않습니다.
이렇게 하면 경사면에서 가만히 서 있을 때 미끄러지거나 떨리는 현상을 방지할 수 있습니다. 움직이기 시작하면 중력을 다시 활성화합니다.
그래야 점프 후 자연스럽게 떨어지니까요. checkSlope 함수는 현재 서 있는 타일이 경사 타일인지 확인합니다.
플레이어 발밑의 타일을 가져와 slope 프로퍼티를 체크하면 됩니다. 더 정교한 슬로프 처리가 필요하다면 Matter Physics를 고려해야 합니다.
Matter.js는 다각형 충돌, 경사면, 물리적으로 정확한 마찰력 등을 지원하는 물리 엔진입니다. Phaser에서 Matter Physics를 활성화하면 훨씬 자연스러운 경사면 이동이 가능합니다.
다만 Arcade Physics보다 복잡하고 성능 부담도 있습니다. 실무에서는 게임의 요구사항에 따라 선택합니다.
간단한 플랫포머라면 Arcade Physics로 충분하고, 물리 퍼즐이나 복잡한 지형이 많은 게임이라면 Matter Physics가 적합합니다. 주의할 점이 있습니다.
경사면에서 점프하면 이상한 방향으로 튕겨나갈 수 있습니다. 점프 시작 시점에 수직 방향으로만 속도를 부여하고, 경사면의 영향을 무시하도록 처리해야 자연스럽습니다.
김개발 씨는 슬로프 처리를 마쳤습니다. 완벽하지는 않지만, 캐릭터가 경사로를 오르내릴 수 있게 되었습니다.
플랫폼 물리의 기본을 모두 익힌 셈입니다.
실전 팁
💡 - 복잡한 슬로프가 많다면 Matter Physics 전환을 고려하세요
- 경사면 정지 시 중력을 비활성화하면 미끄러짐을 방지할 수 있습니다
- Tiled에서 경사 타일에 slope 프로퍼티를 설정해두면 코드에서 쉽게 식별할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
CloudFront CDN 완벽 가이드
AWS CloudFront를 활용한 콘텐츠 배포 최적화 방법을 실무 관점에서 다룹니다. 배포 생성부터 캐시 설정, HTTPS 적용까지 단계별로 알아봅니다.