여러 클래스가 동일한 데이터에 액세스해야하는 경우 데이터를 어디에 선언해야합니까?


38

C ++에 기본 2D 타워 방어 게임이 있습니다.

각 맵은 GameState에서 상속 된 별도의 클래스입니다. 맵은 게임의 각 오브젝트에 로직과 드로잉 코드를 위임하고 맵 경로와 같은 데이터를 설정합니다. 의사 코드에서 논리 섹션은 다음과 같습니다.

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

물체 (크리프, 탑 및 미사일)는 포인터 벡터에 저장됩니다. 타워는 새로운 미사일을 생성하고 목표물을 식별하기 위해 크리프 벡터와 미사일 벡터에 접근 할 수 있어야합니다.

문제는 벡터를 어디에서 선언합니까? Map 클래스의 멤버 여야하고 tower.update () 함수에 인수로 전달되어야합니까? 아니면 세계적으로 선언? 아니면 완전히 누락 된 다른 솔루션이 있습니까?

여러 클래스가 동일한 데이터에 액세스해야하는 경우 데이터를 어디에 선언해야합니까?


1
글로벌 회원은 '추악한'것으로 여겨지지만, 규모가 작은 게임 인 경우 문제가되지 않습니다 (IMHO). 또한 로직을 처리하는 외부 클래스 ( 타워에 이러한 벡터가 필요한 이유 )를 만들고 모든 벡터에 액세스 할 수 있습니다.
Jonathan Connell 2016 년

-1 게임 프로그래밍과 관련이 있다면 피자를 먹는 것도 마찬가지입니다. 좋은 소프트웨어 디자인 서적을 직접
만나보세요

9
@Maik : 소프트웨어 디자인은 게임 프로그래밍과 어떤 관련이 없습니까? 다른 프로그래밍 분야에도 적용되기 때문에 주제를 벗어난 것은 아닙니다.
BlueRaja-대니 Pflughoeft

소프트웨어 설계 패턴의 @BlueRaja 목록은 SO에 더 적합합니다. GD.SE는 소프트웨어 디자인이 아닌 게임 프로그래밍을위한 것입니다.
Maik Semder

답변:


52

