게임 아키텍처 / 디자인 질문-글로벌 인스턴스를 피하면서 효율적인 엔진 구축 (C ++ 게임)


28

게임 아키텍처에 대한 질문이 있습니다. 다른 구성 요소가 서로 통신하는 가장 좋은 방법은 무엇입니까?

이 질문이 이미 백만 번 요청 된 경우 정말 사과하지만 원하는 종류의 정보가있는 항목을 찾을 수 없습니다.

나는 처음부터 게임을 구축하려고 노력했고 (중요한 경우 C ++) 영감을 얻기위한 오픈 소스 게임 소프트웨어 (Super Maryo Chronicles, OpenTTD 및 기타)를 관찰했습니다. 이러한 많은 게임 디자인이 렌더 대기열, 엔티티 관리자, 비디오 관리자 등과 같은 곳에서 전역 인스턴스 및 / 또는 싱글 톤을 사용한다는 것을 알았습니다. 글로벌 인스턴스와 싱글 톤을 피하고 가능한 한 느슨하게 결합 된 엔진을 만들려고 노력하고 있지만 효과적인 디자인 경험이 부족한 장애물을 겪고 있습니다. (이 프로젝트의 동기 중 일부는이 문제를 해결하는 것입니다.)

GameCore다른 프로젝트에서 볼 수있는 전역 인스턴스와 유사한 멤버가 있는 하나의 기본 개체 (예 : 입력 관리자, 비디오 관리자, GameStage모든 엔터티 및 게임 플레이를 제어 하는 개체)가 있는 디자인을 만들었 습니다. 현재로드 된 단계 등). 문제는 모든 것이 GameCore객체에 집중되어 있기 때문에 다른 구성 요소가 서로 통신하기 쉬운 방법이 없다는 것입니다.

예를 들어, 게임의 구성 요소가 다른 구성 요소와 통신해야 할 때마다 (예 : 적의 오브젝트가 렌더 대기열에 자신을 추가하여 렌더 스테이지에 그려지기를 원할 때) Super Maryo Chronicles를 보면 글로벌 인스턴스.

저에게 게임 오브젝트가 관련 정보를 GameCore오브젝트로 다시 전달 하도록해야 GameCore오브젝트가 해당 정보를 필요한 시스템의 다른 컴포넌트 (예 : 위의 상황에서 각 적 오브젝트)로 해당 정보를 전달할 수 있습니다 렌더링 정보를 GameStage객체로 다시 전달하여 객체를 모두 수집하고 다시 전달하여 렌더링 GameCore을 위해 비디오 관리자에게 전달합니다. 이것은 정말로 끔찍한 디자인처럼 느껴지며, 이것에 대한 해결책을 생각하려고했습니다. 가능한 디자인에 대한 나의 생각 :

  1. 글로벌 인스턴스 (Super Maryo Chronicles, OpenTTD 등의 디자인)
  2. 갖는 GameCore모든 개체가 통신되는 중개인과 같은 객체 행위 (현재의 디자인은 상술 한)
  3. 구성 요소와 대화해야 할 다른 모든 구성 요소에 대한 포인터를 제공하십시오 (예 : Maryo 예에서 적군은 대화해야하는 비디오 객체에 대한 포인터를 갖습니다)
  4. 게임을 하위 시스템으로 분할-예를 들어 하위 시스템의 GameCore개체 간 통신을 처리 하는 개체에 관리자 개체가 있습니다.
  5. (다른 옵션? ....)

위의 옵션 4가 최상의 솔루션이라고 생각하지만 디자인에 문제가 있습니다. 아마도 내가 글로벌을 사용하는 것으로 보았던 디자인과 관련하여 생각했기 때문일 것입니다. 현재 디자인에 존재하는 것과 동일한 문제를 겪고 작은 규모로 각 하위 시스템에 복제하는 것 같습니다. 예를 들어, GameStage위에서 설명한 개체는 다소 시도했지만 GameCore개체가 여전히 프로세스에 관여합니다.

