명령패턴을 활용한 이동 구현

2020. 3. 29. 20:30개발일지

MoveCommand 클래스는 MoveComponent 클래스에 내포되어 있으며, 상속받은 자식클래스들은 가상함수 Move() 함수를 구현하도록 강제한다. 명령을 수행하는 함수는 이 클래스를 상속받고 구현한다.

class MoveCommand
{
public:
	MoveCommand(AMoveComponent* _Owner = nullptr)
		: m_MoveDistance(50.f), m_JumpPower(500.f), m_MoveTime(0.1f), OwnerComponent(_Owner), m_CurDistance(0.f), m_CurRadian(0.f){}
	virtual ~MoveCommand() {}
	virtual AVECTOR Move() = 0; //상속받은 자식들이 이 Move()함수를 구현하도록 강제한다.
public:
	AMoveComponent* OwnerComponent;

	float m_MoveDistance;
	float m_JumpPower;

	float m_CurRadian;
	float m_CurDistance;
	float m_MoveTime;
};

MoveCommand를 상속받은 클래스들.

class STOP : public MoveCommand 
{ 
public:
	STOP(AMoveComponent* _Owner) : MoveCommand(_Owner) {}
	virtual AVECTOR Move();
};
class RIGHT : public MoveCommand 
{
public:
	RIGHT(AMoveComponent* _Owner) : MoveCommand(_Owner) {}
	virtual AVECTOR Move();
};
class LEFT : public MoveCommand
{
	public:
		LEFT(AMoveComponent* _Owner) : MoveCommand(_Owner) {}
		virtual AVECTOR Move();
};
class UP : public MoveCommand
{
public:
	UP(AMoveComponent* _Owner) : MoveCommand(_Owner) {}
	virtual AVECTOR Move();
};
class DOWN : public MoveCommand
{
public:
	DOWN(AMoveComponent* _Owner) : MoveCommand(_Owner) {}
	virtual AVECTOR Move();
};

MoveComponent 클래스는 하나의 현재상태만을 가지도록 MoveCommand* m_MoveState 포인터를 멤버로 가진다.

class AMoveComponent : public AComponentBase
{	
public:
	AMoveComponent(AGAMEACTOR* _Owner);
	virtual ~AMoveComponent();

private:
	class MoveCommand
	{
	//////
	};

protected:
	MoveCommand* m_MoveState; //현재 상태
	float m_ElipsedTime;
	
	AVECTOR m_StartPos;

	std::unordered_map<std::string, MoveCommand*> MoveMap; // 상태들을 저장한 해쉬맵

public:
	AVECTOR MoveUpdate();
	void Move(std::string _Direction);
	float GetMoveDistance() { return m_MoveState->m_MoveDistance; }
	bool OnMoving() { return m_MoveState != MoveMap["S"]; }
};

MoveComponent에서 각각의 상태를 표현하는 MoveCommand 하위 객체들을 생성하고 그 포인터들을 맵에 저장한다.

AMoveComponent::AMoveComponent(AGAMEACTOR* _Owner)  : m_ElipsedTime(0.f)
{
	m_OwnerActor = _Owner;

	MoveMap["L"] = new LEFT(this);
	MoveMap["R"] = new RIGHT(this);
	MoveMap["U"] = new UP(this);
	MoveMap["D"] = new DOWN(this);
	MoveMap["S"] = new STOP(this);

	m_MoveState = MoveMap["S"];
}

명령 패턴으로 MoveUpdate() 에 매 업데이트마다 현재 상태를 확인하는 if() ~ else if() 문들을 작성하지 않아도 되도록 되었다. 새로운 명령 객체를 생성할땐 MoveCommand를 상속받는 객체만 구현하면 되고, 호출하는 부분은 코드의 수정이 필요 없게 되었다.

AVECTOR AMoveComponent::MoveUpdate()
{
	AVECTOR DeltaPos = m_MoveState->Move();
	m_ElipsedTime += AGAMETIME::DeltaTime();

	return DeltaPos;
}

 

이동 로직

거리 = 시간 * 속도 공식을 이용하여 한 프레임동안 이동할 거리를 구해 이동시켰다.

