게임 오브젝트는 서로를 어떻게 인식해야합니까?


18

게임 오브젝트를 다형성이면서 동시에 다형성이 아닌 방식으로 구성하는 방법을 찾기가 어렵습니다.

다음은 예입니다. 모든 객체를 update()and로 원한다고 가정합니다 draw(). 그러기 위해서는 GameObject두 가지 가상 순수 메소드가 있고 다형성이 시작 되는 기본 클래스를 정의해야합니다 .

class World {
private:
    std::vector<GameObject*> objects;
public:
    // ...
    update() {
        for (auto& o : objects) o->update();
        for (auto& o : objects) o->draw(window);
    }
};

update 메소드는 특정 클래스 객체가 업데이트해야하는 모든 상태를 처리해야합니다. 사실 각 물체는 주위의 세계에 대해 알아야합니다. 예를 들면 다음과 같습니다.

  • 광산은 누군가와 충돌하는지 알아야합니다.
  • 다른 팀의 병사가 가까이 있는지 병사는 알고 있어야합니다
  • 좀비는 반경 내에서 가장 가까운 뇌가 어디에 있는지 알아야합니다.

수동적 상호 작용 (첫 번째 것과 같은)의 경우 충돌 감지가 특정 충돌의 경우 수행 할 작업을 객체로 객체 자체에 위임 할 수 있다고 생각했습니다 on_collide(GameObject*).

다른 두 정보와 같은 다른 정보는 대부분 게임 세계가 update메소드에 전달하여 쿼리 할 수 ​​있습니다 . 이제 세계는 유형에 따라 객체를 구별하지 않으며 (모든 객체를 단일 다형성 컨테이너에 저장합니다) 실제로 이상적인 world.entities_in(center, radius)것으로 반환 되는 컨테이너는 GameObject*입니다. 물론 군인은 자신의 팀에서 다른 군인을 공격하고 싶지 않으며 좀비는 다른 좀비에 대해서는 그렇지 않습니다. 따라서 행동을 구별해야합니다. 해결책은 다음과 같습니다.

void TeamASoldier::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
            // shoot towards enemy
}

void Zombie::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<Human*>(e))
            // go and eat brain
}

물론 dynamic_cast<>프레임 당 수는 엄청나게 높을 수 있으며, 우리는 모두 얼마나 느릴 dynamic_cast수 있는지 알고 있습니다. on_collide(GameObject*)앞에서 논의한 델리게이트 에도 동일한 문제가 적용됩니다 .

그렇다면 객체가 다른 객체를 인식하고 해당 객체를 무시하거나 유형에 따라 조치를 취할 수 있도록 코드를 구성하는 이상적인 방법은 무엇입니까?


1
다목적 C ++ RTTI 사용자 정의 구현을 찾고 있다고 생각합니다. 그럼에도 불구하고 귀하의 질문은 신중한 RTTI 메커니즘에 관한 것만은 아닙니다. 당신이 요구하는 것은 게임이 사용할 거의 모든 미들웨어 (애니메이션 시스템, 물리학)가 필요합니다. 지원되는 쿼리 목록에 따라 배열의 ID 및 인덱스를 사용하여 RTTI를 둘러 보거나 dynamic_cast 및 type_info에 대한 저렴한 대안을 지원하기위한 완전한 프로토콜을 설계하게됩니다.
teodron

게임 로직에 타입 시스템을 사용하지 말 것을 권합니다. 예를 들어의 결과에 의존하는 대신 기본적으로 반환 되지만 클래스 에서 반환 되도록 재정의되는와 dynamic_cast<Human*>같은 것을 구현 하십시오 . bool GameObject::IsHuman()falsetrueHuman
congusbongus

추가 : 당신은 거의 관심이있을 수있는 객체를 서로에게 엔티티로 보내지 않습니다. 그것은 당신이 정말로 고려해야 할 명백한 최적화입니다.
teodron

