구성 요소 기반 엔터티 시스템에서 메시지 처리를 올바르게 구현하는 방법은 무엇입니까?


30

다음과 같은 엔티티 시스템 변형을 구현하고 있습니다.

  • Entity 클래스 ID를보다 조금 더 그 바인딩 구성 요소를 함께

  • "구성 요소 논리"가없고 데이터 만 있는 여러 구성 요소 클래스

  • 많은 시스템 클래스 (일명 "서브 시스템", "관리자"). 이것들은 모든 엔티티 로직 처리를 수행합니다. 대부분의 경우 시스템은 관심있는 엔티티 목록을 반복하고 각 엔티티에 대해 조치를 수행합니다.

  • 모든 게임 시스템에서 공유 하는 MessageChannel 클래스 개체 입니다. 각 시스템은 특정 유형의 메시지를 구독하여들을 수 있으며 채널을 사용하여 다른 시스템으로 메시지를 브로드 캐스트 할 수도 있습니다.

시스템 메시지 처리의 초기 변형은 다음과 같습니다.

  1. 각 게임 시스템에서 순차적으로 업데이트 실행
  2. 시스템이 구성 요소에 어떤 작업을 수행하고 해당 작업이 다른 시스템에 관심이있는 경우 시스템은 적절한 메시지를 보냅니다 (예 : 시스템 호출).

    messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))

    실체가 움직일 때마다)

  3. 특정 메시지를 구독 한 각 시스템은 메시지 처리 방법을 얻습니다.

  4. 시스템이 이벤트를 처리하고 있고 이벤트 처리 로직이 다른 메시지를 브로드 캐스트해야하는 경우 메시지가 즉시 브로드 캐스트되고 다른 메시지 처리 방법 체인이 호출됩니다.

이 변형은 충돌 감지 시스템 최적화를 시작할 때까지 괜찮습니다 (엔티티 수가 증가함에 따라 실제로 느려졌습니다). 처음에는 간단한 무차별 강제 알고리즘을 사용하여 각 엔티티 쌍을 반복합니다. 그런 다음 특정 셀 영역 안에있는 엔터티를 저장하는 셀 격자가있는 "공간 인덱스"를 추가하여 인접 셀의 엔터티 만 검사 할 수 있습니다.

엔티티가 움직일 때마다 충돌 시스템은 엔티티가 새로운 위치의 무언가와 충돌하는지 확인합니다. 그렇다면 충돌이 감지됩니다. 충돌하는 두 엔티티가 모두 "물리적 객체"인 경우 (둘 다 RigidBody 구성 요소를 가지고 있고 동일한 공간을 차지하지 않도록 서로 밀도록 의도 된 경우) 전용 강체 분리 시스템은 움직임 시스템에 엔티티를 일부로 이동하도록 요청합니다 그들을 분리 할 특정 위치. 이로 인해 이동 시스템은 변경된 엔티티 위치를 알리는 메시지를 보냅니다. 충돌 감지 시스템은 공간 인덱스를 업데이트해야하기 때문에 반응합니다.

