본문 바로가기

카테고리 없음

플랫포머 툴킷을 뜯어보자

 

Platformer Toolkit

액션 게임의 캐릭터 조작과 이동에 대한 기본 개념을 담은 툴킷이다. 조작감이 게임플레이에 어떤 영향을 미치는 지 이해할 수 있는 인터랙티브 비디오 에세이다.

https://gmtk.itch.io/platformer-toolkit

 

Platformer Toolkit by Game Maker's Toolkit

From Game Maker's Toolkit

gmtk.itch.io

해당 링크를 통해서 빌드와 소스를 다운로드 받을 수 있다. (물론 원하는 경우 소액 기부도 가능하다)

 

이 플랫포머 툴킷을 사용하여 플랫포머 게임 캐릭터의 조작감을 좌우하는 변수들을 조정해보면서 플랫포머 캐릭터가 어떻게 디자인되는지를 직접 확인할 수 있다.

 

오늘은 해당 에세이의 내용을 정리하고 직접 소스 코드를 뜯어보면서 구현을 어떻게 하고 있는지 정리해보려고 한다. 사실 시리즈 물이 될 거 같다. 왜냐하면 플랫폼 게임의 움직임과 관련된 변수만 해도 30개가 넘기 때문이다,,, 플랫포머 게임 디자인에서 정말 핵심적인 내용들을 다루고 있기 때문에 언젠가 분석해봐야지 생각하고 있었는데 최근 작업 중에 플레이어의 움직임을 연구해야할 태스크가 생기면서 이번 기회를 통해 알게 된 내용들을 정리하려고 한다.

 

조작감(Game Feel)의 3요소

조작감은 플레이어가 게임 내 요소를 제어하고 상호 작용하는 과정에서 발생하는 감각과 경험을 의미한다.

게임의 첫 인상이자 장르를 불문하고 성패를 결정하는 주요 요소이다.

플레이어의 조작에 의해 캐릭터가 ~~~하게 움직인다! 가 정의되어야 하는데 예컨대 입력을 빠르고 정확하게 처리하는 반응성이 중요할수 있고 관성과 힘을 느낄 수 있는 무게감있는 조작감이 중요할 수도 있다. 추구하는 조작감은 게임마다 다를 것이다.

 

에세이에서는 좋은 조작감을 다루기 위해서

  1. 변수 튜닝
  2. 플레이어를 위해 속이기 (Cheat)
  3. Juice 추가

이 세 가지를 변경함으로써 의도한 조작감을 표현할 수 있다고 보았다.

 

오늘은 이 플랫포머 툴킷이 캐릭터 이동을 구현한 방법과 변수 튜닝과 관련해서 Running에 관한 내용을 살펴보겠다.

 

캐릭터 이동 구현

CharacterMovement.cs에서 입력 , 속도 계산 , Rigidbody를 통한 이동 적용으로 이루어진다.

    public void OnMovement(InputAction.CallbackContext context)
    {
        //This is called when you input a direction on a valid input type, such as arrow keys or analogue stick
        //The value will read -1 when pressing left, 0 when idle, and 1 when pressing right.

        if (moveLimit.characterCanMove)
        {
            directionX = context.ReadValue<float>();
        }
    }

 

Input System을 통해 x축 방향 값을 입력받고 있다.

오른쪽 방향키를 누르면 값이 1 , 왼쪽을 누르면 -1 이 입력되며 캐릭터의 이동 방향을 반전시키는 방식이다.

    private void Update()
    {
    	...

        //Calculate's the character's desired velocity - which is the direction you are facing, multiplied by the character's maximum speed
        //Friction is not used in this game
        desiredVelocity = new Vector2(directionX, 0f) * Mathf.Max(maxSpeed - friction, 0f);

    }

 

아까 구한 방향 directionX에 따라 maxSpeed를 곱해 목표 속도를 계산한다.

private void FixedUpdate()
    {
       ...
       
        //Get the Rigidbody's current velocity
        velocity = body.velocity;

        //Calculate movement, depending on whether "Instant Movement" has been checked
        if (useAcceleration)
        {
            runWithAcceleration();
        }
        
        ...
    }

 

그리고 물리 엔진 연산이 들어가 running 동작을 처리하게 된다. Rigidbody2D 컴포넌트에서 현재 속도를 받아와 속도 계산 로직에 따라 velocity 메소드에 반영하는 식으로 물리 기반 이동을 구현한다.

