Priv's Blog

움직임(물리)을 구현하자 본문

Dev. Study Note/Unity

움직임(물리)을 구현하자

Priv 2024. 10. 15. 23:28


 

 

1. 개요

게임을 구현할 때 사물을 움직이게 하는 것은 가장 기본적이면서도 중요한 부분입니다.

일반적으로 자연스러운 움직임을 구현하고 싶다면, 물리 역학 계산을 거쳐서 사물에 '힘을 가해' 움직입니다.

경우에 따라서 Transform.translate( ) 메서드처럼 처음부터 그냥 좌표 값 자체를 바꿔버리는 방법도 있지만, 이는 물리적인 영향을 무시하기 때문에 자연스러움(현실적)과는 거리가 멉니다.

Unity 엔진에서는 물리를 구현하기 위해 Rigidbody 컴포넌트를 사용합니다.

이 Rigidbody를 이용하여 사물에 힘을 가하고 그 힘으로 물체를 움직이고 싶을 때는 일반적으로 AddForce( ) 메서드를 사용하죠.

이 AddForce( ) 메서드는 가하는 힘(Force)과 그 힘을 가하는 방식을 매개변수로 받으며, 힘을 가하는 방식에 따라서 결과가 달라지기도 합니다.

여기서 주의할 점은 결국 사물을 움직이게 만드는 것은 '힘'이라는 점입니다.

즉, 우리는 코드를 짜서 사물을 움직이게 만들고 싶다면, 이 '힘'에 대한 것을 제공해야 합니다.

이는 다시 말해 사물의 움직이는 속도, 현재 속도 등의 정보는 이 힘을 가해서 만들어진, 우리가 되돌려 받는 '결과물'이라는 겁니다.

여태껏 이 개념을 놓친 채 코드를 짜다 보니 사물이 움직이는 속도를 가공할 생각에 집착하게 되었고, 이 때문에 아주 단순하고 기본적인 기능 구현에서도 반복적인 코드 수정과 검색이 이루어지면서 오랜 시간을 잡아먹는 문제가 종종 발생하기도 했습니다.

 


 

2. 어떤 힘을 원하는가?

전철을 움직여야 합니다.

이 전철은 7칸으로 되어 있으며, 맨 앞에 엔진이 있죠.
(고증은 그냥 가볍게 무시합시다)

각 칸의 무게는 무려 30t, 총합이 무려 210t으로 정말 어마어마한 무게입니다.

가속도는 15km/h^2이며, 현재 정차한 상태이므로 현재 속도는 당연히 0km/h입니다.

최고 속도는 그다지 높지 않습니다. 80km/h 정도입니다.

이 전철을 움직여야 합니다.

Rigidbody를 사용해서, 가급적 현실적으로요.
(최적화도 그냥 가볍게 무시.... 해도 되겠죠?!)

 

2.1. 속도, 속도를 보자...

이전까지 가지고 있었던 잘못된 접근법으로 한 번 생각해 보겠습니다.

먼저 현재 속도, 가속도, 최고 속도에 대한 변수가 필요하고, 진행 방향을 알아야 움직이게 할 수 있으니 Transform 컴포넌트를 참조해야 합니다.

그 외 여러 가지 잡다한 것들도 함께 선언해 보죠.

[Header("Vehicle Component")]
[SerializeField] private GameControlTypeManager.TrainType trainType;
[SerializeField] private GameControlTypeManager.TrainStatus trainStatus;
[SerializeField] private GameObject engineCar;
[SerializeField] private List<GameObject> jointCars;

[Space(25f)]

[Header("Vehicle Setting")]
[SerializeField] private float vehicleCurrentSpeed;     // n km/h
[SerializeField] private float vehicleMaxSpeed;         // 80 km/h
[SerializeField] private float vehicleAcceleration;     // 15 km/h^2

private Transform vehicleTransform;
private Rigidbody vehicleRigidbody;

속도를 올릴 겁니다.

열차가 움직이면 속도가 올라가니까요.

그런데 여기서 문제가 생겼네요. Unity 엔진은 km/h 단위를 쓰지 않고 m/s 단위를 사용합니다.

괜히 귀찮게 3.6f를 나눠주게 생겼습니다.

뭐... 어쩔 수 없으니 그냥 나눠줍니다.

현재 속도는 실시간으로 바뀌니까 제외합니다.

private void Init() {
    this.vehicleMaxSpeed /= 3.6f;       // km/h -> m/s  
    this.vehicleAcceleration /= 3.6f    // km/h^2 -> m/s^2
    this.vehicleTransform = this.engineCar.transform;
    this.vehicleRigidbody = this.engineCar.GetComponent<Rigidbody>();
}

private void Awake() {
    Init();
}

이제 진짜 속도를 올릴 겁니다.

현재 속도와 가속도를 알고 있고, 현재 속도는 최고 속도를 넘지 말아야 하며, 가속도만큼 속도가 올라갈 겁니다.