셀이 반복되는 동안 셀의 내용 (C #의 일반 Entity 오브젝트 목록)이 수정되어 반복자가 예외를 발생시키기 때문에 문제가 발생하는 경우가 있습니다.

그렇다면 충돌을 검사하는 동안 충돌 시스템이 중단되는 것을 어떻게 방지 할 수 있습니까?

물론 셀 내용이 올바르게 반복되도록하는 "영리한"/ "까다로운"논리를 추가 할 수는 있지만 충돌 시스템 자체에 문제가있는 것은 아니라고 생각합니다 (다른 시스템에서도 비슷한 문제가 있음). 메시지는 시스템에서 시스템으로 이동할 때 처리됩니다. 내가 필요한 것은 특정 이벤트 처리 방법이 중단없이 작동하도록하는 방법입니다.

내가 시도한 것 :

  • 수신 메시지 대기열 . 일부 시스템이 메시지를 브로드 캐스트 할 때마다 메시지가 관심이있는 시스템의 메시지 큐에 추가됩니다. 이 메시지는 시스템 업데이트가 각 프레임에서 호출 될 때 처리됩니다. 문제 : 시스템 A가 시스템의 B 대기열에 메시지를 추가하면 시스템 B가 시스템 A보다 나중에 동일한 게임 프레임에서 업데이트되어야하는 경우 잘 작동합니다. 그렇지 않으면 메시지가 다음 게임 프레임을 처리하게합니다 (일부 시스템에서는 바람직하지 않음)
  • 발신 메시지 대기열 . 시스템이 이벤트를 처리하는 동안 브로드 캐스트하는 모든 메시지가 발신 메시지 큐에 추가됩니다. 메시지는 시스템 업데이트가 처리 될 때까지 기다릴 필요가 없습니다. 초기 메시지 처리기가 작업을 마치면 "바로"처리됩니다. 메시지 처리로 인해 다른 메시지가 브로드 캐스트되면 발신 큐에도 추가되므로 모든 메시지가 동일한 프레임으로 처리됩니다. 문제: 엔터티 수명 시스템 (시스템으로 엔터티 수명 관리를 구현 한 경우)이 엔터티를 생성하면 일부 시스템 A 및 B에이를 알립니다. 시스템 A가 메시지를 처리하는 동안 메시지 체인이 생성되어 결국에는 생성 된 엔티티가 손상됩니다 (예 : 총알 엔티티가 장애물과 충돌 할 때 생성되어 총알 자체 파괴). 메시지 체인이 해결되는 동안 시스템 B는 엔티티 작성 메시지를받지 않습니다. 따라서 시스템 B가 엔티티 삭제 메시지에도 관심이있는 경우이를 가져오고 "체인"분석이 완료된 후에 만 ​​초기 엔티티 작성 메시지를받습니다. 이로 인해 삭제 메시지가 무시되고 작성 메시지가 "수락"됩니다.

편집-질문, 의견에 대한 답변 :

  • 충돌 시스템이 반복하는 동안 셀의 내용을 수정하는 사람은 누구입니까?

충돌 시스템이 일부 엔티티와 인접 엔티티에서 충돌 검사를 수행하는 동안 충돌이 감지 될 수 있으며 엔티티 시스템은 다른 시스템에 의해 즉시 반응 할 메시지를 보냅니다. 메시지에 대한 반응으로 다른 메시지가 생성되어 즉시 처리 될 수도 있습니다. 따라서 일부 다른 시스템은 충돌 충돌 검사가 아직 완료되지 않았어도 충돌 시스템이 즉시 처리해야한다는 메시지를 생성 할 수 있습니다 (예 : 엔티티가 이동하여 충돌 시스템이 공간 인덱스를 업데이트해야 함).

  • 글로벌 발신 메시지 대기열로 작업 할 수 없습니까?

최근에 단일 글로벌 대기열을 시도했습니다. 새로운 문제가 발생합니다. 문제 : 탱크 엔티티를 벽 엔티티로 옮깁니다 (탱크는 키보드로 제어 됨). 그런 다음 탱크 방향을 변경하기로 결정했습니다. 탱크와 벽을 각 프레임을 분리하기 위해 CollidingRigidBodySeparationSystem은 탱크를 벽에서 가장 작은 양만큼 이동시킵니다. 분리 방향은 탱크의 이동 방향의 반대 방향이어야합니다 (게임 그림이 시작될 때 탱크는 절대로 벽으로 이동하지 않은 것처럼 보입니다). 그러나 방향은 NEW 방향과 반대가되어 탱크를 처음과 다른 벽면으로 이동시킵니다. 문제가 발생하는 이유 : 메시지가 현재 처리되는 방식입니다 (간단한 코드).

public void Update(int deltaTime)
{   
    m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
    while (m_messageQueue.Count > 0)
    {
        Message message = m_messageQueue.Dequeue();
        this.Broadcast(message);
    }
}

private void Broadcast(Message message)
{       
    if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
    {
        // NOTE: all IMessageListener objects here are systems.
        List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
        foreach (IMessageListener listener in messageListeners)
        {
            listener.ReceiveMessage(message);
        }
    }
}

코드는 다음과 같이 흐릅니다 (첫 번째 게임 프레임이 아니라고 가정합니다).

  1. 시스템이 TimePassedMessage 처리를 시작합니다
  2. InputHandingSystem은 키 누름을 엔티티 조치로 변환합니다 (이 경우 왼쪽 화살표는 MoveWest 조치로 바)). 엔티티 조치는 ActionExecutor 구성 요소에 저장됩니다.
  3. 엔티티 조치에 대한 조치로 ActionExecutionSystem 은 MovementDirectionChangeRequestedMessage를 메시지 큐의 끝에 추가합니다.
  4. MovementSystem 은 Velocity 컴포넌트 데이터를 기반으로 엔티티 위치를 이동하고 PositionChangedMessage 메시지를 큐의 끝에 추가합니다. 이전 프레임의 이동 방향 / 속도를 사용하여 이동합니다 (북쪽이라고합시다).
  5. 시스템이 TimePassedMessage 처리를 중지합니다
  6. 시스템이 MovementDirectionChangeRequestedMessage 처리를 시작합니다
  7. MovementSystem 은 요청에 따라 엔티티 속도 / 이동 방향을 변경합니다
  8. 시스템이 MovementDirectionChangeRequestedMessage 처리를 중지합니다
  9. 시스템이 PositionChangedMessage 처리를 시작합니다
  10. CollisionDetectionSystem 은 엔티티가 이동했기 때문에 다른 엔티티 (탱크가 벽 안으로 들어가는)에 부딪 혔음을 감지합니다. CollisionOccuredMessage를 대기열에 추가합니다.
  11. 시스템이 PositionChangedMessage 처리를 중지합니다
  12. 시스템이 CollisionOccuredMessage 처리를 시작합니다
  13. 충돌 RigidBodySeparationSystem 은 탱크와 벽을 분리하여 충돌에 반응합니다. 벽은 정적이므로 탱크 만 이동합니다. 탱크의 이동 방향은 탱크의 출처를 나타내는 지표로 사용됩니다. 반대 방향으로 오프셋

