엔티티 커뮤니케이션은 어떻게 작동합니까?


115

두 가지 사용자 사례가 있습니다.

  1. 메시지를 어떻게 entity_A보내 시겠습니까? take-damageentity_B
  2. 의 HP 는 어떻게 entity_A문의 entity_B합니까?

지금까지 내가 만난 것은 다음과 같습니다.

  • 메시지 대기열
    1. entity_Atake-damage메시지를 작성하여 entity_B의 메시지 큐에 게시합니다 .
    2. entity_Aquery-hp메시지를 작성하여에 게시합니다 entity_B. entity_B답례로 response-hp메시지를 작성하여에 게시합니다 entity_A.
  • 발행 / 구독
    1. entity_Btake-damage메시지를 구독 합니다 (일부 선제 필터링으로 관련 메시지 만 전달 될 수 있음). 참조 entity_A하는 take-damage메시지를 생성 합니다 entity_B.
    2. entity_Aupdate-hp메시지를 구독 합니다 (아마도 필터링 됨). 모든 프레임 entity_Bupdate-hp메시지를 브로드 캐스트 합니다.
  • 신호 / 슬롯
    1. ???
    2. entity_A연결 update-hp하는 슬롯 entity_B의의 update-hp신호.

더 좋은 것이 있습니까? 이러한 커뮤니케이션 체계가 게임 엔진의 엔터티 시스템과 어떻게 연결 될지에 대해 올바르게 이해하고 있습니까?

답변:


67

좋은 질문! 질문에 대한 구체적인 질문을하기 전에 다음과 같이 말씀 드리겠습니다. 단순성의 힘을 과소 평가하지 마십시오. Tenpn이 맞습니다. 이러한 접근 방식으로 수행하려는 모든 것은 함수 호출을 연기하거나 호출자와 수신자를 분리하는 우아한 방법을 찾는 것입니다. 나는 코 루틴을 이러한 문제의 일부를 완화시키는 놀랍도록 직관적 인 방법으로 추천 할 수 있지만, 주제가 약간 다릅니다. 때로는 함수를 호출하고 엔티티 A가 엔티티 B에 직접 연결되어 있다는 사실에 따라 생활하는 것이 좋습니다. YAGNI를 참조하십시오.

즉, 간단한 메시지 전달과 결합 된 신호 / 슬롯 모델에 만족했습니다. 매우 빡빡한 일정을 가진 상당히 성공적인 iPhone 타이틀을 위해 C ++과 Lua에서 사용했습니다.

신호 / 슬롯 사건의 경우, 엔티티 A가 엔티티 B가 한 일에 응답하여 무언가를하도록하려면 (예 : 무언가가 죽으면 문을 잠금 해제) 엔티티 A가 엔티티 B의 사망 이벤트에 직접 가입하도록 할 수 있습니다. 또는 엔터티 A는 엔터티 그룹 각각을 구독하고, 발생한 각 이벤트에서 카운터를 증가시키고, N 개가 사망 한 후 도어를 잠금 해제 할 수 있습니다. 또한 "엔터티 그룹"및 "N 중 하나"는 일반적으로 레벨 데이터에 정의 된 디자이너입니다. (제외 적으로, 이것은 코 루틴이 실제로 빛날 수있는 영역입니다. 예를 들어 WaitForMultiple ( "Dying", entA, entB, entC); door.Unlock ();)

그러나 C ++ 코드와 밀접하게 결합 된 반응이나 본질적으로 임시 게임 이벤트 : 손상 처리, 무기 재 장전, 디버깅, 플레이어 중심 위치 기반 AI 피드백과 관련하여 번거로울 수 있습니다. 메시지 전달이 공백을 채울 수있는 곳입니다. 기본적으로 "이 지역의 모든 개체에게 3 초 안에 피해를 입히도록 지시하십시오"또는 "물리학을 완료 할 때마다 내가 총을 쏜 사람을 알아낼 때마다이 스크립트 기능을 실행하도록 지시합니다."와 같이 요약됩니다. 발행 / 구독 또는 신호 / 슬롯을 사용하여이를 잘 수행하는 방법을 알아내는 것은 어렵습니다.