이동시 점프를 표현하기 위해 MoveTime 동안 sin 함수를 따라 2PI 만큼 이동시켰다.

AVECTOR AMoveComponent::LEFT::Move()
{
	//MoveTime 시간동안 MoveDistance를 이동한다
	//거리 = 시간 * 속도
	//한프레임에 이동할 거리 = 한 프레임 시간 * 속도
	//속도 = 거리 / 시간
	float MoveX = -AGAMETIME::DeltaTime() * (m_MoveDistance / m_MoveTime);
	m_CurDistance += abs(MoveX);
	if (m_CurDistance <= m_MoveDistance)
		OwnerComponent->m_OwnerActor->MovePosX(MoveX);

	//MoveTime시간동안 sin곡선을 따라 2PI 이동.
	float Radian = 2 * AMATH::PI * (OwnerComponent->m_ElipsedTime / m_MoveTime);
	float JumpY = m_JumpPower * sinf(Radian) * AGAMETIME::DeltaTime();
	OwnerComponent->m_OwnerActor->MovePosY(JumpY);
	

	if (OwnerComponent->m_ElipsedTime >= m_MoveTime)
	{
		OwnerComponent->m_MoveState = OwnerComponent->MoveMap["S"];
		m_CurDistance = 0.f;
		//X 오차 보정
		OwnerComponent->m_OwnerActor->SetPosX(OwnerComponent->m_StartPos.X - m_MoveDistance);
		//Y 오차 보정
		OwnerComponent->m_OwnerActor->SetPosY(OwnerComponent->m_StartPos.Y);
		return{ AVECTOR(0.f, 0.f) };
	}

	return { AVECTOR(MoveX, 0.f) };
}

 

오차 누적 없애기.

고정된 값으로 한번에 이동하는 것이 아닌 프레임별로 이동을 누적해가기 때문에 이동의 마지막 프레임에서 오차가 생길 수 있다. 이동 횟수가 많아질수록 오차가 누적되어 점점 위치에서 벗어나게 될 수 있기때문에 마지막 프레임에서 원래 목표로 하던 위치로 이동시켜 주어 오차를 없앴다.

if (OwnerComponent->m_ElipsedTime >= m_MoveTime)
	{
		OwnerComponent->m_MoveState = OwnerComponent->MoveMap["S"];
		m_CurDistance = 0.f;
		//X 오차 보정
		OwnerComponent->m_OwnerActor->SetPosX(OwnerComponent->m_StartPos.X - m_MoveDistance);
		//Y 오차 보정
		OwnerComponent->m_OwnerActor->SetPosY(OwnerComponent->m_StartPos.Y);
		return{ AVECTOR(0.f, 0.f) };
	}

키를 입력받고 호출하는 부분. 키를 입력받았을때 이미 이동중이라면 키 입력을 무시한다.

 

void APLAYER::Input()
{
	if (MoveComponent->OnMoving())
		return;
	
	if (AGAMEINPUT::DOWN(L"L"))
	{
		MoveComponent->Move("L");
	}
	if (AGAMEINPUT::DOWN(L"R"))
	{
		MoveComponent->Move("R");
	}
	if (AGAMEINPUT::DOWN(L"U"))
	{
		MoveComponent->Move("U");
	}
	if (AGAMEINPUT::DOWN(L"D"))
	{
		MoveComponent->Move("D");
	}
}

 

MoveComponent와 Camera의 디커플링

Move 컴포넌트가 카메라의 존재를 몰라도 카메라를 이동시킬 수 있도록 DeltaPosition을 리턴받아 카메라를 이동시킨다.

void APLAYER::Update()
{
	Input();
	m_DeltaPos = MoveComponent->MoveUpdate();	
	PlayerCamera->CameraMove(m_DeltaPos);
}

'개발일지' 카테고리의 다른 글

랜덤 몬스터 스폰  (0) 2020.04.04
랜덤맵 제작 3  (0) 2020.04.03
랜덤맵 제작 2  (0) 2020.04.03
랜덤맵 제작 1  (0) 2020.04.01
카메라 컴포넌트의 설계  (0) 2020.03.28