BUG : 탱크가이 프레임을 움직였을 때, 이전 프레임의 이동 방향을 사용하여 이동했지만, 분리 될 때이 프레임의 이동 방향은 이미 다르지만 사용되었습니다. 그것이 작동하는 방식이 아닙니다!

이 버그를 방지하려면 이전 이동 방향을 어딘가에 저장해야합니다. 이 특정 버그를 수정하기 위해 일부 구성 요소에 추가 할 수 있지만이 경우 메시지를 처리하는 근본적으로 잘못된 방법을 나타내지 않습니까? 분리 시스템이 어떤 이동 방향을 사용해야합니까? 이 문제를 어떻게 우아하게 해결할 수 있습니까?

  • gamadu.com/artemis를 읽고 Aspects에서 수행 한 작업을 확인하고, 어느 측면에서 현재 문제를 해결하고 있는지 확인할 수 있습니다.

사실, 나는 꽤 오랫동안 Artemis에 익숙했습니다. 소스 코드를 조사하고 포럼 등을 읽었습니다. 그러나 "Aspects"가 몇 군데에서만 언급 된 것을 보았습니다. 이해하는 한, 기본적으로 "Systems"를 의미합니다. 그러나 아르테미스 측이 어떻게 내 문제를 어떻게 해결하는지 알 수 없습니다. 심지어 메시지를 사용하지 않습니다.

  • "엔터티 통신 : 메시지 큐 대 발행 / 구독 대 신호 / 슬롯"도 참조하십시오.

엔티티 시스템에 관한 모든 gamedev.stackexchange 질문을 이미 읽었습니다. 이것은 내가 직면 한 문제를 논의하지 않는 것 같습니다. 뭔가 빠졌습니까?

  • 그리드를 업데이트 할 때 충돌 시스템의 일부이므로 이동 메시지에 의존 할 필요가 없습니다.

