System

ECS에서 System 컴포넌트는 로직 처리를 위한 클래스를 지칭하는 것으로 ISystem 인터페이스 클래스를 상속합니다.

EcxRx에서 System 컴포넌트를 작성하기 위해서는 ReactToXXX와 Execute 함수를 정의하는 방법에 대해서 이해해야 합니다.

  • ReactToXXX 함수에서는 Rx의 스트림 소스로 어떤 이벤트를 어떻게 흘려 보낼지에 대한 처리한다.
  • Execute 함수에서는 ReacToXXX 함수에서 발생한 이벤트의 구독(Subscribe)에 대한 처리를 한다.

EcsRx의 System에서는 Group에 대한 처리인지, Entiy에 대한 처리인지 혹은 Group이나 Entity의 내부가 아니라 이들 시스템 외부에서의 처리인지와 같이 처리의 편의성을 위해 몇 가지 클래스로 나누어져 있으며 이들 클래스에 따라 ReactToXXX 함수의 이름이 다릅니다.

  • ISetupSystem
  • IReactToGroupSystem.ReactToGroup
  • IReactToEntitySystem.ReactToEntity
  • IManualSystem
  • EventReactionSystem
  • IReactToDataSystem.ReactToEntity

클래스에 따라 ReacToXXX 대신 다른 함수를 정의해야 할 수도 있지만 Rx의 스트림 소스에 대한 처리를 한다는 점은 동일합니다.

그럼, 이제 이들 System 타입별로 EcxRx에서 어떻게 System 컴포넌트를 구현하는지에 대해서 살펴 보도록 하겠습니다.

System 타입

ISetupSystem

(coming soon...)

IReactToGroupSystem

Group에 속하는 모든 Entity들의 변경 사항을 한번의 Subsribe로 처리한다.

PlayerMovementSystem.cs

public class PlayerMovementSystem : IReactToGroupSystem
{
    private readonly IGroup _targetGroup = new Group(typeof(MovementComponent), typeof(PlayerComponent));