이것은 쉽게 과잉 일 수 있습니다 (tenpn의 예와 비교). 많은 행동을 취하면 비효율적 일 수 있습니다. 그러나 단점에도 불구하고이 "메시지 및 이벤트"접근 방식은 스크립트 게임 코드 (예 : Lua)와 매우 잘 맞습니다. 스크립트 코드는 C ++ 코드를 전혀 고려하지 않고 자체 메시지와 이벤트를 정의하고 반응 할 수 있습니다. 또한 스크립트 코드는 레벨 변경, 소리 재생 또는 무기가 TakeDamage 메시지가 얼마나 많은 피해를 주는지 등 C ++ 코드를 트리거하는 메시지를 쉽게 보낼 수 있습니다. luabind로 끊임없이 속일 필요가 없었기 때문에 시간이 많이 절약되었습니다. 그리고 그것은 내 많은 luabind 코드를 한곳에 보관할 수있게 해주었습니다. 제대로 연결되면

또한 유스 케이스 # 2에 대한 나의 경험은 다른 방향으로 이벤트로 처리하는 것이 더 낫다는 것입니다. 개체의 건강 상태를 묻지 않고 건강이 크게 변경 될 때마다 이벤트를 시작하거나 메시지를 보냅니다.

btw의 인터페이스 측면에서 EventHost, EventClient 및 MessageClient라는 세 가지 클래스로이 모든 것을 구현했습니다. EventHosts는 슬롯을 만들고 EventClients는 구독 / 연결하며 MessageClients는 대리자를 메시지와 연결합니다. MessageClient의 대리자 대상이 반드시 연결을 소유 한 동일한 개체 일 필요는 없습니다. 즉, MessageClient는 메시지를 다른 객체로 전달하기 위해서만 존재할 수 있습니다. 그러나 호스트 / 클라이언트 은유는 부적절합니다. 소스 / 싱크가 더 나은 개념 일 수 있습니다.

미안, 나는 거기에서 약간 으르렁 거렸다. 그것은 나의 첫 번째 대답입니다 :) 나는 그것이 이해되기를 바랍니다.


답변 해주셔서 감사합니다. 훌륭한 통찰력. 메시지 전달을 디자인하는 이유는 Lua 때문입니다. 새로운 C ++ 코드없이 새로운 무기를 만들 수 있기를 원합니다. 그래서 당신의 생각은 내 대답하지 않은 몇 가지 질문에 대답했습니다.
deft_code

코 루틴에 관해서는 나도 코 루틴에 대한 위대한 신자이지만 C ++에서는 결코 그들과 놀지 못합니다. 루아 코드에서 코 루틴을 사용하여 차단 호출 (예 : 사망 대기)을 처리하려는 모호한 희망이있었습니다. 노력할만한 가치가 있었습니까? C ++에서 코 루틴에 대한 열망에 시달릴 수 있습니다.
deft_code

마지막으로, 아이폰 게임은 무엇입니까? 사용한 엔터티 시스템에 대한 자세한 정보를 얻을 수 있습니까?
deft_code

2
엔터티 시스템은 대부분 C ++로 구성되었습니다. 예를 들어 Imp의 동작을 처리하는 Imp 클래스가있었습니다. Lua는 스폰 또는 메시지를 통해 Imp의 매개 변수를 변경할 수 있습니다. Lua의 목표는 빡빡한 일정에 맞추는 것이 었으며 Lua 코드를 디버깅하는 데 시간이 많이 걸립니다. 우리는 Lua를 사용하여 스크립트 레벨 (어떤 엔티티가 어디로 가야하는지, 트리거를 칠 때 발생하는 이벤트) Lua에서는 SpawnEnt ( "Imp")와 같이 말할 수 있습니다. 여기서 Imp는 수동으로 등록 된 팩토리 연관입니다. 항상 하나의 글로벌 엔티티 풀로 생성됩니다. 좋고 간단합니다. 우리는 많은 smart_ptr과 weak_ptr을 사용했습니다.
BRaffle

