Akka.NET 기본 01

공부한 내용을 개인적으로 정리합니다.
C#으로 작성됩니다

서론

akka는 액터 중심으로 돌아갑니다. 액터는 UE나 Unity의 각종 게임엔진의 오브젝트와 상당히 유사합니다. 한 오브젝트 내부에 여러 다른 오브젝트들이 붙어서 구성되며,

각자 할 일을 따로 수행합니다.


기존 클래스 사용시 하나의 클래스내부에서 무한루프를 돌면 쓰레드를 사용하지 않는 이상 프로그램이 올 스탑 액터는 각각 따로 행동하기 때문에 하나가 무한루프를 돈다고 나머지 액터들이 멈추거나 하지 않는다.

게임엔진에서 대부분 그러하듯, 각각의 객체끼리의 상호작용이 일어나며, 이것을 akka 액터에서는 대화(Tell)로 처리하고 있다.

컴포넌트 구조를 가져와서 적용하면 꽤나 잘 나올거 같은 느낌


기본설정

.Net환경에서 사용하기 위해 NuGet을 통해서 Akka를 설치
ex_screenshot

ex_screenshot 이것으로 끝. 간단하다.


액터(Actor)

액터는 액터시스템(ActorSystem)에 의해서 관리되고 있다. 절대 직접 new 키워드로 생성하지 말란 뜻이다 UE Object를 절대 new로 생성하지 말라는것과 마찬가지. 시스템이 알아서 관리하게 냅두자… 또한 액터끼리의 대화도 액터 시스템을 통해서 하게 된다. (액터 시스템이 생성된 모든 액터를 들고 있고, 액터시스템을 통해서 대화할 액터를 찾아 이야기 하는 방식인듯?)

액터 클래스 생성

액터 클래스는 Akka 네임스페이스에 선언되어있는 UntypedActor를 상속받으면 된다. (다른 Actor 클래스[RecivedActor]를 상속받을수도 있지만, 가장 처음 만난 예시가 그랬다) 또한 UntypedActor는 추상클래스라 반드시 구현해야 하는 함수가 있다.

1
 protected abstract void OnReceive(object message);

OnReceive함수는 액터사이에 대화 중 대화를 받았을 때 호출되는 함수다. 해당 함수 내부에서 어떤 말을 들었는지 판단하고, 처리한다.

1
2
3
4
5
6
7
8
9
using Akka.Actor;

 public class MyActor : UntypedActor
 {
   protected override void OnReceive(object message)
   {
  
   }
}

액터를 생성하는 코드는 다음과 같다.

  1. 먼저 액터 시스템을 생성한다.
1
ActorSystem as = ActorSystem.Create("액터시스템이름");
  1. 생성한 액터 시스템을 통해 액터를 생성한다.
