Pawn 클래스의 이동과 충돌을 언리얼에서 지원하는 Physics를 사용하지 않고 직접 구현해보았다. 해당 주제의 기능 구현에 중점을 두었으므로 애니메이션등 비주얼적인 요소는 다루지 않는다. 컨트롤러나 InputAction을 바인딩하는 부분 또한 다른 블로그를 참조하길 바란다. 이 게시글은 오일러 방법(Euler's Method)을 통해 Pawn의 위치를 직접 업데이트 하고 실제 물리엔진에서 사용하는 방법과는 차이는 있지만 나름 물리적인 연산을 통해 충돌을 구현하는 방법에 대해서 다루겠다.
우선 Force를 받아 이동과 다음 위치를 계산하는 기능을 갖고있는 Playable 클래스를 생성한다. 해당 클래스를 상속받아 입력 콜백 함수들을 재정의하여 사람, 자동차, 비행기 등 동적으로 움직이는 모든 물체를 구현할 수 있도록 하였다.
Playable.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "PlayableController.h"
#include "Playable.generated.h"
UCLASS()
class ASSIGNMENT_API APlayable : public APawn
{
GENERATED_BODY()
public:
APlayable();
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
protected:
virtual void BeginPlay() override;
public:
UPROPERTY(VisibleAnywhere, Category = "Character")
USceneComponent* SceneComponent;
UPROPERTY(VisibleAnywhere, Category = "Character")
UCapsuleComponent* CapsuleComponent;
UPROPERTY(VisibleAnywhere, Category = "Character")
USkeletalMeshComponent* SkeletalMeshComponent;
UPROPERTY(VisibleAnywhere, Category = "Camera")
USpringArmComponent* SpringArmComponent;
UPROPERTY(VisibleAnywhere, Category = "Camera")
UCameraComponent* CameraComponent;
UFUNCTION() virtual void Move(const FInputActionValue& Value) PURE_VIRTUAL(APlayable::Move, ;);
UFUNCTION() virtual void Look(const FInputActionValue& Value) PURE_VIRTUAL(APlayable::Look, ;);
UFUNCTION() virtual void PressSpace(const FInputActionValue& Value) PURE_VIRTUAL(APlayable::PressSpace, ;);
UFUNCTION() virtual void PressShift(const FInputActionValue& Value) PURE_VIRTUAL(APlayable::PressShift, ;);
void AddForce(FVector ExternalForce);
protected:
UPROPERTY(EditAnywhere, Category = "Physics") float MoveScalar;
UPROPERTY(EditAnywhere, Category = "Physics") float Mass;
UPROPERTY(EditAnywhere, Category = "Physics") float Drag;
UPROPERTY(EditAnywhere, Category = "Physics") float Gravity;
bool bIsGround;
bool bUseGravity;
private:
FVector Force;
FVector Velocity;
void InitConstant();
void AddGravity();
// NOTE: Semi-implicit Euler integration
// https://en.wikipedia.org/wiki/Semi-implicit_Euler_method 참조
void Integration(float DeltaTime);
void HandleCollision(float DeltaTime);
void UpdatePosition(float DeltaTime);
};
컴포넌트들을 제외하고 사용된 변수에 대한 설명은 다음과 같다.
MoveScalar | 물체의 이동 방향에 곱해질 Scalar |
Mass | 물체의 질량 (현재는 사용되지 않는다.) |
Drag | 마찰, 공기 저항 등 항력을 간소화화여 Damping으로 적용함. (0 ~ 1) |
Gravity | 중력 가속도 |
bIsGround | 물체가 착지 중인지(Jump가 가능한지) |
bUseGravity | 중력을 적용중인지 |
Force | 현재 물체에 적용될 힘 |
Velocity | 현재 물체에 적용될 속도 |
우선 충돌에서 Impulse까지는 다루지 않으므로 Mass의 영향이 없지만 물리 기반에서 Mass가 빠지면 섭하니까 넣어줬다.
Tick에서 AddGravity, Integration, HandleCollision, UpdatePosition 네가지 함수를 순차적으로 실행시켜 최종적으로 물체를 이동시킨다. 필자가 사용한 위치를 구하는 방법은 Semi-implicit Euler integration 을 참조하였으며, Velocity를 먼저 업데이트하여 다음 위치를 예측하고 해당 위치에 충돌이 발생하면 Velocity의 방향을 수정하며 업데이트 된 Velocity로 위치를 업데이트 한다. 참조에 따르면 위치를 업데이트하고 속도를 업데이트하는 Explicit Euler 방법보다 더 안정적이며 마찬가지로 가성비가 좋아서 상용게임엔진에서 자주 쓰이는 방법이라고 한다.
void APlayable::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
AddGravity();
Integration(DeltaTime);
HandleCollision(DeltaTime);
UpdatePosition(DeltaTime);
}
우선 Add Gravity는 물체가 중력이 적용중이고 바닥에 착지중이 아닐때 물체에 지속적으로 중력을 적용시킨다.
void APlayable::AddGravity()
{
if (!bUseGravity || bIsGround)return;
FVector GravityForce = FVector(0.0f, 0.0f, -Mass * Gravity);
AddForce(GravityForce);
}
Itergration 함수는 현재 물체에 적용될 힘으로부터 가속도를 구하여 속도를 계산한다. 여기서 입력된 Drag 수치(0~1)에 따라 Damping을 추가하는데 Damping 적용 방법은 링크를 참조하여 서로 다른 컴퓨팅 환경에서도 동일한 Damping이 적용되도록 하였다.
void APlayable::Integration(float DeltaTime)
{
// 가속도 계산 (F = ma)
FVector Acceleration = Force * (1 / Mass);
Velocity += Acceleration * DeltaTime;
// 항력 추가 https://code.google.com/archive/p/bullet/issues/74 참조
Velocity *= FMath::Pow(1 - Drag, DeltaTime);
// 작은 속도는 0으로 설정
if (Velocity.SizeSquared() < 0.1f)
{
Velocity = FVector::ZeroVector;
}
// 속도 계산 후 Force 초기화
Force = FVector::ZeroVector;
}
UpdatePosition 에서 업데이트된 속도로 위치를 계산한다.
void APlayable::UpdatePosition(float DeltaTime)
{
if (Velocity.IsNearlyZero()) return;
AddActorWorldOffset(Velocity * DeltaTime);
}
여기까지 구현이 되었으면 입력 이벤트에서 입력 키에 따른 적절한 방향에 AddForce를 해주면 물체가 움직이는 것을 볼 수 있다. 만약 Drag가 0이라면 해당 물체는 힘이 주어진 방향으로 등가속도 운동을 하게 되고 Drag를 조절하여 물체의 Damping을 추가할 수 있다.
'Unreal 5 > Study' 카테고리의 다른 글
1. [Unreal 5 / C++] 2D 절차적 맵 생성 알고리즘 (미로 생성 알고리즘) (0) | 2025.02.21 |
---|---|
[Unreal 5 / C++] 3. Pawn 클래스로 3D 캐릭터 만들기 - 이동 및 비행체 (0) | 2025.01.31 |
[Unreal 5 / C++] 2. Pawn 클래스로 3D 캐릭터 만들기 - Collision (1) | 2025.01.31 |
[Unreal 5/C++] 회전 발판과 움직이는 장애물 퍼즐 스테이지 (0) | 2025.01.22 |
[Unreal 5] 언리얼 엔진 C++ 개발 환경 설정과 기본 구조 익히기 (0) | 2025.01.20 |