무슨 말인지 잘 모르겠습니다. CollisionDetectionSystem의 이전 구현은 업데이트에서 충돌을 확인하지만 (TimePassedMessage가 처리 된 경우) 성능으로 인해 가능한 한 확인을 최소화해야했습니다. 따라서 엔터티가 움직일 때 충돌 검사로 전환했습니다 (게임에서 대부분의 엔터티는 정적 임).


나에게 분명하지 않은 것이 있습니다. 충돌 시스템이 반복하는 동안 셀의 내용을 수정하는 사람은 누구입니까?
Paul Manta 2019

글로벌 발신 메시지 대기열로 작업 할 수 없습니까? 따라서 시스템이 완료된 후 시스템의 모든 메시지가 전송됩니다. 여기에는 시스템 자체 소멸이 포함됩니다.
Roy T.

이 복잡한 디자인을 유지하려면 @RoyT를 따라야합니다. 조언은 시퀀싱 문제를 처리 할 수있는 유일한 방법입니다 (복잡하고 시간 기반 메시징이없는). gamadu.com/artemis 를 읽고 Aspects에서 수행 한 작업 을 확인하고, 어느 측면에서 현재 문제를 해결하고 있는지 확인할 수 있습니다.
Patrick Hughes


2
CTP를 다운로드하고 일부 코드를 컴파일하여 Axum 이 어떻게 수행했는지 배우고 ILSpy를 사용하여 결과를 C #으로 리버스 엔지니어링 할 수 있습니다. 메시지 전달은 액터 모델 언어의 중요한 기능이며 Microsoft가 자신이하는 일을 알고 있다고 확신하므로 '최상의'구현을 찾을 수 있습니다.
조나단 디킨슨

답변:


12

God / Blob 객체 안티 패턴에 대해 들어봤을 것입니다. 문제는 God / Blob 루프입니다. 메시지 전달 시스템을 땜질하는 것은 기껏해야 반창고 솔루션을 제공하며 최악의 시간 낭비입니다. 사실, 당신의 문제는 게임 개발과 전혀 관련이 없습니다. 컬렉션을 여러 번 반복하면서 컬렉션을 수정하려고 시도했으며 솔루션은 항상 같습니다. 세분화, 세분화, 세분화.

귀하의 질문에 대한 문구를 이해 한 것처럼 현재 충돌 시스템을 업데이트하는 방법은 다음과 같습니다.

for each possible collision
    check for collision
    handle collision
    modify collision world to reflect change // exception happens here

이와 같이 작성하면 루프에 하나만 있어야 할 때 세 가지 책임이 있음을 알 수 있습니다. 문제를 해결하려면 현재 루프를 세 가지 알고리즘 패스를 나타내는 세 개의 개별 루프로 분할하십시오 .

for each possible collision
    check for collision, record it if a collision occurs

for each found collision
    handle collision, record the collision response (delete object, ignore, etc.)

for each collision response
    modify collision world according to response

원래 루프를 세 개의 하위 루프로 세분화하면 더 이상 현재 반복되는 컬렉션을 수정하려고 시도하지 않습니다. 또한 원래 루프에서보다 더 많은 작업을 수행하지 않으며 실제로 동일한 작업을 여러 번 순차적으로 수행하여 일부 캐시 승리를 얻을 수 있습니다.

코드에 병렬 처리 를 도입 할 수 있다는 추가 이점도 있습니다 . 각 루프 반복은 잠재적으로 충돌 세계에서 읽고 쓸 수 있기 때문에 결합 루프 방식은 본질적으로 직렬입니다 (기본적으로 동시 수정 예외에서 알려주는 것입니다). 그러나 위에서 제시 한 세 가지 서브 루프는 모두 읽거나 쓰지만 둘다는 아닙니다. 최소한 첫 번째 패스에서 가능한 모든 충돌을 확인하고 난처하게 평행을 이루 었으며 코드 작성 방법에 따라 두 번째 및 세 번째 패스도 마찬가지입니다.