1
IActorRef actorRef = as.ActorOf(Props.Create(()=>new MyActor(),"내가생성하는액터의이름");

혹은

1
IActorRef actorRef = as.ActorOf<MyActor>("내가생성하는액터이름");

두 번째 방법은 나에게 익숙한 방법이다. 또한, 이후에 액터를 구분짓기 위해서 액터 이름을 같이 넘겨주는 것이 나중에 도움이 된다.

생성되어진 액터는 참조로 받아 저장된다.


기다리기

액터 위주의 프로그램이 동작 중일 때, 프로그램을 종료하지 않고 대기하라는 명령이 없다면 액터들이 다른일을 하는 도중이라도 그냥 프로그램이 종료된다. Main문에 다음 코드를 추가하는 것으로 프로그램 종료를 막을 수 있다.

1
actorSystem.WhenTerminated.Wait();

Context

액터 내부에는 이 액터의 정보를 저장하고 있는 Context라는 변수가 있다. 해당 변수에는 액터간의 계층구조에 대한 정보, 메시지를 보낸 대상에 대한 정보, 이 액터의 상태정보 등등이 담겨있다.


Props

액터를 생성하는 코드 안에 들어가는 객체. 액터를 생성하기 위해 필요하다. Props를 이용해서 ActorSystem이 액터를 생성하게 된다. Props도 액터와 마찬가지로 절대 new 키워드로 직접 생성하지 말 것 다음과 같은 코드로 생성 가능

1
Props prop = Props.Create(()=>new MyActor(),"내 액터 이름");

혹은

1
Props prop = Props.Create<MyActor>();

액터와 컴포넌트

UE와 비교해서 생각하자.

한 오브젝트는 여러 개의 수 많은 컴포넌트로 이루어져 있다. 비슷한 개념으로 하나의 액터에 여러 Child액터를 붙이는 것이 가능하다.

요걸 이용해서 대충 적당히 플레이어 캐릭터를 구성해보려 한다.

적당히 계층구조를 다음과 같이 잡는다고 하고,

  • Player
    • StatusComponent
    • EquipmentComponent

클래스를 작성한다면,

1
2
3
4
5
6
7
8
9
10
11
12
 public class Player : UntypedActor
 {

        IActorRef StatusComponent;
        IActorRef EquipmentComponent;

        public Player()
        {
            StatusComponent = Context.ActorOf<StatusComponent>("StatusComponent");
            EquipmentComponent = Context.ActorOf<EquipmentComponent>("EquipmentComponent");
        }
}

기존 UE생성자에서 컴포넌트 만들어 붙이는 것과 거의 비슷한 느낌이다. 액터 내부에서 자식 액터를 생성해야 할 때는 Props가 아닌 Context를 이용해서 생성해야 계층구조가 정상적으로 구성된다 최상위 액터를 생성해야 한다면 기존처럼 Props를 이용해서 액터를 생성하면 된다.


좀 더 UE에 맞춰서 만들어 보자

최상위 UObject를 만들고,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   public class UObject : UntypedActor
    {

        public UObject()
        {
            Console.WriteLine("Call UObject Construct : "+Self.ToString());
        }

        protected override void OnReceive(object message)
        {
            //오브젝트 단계에서는 아무것도 안 함.
  
        }
    }

액터클래스로 분기하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 public class AActor : UObject
    {
        public AActor() : base()
        {

            Console.WriteLine("Call Actor construct : " + Self.ToString());
           
        }


        protected override void OnReceive(object message)
        {

        }
    }

컴포넌트로도 분기하자.

1
2
3
4
5
6
7
   public class UActorComponent : UObject
    {
        public UActorComponent() : base()
        {
            Console.WriteLine("Call ActorComponent construct : " + Self.ToString());
        }
    }

이제 이것들을 가지고 잘 조립하면 될 것 같다.

액터의 계층구조

기존 UE에서 오브젝트 계층구조를 구성하던것과 거의 유사하다. 계층구조를 알아보기 위해서 위에서 만들었던 플레이어를 조금 수정해본다.

  • Player
    • StatusComponent
      • StatusDisplayComponent
    • EquipmentComponent

스테이터스 컴포넌트의 자식으로 스테이터스를 보여주는 컴포넌트를 추가했다고 치자.

1
2
3
4
5
6
7
8
9
10
11
12
public class UStatusComponent : UActorComponent
    {

        IActorRef StatusDisplayComponent;

        public UStatusComponent() : base()
        {
            Console.WriteLine("Call StatusComponent Construct : " + Self.ToString());

            StatusDisplayComponent = Context.ActorOf(Props.Create<UStatusDisplayComponent>(), "DisplayComponent");
        }
    }

이렇게 구성되어 있을 때, 각각 오브젝트들의 자기 정보를 찍어보면,(생성자에서 찍었다)

1
Console.WriteLine(Self.ToString());

ex_screenshot 일단은 잘 모르겠지만, PlayerCharacter아래, EquipmentComponent가 붙었고 PlayerCharacter아래, StatusComponent가 붙었다. 추가로 StatusComponent아래 DisplayComponent가 붙은것을 확인 가능하다. 여기서 표현되는 이름은 클래스명이 아니라 액터 생성시 던져준 액터 이름이 표시된다

PlayerCharacter와 그 아래 구성된 것은 알겠지만, 나머지 앞쪽에 있는 것들은 무엇이냐 하면 다음 구성표를 보면 알 수 있다. ex_screenshot

액터시스템은 최상단 루트(/) 가 있고, 최상단 루트에 두개의 액터가 붙어있다. 왼쪽에 보이는 루트 액터는 사용자가 생성하는 액터가 자식으로 붙으며, 오른쪽에 있는 액터는 사용자가 몰라도 되는, 액터 시스템에 필요한 액터들이 붙는다. 액터 정보를 찍었을 때, 나오는 앞 경로들은 최상단 루트와 사용자 액터 루트의 경로가 붙은 것이다.

대충 위 그림에 붙여보자면 다음처럼 구성될 것이다.

ex_screenshot