1
BananaRaffle :이 답변에 대한 정확한 요약이라고 말 하시겠습니까? "게시 한 3 가지 솔루션 모두 다른 솔루션과 마찬가지로 용도가 있습니다. 완벽한 솔루션 하나를 찾지 말고 필요한 곳에서 사용하십시오. "
Ipsquiggle

76
// in entity_a's code:
entity_b->takeDamage();

상업적 게임이 어떻게되는지 물었습니다. ;)


8
다운 투표? 진심으로, 그것이 정상적으로 수행되는 방식입니다! 엔터티 시스템은 훌륭하지만 초기 이정표를 맞추는 데 도움이되지 않습니다.
tenpn

나는 전문적으로 Flash 게임을 만들고 이것이 내가하는 방법입니다. enemy.damage (10)를 호출 한 다음 공개 게터로부터 필요한 정보를 찾습니다.
Iain

7
이것이 상용 게임 엔진이하는 방식입니다. 그는 농담하지 않습니다. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer 등)는 일반적으로 수행되는 방식입니다.
AA Grapsas

3
상용 게임에도 "손상"철자가 틀렸습니까? :-P
Ricket

15
예, 그들은 무엇보다도 미 스펠 데미지를 입습니다. :)
LearnCocos2D

17

더 심각한 답변 :

칠판이 많이 사용되는 것을 보았습니다. 간단한 버전은 엔티티의 HP와 같은 것으로 업데이트 된 엔티티이며, 엔티티는 쿼리 할 수 ​​있습니다.

칠판은이 실체에 대한 세계의 관점 (B의 칠판에 HP가 무엇인지 물어보기)이거나 실체의 세상에 대한 관점 일 수 있습니다 (A는 칠판에 질의하여 A의 HP가 무엇인지 확인).

프레임의 동기화 지점에서만 칠판을 업데이트하는 경우 나중에 모든 스레드에서 칠판을 읽을 수 있으므로 멀티 스레딩을 구현하기가 매우 간단합니다.

고급 칠판은 문자열을 값에 매핑하는 해시 테이블과 비슷할 수 있습니다. 이것은 유지 관리가 용이하지만 런타임 비용이 분명히 있습니다.

칠판은 전통적으로 단방향 통신입니다. 손상으로 인한 디싱은 처리하지 않습니다.


나는 지금까지 칠판 모델에 대해 들어 본 적이 없다.
deft_code

또한 이벤트 큐 또는 발행 / 구독 모델과 ​​동일한 방식으로 종속성을 줄이는 데 유용합니다.
tenpn

2
이것은 또한 "이상적인"E / C / S 시스템이 어떻게 작동해야하는지에 대한 정식 "정의"입니다. 컴포넌트는 칠판을 구성합니다. 시스템은 그 위에 작용하는 코드입니다. (물론, long long int순수한 ECS 시스템 에서는 엔티티가 s이거나 유사합니다.)
BRPocock

6

나는이 문제를 조금 연구했고 좋은 해결책을 보았습니다.

기본적으로 서브 시스템에 관한 것입니다. tenpn에서 언급 한 칠판 아이디어와 비슷합니다.

엔터티는 구성 요소로 만들어 지지만 속성 백입니다. 엔터티 자체에서는 동작이 구현되지 않습니다.

엔터티에 Health 구성 요소와 Damage 구성 요소가 있다고 가정 해 봅시다.

그런 다음 ActionManager, DamageSystem, HealthSystem과 같은 MessageManager와 3 개의 하위 시스템이 있습니다. 어느 시점에서 ActionSystem은 게임 세계에서 계산을 수행하고 이벤트를 생성합니다.

HIT, source=entity_A target=entity_B power=5

이 이벤트는 MessageManager에 공개됩니다. 이제 특정 시점에서 MessageManager는 보류중인 메시지를 검토하고 DamageSystem이 HIT 메시지를 구독했음을 발견합니다. 이제 MessageManager는 HIT 메시지를 DamageSystem에 전달합니다. DamageSystem은 Damage 구성 요소가있는 엔티티 목록을 살펴보고 적 중력 또는 두 엔티티의 다른 상태 등에 따라 피해 점을 계산하고 이벤트를 게시합니다.

