이 초빙 포스트는 Pathos Interactive의 설립자 및 소프트웨어 아키텍터인 Christoffer Andersson씨가 작성한 것 입니다. Pathos Interactive는 스웨덴의 묄른달에 본사가 있는 게임 스튜디오 입니다. Pathos Interactive는 2015년도에 설립되었고 현재 Bannermen 을 개발중에 있습니다.
게임 소개
Bannermen는 고전적인 RTS 장르를 새롭게 하는 목표를 가진 RTS 게임입니다. Bannermen에는 기지 구축, 자원관리와 적 군대들과 전투하는 주요 미션으로 이루어지는 단일 플레이어 캠페인과 몇 개의 멀티플레이어가 포함될 것입니다. 맵과 임무는 길이와 모양이 다양합니다. 이 게임에는 플레이어들이 환경과 상호작용 할 수 있는 것을 허용하는 다이나믹 환경이라고 하는 것도 포함되어 있습니다. 한 가지 예를 들어보면 플레이어들은 지도상에서 종교적 명소를 제어하여 다양한 자연의 힘을 제어 할 수 있습니다. 레벨 환경에 따라 다양한 동적 환경을 사용할 수 있습니다. 목표는 플레이어가 거침을 느끼고 조작하면서 보다 훌륭한 전략을 사용하여 더 재미있는 전투를 하는 것입니다. 이 작업에 대하여 관심이 있으시면 사전 알파 데모를 확인해보세요.
Lockstep 선택하기
Bannermen의 가장 큰 포인트 중 하나는 최신 네트워킹 솔루션을 지원하는 것이었습니다. 요즘 대부분의 플레이어는 호스팅, 포트 포워딩, 네트워크 대역폭의 걱정없이 진행되는 경험을 기대합니다. Photon의 네트워킹 모델은 이러한 요구 사항에 가장 적합했습니다. 네트워크 대역폭을 낮게 유지하면서 더 많은 수의 유닛을 지원하기를 원했기 때문에 Bannermen에 락스텝 모델을 구현하기로 결정했습니다. "락스텝"에 익숙하지 않다면, 기본적으로 모든 프레임이 모든 클라이언트에서 동기화된다는 것을 의미한다고 이해하면 됩니다. 그러면 네트워크 대역폭 사용을 최소화하면서 플레이어 입력만 보내는 것으로도 충분합니다.
Bannermen은 언리얼 엔진 4에서 제작되었습니다. 언리얼 엔진은 엄청난 엔진입니다. 유일한 문제는 Unity와는 달리 현재 락스텝에 대한 지원이 없다는 것입니다. 우리는 C ++ SDK로 lockstep을 직접 구현했습니다.
동기화 유지하기
게임에서 락스텝을 구현하면 게임의 전체 아키텍처에 영향을 미치므로 설계 프로세스의 모든 단계에서 고려해야 합니다. 모든 프레임이 모든 클라이언트에서 동기화 되어야 하므로 입력을 동기화 해야 하며 각 프레임은 결정적이어야 합니다. 게임 로직과 관련된 모든 것은 결정론적이어야 합니다. 클라이언트가 특정 프레임을 입력받는 경우 상태는 나중에 임의의 프레임 수 만큼 동기화 된 상태로 유지되어야 합니다. 올바르게 실행되지 않으면 큰 문제가 발생할 수 있는데, 이는 동기화가 되지 않았다면 게임이 동기화 되거나 종료 되어야 합니다. 그러나 올바르게 구현된다면 이런 비동기 상태는 결코 발생하지 않습니다.
불행히도 비동기화는 여러 방식으로 발생할 수 있으므로 주의해야 합니다. 예 : 부동 소수점 또는 더블을 사용하면 플랫폼이나 컴퓨터에 따라 결정적이지 않을 수 있습니다. 멀티 스레딩을 사용하면 더 어려워집니다. 다중 스레드 코드는 항상 모든 클라이언트에서 동일한 상태가 되어야 합니다.
입력 평가
Bannermen에서 한 가지 문제는 두 명의 작업자가 동일한 프레임에서 자원을 입력하려고 할 때였습니다. 자원에 한 명의 작업자만 남을 수 있는 공간이 있다면 모든 클라이언트에서 결정적으로 한 명의 작업자를 선택해야 했습니다. 작은 문제처럼 들릴 수도 있지만 매우 드물게 결정적이지 않은 경우 게임에서 동기화가 해제 될 때 문제가 발생합니다. 모든 클라이언트에서 항상 동일한 순서로 입력을 평가하는 데 도움이 되지만 부족할 수 있습니다. 또한, 우리는 어떤 플레이어에게도 우선 순위나 불평등한 이점을 주고 싶지 않습니다. 이번에는 "자원 입력” 작업을 두 단계로 나누었습니다. 먼저 모든 작업자가 후보로 등록 할 수 있도록 하십시오. 그런 다음 시간의 경과에 따라 공정하게 나타날 수 있는 작업자의 고유한 네트워크 ID를 기반으로 한 결정론적 무작위 알고리즘을 사용합니다. 그러나 이것은 실제로 플레이어가 인지할 수 있는 것은 아닙니다.
부동 소수점 수학
Bannermen에서 부동 소수점 문제를 피하기 위해 고정 소수점 수학을 위한 자체 수학 라이브러리를 작성했습니다. 고정 소수점 수학을 사용하여 고정 소수점 연산을 기반으로 하는 자체 탐색 메쉬, 경로 찾기, 충돌 시스템등을 구현했습니다. 언리얼 엔진의 메인 스레드와 별도로 시뮬레이션 스레드를 실행합니다. 이 스레드는 모든 실제 게임 논리를 시뮬레이션 합니다. 그런 다음 모든 프레임의 게임 상태를 언리얼 엔진의 메인 스레드와 동기화합니다. 이를 통해 우리는 게임 로직이나 적어도 매우 가벼운 로직없이 언리얼 엔진을 실행하는 것과 거의 같은 훌륭한 FPS를 유지하면서 많은 그래픽 효과를 사용할 수 있습니다.
언리얼 엔진에서 락스텝 지원을 구현하려면 기존의 고정 소수점 수학 라이브러리를 구현하거나 사용하는 것이 좋습니다. 정밀도를 가지고 놀 수도 있고 필요에 따라 다른 정도의 정밀도를 지원 할 수도 있습니다. 다음은 시작하기위한 매우 기본적인 구현입니다:
#pragma once
#include "FixedPoint.generated.h"
USTRUCT()
struct FFixedPoint
{
GENERATED_USTRUCT_BODY()
FFixedPoint();
FFixedPoint(uint32 inValue);
FFixedPoint(const FFixedPoint &otherFixedPoint);
FFixedPoint& operator=(int32 intValue);
FFixedPoint& operator=(const FFixedPoint &otherFixedPoint);
FFixedPoint operator+(const FFixedPoint &otherFixedPoint) const;
FFixedPoint operator-(const FFixedPoint &otherFixedPoint) const;
FFixedPoint operator*(const FFixedPoint &otherFixedPoint) const;
FFixedPoint operator/(const FFixedPoint &otherFixedPoint) const;
static FFixedPoint createFromInt(int32 value);
static FFixedPoint createFromFloat(float value);
// use UPROPERTY so unreal engine can save the value!
UPROPERTY()
uint32 value;
static const int32 FractionBits = 8;
static const int32 FirstIntegerBitSet = 1 << FractionBits;
};
Copy
#include "YourHeader.h"
#include "FixedPoint.h"
FFixedPoint::FFixedPoint()
: value { 0 }
{
}
FFixedPoint::FFixedPoint(uint32 inValue)
: value { inValue }
{
}
FFixedPoint::FFixedPoint(const FFixedPoint &otherFixedPoint)
{
value = otherFixedPoint.value;
}
FFixedPoint& FFixedPoint::operator=(const FFixedPoint &otherFixedPoint)
{
value = otherFixedPoint.value;
return *this;
}
FFixedPoint& FFixedPoint::operator=(int32 intValue)
{
value = intValue << FractionBits;
return *this;
}
FFixedPoint FFixedPoint::operator+(const FFixedPoint &otherFixedPoint) const
{
int32 result = value + otherFixedPoint.value;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::operator-(const FFixedPoint &otherFixedPoint) const
{
int32 result = value - otherFixedPoint.value;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::operator*(const FFixedPoint &otherFixedPoint) const
{
int32 result = value * otherFixedPoint.value;
// rounding of last bit, can be removed for performance
result = result + ((result & 1 << (FractionBits - 1)) << 1);
result = result >> FractionBits;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::operator/(const FFixedPoint &otherFixedPoint) const
{
int32 result = (value << FractionBits) / otherFixedPoint.value;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::createFromInt(int32 value)
{
int32 newValue = value << FractionBits;
return FFixedPoint(newValue);
}
FFixedPoint FFixedPoint::createFromFloat(float value)
{
int32 newValue = value * FirstIntegerBitSet;
return FFixedPoint(newValue);
}
저는 게임 로직에서 float 메소드를 사용하는 것을 권장하지 않지만, 저장 될 수 있는 편집기에서 값을 초기화하는 것이 편리 할 수 있습니다. 더 많은 연산자와 수학 함수를 구현하고 청사진에 쉽게 노출시킬 수도 있습니다 (언리얼 엔진의 비주얼 스크립팅).
마지막으로 언급해야 할 것은 디버깅 할 때 이해할 수 있는 형식으로 고정 소수점 값을 볼 수 있도록 하는 방법입니다. "natvis"파일을 사용하여 값을 float 또는 double로 시각화하여 더 쉽게 사용할 수 있도록하는 것이 좋습니다. 다음은 "natvis"파일이 있거나 없는 예제입니다.
Visual Studio 2017 에서는 “natvis” 파일의 위치는 설정된 디렉토리에 따라 다르지만 “C:\Users\\Documents\Visual Studio 2017\Visualizers” 과 유사합니다.
마지막으로 언리얼 엔진의 Bannermen을 위한 lockstep 구현 작업은 많았습니다. 그러나 이제 작업이 완료되면 Photon의 모든 기능을 사용할 수 있습니다. 또한 언리얼 엔진에서 순전히 실행했던 초기 프로토 타입과 비교하여 성능이 크게 향상되었습니다. 네트워크 대역폭은 현저하게 줄어들었으며, 시뮬레이션 틱이 언리얼 엔진의 메인 스레드와 병렬로 실행되기 때문에 더 많은 것을 할 수 있는 자유로운 리소스가 있습니다. 전반적으로 Bannermen과 언리얼 엔진에 완벽하게 적합한 락스텝 모델과 결합된 Photon의 통합을 발견했습니다.