나는 이것에 전적으로 동의합니다. 나는 내 게임에서 이와 매우 비슷한 접근법을 사용하고 있으며 이것이 장기적으로 가치가 있다고 생각합니다. 이것이 충돌 시스템 (또는 관리자)이 작동하는 방식입니다 (메시지 시스템이 전혀 없을 수 있다고 생각합니다).
Emiliano

11

구성 요소 기반 엔터티 시스템에서 메시지 처리를 올바르게 구현하는 방법은 무엇입니까?

동기 및 비동기의 두 가지 유형의 메시지를 원한다고 말하고 싶습니다. 동기식 메시지는 즉시 처리되는 반면 비동기식은 동일한 스택 프레임에서 처리되지 않지만 동일한 게임 프레임에서 처리 될 수 있습니다. 일반적으로 "메시지 클래스 별"기반으로 결정됩니다 (예 : "모든 EnemyDied 메시지는 비동기식입니다").

일부 이벤트는 이러한 방법 중 하나를 사용하여 훨씬 쉽게 처리 됩니다. 예를 들어, 내 경험에 따르면 ObjectGetsDeletedNow-이벤트가 훨씬 덜 섹시하고 콜백은 ObjectWillBeDeletedAtEndOfFrame보다 구현하기가 훨씬 어렵습니다. 그런 다음 "veto"와 같은 메시지 처리기 (Shield-effect가 DamageEvent를 수정하는 것처럼 실행 중에 특정 작업을 취소하거나 수정할 수있는 코드 )는 비동기 환경에서는 쉽지 않지만 케이크 조각입니다. 동기식 호출.

경우에 따라 비동기가 더 효율적일 수 있습니다 (예 : 나중에 객체가 삭제 될 때 일부 이벤트 핸들러를 건너 뛸 수 있음). 때로는 동기식이 더 효율적입니다. 특히 이벤트 의 매개 변수 를 계산하는 데 비용이 많이 들고 이미 계산 된 값 대신 특정 매개 변수를 검색하기 위해 콜백 함수를 전달하려고합니다 (아무도이 특정 매개 변수에 관심이없는 경우).

동기 전용 메시지 시스템에 대한 또 다른 일반적인 문제점을 이미 언급했습니다. 동기식 메시지 시스템에 대한 나의 경험에 따르면, 가장 일반적인 오류 및 슬픔 중 하나는 이러한 목록을 반복하면서 목록을 변경하는 것입니다.

생각해보십시오 : 동기식 (일부 조치의 모든 사후 영향을 즉시 처리) 및 메시지 시스템 (발신자와 수신자를 분리하여 발신자가 조치에 반응하는 사람을 알 수 없음)이므로 쉽게 수행 할 수 없습니다. 그런 고리를 발견하십시오. 내가 말하는 것은 이런 종류의 자체 수정 반복을 많이 처리 할 준비를하는 것입니다. "디자인에 의한"종류입니다. ;-)

충돌을 검사하는 동안 충돌 시스템이 중단되지 않도록하려면 어떻게해야합니까?

충돌 감지와 관련된 특정 문제의 경우 충돌 이벤트를 비동기로 만드는 것이 좋을 수 있으므로 충돌 관리자가 완료되고 나중에 하나의 배치로 (또는 프레임의 일부 지점에서) 실행될 때까지 큐에 대기됩니다. 이것이 솔루션 "들어오는 대기열"입니다.

문제 : 시스템 A가 시스템의 B 대기열에 메시지를 추가하는 경우 시스템 B가 시스템 A보다 나중에 동일한 게임 프레임에서 업데이트 될 경우 제대로 작동합니다. 그렇지 않으면 메시지가 다음 게임 프레임을 처리하게합니다 (일부 시스템에서는 바람직하지 않음)

쉬운:

while (! queue.empty ()) {queue.pop (). handle (); }