@congusbongus vtable 및 사용자 정의 IsA재정의를 사용하는 것은 실제로 동적 캐스팅보다 약간 더 나은 것으로 나타났습니다. 가장 좋은 방법은 사용자가 가능하면 전체 엔터티 풀에 대해 맹목적으로 반복하는 대신 정렬 된 데이터 목록을 갖는 것입니다.
teodron

4
@ Jefffrey : 이상적으로는 유형별 코드를 작성하지 않습니다. 당신은 쓰기 인터페이스 - 특정 코드 (일반적인 의미에서 "인터페이스"). A에 대한 귀하의 논리 TeamASoldier와는 TeamBSoldier정말 동일합니다 - 다른 팀에 사람의 샷입니다. 다른 개체에 필요한 GetTeam()것은 가장 구체적이고, congusbongus의 예에 따르면, 더 많은 IsEnemyOf(this)종류의 인터페이스 로 추상화 될 수있는 방법 입니다. 이 코드는 군인, 좀비, 플레이어 등의 분류 학적 분류에 신경 쓸 필요가 없습니다. 유형이 아닌 상호 작용에 중점을 둡니다.
Sean Middleditch

답변:


11

각 엔티티 자체의 의사 결정을 구현하는 대신 컨트롤러 패턴으로 갈 수도 있습니다. 모든 객체 (중요한 것)를 인식하고 동작을 제어하는 ​​중앙 컨트롤러 클래스가 있습니다.

MovementController는 이동할 수있는 모든 객체의 이동을 처리합니다 (경로 찾기, 현재 이동 벡터를 기반으로 위치 업데이트).

MineBehaviorController는 모든 광산과 모든 군인을 확인하고 군인이 너무 가까워지면 광산이 폭발하도록 명령합니다.

ZombieBehaviorController는 주변의 모든 좀비와 병사를 확인하고 각 좀비에 가장 적합한 대상을 선택한 다음 이동하여 공격하도록 명령합니다 (이동 자체는 MovementController에 의해 처리됨).

SoldierBehaviorController는 전체 상황을 분석 한 다음 모든 병사에 대한 전술 지시 사항을 제시합니다 (여기서 이동하면이 사람을 치료합니다 ...). 이러한 상위 레벨 명령의 실제 실행은 하위 레벨 컨트롤러에서도 처리됩니다. 그것에 노력을 기울이면 AI가 매우 현명한 협력 결정을 내릴 수 있습니다.


1
아마도 이것은 Entity-Component 아키텍처에서 특정 유형의 구성 요소에 대한 논리를 관리하는 "시스템"이라고도합니다.
teodron

그것은 C 스타일 솔루션처럼 들립니다. 컴포넌트는 std::maps 로 그룹화 되고 엔티티는 ID 일 뿐이므로 일종의 유형 시스템을 만들어야합니다 (렌더러가 무엇을 그릴 지 알아야하기 때문에 태그 컴포넌트 일 수도 있음). 우리가 원치 않으면 그리기 구성 요소가 필요합니다. 그러나 위치 구성 요소가 어디에서 그려 질지 알아야하기 때문에 매우 복잡한 메시징 시스템으로 해결하는 구성 요소 간의 종속성을 만듭니다. 이것이 당신이 제안하는 것입니까?
구두

1
@ 제프리 (Jefffrey) "그것이 C 스타일 솔루션처럼 들린다"-그것이 사실 일지라도 왜 반드시 나쁜 것입니까? 다른 문제는 유효 할 수 있지만 해결 방법이 있습니다. 불행히도 의견이 너무 짧아서 각 내용을 제대로 설명 할 수 없습니다.
Philipp

1
@Jefffrey 컴포넌트 자체에 로직이없고 "시스템"이 모든 로직을 처리 할 수있는 접근 방식을 사용하면 컴포넌트 간의 종속성을 생성하지 않으며 매우 복잡한 메시징 시스템이 필요하지 않습니다 (적어도 복잡하지는 않음) . 예를 들면 다음과 같습니다. gamadu.com/artemis/tutorial.html