음... 더해버립시다. 프레임 단위로 계산하는 것이니 Time.deltaTime도 잊지 말고요.

private void FixedUpdate() {
    VehicleEngineControl();
}

private void VehicleEngineControl() {
    if (this.vehicleCurrentSpeed < this.vehicleMaxSpeed) {
        this.vehicleCurrentSpeed += this.vehicleAcceleration * Time.deltaTime;
    }
}

속도가 올라갑니다!

현재 속도에 가속도를 더했으니, 1 FPS에서 값을 한 번 더하면 (현재 속도 + 가속도)가 되고, 2 FPS에서 값을 한 번 더 더하면 (현재 속도 + 가속도 + 가속도)가 되겠군요.

최고 속도를 넘지 않는 조건이 참이라면 현재 속도는 계속 올라갈 것입니다.

근데 여기서 현재 속도는 그냥 float 변수 값입니다.

아무거도 할 수 없는 열차는 지금 꼼짝도 안 합니다. 로빈과 함께 팝콘이나 뜯고 있다고요.

현실적인 움직임을 원해서 Rigidbody를 사용하고 싶었습니다.

이번에도 그냥 넣어봅니다.

private void FixedUpdate() {
    VehicleEngineControl();
}

private void VehicleEngineControl() {
    if (this.vehicleCurrentSpeed < this.vehicleMaxSpeed) {
        this.vehicleCurrentSpeed += this.vehicleAcceleration * Time.deltaTime;
    }
    
    this.vehicleRigidbody.AddForce(this.vehicleTransform.forward * (this.vehicleCurrentSpeed / 3.6f), ForceMode.Force);
}

맙소사, 결과가 절망적입니다.

꼼짝도 안 해요. 이제 열차는 로빈의 캐러멜 팝콘을 뺏어먹을 수 없단 말입니다.

열차가 너무 무거워서 그런 것인지 궁금해 가속도와 최고 속도 값을 몇 만 단위로 올려봐도 결과는 달라지지 않습니다.

코드를 노려보던 도중 애초에 식이 뭔가 이상하다는 것을 깨닫고 로빈의 팝콘을 훔치기 시작합니다.

 

2.2. 힘(F = ma)을 원한다.

힘이 필요합니다.

그것도 강력하고 묵직한 힘이. 속도는 그저 나약한 겁쟁이들의 것, 묵직한 한 방의 힘을 찬양하라.

아인슈타인 선생님을 떠올리며 망상에 잠깁니다. 누구는 KB 단위 용량으로 배관공 게임도 뚝딱 완성해 대박을 쳤는데, 지금은 고작 열차 하나 못 움직이고 있습니다.

AddForce( ) 메서드입니다.

힘을, 더하죠.

힘이 필요한데 여태까지 속도에만 집착했습니다.

그까짓 float 변수 하나가 뭐라고!

다시 접근하기 위해 공식을 꺼냅니다. F = ma, 아름답죠.

힘은 일반적으로 뉴턴(N) 단위를 사용해 표현합니다. 위의 공식에서 F(orce)에 해당합니다.

힘은 질량에 비례합니다. M(agnitude)입니다.

질량만 있다고 다 되는 게 아닙니다. 가속도도 필요하죠. A(cceleration)입니다.

즉, 사물이 무거워지면, 그만큼 가속도도 올라가야 합니다. 그 두 가지 개념을 곱했을 때 힘이 나옵니다.

그 힘이 어떠한 방향으로 사물에 가해졌을 때, 사물은 움직입니다.
(작용이니 반작용이니 그런 건 넘어갑시다)

사물이 움직이면, 속도가 생기죠. 그 속도는 km/h 단위나 m/s 단위 등으로 표현합니다.
(마일? 아니다 이 악마야)

가속도도 결국은 속도입니다. 그리고 Unity는 힘세고 강한 기운이 들기에 m/s 단위를 사용합니다.

하지만 우리는 km/h 단위가 더 익숙하므로, '진짜' 가속도를 계산하고 싶으면 가속도 값을 3.6f로 나눠야 합니다.

질량은 kg입니다. Unity에서도 다행히 kg이죠.

즉, (힘 = 열차의 질량 * 가속도)라는 식이 탄생합니다.

그리고 AddForce( ) 메서드는 매개변수로 힘을 원합니다. 아, 힘을 가하는 방향도 원합니다.

이걸 정리하면, AddForce(열차의 질량 * 가속도 * 힘을 가할 방향, 힘을 가하는 방식)이라는 결론이 도출됩니다.

쉽네요.

그럼 잠깐, 열차의 현재 속도를 알아야 최고 속도를 넘기지 않을 텐데 그건 어떻게 구할까요?