    // ISystem의 인터페이스
    public IGroup TargetGroup { get { return _targetGroup; } }
    ...
    // IReactToGroupSystem의 인터페이스
    public IObservable<GroupAccessor> ReactToGroup(GroupAccessor @group)
    {
        return _eventSystem.Receive<PlayerTurnEvent>().Select(x => @group);
    }
    ...
    // IReactToGroupSystem의 인터페이스
    public void Execute(IEntity entity)
    {
      ...
    }

모든 System 클래스에서는 TargetGroup 인터페이스를 구현해야 합니다.

PlayerMovementSystem System은 MovementComponentPlayerComponent 컴포넌트를 가지는 Entity를 위한 System이라는 것을 지정하는 역할을 합니다.

ReactToGroup 인터페이스의 구현은 Group 대한 Rx 처리로 이해하면 쉽습니다.

PlayerMovementSystem.ReactToGroup에서의 처리는 PlayerTurnEvent 이벤트가 발생할 때마다 PlayerMovementSystem.Execute 함수가 Rx의 IObservable.Subsribe 내에서 호출됩니다.

매 Update 프레임마다 처리해야 하는 경우라면 Observable.EveryUpdate를 사용하면 됩니다.

PlayerMovementSystem.cs

public IObservable<GroupAccessor> ReactToGroup(GroupAccessor @group)
{
    return Observable.EveryUpdate().Select(x => @group);
}

그러면 PlayerTurnEvent 메시지가 발생할 때마다 실행되는 PlayerMovementSystem.Execute 함수를 살펴 보죠.

PlayerMovementSystem.cs

public void Execute(IEntity entity)
{
    var movementComponent = entity.GetComponent<MovementComponent>();

    // 움직이고 있는 중이라면 리턴
    if (movementComponent.Movement.Value != Vector2.zero)
    { return; }

    if (entity.HasComponent<StandardInputComponent>())
    {
        // StandardInputComponent의 대기중인 값을 MovementComponent에 할당
        var inputComponent = entity.GetComponent<StandardInputComponent>();
        movementComponent.Movement.Value = inputComponent.PendingMovement;
        inputComponent.PendingMovement = Vector2.zero;
        return;
    }

IReactToEntitySystem

MovementSystem.cs

public class MovementSystem : IReactToEntitySystem
{
    ...
    private readonly IGroup _targetGroup = new Group(typeof(ViewComponent), typeof(MovementComponent));
    ...
    public IGroup TargetGroup { get { return _targetGroup; } }
    ...
    public IObservable<IEntity> ReactToEntity(IEntity entity)
    {
        var movementComponent = entity.GetComponent<MovementComponent>();
        return movementComponent.Movement
                    .DistinctUntilChanged()
                    .Where(x => x != Vector2.zero)
                    .Select(x => entity);
    }

    public void Execute(IEntity entity)
    {
      ...
    }
    ...

MovementSystem의 TargetGroup은 ViewComponentMovementComponent 컴포넌트입니다. 이것이 의미하는 바는 이 System은 ViewComponentMovementComponent 컴포넌트를 가지는 Entity를 위한 System 중 하나라는 이야기입니다.

ReactToEntity 인터페이스의 구현은 Entity 대한 Rx 처리로 이해할 수 있습니다.

MovementSystem.ReactToEntity에서의 처리를 보면 MovementComponent.Movement의 값의 변경이 있을 때 이 값이 Vector2.Zero가 아닌 경우라면 Subsribe에서 MovementSystem의 Execute가 호출됩니다.

IManualSystem

System은 Entity의 로직을 처리하는 것이 일반적이지만 만약에 Enity 내부가 아닌 외부에서 Entity에 접근해서 로직을 처리하는 일이 필요한 경우에 사용할 수 있는 것이 바로 IManualSystem입니다.

ecsrx.roguelike2d 예제에서 플레이어 캐릭터 및 적 캐릭터의 위치 변경 처리를 하는 TurnsSystem은 IManualSystem 인터페이스 클래스를 상속하는 System 클래스입니다.

IManualSystem 인터페이스는 StartSystem과 StopSystem의 두 인터페이스의 구현이 필요합니다.

IManualSystem.cs

public interface IManualSystem : ISystem
{
    void StartSystem(GroupAccessor group);
    void StopSystem(GroupAccessor group);
}

StartSystem은 System 시작시 호출되는 함수입니다.

TurnsSystem.cs

public class TurnsSystem : IManualSystem
{
  ...
  public void StartSystem(GroupAccessor @group)
  {
    this.WaitForScene().Subscribe(x => _level = _levelAccessor.Entities.First());

    _updateSubscription = Observable.EveryUpdate().Where(x => IsLevelLoaded())
        .Subscribe(x => {
            if (_isProcessing) { return; }
            MainThreadDispatcher.StartCoroutine(CarryOutTurns(@group));
        });
  }
}

MainThreadDispatcher.StartCoroutine 함수는 UniRx에서 제공하는 코루틴 실행 함수입니다.

플레이어 캐릭터와 적 캐릭터의 이동 처리는 CarryOutTurns 코루틴 함수에서 처리합니다.

TurnsSystem.cs

private IEnumerator CarryOutTurns(GroupAccessor @group)
{
    _isProcessing = true;
    yield return new WaitForSeconds(_gameConfiguration.TurnDelay);

    if(!@group.Entities.Any())
    { yield return new WaitForSeconds(_gameConfiguration.TurnDelay); }

    // Enemy Entity의 이동 이벤트 메시지 처리.
    var enemies = @group.Entities.ToArray();
    foreach (var enemy in enemies)
    {
        _eventSystem.Publish(new EnemyTurnEvent(enemy));
        yield return new WaitForSeconds(_gameConfiguration.MovementTime);
    }

    // Player Entity의 이동 이벤트 메시지 처리.
    _eventSystem.Publish(new PlayerTurnEvent());

    _isProcessing = false;
}

그런데 왜 매번 Update 프레임마다 PlayerTurnEvent 이벤트 메시지를 보낼까요?

PlayerMovementSystem의 Execute 함수를 PlayerTurnEvent 메시지가 발생할 때마다 실행하기 위해서입니다.

PlayerMovementSystem.cs

public class PlayerMovementSystem : IReactToGroupSystem
{
  public IObservable<GroupAccessor> ReactToGroup(GroupAccessor @group)
  {
      return _eventSystem.Receive<PlayerTurnEvent>().Select(x => @group);
  }  
}

EventReactionSystem

EventReactionSystem은 ManualSyste처럼 Entity 외부에 위치해서 메시지 이벤트에 대한 처리를 담당하는 System의 한 형태입니다.

EnemyAttackedSystem.cs

public class EnemyAttackedSys : EventReactionSystem<EnemyHitEvent>
{
    public override void EventTriggered(EnemyHitEvent eventData)
    {
        ...
    }
}

EnemyHitEvent 이벤트 메시지가 발송되면 정의한 EnemyAttackedSys.EventTriggered 함수를 호출해서 처리한다고 이해하면 됩니다.

아래는 UniRx의 MessageBroker를 이용해서 플레이어 캐릭터가 적 캐릭터를 공격하는 EnemyHitEvent 이벤트 메시지의 발송 처리를 하는 코드입니다.

MovementSystem.cs

private void EnemyHit(IEntity enemy, IEntity player)
{
    _eventSystem.Publish(new EnemyHitEvent(enemy, player));
}

EnemyHitEvent, PlayerHitEvent, WallHitEvent 의 충돌 이벤트는 모두 이동시 발생하는 이벤트 메시지로 MovementSystem.Execute 함수에서 처리합니다.

MovementSystem.cs

public void Execute(IEntity entity)
{
    var view = entity.GetComponent<ViewComponent>().View;
    var movementComponent = entity.GetComponent<MovementComponent>();

    Vector2 currentPosition = view.transform.position;
    var destination = currentPosition + movementComponent.Movement.Value;
    var collidedObject = CheckForCollision(view, currentPosition, destination);
    var canMove = collidedObject == null;

    var isPlayer = entity.HasComponent<PlayerComponent>();

    if (!canMove)
    {
        movementComponent.Movement.Value = Vector2.zero;

        var entityView = collidedObject.GetComponent<EntityView>();
        if(!entityView) { return; }

        if (isPlayer && collidedObject.tag.Contains("Wall"))
        { WallHit(entityView.Entity, entity); }

        // 현재 움직인 Entity가 플레이어 캐릭터이고 이 Entity가 충돌한 객체의 tag가 "Enemy"  인 경우
        // 플레이어 캐릭터의 이동 방향에 적 캐릭터가 위치해 있어 움직일 수 없는 경우
        // 플레이어 캐릭터가 적 캐릭터를 공격!
        if (isPlayer && collidedObject.tag.Contains("Enemy"))
        { EnemyHit(entityView.Entity, entity); }

        // 적 캐릭터의 이동 방향에 플레이어 캐릭터가 위치해 있어 움직일 수 없는 경우
        // 적 캐릭터가  플레이어 캐릭터를 공격!
        if(!isPlayer && collidedObject.tag.Contains("Player"))
        { PlayerHit(entityView.Entity, entity); }

        return;
    }

    // MicroCroroutine으로 코루틴 함수 실행
    var rigidBody = view.GetComponent<Rigidbody2D>();
    MainThreadDispatcher.StartUpdateMicroCoroutine(
        SmoothMovement(view, rigidBody, destination, movementComponent));

    _eventSystem.Publish(new EntityMovedEvent(isPlayer));

    if (isPlayer)
    {
        var playerComponent = entity.GetComponent<PlayerComponent>();
        playerComponent.Food.Value--;
    }
}

MainThreadDispatcher.StartUpdateMicroCoroutine 함수는 UniRx에서 제공하는 MicroCroroutine 함수 기능으로 기본적인 코루틴 함수와 비교해서 훨씬 더 효율적인 메모리 사용과 더 빠른 속도를 가진 코루틴 실행 기능입니다. 함수 호출시 managed-unmanaged 코드간 함수 호출로 발생하는 오버헤드를 피함으로써 10배 정도 빠르게 실행하는 것이 가능합니다. 단, 사용시 yield return null 만 리턴할 수 있다는 점에 주의해야 합니다.

IReactToDataSystem

IReactToDataSystem은 IReactToEntitySystem과 유사하지만 Entity가 아닌 데이터의 이벤트 스트림을 Rx를 이용해서 처리하는 System입니다.

IObservable<CollisionEvent> ReactToEntity(IEntity entity)
{
    // EntityCollisionEvent 메시지를 받은 경우, 메시지의 collidee가 인자인 entity와
    // 동일하면 Execute 를 실행.
    return MessageBroker.Receive<EntityCollisionEvent>()
        .Single(x => x.collidee == entity);
}

Rx의 Single 연산자는 이벤트 통지를 한번만 하고 OnCompoleted 완료 메시지를 통지하는 연산자입니다. 즉, OnNext 이벤트 통지가 없는 연산자입니다.

IReactToDataSystem의 경우 Execute 함수에서 CollisionEvent라는 데이터를 처리할 수 있다는 점이 IReactToEntitySystem이나 IReactToGroupSystem과 다른 점입니다.

results matching ""

    No results matching ""