본문으로 바로가기

3장 벡터와 물리 연습문제 3.3

문제

뉴턴 물리를 사용하도록 MoveComponent를 수정해본다.

  • 조건1: MoveComponent가 멤버 변수로서 질량, 힘의 총합, 그리고 속도를 가지도록 변경한다.
  • 조건2: Update함수에서 전방 이동에 대한 코드를 변경해서 힘으로부터 가속도를, 가속도로부터 속도를, 그리고 속도로부터 위치를 계산한다.
  • 조건3: MoveComponent에 힘을 설정하는 메소드를 구현한다.
    • Vector2를 파라미터로 받는 AddForce함수를 추가하고 힘의 총합 변수에 그 힘을 더하 방식을 권장한다.
    • 이방식을 통해서 충격력의 경우 AddForce를 한번만 호출하고, (중력같은) 일정 하게 작용하는 힘에 대해서는 프레임마다 AddForce를 호출해야 한다.

풀이

문제에서 구체적으로 어떤 리소스를 사용할지와 요구기능을 어떻게 시연해 보일지에 대해서는 구체적으로 지정되어 있지는 않아서 명확한 개발 목표 설정읠 위해서 임의로 몇 가지 요구사항을 더 추가해보았다.

  • 추가1: 리소스는 2장에 나온 로켓을 활용할 것이며, 물체의 진행 방향에 맞게 엑터가 회전할 수 있어야 한다.
  • 추가2: 스페이스바를 누르는 동안 로켓은 우측 상단으로 추진력을 얻게되고, 화면 하단 방향으로 항상 중력이 일정하게 작용해야한다. 조금 더 현실성을 높이기 위해서 공기저항력이 일정한 계수로 항상 작용한다. 그 외의 다른 움직임은 존재하지 않는다.

Exercise_3_3 전체 소스

구현 결과물

▲ 연습문제 3.3 시연 영상

1단계

  • 조건1: MoveComponent가 멤버 변수로서 질량, 힘의 총합, 그리고 속도를 가지도록 변경한다.
    • 다음과 같이 MoveComponent 클래스에 mMass, mForces, mVelocity를 추가했다.
    • 추가2 조건을 만족시키기 위해서 중력가속도 변수 GA와 공기저항계수 mAirResistance변수를 const 변수로 선언하고, 생성자에서 초기화 한다.
  • 조건3: MoveComponent에 힘을 설정하는 메소드를 구현한다.
    • MoveComponent 클래스에 AddForce(Vector2) 함수를 구현했다.

MoveComponent.h의 선언

#pragma once
#include "Component.h"
#include "Math.h"

class MoveComponent : public Component
{
public:
    // 먼저 업데이트되도록 갱신 순서값을 낮춤
    MoveComponent(class Actor* owner, int updateOrder = 10);

    void Update(float deltaTime) override;

    // 물리연산을 위한 멤버함수
    void AddForce(Vector2 force) { mForces += force; }

    // ... (Get,Set함수 생략)...

private:
    float mMass;
    Vector2 mForces;
    Vector2 mAccele;
    Vector2 mVelocity;

    // 중력 가속도(gravitational acceleration)
    const Vector2 GA;
    // 공기 저항
    const float mAirResistance;
};

MoveComponent.cpp의 MoveComponent::MoveComponent(Actor*, int)

MoveComponent::MoveComponent(Actor* owner, int updateOrder)
    :Component(owner, updateOrder)
    , mMass(1.0f)
    , mForces(Vector2(0.0f, 0.0f))
    , mAccele(Vector2(0.0f, 0.0f))
    , mVelocity(Vector2(0.0f, 0.0f))
    , GA(Vector2(0.0f, 300.0f)) // 중력가속도를 화면 하단 방향으로 300.0f로 임의 설정
    , mAirResistance(0.01f)     // 공기저항 계수를 임의로 0.01f로 설정
{

}

2단계

  • 추진력벡터, 질량, 입력받을 key는 우주선Ship의 생성자에서 설정해줄 수 있다.

Ship.cpp 의 Ship::Ship(Game*)