누구든지 여기에 디자인 조언을 제공 할 수 있습니까?

감사!


1
싱글 톤이 훌륭한 디자인이 아니라는 본능을 이해합니다. 내 경험에 의하면, 그들은 시스템에서 통신을 관리 할 수있는 간단한 방법 봤는데
모트 버틀러

4
모범 사례인지 알 수 없으므로 의견으로 추가하십시오. InputSystem, GraphicsSystem 등과 같은 하위 시스템으로 구성된 중앙 GameManager가 있습니다. 각 하위 시스템은 GameManager를 생성자의 매개 변수로 사용하여 클래스 개인 멤버에 대한 참조를 저장합니다. 그 시점에서 GameManager 참조를 통해 다른 시스템에 액세스하여 다른 시스템을 참조 할 수 있습니다.
Inisheer

이 질문은 게임 디자인이 아니라 코드에 관한 것이기 때문에 태그를 변경했습니다.
Klaim

이 스레드는 조금 오래되었지만 정확히 같은 문제가 있습니다. 나는 OGRE를 사용하고 최선의 방법을 사용하려고합니다. 제 생각에 옵션 # 4가 최선의 방법입니다. Advanced Ogre Framework와 같은 것을 만들었지 만 모듈 방식이 아닙니다. 키보드 적중과 마우스 움직임 만 얻는 하위 시스템 입력 처리가 필요하다고 생각합니다. 내가 알지 못하는 것은 하위 시스템간에 이러한 "통신"관리자를 어떻게 만들 수 있습니까?
Dominik2000

1
안녕하세요 @ Dominik2000, 이것은 포럼이 아닌 Q & A 사이트입니다. 질문이있는 경우 기존 질문에 대한 답변이 아니라 실제 질문을 게시해야합니다. 자세한 내용은 FAQ 를 참조하십시오.
Josh

답변:


19

게임에서 글로벌 데이터를 구성하기 위해 사용하는 것은 ServiceLocator 디자인 패턴입니다. Singleton 패턴과 비교하여이 패턴의 장점은 응용 프로그램 실행 중에 글로벌 데이터의 구현이 변경 될 수 있다는 것입니다. 또한 런타임 중에도 전역 객체를 변경할 수 있습니다. 또 다른 장점은 전역 객체의 초기화 순서를 관리하는 것이 더 쉽다는 것인데, 이는 특히 C ++에서 매우 중요합니다.

예 (C ++ 또는 Java로 쉽게 변환 할 수있는 C # 코드)

렌더링 렌더링을위한 일반적인 작업이있는 렌더링 백엔드 인터페이스가 있다고 가정 해 보겠습니다.

public interface IRenderBackend
{
    void Draw();
}

그리고 기본 렌더 백엔드 구현이 있습니다.

public class DefaultRenderBackend : IRenderBackend
{
    public void Draw()
    {
        //do default rendering stuff.
    }
}

일부 디자인에서는 렌더 백엔드에 전 세계적으로 액세스 할 수있는 것이 합법적입니다. 에서 싱글 패턴 각 수단 IRenderBackend의 구현은 전역 고유 인스턴스로서 구현되어야한다. 그러나 ServiceLocator 패턴을 사용할 때는이 작업이 필요하지 않습니다.

방법은 다음과 같습니다.

public class ServiceLocator<T>
{
    private static T currGlobalInstance;

    public static T Service
    {
        get { return currGlobalInstance; }
        set { currGlobalInstance = value; }
    }
}

전역 객체에 액세스하려면 먼저 객체를 초기화해야합니다.

//somewhere during program initialization
ServiceLocator<IRenderBackend>.Service = new DefaultRenderBackend();

//somewhere else in the code
IRenderBackend currentRenderBackend = ServiceLocator<IRenderBackend>.Service;

런타임 동안 구현이 어떻게 달라질 수 있는지 보여주기 위해 게임에 미니 게임이 있고 렌더링이 아이소 메트릭이고 IsometricRenderBackend 를 구현한다고 가정 해 보겠습니다 .

public class IsometricRenderBackend : IRenderBackend
{
    void draw()
    {
        //do rendering using an isometric view
    }
}

현재 상태에서 미니 게임 상태로 전환 할 때 서비스 로케이터가 제공하는 전역 렌더 백엔드 만 변경하면됩니다.

ServiceLocator<IRenderBackend>.Service = new IsometricRenderBackend();

또 다른 장점은 널 서비스도 사용할 수 있다는 것입니다. 예를 들어, ISoundManager 서비스가 있고 사용자가 사운드를 끄고 싶을 경우, 메소드가 호출 될 때 아무것도하지 않는 NullSoundManager를 구현할 수 있습니다. 따라서 ServiceLocator 의 서비스 객체를 NullSoundManager 객체 로 설정하면 달성 할 수 있습니다 이로 인해 거의 작업이 이루어지지 않습니다.

요약하면, 때로는 전역 데이터를 제거하는 것이 불가능할 수도 있지만, 이것이 데이터를 올바르게 객체 지향 방식으로 구성 할 수 없다는 의미는 아닙니다.


나는 이것을 전에 살펴 보았지만 실제로 내 디자인에 구현하지는 않았습니다. 이번에는 계획하고 있습니다. 감사합니다 :)
Awesomania