프로그램 전체에서 클래스의 단일 인스턴스가 필요한 경우 해당 클래스를 서비스라고 합니다. 프로그램에서 서비스를 구현하는 몇 가지 표준 방법이 있습니다.

  • 글로벌 변수 . 가장 구현하기 쉽지만 최악의 디자인입니다. 너무 많은 전역 변수를 사용하는 경우 서로 너무 많은 모듈에 의존하는 모듈을 작성 ( 강력한 커플 링 )하여 논리 흐름을 따르기가 매우 어려워집니다. 전역 변수는 멀티 스레딩에 적합하지 않습니다. 전역 변수를 사용하면 객체의 수명을 추적하기가 어려워지고 네임 스페이스가 복잡해집니다. 그러나 가장 성능이 좋은 옵션이므로 사용할 수 있고 사용해야하는 시간이 있지만 여분으로 사용하십시오.
  • 싱글 톤 . 10 ~ 15 약 년 전, 싱글이었다 에 대해 알 수있는 큰 디자인 패턴. 그러나 요즘 그들은 내려다보고 있습니다. 그것들은 멀티 스레드보다 훨씬 쉽지만 한 번에 하나의 스레드로 사용을 제한해야합니다. 항상 원하는 것은 아닙니다. 수명 변수는 전역 변수와 마찬가지로 어렵습니다. 일반적인 싱글 톤 클래스는 다음과 같습니다.

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
    
  • 의존성 주입 (DI) . 이것은 서비스를 생성자 매개 변수로 전달한다는 의미입니다. 서비스를 클래스에 전달하려면 서비스가 이미 존재해야하므로 두 서비스가 서로 의존 할 수있는 방법이 없습니다. 98 %의 경우, 이것이 원하는 것입니다 (다른 2 %의 경우 항상 setWhatever()메소드를 작성하고 나중에 서비스에 전달할 수 있음) . 이로 인해 DI에는 다른 옵션과 동일한 커플 링 문제가 없습니다. 각 스레드는 단순히 모든 서비스의 자체 인스턴스를 가질 수 있고 반드시 필요한 인스턴스 만 공유 할 수 있기 때문에 멀티 스레딩과 함께 사용할 수 있습니다. 또한 관심이 있다면 코드를 단위 테스트 가능하게 만듭니다.

    의존성 주입의 문제점은 더 많은 메모리를 차지한다는 것입니다. 이제 클래스의 모든 인스턴스는 사용할 모든 서비스에 대한 참조가 필요합니다. 또한 서비스가 너무 많으면 사용하기가 번거 롭습니다. 다른 언어에서는이 문제를 완화시키는 프레임 워크가 있지만 C ++의 리플렉션 부족으로 인해 C ++의 DI 프레임 워크는 수동으로 수행하는 것보다 훨씬 많은 작업을 수행하는 경향이 있습니다.

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.
    

    다른 예는 이 페이지를 참조 하십시오 (C # DI 프레임 워크 Ninject 문서).

    의존성 주입은이 문제에 대한 일반적인 해결책이며 StackOverflow.com에서 이와 같은 질문에 가장 많이 반향되는 답변입니다. DI는 IoC ( Inversion of Control ) 한 유형입니다 .

  • 서비스 로케이터 . 기본적으로 모든 서비스의 인스턴스를 보유하는 클래스입니다. 당신은 할 수 반사를 사용하여 그것을 할 , 또는 당신은 단지에 새로운 서비스를 만들 때마다 새로운 인스턴스를 추가 할 수 있습니다. - 이전과 당신은 여전히 같은 문제가 어떻게 수업이 로케이터 액세스합니까? -위의 방법으로 해결할 수 있지만 이제는 ServiceLocator수십 개의 서비스가 아닌 수업 을 위해서만 수행하면 됩니다. 이런 종류의 일에 관심이 있다면이 방법은 단위 테스트도 가능합니다.

    서비스 로케이터는 또 다른 형태의 IoC (Inversion of Control)입니다. 일반적으로 자동 종속성 주입을 수행하는 프레임 워크에도 서비스 로케이터가 있습니다.

    XNA (Microsoft의 C # 게임 프로그래밍 프레임 워크) 에는 서비스 로케이터가 포함됩니다. 이에 대한 자세한 내용은 이 답변을 참조하십시오 .


그건 그렇고, IMHO 타워는 크립에 대해 알아야합니다. 모든 타워의 크리프 목록을 단순히 반복하지 않는 한 사소한 공간 분할 을 구현해야 할 것입니다 . 그런 종류의 논리는 towers 클래스에 속하지 않습니다.


의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Josh

내가 읽은 최고의 가장 명확한 답변 중 하나입니다. 잘 했어. 그래도 서비스는 항상 공유되어야한다고 생각했습니다.
Nikos

5

저는 개인적으로 여기서 다형성을 사용할 것입니다. missile벡터, tower벡터 및 벡터 가있는 이유는 creep모두 같은 함수를 호출 할 때입니다. update? 이유는 몇 가지 기본 클래스에 대한 포인터의 벡터가없는 Entity또는 GameObject?

디자인하는 좋은 방법은 '이것이 소유권 측면에서 의미가 있는가?'라고 생각하는 것입니다. 분명히 타워는 자신을 업데이트하는 방법을 소유하지만 맵은 모든 객체를 소유합니까? 전 세계로 가면 탑과 섬뜩한 것이 아무것도 없다는 말입니까? 글로벌은 일반적으로 나쁜 솔루션입니다. 나쁜 디자인 패턴을 조장하면서도 작업하기가 훨씬 쉽습니다. '이걸 끝내고 싶니?' 그리고 '재사용 할 수있는 것을 원합니까?'

이를 해결하는 한 가지 방법은 일종의 메시징 시스템입니다. 는 tower받는 메시지를 보낼 수 있습니다 map그것이 충돌 것을 (? 주인에가 액세스 할 수있는, 아마도 참조) creep, 그리고는 map그 다음 이야기 creep가 히트이었다. 이것은 매우 깨끗하고 데이터를 분리합니다.

다른 방법은지도 자체에서 원하는 것을 검색하는 것입니다. 그러나 업데이트 순서에 문제가있을 수 있습니다.


1
다형성에 대한 당신의 제안은 실제로 관련이 없습니다. 그것들을 별도의 벡터에 저장하여 그리기 코드 (특정 객체를 먼저 그려야 할 곳) 또는 충돌 코드와 같이 각 유형을 개별적으로 반복 할 수 있습니다.
Juicy

내 목적을 위해지도는 엔티티를 소유합니다. 여기서지도는 '레벨'과 유사합니다. 메시지에 대한 당신의 생각을 고맙겠습니다.
Juicy

1
게임 성능에서 중요합니다. 따라서 동일한 객체 시간의 벡터는 더 나은 참조 지역성을 갖습니다. 또한 가상 포인터가있는 다형성 객체는 업데이트 루프에 인라인 될 수 없기 때문에 성능이 끔찍합니다.
Zan Lynx

0

엄격한 객체 지향 프로그래밍 (OOP)이 고장난 경우입니다.

OOP의 원칙에 따라 클래스를 사용하여 관련 동작으로 데이터를 그룹화해야합니다. 그러나 서로 관련이없는 데이터 (타워 및 크립)가 필요한 동작 (타겟팅)이 있습니다. 이 상황에서 많은 프로그래머는 동작을 필요한 데이터의 일부 (예 : 타워는 타겟팅을 처리하지만 크립에 대해서는 알지 못함)와 연관 시키려고하지만 다른 옵션 이 있습니다. 동작을 데이터와 그룹화하지 마십시오.

타겟팅 동작을 타워 클래스의 메소드로 만드는 대신 타워를 받아들이고 인수로 크리프하는 무료 함수로 만드십시오. 이를 위해서는 타워와 크리프 클래스에 남아있는 멤버를 더 많이 만들어야 할 수도 있습니다 . 데이터 숨기기는 유용하지만 그 자체가 목적이 아니라 수단이므로 노예가되어서는 안됩니다. 게다가 개인 멤버 만이 데이터에 대한 액세스를 제어하는 ​​유일한 방법은 아닙니다. 데이터가 함수에 전달되지 않고 전역이 아닌 경우 해당 함수에서 효과적으로 숨겨집니다. 이 기술을 사용하여 전역 데이터를 피할 수 있으면 실제로 캡슐화 가 향상 될 수 있습니다 .

이 방법의 극단적 인 예는 엔터티 시스템 아키텍처입니다.

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