Running 상태에서의 변수

  • Accereration
    • 입력을 주었을 때 최고 속도에 도달하기까지의 가속도이다. Max Speed까지 속도가 Linear하게 올라간다.
  • 최고 속도 (Max Speed)
  • Deceleration
    • 입력을 놓았을 때 정지까지 걸리는 감속 속도이다. Linear하게 표현되어 있다.
  • Turn Speed
    • 달리던 방향과 반대 방향 키를 눌렀을 때의 감속 속도를 뜻한다. 즉 방향 전환의 민감도이다.
    private void runWithAcceleration()
    {
        ...

        if (pressingKey)
        {
        	...
            {
                //If they match, it means we're simply running along and so should use the acceleration stat
                maxSpeedChange = acceleration * Time.deltaTime;
            }
        }
        else
        {
            //And if we're not pressing a direction at all, use the deceleration stat
            maxSpeedChange = deceleration * Time.deltaTime;
        }

       ...

    }

 

입력이 들어왔을 때 매 프레임당 (Time.deltaTime 으로 프레임 당 쪼갰음) acceleration 만큼 가속도를 적용한다. 반대로 입력을 떼면 maxSpeedChange가 deceleration으로 변경되어 감속이 적용되는 구조이다.

    private void runWithAcceleration()
    {
    	...
        
        //Move our velocity towards the desired velocity, at the rate of the number calculated above
        velocity.x = Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);

        //Update the Rigidbody with this new velocity
        body.velocity = velocity;

    }

 

현재 속도 (velocity.x)를 목표 속도(desiredVelocity.x) 에 가까워지도록 매 프레임마다 acceleration 혹은 deceleration 만큼 선형 보간으로 최고 속도에 도달하도록 만들고 계산한 현재 속도를 rigidbody에 적용시킨다.

    private void runWithAcceleration()
    {
    	...
        
        if (pressingKey)
        {
            //If the sign (i.e. positive or negative) of our input direction doesn't match our movement, it means we're turning around and so should use the turn speed stat.
            if (Mathf.Sign(directionX) != Mathf.Sign(velocity.x))
            {
                maxSpeedChange = turnSpeed * Time.deltaTime;
            }

        ...

 

TurnSpeed는 달리던 방향과 반대 방향으로 키를 입력했을 때 빠르게 방향 전환 되는지를 결정한다.

입력 방향과 현재 이동 방향을 비교해서 방향 전환이 이루어짐이 확인 되면 maxSpeedChange를 turnSpeed 변수로 제어함으로 적용시켰다.

 

Long Accerlation은 소닉같이 점진적으로 속도가 붙는 무게감있고 통제 불가능한 속도감을 표현하기 위해 필요할 수 있다. Short Acceleration은 즉각적인 반응과 빠른 반응성을 위해 셀레스트 같은 게임에서 필요할 수 있다. 즉 이런 가속과 감속에 관한 Running 변수들에는 절대적인 정답이 없고 내가 주고자 하는 재미에 따라 달라질 수 있는 것이다.


플랫포머 툴킷 역시 물리 엔진이 포함된 RigidBody를 활용하여 플랫포머 움직임을 구현하고 있다. 작업중인 프로젝트에서 정밀한 조작감을 주기 위해 별도의 물리 시스템을 직접 만들어야 하나 고민했지만 여기서도 확인한 것 처럼 RigidBody를 사용해도 충분히 컨트롤 가능하게 구현이 되나보다.

 

예상은 했지만 팀 내에서도 조작감에 대한 의견 차이는 벌써부터 존재한다. 동선 설계를 통제하기 위해 즉각적인 반응을 주자는 입장, 그리고 반대로 조작의 어려움이 이 게임의 묘미가 되기에 통제 불가능한 정신없음을 표현하자는 입장이 있다.

 

개인적으로는 플레이어가 어느 정도는 통제 가능한 조작감이 있어야 한다는 쪽에 가까운 입장이지만 서로 다른 조작감을 테스트하고 빌드로 제시해보는 것이 가장 실질적인 방법이라 생각했다. 그래서 지금 이 시스템을 분석하고 있는 이유도 결국 우리 게임에서 가장 잘 맞는 조작감을 선택할 수 있도록 확장성 있는 컨트롤러를 구현하려는 데에 있다.

 

이전 일정들이 마무리 되니 오늘 acceleration을 배운 것 처럼 개발에도 점점 가속이 붙고 있다. 하하~