3
@Erevis 따라서 기본적으로 다형성 객체에 대한 전역 참조를 설명합니다. 차례로, 이것은 단지 이중 간접적이다 (포인터-> 인터페이스-> 구현). C ++에서는 다음과 같이 쉽게 구현할 수 있습니다 std::unique_ptr<ISomeService>.
Shadows In Rain

1
초기화 전략을 "처음 액세스시 초기화"로 변경하고 외부 코드 시퀀스를 할당하여 서비스를 로케이터에 푸시하지 않아도됩니다. 서비스에 "의존"목록을 추가하여 하나가 초기화 될 때 다른 서비스를 자동으로 설정하고 main.cpp에서 누군가가 그렇게하는 것을 기억하지 않도록 자동으로 설정합니다. 향후 조정을위한 유연성을 갖춘 좋은 답변입니다.
Patrick Hughes

4

게임 엔진을 설계하는 방법에는 여러 가지가 있으며 실제로는 선호도에 따라 결정됩니다.

기본을 벗어나기 위해 일부 개발자는 종종 최고 수준의 클래스가 커널, 코어 또는 프레임 워크 클래스라고하는 피라미드와 유사하게 디자인하는 것을 선호합니다. 오디오, 그래픽, 네트워크, 물리, AI 및 작업, 엔터티 및 리소스 관리 일반적으로 이러한 서브 시스템은이 프레임 워크 클래스에 의해 노출되며 일반적으로이 프레임 워크 클래스를 적절한 경우 생성자 인수로 자신의 클래스에 전달합니다.

옵션 # 4에 대한 귀하의 생각으로 올바른 길을 가고 있다고 생각합니다.

통신 자체와 관련하여 항상 직접 함수 호출 자체를 의미 할 필요는 없습니다. 통신을 사용 Signal and Slots하거나 사용 하는 간접적 인 방법을 사용하든간에 통신이 발생할 수있는 많은 간접 방법이 있습니다 Messages.

때로는 게임에서 프레임 속도가 육안으로 유동적 일 수 있도록 게임 루프를 최대한 빠르게 움직 이도록 조치를 비동기식으로 수행하는 것이 중요합니다. 플레이어는 느리고 고르지 않은 장면을 좋아하지 않기 때문에 우리는 물건을 계속 흐르게하지만 논리는 흐르지 만 점검하고 순서를 정하는 방법을 찾아야합니다. 비동기 작업은 그 자리에 있지만 모든 작업에 대한 답은 아닙니다.