메시지가 남아 있지 않을 때까지 큐를 반복해서 실행하십시오. (이제 "endless loop"를 비명을 지르는 경우 다음 프레임으로 지연 될 경우이 메시지가 "message spamming"으로 표시 될 것입니다. 기분이 좋으면;))


비동기 메시지가 처리 될 때 정확히 "언제"에 대해서는 언급하지 않습니다. 제 생각에는 충돌 감지 모듈이 메시지를 완료 한 후 메시지를 플러시 할 수 있습니다. 이것을 "루프가 끝날 때까지 지연된 동기식 메시지"또는 "반복하는 동안 수정할 수있는 방식으로 반복을 구현하는"멋진 방법으로 생각할 수도 있습니다.
Imi

5

실제로 ECS의 데이터 지향 설계 특성을 활용하려는 경우이를 수행하는 가장 DOD 방식에 대해 생각할 수 있습니다.

BitSquid 블로그 (특히 이벤트에 대한 부분)를 살펴보십시오 . ECS와 잘 맞 물리는 시스템이 제시됩니다. ECS의 시스템이 구성 요소 별 방식과 동일한 방식으로 모든 이벤트를 깔끔한 메시지 별 대기열로 버퍼링하십시오. 이후에 업데이트 된 시스템은 특정 메시지 유형에 대해 큐를 효율적으로 반복하여 처리 할 수 ​​있습니다. 아니면 그냥 무시하십시오. 어느 쪽이든

예를 들어 CollisionSystem은 충돌 이벤트로 가득 찬 버퍼를 생성합니다. 충돌 후 실행 된 다른 시스템은 목록을 반복하여 필요에 따라 처리 할 수 ​​있습니다.

메시지 등록 등의 복잡성없이 ECS 디자인의 데이터 지향 병렬 특성을 유지합니다. 실제로 특정 유형의 이벤트를 처리하는 시스템 만 해당 유형의 큐를 반복하고 메시지 큐에 대해 일관된 단일 패스 반복을 수행하는 것이 최대한 효율적입니다.

각 시스템에서 구성 요소를 일관되게 정렬 한 상태로 유지하면 (예 : 엔티티 ID 또는 이와 유사한 방식으로 모든 구성 요소를 정렬) 메시지를 반복하여 구성 요소에서 해당 구성 요소를 찾을 수있는 가장 효율적인 순서로 메시지가 생성된다는 이점도 있습니다 처리 시스템. 즉, 엔터티 1, 2 및 3이있는 경우 메시지는 순서대로 생성되며 메시지를 처리하는 동안 수행되는 구성 요소 조회는 주소 순서가 엄격하게 증가합니다 (가장 빠름).


1
+1이지만이 방법에는 단점이 없습니다. 이로 인해 시스템 간의 상호 종속성을 하드 코딩하지 않습니까? 아니면 이러한 상호 의존성은 어떤 식 으로든 하드 코딩되어 있어야합니까?
Patryk Czachurski

2
@Daedalus : 게임 로직이 올바른 로직을 수행하는 물리학 업데이트를 필요로하는 경우, 당신은 어떻게되어 있지 그 의존성을해야 할 것? pubsub 모델을 사용하더라도 다른 시스템에서만 생성되는 메시지 유형을 명시 적으로 구독해야합니다. 의존성을 피하는 것은 어렵고 대부분 올바른 레이어를 알아내는 것입니다. 예를 들어 그래픽과 물리는 독립적이지만 보간 된 물리 시뮬레이션 업데이트가 그래픽 등에 반영되도록하는 더 높은 레벨의 접착 레이어가 있습니다.
Sean Middleditch

이것이 정답입니다. 이를 수행하는 간단한 방법은 충돌이 발생한 후 작업을 수행하는 데 관심이있는 모든 시스템에서 처리 할 CollisionResolvable과 같은 새로운 유형의 구성 요소를 만드는 것입니다. 이는 Drake의 제안에 잘 맞지만 모든 세분화 루프를위한 시스템이 있습니다.
user8363
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.