1

우선, 가능할 때마다 개체가 서로 독립적으로 유지되도록 기능을 구현하십시오. 특히 멀티 스레딩을 위해 그렇게하고 싶습니다. 첫 번째 코드 예제에서 모든 객체 세트는 CPU 코어 수와 일치하는 세트로 나뉘어 매우 효율적으로 업데이트 될 수 있습니다.

그러나 말했듯이 일부 기능에는 다른 개체와의 상호 작용이 필요합니다. 즉, 모든 객체의 상태는 특정 시점에서 동기화되어야합니다. 즉, 응용 프로그램은 모든 병렬 작업이 먼저 완료 될 때까지 기다렸다가 상호 작용이 포함 된 계산을 적용해야합니다. 이러한 동기화 지점의 수는 일부 스레드가 다른 스레드가 완료 될 때까지 기다려야한다는 것을 항상 암시하는 것이 좋습니다.

따라서 다른 객체 내부에서 필요한 객체에 대한 정보를 버퍼링하는 것이 좋습니다. 이러한 전역 버퍼가 제공되면 객체를 서로 독립적으로 업데이트 할 수 있지만 자신과 전역 버퍼에만 의존하여 더 빠르고 유지 관리하기 쉽습니다. 고정 된 타임 스텝에서, 예를 들어 각 프레임 후에, 현재 객체의 상태로 버퍼를 업데이트하십시오.

따라서 프레임 당 한 번하는 작업은 1. 현재 객체의 상태를 전체적으로 버퍼링하고, 2. 자신과 버퍼를 기준으로 모든 객체를 업데이트합니다. 3. 객체를 그린 다음 버퍼를 다시 시작합니다.


1

컴포넌트 기반 시스템을 사용하십시오. 여기에는 동작을 정의하는 하나 이상의 컴포넌트가 포함 된 베어 본 GameObject가 있습니다.

예를 들어 어떤 객체가 항상 좌우로 움직여야한다고 가정하면 (플랫폼) 그러한 컴포넌트를 만들어 GameObject에 연결할 수 있습니다.

이제 게임 오브젝트가 항상 천천히 회전한다고 가정하면,이를 수행하는 별도의 구성 요소를 만들어 GameObject에 연결할 수 있습니다.

코드를 복제하지 않고는 어려워지는 전통적인 클래스 계층 구조에서 회전하는 이동 플랫폼을 원한다면 어떨까요?

이 시스템의 장점은 Rotatable 또는 MovingPlatform 클래스를 사용하는 대신 이러한 두 구성 요소를 모두 GameObject에 연결하고 이제 AutoRotates하는 MovingPlatform을 가지고 있다는 것입니다.

모든 컴포넌트에는 'requiresUpdate'속성이 있습니다. true 인 반면 GameObject는 해당 컴포넌트에서 'update'메소드를 호출합니다. 예를 들어, Draggable 컴포넌트를 가지고 있다고 가정하면, 마우스 다운시 (GameObject 위에있는 경우)이 컴포넌트는 'requiresUpdate'를 true로 설정 한 다음 마우스 업시 false로 설정할 수 있습니다. 마우스를 눌렀을 때만 마우스를 따라갑니다.

Tony Hawk Pro Skater 개발자 중 한 명이 실제로 작성했으며 읽을 가치가 있습니다. http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/


1

상속보다 구성을 선호하십시오.