DAMAGE, source=entity_A target=entity_B amount=7

HealthSystem은 DAMAGE 메시지를 구독했으며 이제 MessageManager가 DAMAGE 메시지를 HealthSystem에 공개하면 HealthSystem은 Health 컴포넌트를 사용하여 entity_A 및 entity_B 엔티티 모두에 액세스 할 수 있으므로 HealthSystem은 계산을 수행 할 수 있으며 해당 이벤트를 공개 할 수도 있습니다. MessageManager로 이동).

이러한 게임 엔진에서 메시지 형식은 모든 구성 요소와 하위 시스템 간의 유일한 연결입니다. 서브 시스템과 엔티티는 완전히 독립적이며 서로를 알지 못합니다.

실제 게임 엔진 이이 아이디어를 구현했는지 여부는 알 수 없지만 꽤 견고하고 깨끗해 보입니다. 언젠가는 내 취미 수준의 게임 엔진을 위해 직접 구현하기를 바랍니다.


이것은 허용되는 답변 IMO보다 훨씬 나은 답변입니다. 분리, 유지 및 확장 가능 (및 농담 답변과 같은 결합 재앙이 entity_b->takeDamage();아님)
Danny Yaroslavski

4

글로벌 메시지 큐가없는 이유는 다음과 같습니다.

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

와:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

게임 루프 / 이벤트 처리가 끝날 때 :

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

이것이 커맨드 패턴이라고 생각합니다. 그리고 파생 상품이 정의하고 수행 Execute()하는의 순수한 가상입니다 Event. 여기에 :

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}

3

게임이 싱글 플레이어 인 경우 목표 객체 방법을 사용하십시오 (tenpn이 제안한대로).

멀티 플레이어 (멀티 클라이언트가 정확해야 함) 인 경우 (또는 지원하려는 경우) 명령 대기열을 사용하십시오.

  • A가 클라이언트 1의 B에 피해를 입히면 피해 이벤트를 대기열에 넣습니다.
  • 네트워크를 통해 명령 대기열을 동기화
  • 대기중인 명령을 양쪽에서 처리하십시오.

2
부정 행위를 피하는 것이 진지한 경우 A는 클라이언트의 B를 전혀 손상시키지 않습니다. A를 소유 한 클라이언트는 "attack B"명령을 서버로 전송하는데, 이는 tenpn이 말한 것과 정확히 일치합니다. 그런 다음 서버는 해당 상태를 모든 관련 클라이언트와 동기화합니다.

@Joe : 그렇습니다. 고려할만한 올바른 서버가 있다면 서버 부하를 피하기 위해 클라이언트 (예 : 콘솔)를 신뢰하는 것이 좋습니다.
Andreas

2

나는 말할 것입니다 : 손상으로부터 즉각적인 피드백을 명시 적으로 필요로하지 않는 한 어느 것도 사용하지 마십시오.

손해-취득 엔티티 / 구성 요소 / 물건은 이벤트를 로컬 이벤트 큐 또는 손해 이벤트를 보유하는 동일한 레벨의 시스템으로 푸시해야합니다.

엔터티 a에서 이벤트를 요청하고 엔터티 b로 전달하는 두 엔터티에 액세스 할 수있는 오버레이 시스템이 있어야합니다. 언제 어디서나 이벤트를 전달할 수있는 일반적인 이벤트 시스템을 만들지 않으면 서 코드를보다 쉽게 ​​디버깅하고, 성능을 쉽게 측정하고, 이해하고 읽고 이해하기 쉽게하는 명확한 데이터 흐름을 만듭니다. 보다 잘 설계된 시스템으로 이어집니다.


1

그냥 전화 해 query-hp에 의해 요청 된 hp를 수행하지 마십시오. 해당 모델을 따르는 경우 상처를 입을 수 있습니다.

Mono Continuations도 살펴볼 수 있습니다. NPC에 이상적이라고 생각합니다.


1