Ship::Ship(Game* game)
    : Actor(game)
{
    AnimSpriteComponent* asc = new AnimSpriteComponent(this);
    std::vector<SDL_Texture*> anims = {
        game->GetTexture("../Assets/Ship01.png"),
        game->GetTexture("../Assets/Ship02.png"),
        game->GetTexture("../Assets/Ship03.png"),
        game->GetTexture("../Assets/Ship04.png")
    };
    asc->SetAnimTextures(anims);

    // Create InputComponent and set keys/force
    InputComponent* ic = new InputComponent(this);
    ic->SetForceKey(SDL_SCANCODE_SPACE);
    ic->SetDForce(Vector2(1500.0f,-20000.0f));
    ic->SetMass(30.0f);
}

3단계

  • InputComponent 클래스에서 "우주선의 움직임과 관련된 기존코드"를 "추진력을 입력받으면 AddForce()를 호출하는 코드"로 수정하였다.

InputComponent.h의 선언

class InputComponent : public MoveComponent
{
public:
    InputComponent(class Actor* owner);
    void ProcessInput(const uint8_t* keyState) override;

    // ... (Get,Set함수 생략)...

private:
    // 추진력(Driving Force)
    Vector2 mDForce;
    // 추진력을 입력하는 키
    int mForceKey;
};

InputComponent.cpp의 InputComponent::ProcessInput(const uint8_t*)

void InputComponent::ProcessInput(const uint8_t* keyState)
{
    // Actor의 상태가 paused라면 Actor 입력 안받음
    if (mOwner->GetState() == Actor::EPaused)
        return;

    // ForceKey가 입력되면 mDForce만큼의 힘이 더해짐.
    if (keyState[mForceKey])
    {
        AddForce(mDForce);
    }
}

4단계

  • 조건2: Update함수에서 전방 이동에 대한 코드를 변경해서 힘으로부터 가속도를, 가속도로부터 속도를, 그리고 속도로부터 위치를 계산한다.
    • MoveComponent::Update(float) 함수에서 물리 연산을 진행한다. (여기서는 크게 고려대상은 아니지만 물리 연산을 진행하는 Update함수는 가변 시간 단계 문제를 고려하여서 고정 레이트로 호출되어야 한다.)
    • 중력: \(F_g = mg\)
    • 공기저항: \(F_r = -kv\)
    • 가속도: \(\vec{a} = F_{all} / m\)
    • 속도: \(\vec{v} = \vec{a} * dt\)
    • 위치: \(pos = pos_{현재} + \vec{v} * dt\)
  • 추가1: 리소스는 2장에 나온 로켓을 활용할 것이며, 물체의 진행 방향에 맞게 엑터가 회전할 수 있어야 한다.
    • 물체의 회전을 위해서 아래의 공식을 활용하여 회전 값 계산
    • 엑터의 회전값: \(\theta = \arctan( \vec{y} / \vec{x} )\)

MoveComponent.cpp의 MoveComponent::Update(float)

void MoveComponent::Update(float deltaTime)
{
    // 매 프레임마다 중력 작용
    AddForce(GA * mMass);
    // 매 프레임마다 공기저항력 작용
    AddForce(mVelocity * mAirResistance * (-1.0f));
    // 가속도 = 힘의 총량 / 질량
    mAccele = mForces * (1/mMass);
    // 속도 = 가속도 * 시간
    mVelocity += mAccele * deltaTime;
    // 속도벡터로 actor의 회전값 계산. SDL의 y 좌표는 아랫쪽이 양수이기 때문에 y값 반전
    float rot = Math::Atan2(-mVelocity.y, mVelocity.x);
    mOwner->SetRotation(rot);

    Vector2 pos = mOwner->GetPosition();
    //SDL의 y좌표계는 아래쪽 방향이 양의방향이기 때문에 y값은 빼준다
    pos.x += mVelocity.x * deltaTime;
    pos.y += mVelocity.y * deltaTime;
    mOwner->SetPosition(pos);

    mForces = Vector2(0.0f, 0.0f);
}