이것을 제외하고 나의 가장 강력한 충고는 : "이것이 최고의 유연성을 원한다"는 사고 방식에 빠지지 마십시오. 유연성은 크지 만 게임과 같은 유한 시스템 에는 전체를 구성하는 데 사용되는 원자 부분이 있다는 것을 기억하십시오 . 어떤 식 으로든 처리는 사전 정의 된 원자 유형에 의존합니다. 다시 말해, 코드가 없다면 "모든"유형의 데이터 (가능하다면)를 제공하는 것이 장기적으로 도움이되지 않을 것입니다. 기본적으로 모든 코드는 알려진 사양을 기반으로 데이터를 구문 분석 / 처리해야합니다. 이는 사전 정의 된 유형 세트를 의미합니다. 얼마나 큰가요? 당신에게 달려 있습니다.

이 기사 는 강력하고 성능이 뛰어난 엔터티 구성 요소 아키텍처를 통해 게임 개발에서 컴포지션 오버 상속 원칙에 대한 통찰력을 제공합니다.

미리 정의 된 구성 요소의 상위 집합의 (서로 다른) 하위 집합으로 엔터티를 구축하면 AI의 배우자 구성 요소의 상태를 읽음으로써 AI를 통해 세계와 그 주변의 배우자를 이해하는 구체적인 방법을 제공 할 수 있습니다.


1

개인적으로 Draw 클래스를 Object 클래스 자체에서 유지하는 것이 좋습니다. 객체 위치 / 좌표를 객체 자체에서 멀리 유지하는 것이 좋습니다.

그 draw () 메소드는 OpenGL, OpenGL ES, Direct3D의 저수준 렌더링 API, 해당 API 또는 엔진 API의 래핑 레이어를 처리합니다. 예를 들어 OpenGL + OpenGL ES + Direct3D를 지원하려면 그 사이를 전환해야 할 수도 있습니다.

해당 GameObject에는 메시 또는 셰이더 입력, 애니메이션 상태 등 더 큰 번들과 같은 시각적 모양에 대한 기본 정보 만 포함해야합니다.

또한 유연한 그래픽 파이프 라인을 원할 것입니다. 카메라와의 거리를 기준으로 개체를 주문하려는 경우 어떻게됩니까? 또는 재료 유형. '선택된'객체를 다른 색상으로 그리려면 어떻게됩니까? 객체에서 draw 함수를 호출 할 때 실제로 soo로 끝나는 대신 렌더가 수행 할 액션의 명령 목록에 추가합니다 (스레딩에 필요할 수 있음). 다른 시스템에서도 그런 일을 할 수 있지만 PITA입니다.

내가 권장하는 것은 직접 그리는 대신 원하는 모든 객체를 다른 데이터 구조에 바인딩하는 것입니다. 이 바인딩은 실제로 객체 위치와 렌더링 정보에 대한 참조 만 있으면됩니다.

레벨 / 청크 / 영역 /지도 / 허브 / 전체 월드 / 공간 인덱스가 제공되는 것에 관계없이 여기에는 객체가 포함되며 좌표 쿼리를 기반으로 객체가 반환되며 간단한 목록 또는 Octree와 같은 것일 수 있습니다. 또한 제 3 자 물리 엔진이 물리 장면으로 구현 한 것에 대한 래퍼 일 수도 있습니다. "카메라 주변에 여분의 영역이있는 카메라 뷰에있는 모든 객체 쿼리"또는 모든 것을 전체 렌더링 할 수있는 간단한 게임과 같은 작업을 수행 할 수 있습니다.

공간 인덱스에는 실제 위치 정보가 포함되어 있지 않아도됩니다. 다른 객체의 위치와 관련하여 트리 구조로 객체를 저장하여 작동합니다. 그것들은 위치에 따라 객체를 신속하게 조회 할 수있는 일종의 손실 캐시 일 수 있습니다. 실제 X, Y, Z 좌표를 복제 할 필요는 없습니다. 당신이 유지하고 싶다면 할 수 있다고 말했다