플레이어 A와 B가 같은 update () 사이클에서 서로를 때리면 어떻게됩니까? 사이클 1의 플레이어 B에 대한 Update () 전에 플레이어 A에 대한 Update ()가 발생한다고 가정합니다 (또는 틱 또는 호출). 내가 생각할 수있는 두 가지 시나리오가 있습니다.

  1. 메시지를 통한 즉각적인 처리 :

    • 플레이어 A.Update ()는 플레이어가 B를 치고 싶어하는 것을보고, 플레이어 B는 손상을 알리는 메시지를받습니다.
    • player B.HandleMessage ()는 플레이어 B의 히트 포인트를 업데이트합니다 (그는 죽습니다)
    • 플레이어 B.Update ()는 플레이어 B가 죽었다는 것을 본다. 그는 플레이어 A를 공격 할 수 없다

이것은 불공평합니다. 플레이어 A와 B는 서로 충돌해야합니다. 플레이어 B는 나중에 엔티티 / 게임 오브젝트가 update ()를 얻었 기 때문에 A를 치기 전에 사망했습니다.

  1. 메시지 큐

    • 플레이어 A.Update ()는 플레이어가 B를 치고 싶어하는 것을 보았습니다. 플레이어 B는 손상을 알리는 메시지를 받아서 대기열에 저장합니다.
    • 플레이어 A.Update ()가 대기열을 확인합니다. 비어 있습니다.
    • 플레이어 B.Update ()는 먼저 움직임을 확인하여 플레이어 B가 플레이어 A에게 메시지를 보내도록합니다.
    • 플레이어 B.Update ()는 또한 대기열의 메시지를 처리하고 플레이어 A의 피해를 처리합니다.
    • 새로운주기 (2) : 선수 A가 체력 물약을 마시고 싶어서 선수 A.Update ()가 호출되고 이동이 처리됩니다.
    • Player A.Update ()는 메시지 대기열을 확인하고 플레이어 B의 피해를 처리합니다.

플레이어 A는 같은 턴 / 사이클 / 틱에서 히트 포인트를 가져옵니다!


4
당신은 실제로 질문에 대답하지 않지만 당신의 대답은 훌륭한 질문 그 자체가 될 것이라고 생각합니다. 왜 그런 "불공정 한"우선 순위를 해결하는 방법을 물어 보지 않겠습니까?
bummzack

나는 대부분의 게임들이이 불공평에 대해 신경을 쓰는 것이 의심 스럽다. 간단한 해결 방법 중 하나는 업데이트 할 때 엔티티 목록을 앞뒤로 반복하는 것입니다.
Kylotan

나는 2 개의 호출을 사용하므로 모든 엔티티에 대해 Update ()를 호출 한 다음 루프 후에 다시 반복하고 같은 것을 호출합니다 pEntity->Flush( pMessages );. entity_A가 새로운 이벤트를 생성 할 때, 해당 프레임에서 entity_B에 의해 읽히지 않습니다 (포션을 가져갈 기회도 있습니다). 그러면 손상을 받고 그 후에 큐에서 마지막으로 될 포션 치유 메시지를 처리합니다. . 물약 메시지가 queue : P의 마지막이므로 플레이어 B는 여전히 죽지 만 죽은 엔티티에 대한 포인터 지우기와 같은 다른 종류의 메시지에 유용 할 수 있습니다.
Pablo Ariel

프레임 수준에서 대부분의 게임 구현은 단순히 불공평하다고 생각합니다. Kylotan이 말했듯이.
v.oddou

이 문제는 정말 해결하기 쉽습니다. 메시지 처리기 등에서 서로에게 피해를 주면됩니다. 메시지 처리기 내부에서 플레이어를 죽은 것으로 표시해서는 안됩니다. "Update ()"에서 단순히 "if (hp <= 0) die ();" (예 : "Update ()"시작 부분). 그렇게하면 둘 다 동시에 서로를 죽일 수 있습니다. 또한 : 종종 플레이어에게 직접 피해를 입히지 않고 총알과 같은 중간 물체를 통해 피해를줍니다.
Tara
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.