뭐, 나중에 생각합니다.

    private void FixedUpdate() {
        VehicleEngineControl();
    }

    private void VehicleEngineControl() {      
        if (this.vehicleCurrentSpeed < this.vehicleMaxSpeed) {
            this.vehicleRigidbody.AddForce(this.vehicleTransform.forward * (this.vehicleAcceleration * this.vehicleRigidbody.mass * this.jointCars.Count), ForceMode.Force);    // Acceleration(m/s) * deltaTime * Mass 
        }
    }
}

...일단은 짭니다.

어쨌거나 현재 속도가 최고 속도보다 낮아야 힘을 가하는 건 지금도 짤 수 있으니까요.

질량을 구할 때 전체 질량으로 계산하고 싶었으니, 모든 칸의 질량을 다 가져와서 210t으로 적용합니다.

이제 가긴 가네요!

열차가 로빈의 팝콘을 다시 노리기 시작합니다.

이제 속도를 살펴보죠.

현재 속도는 사실 간단하게 참조로 구할 수 있습니다.

Rigidbody에는 velocity(속도)라는 이름의 프로퍼티가 있습니다. getter, setter 모두 존재하죠.

이 velocity는 말 그대로 해당 Rigidbody에 적용되고 있는 속도 값을 구해서 던져줍니다.

그런데 안타깝게도, 이 velocity 프로퍼티는 Vector3 타입입니다. Float이 아니네요!

...라는 안타까움을 딱하게 여긴 것인지 magnitude라는 프로퍼티도 존재합니다.

velocity의 magnitude, 즉 '속도의 크기'를 던져주는 프로퍼티입니다. 이건 float 타입이며 getter만 있습니다.

그럼 이제 다 구했습니다. 그냥 대입해 줍니다.

    private void VehicleEngineControl() {
        this.vehicleCurrentSpeed = this.vehicleRigidbody.velocity.magnitude;    // Current Speed (velocity Magnitude)
        
        if (this.vehicleCurrentSpeed < this.vehicleMaxSpeed) {
            this.vehicleRigidbody.AddForce(this.vehicleTransform.forward * (this.vehicleAcceleration * this.vehicleRigidbody.mass * this.jointCars.Count), ForceMode.Force);    // Acceleration(m/s) * deltaTime * Mass 
        }
    }

잘 가는 거 같은데... 어째 열차가 무슨 새총처럼 통통 튀는 느낌입니다.

자동차 액셀도 저렇게 밟다가 면허 뺏길 거 같습니다. 조금 더 고민해 봅시다.

일단 처음 구현하고자 했던 망상을 다시 떠올려봅니다.

열차는 속도를 자연스럽게(물리 연산 기반으로) 올려서 주행하고, 최고 속도를 넘길 수 없으며, 최고 속도에 다다르면 그 속도로 쭉 주행한다.

가속도 변수의 값은 정지, 역주행, 가속에 활용한다.

최고 속도에 다다르면 그 속도를 쭉 유지해야 합니다.

Rigidbody의 velocity가 현재 속도를 구할 수 있다는 건 이미 파악했습니다. setter가 있다는 것도요.

이는 즉, velocity에 값을 대입할 수 있다는 뜻입니다.

주행 방향은 어차피 열차가 이미 주행 중인 방향을 그대고 쓰겠죠.

최고 속도 값도 알고 있습니다.

주행 중인 방향은 Rigidbody의 velocity의 타입이 Vector3니까 간단히 구할 수 있습니다.

벡터에는 방향성이 있으니까요.

어느 방향으로 주행 중인지, 얼마나 큰 힘으로 주행 중인지를 같이 구할 수 있다는 것입니다.

거기에 최고 속도를 곱해서... 다시 대입하면...?

    private void VehicleEngineControl() {
        this.vehicleCurrentSpeed = this.vehicleRigidbody.velocity.magnitude;    // Current Speed (velocity Magnitude)
        
        if (this.vehicleCurrentSpeed < this.vehicleMaxSpeed) {
            this.vehicleRigidbody.AddForce(this.vehicleTransform.forward * (this.vehicleAcceleration * this.vehicleRigidbody.mass * this.jointCars.Count), ForceMode.Force);    // Acceleration(m/s) * deltaTime * Mass 
        }
        else {
            this.vehicleRigidbody.velocity = this.vehicleRigidbody.velocity.normalized * this.vehicleMaxSpeed;
        }
    }

 



잘 되네요!

음, 근데 이거 현실적으로 구현한 건 좋은데 쓸데없이 자원을 낭비하는 거 같다는 생각도 드네요.

아무튼 결국은 힘입니다. 8 war!

 


 

3. 결론

움직임에서 속도는 결과물이다. 힘을 알아야 움직인다.

힘을 가하면 속도가 결과물로 나오는 것.

F = ma

벡터는 방향과 힘의 크기 모두를 가진다.

접근할 때 좀 더 신중하게, 막히면 좀 더 본질적인 걸로 하나씩 '깊게' 파고들자.

힘세고 강한 아침.

 


 


수고하셨습니다!


Comments