실제로 게임 오브젝트에는 자체 위치 정보가 포함되어 있지 않아도됩니다. 예를 들어, 레벨에 놓이지 않은 오브젝트에는 x, y, z 좌표가 없어야합니다. 특수 색인에 포함시킬 수 있습니다. 실제 참조를 기준으로 객체의 좌표를 조회해야하는 경우 객체와 장면 그래프를 바인딩해야합니다 (씬 그래프는 좌표를 기준으로 객체를 반환하기위한 것이지만 객체를 기반으로 좌표를 반환하는 속도는 느림). .

레벨에 객체를 추가 할 때. 다음을 수행합니다.

1) 위치 구조를 만듭니다.

 class Location { 
     float x, y, z; // Or a special Coordinates class, or a vec3 or whatever.
     SpacialIndex& spacialIndex; // Note this could be the area/level/map/whatever here
 };

이는 타사 물리 엔진의 객체에 대한 참조 일 수도 있습니다. 또는 다른 위치 (추적 카메라 또는 부착 된 객체 또는 예)를 참조하여 오프셋 좌표가 될 수 있습니다. 다형성을 사용하면 정적 또는 동적 객체인지에 따라 다를 수 있습니다. 좌표가 업데이트 될 때 공간 인덱스에 대한 참조를 유지함으로써 공간 인덱스도 될 수 있습니다.

동적 메모리 할당이 걱정되면 메모리 풀을 사용하십시오.

2) 객체, 객체의 위치 및 장면 그래프 간의 바인딩 / 링크.

typedef std::pair<Object, Location> SpacialBinding.

3) 바인딩은 적절한 지점에서 레벨 내부의 공간 인덱스에 추가됩니다.

렌더링을 준비 할 때

1) 카메라를 가져옵니다 (위치는 플레이어 캐릭터를 추적하고 렌더러가 실제로 필요한 모든 것을 참조한다는 점을 제외하고는 다른 객체 일뿐입니다).

2) 카메라의 SpacialBinding을 가져옵니다.

3) 바인딩에서 공간 인덱스를 가져옵니다.

4) 카메라에 보이는 물체를 쿼리합니다.

5A) 시각 정보를 처리해야합니다. GPU 등에 텍스처가 업로드되었습니다. 이것은 (레벨로드와 같은) 사전에 가장 잘 수행되지만 런타임에 수행 될 수도 있습니다 (오픈 월드의 경우 청크에 가까울 때로드 할 수 있지만 여전히 미리 수행해야 함).

5B) 선택적으로 캐시 된 렌더 트리를 구축합니다. 깊이 / 재료 정렬을 원하거나 근처에있는 객체를 추적하려는 경우 나중에 볼 수 있습니다. 그렇지 않으면 게임 / 성능 요구 사항에 따라 공간 인덱스를 쿼리 할 수 ​​있습니다.

렌더러에는 Object와 좌표 사이를 연결하는 RenderBinding 객체가 필요할 것입니다.

class RenderBinding {
    Object& object;
    RenderInformation& renderInfo;
    Location& location // This could just be a coordinates class.
}

그런 다음 렌더링 할 때 목록을 통해 실행하십시오.

위의 참조를 사용했지만 스마트 포인터, 원시 포인터, 객체 핸들 등이 될 수 있습니다.

편집하다:

class Game {
    weak_ptr<Camera> camera;
    Level level1;

    void init() {
        Camera camera(75.0_deg, 1.025_ratio, 1000_meters);
        auto template_player = loadObject("Player.json")
        auto player = level1.addObject(move(player), Position(1.0, 2.0, 3.0));
        level1.addObject(move(camera), getRelativePosition(player));

        auto template_bad_guy = loadObject("BadGuy.json")
        level1.addObject(template_bad_guy, {10, 10, 20});
        level1.addObject(template_bad_guy, {10, 30, 20});
        level1.addObject(move(template_bad_guy), {50, 30, 20});
    }

    void render() {
        camera->getFrustrum();
        auto level = camera->getLocation()->getLevel();
        auto object = level.getVisible(camera);
        for(object : objects) {
            render(objects);
        }
    }