동기식 통신과 비동기식 통신을 혼합하여 사용할 수 있습니다. 적절한 것을 선택하십시오. 그러나 서브 시스템 사이에서 두 가지 스타일을 모두 지원해야합니다. 두 가지 모두에 대한 지원을 설계하면 미래에도 도움이 될 것입니다.


1

역 또는 주기적 종속성이 없는지 확인해야합니다. 당신은 클래스가 예를 들어 Core, 이것은 CoreLevel와가 Level의 목록이 Entity다음 종속성 트리은 다음과 같은 모양입니다 :

Core --> Level --> Entity

그래서,이 초기 종속성 트리를 주어, 당신은 안 Entity의존하지 LevelCore, 그리고 Level에 의존해서는 안됩니다 Core. 종속성 트리에서 더 높은 데이터에 액세스 Level하거나 Entity액세스해야하는 경우 참조로 매개 변수로 전달해야합니다.

다음 코드 (C ++)를 고려하십시오.

class Core;
class Entity;
class Level;

class Level
{
    public:
        Level(Core& coreIn) : core(coreIn) {}

        Core& core;
}

class Entity
{
    public:
        Entity(Level& levelIn) : level(levelIn) {}

        Level& level;
}

이 기술을 사용하면 각각 Entity에 대한 액세스 권한 LevelLevel있고에 대한 액세스 권한이 있음을 알 수 Core있습니다. 각각 Entity은 동일한 Level메모리 낭비에 대한 참조를 저장합니다 . 이 사실을 알았을 때, 각각에 Entity대한 액세스 권한이 실제로 필요한지 여부를 질문해야 합니다 Level.

내 경험상 A) 역 의존성을 피할 수있는 확실한 솔루션이나 B) 전역 인스턴스와 싱글 톤을 피할 수있는 방법이 없습니다.


뭔가 빠졌습니까? '엔터티가 레벨에 의존해서는 안됩니다'를 언급 한 다음 ctor를 'Entity (Level & levelIn)'라고 설명합니다. 종속성이 ref에 의해 전달되었지만 여전히 종속성임을 이해합니다.
Adam Naylor

@AdamNaylor 요점은 때로는 역 의존성을 필요로하며 참조를 전달하여 전역을 피할 수 있다는 것입니다. 그러나 일반적으로 이러한 종속성을 완전히 피하는 것이 가장 좋으며이를 수행하는 방법이 항상 명확한 것은 아닙니다.
사과

0

따라서 기본적으로 전역 변경 가능 상태 를 피하고 싶 습니까? 로컬, 불변 또는 상태가 아닌 상태로 만들 수 있습니다. 후자는 가장 효율적이고 유연합니다. 그것은 은폐 숨기기로 알려져 있습니다.

class ISomeComponent // abstract base class
{
    //...
};

extern ISomeComponent & g_SomeComponent; // will be defined somewhere else;

0

문제는 실제로 성능을 저하시키지 않고 커플 링을 줄이는 방법에 관한 것 같습니다. 모든 전역 객체 (서비스)는 일반적으로 게임 실행 중 변경할 수있는 일종의 컨텍스트를 형성합니다. 이런 의미에서 서비스 로케이터 패턴은 컨텍스트의 다른 부분을 애플리케이션의 다른 부분으로 분산 시키며, 이는 원하는 것이거나 아닐 수도 있습니다. 또 다른 실제 접근 방식은 다음과 같은 구조를 선언하는 것입니다.

struct sEnvironment
{
    owning<iAudio*> m_Audio;
    owning<iRenderer*> m_Renderer;
    owning<iGameLevel*> m_GameLevel;
    ...
}

그리고 소유하지 않은 raw pointer로 전달하십시오 sEnvironment*. 여기서 포인터는 인터페이스를 가리 키므로 서비스 로케이터와 비슷한 방식으로 커플 링이 줄어 듭니다. 그러나 모든 서비스는 한 곳에 있습니다 (좋거나 좋지 않을 수 있음). 이것은 또 다른 접근법입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.