    void render(Object& object) {
        auto ri = object.getRenderInfo();
        renderVBO(ri.getVBO());
    }

    Object loadObject(string file) {
        Object object;
        // Load file from disk and set the properties
        // Upload mesh data, textures to GPU. Load shaders whatever.
        object.setHitPoints(// values from file);
        object.setRenderInfo(// data from 3D api);
    }
}

class Level {
    Octree octree;
    vector<ObjectPtr> objects;
    // NOTE: If your level is mesh based there might also be a BSP here. Or a hightmap for an openworld
    // There could also be a physics scene here.
    ObjectPtr addObject(Object&& object, Position& pos) {
        Location location(pos, level, object);
        objects.emplace_back(object);
        object->setLocation(location)
        return octree.addObject(location);
    }
    vector<Object> getVisible(Camera& camera) {
        auto f = camera.getFtrustrum();
        return octree.getObjectsInFrustrum(f);
    }
    void updatePosition(LocationPtr l) {
        octree->updatePosition(l);
    }
}

class Octree {
    OctreeNode root_node;
    ObjectPtr add(Location&& object) {
        return root_node.add(location);
    }
    vector<ObjectPtr> getObjectsInRadius(const vec3& position, const float& radius) { // pass to root_node };
    vector<ObjectPtr> getObjectsinFrustrum(const FrustrumShape frustrum;) {//...}
    void updatePosition(LocationPtr* l) {
        // Walk up from l.octree_node until you reach the new place
        // Check if objects are colliding
        // l.object.CollidedWith(other)
    }
}

class Object {
    Location location;
    RenderInfo render_info;
    Properties object_props;
    Position getPosition() { return getLocation().position; }
    Location getLocation() { return location; }
    void collidedWith(ObjectPtr other) {
        // if other.isPickup() && object.needs(other.pickupType()) pick it up, play sound whatever
    }
}

class Location {
    Position position;
    LevelPtr level;
    ObjectPtr object;
    OctreeNote octree_node;
    setPosition(Position position) {
        position = position;
        level.updatePosition(this);
    }
}

class Position {
    vec3 coordinates;
    vec3 rotation;
}

class RenderInfo {
    AnimationState anim;
}
class RenderInfo_OpenGL : public RenderInfo {
    GLuint vbo_object;
    GLuint texture_object;
    GLuint shader_object;
}

class Camera: public Object {
    Degrees fov;
    Ratio aspect;
    Meters draw_distance;
    Frustrum getFrustrum() {
        // Use above to make a skewed frustum box
    }
}

서로를 '인식'하는 것. 충돌 감지입니다. 아마도 Octree에서 구현 될 것입니다. 기본 객체에 콜백을 제공해야합니다. 이 물건은 Bullet과 같은 적절한 물리 엔진에 의해 가장 잘 처리됩니다. 이 경우 Octree를 PhysicsScene 및 Position으로 CollisionMesh.getPosition ()과 같은 링크로 바꿉니다.


와우, 이것은 매우 좋아 보인다. 나는 기본 아이디어를 파악했다고 생각하지만 더 많은 예가 없다면 이것의 외부 견해를 얻을 수는 없습니다. 이에 대한 더 많은 참조 또는 실제 예가 있습니까? (그동안 잠시 동안이 답변을 계속 읽겠습니다).
신발

실제로 어떤 예도 가지고 있지 않습니다. 시간이 가면 내가 할 계획입니다. 나는 전체 클래스의 몇 가지 더 추가하고 도움이된다면,이 볼 . 객체 클래스와 관련이 있거나 렌더링하는 것보다 객체 클래스에 관한 것입니다. 내가 직접 구현하지 않았으므로 함정, 운동해야 할 비트 또는 성능 문제가있을 수 있지만 전반적인 구조는 괜찮습니다.
David C. Bishop
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.