Akka.NET 으로 온라인 게임 서버 만들기

Akka.NET 으로 온라인 게임 서버 만들기

Online game server on Akka.NET Esun Kim [email protected] | github/veblush Agenda 1. Introduction 2. Why Akka.NET ? 3. Introduction to Akka.NET 4. Addition to Akka.NET 5. Real-time online Tic-tac-toe 6. Conclusion Introduction

Why and what did I do? Previous game servers KartRider (2004) Everplanet (2010) MonsterSweeperz P2P game Server

MMORPG server Mobile game server C++ / IOCP socket C++ / IOCP socket C# / IOCP socket (2015) Development Process

Small team - No dedicated server dev. - Everyone does everything. - Client is developed first and server will be followed. - e.g. Server of KartRider was built just before 3rd closed-beta test. Development Process Rapid development: Good and Bad - 2-4 weeks working days for building server architecture. - Only meets minimum requirements. - Agile and no wastes. - But hits the architectural limitation soon.

- a few years after launch? - Very hard to revamp core design of server in live service. - Its better to have more general and flexible library for creating new game contents that has never been Research Invest time for getting flexible tools - 12 weeks - Includes research and building demo games. - Research - Akka.NET, Orleans and more - Building demo app and game - Chatty, Tic-tac-toe and Snake

- These tools will be used for making next game. Why Akka.NET ? What is Akka.NET? Akka.NET ? A toolkit providing actor-base concurrency on the JVM. Akka.NET is a port of the Akka to .NET. Why actor model? Stateful online game objects

Actor State M3 M2 - User, Character, World, and so on. - Real-time interaction between objects. M1 Behavior Actor is good to deal with many stateful objects.

- Already adopted and used widely in the game industry. Why actor model? // actor.receiver UserActor.OnChat(Chat m) { _count += 1; Print(m.Message); } // sender GetUserActor().Send( new Chat { Message = "Hello?" }); UserActor _count

M3 M2 M1 OnChat Why .NET? To make client and server use same language - Client is built with Unity3D and written with C# on .NET. - Client and server can share domain code. - No context switch cost for developing both at the

same time. Good platform - Good performance :) - Can target multiple platforms with .NET Core. - Can use Linux without worrying about subtle Why Akka.NET ? Considerations to choose one - Should be an active open source project. - Should be stable enough to be used right now. - Should be not hard to support .NET 3.5. - Unity3D still relies on .NET 3.5.

Candidates - In-house library - Orleans http://dotnet.github.io/orleans - Akka.NET http://getakka.net Candidates: In-house library The way to improve in-house library - Small actor and network library which has been written for the previous project MonsterSweeperz - Requires tons of effort to support general genre games. Priority - We need to concentrate on making game rather than

library. - It must be fun to build new library, but product itself is first. Candidates: Orleans Open source project by MS Research - 1.0 was released in 2015 after a few years research. - Cloud agnostic. - Different with Azure Service Fabric. Virtual Actor - New concept, Virtual Actor - Its like a virtual memory or GC to help easy programmin

- Seems promising but Im not sure of this model yet. Candidates: Akka.NET .NET port of the Akka library - Akka 1.0 was released in 2011 and has been used widely. - Akka.NET 1.0 was released in 2015. Modular architecture - Can use only what you need. - Can replace a module with another. Chosen: Akka.NET Strong points - Easy to customize with modules.

- Classic API model but stable and familiar. - Open source! Week points - Requires more time to be stable. - Performance improvement for Remote is ongoing. Introduction to Akka.NET. Skim over basic things. Actor Actor

- Has state. - Receives messages. - Processes one message at a time. State M3 M2 M1 Behavior Actor class HelloActor : ReceiveActor { int _count; HelloActor

_count M3 public HelloActor() { Receive(m => { _count += 1; Console.WriteLine($"Hello {m.Who}")}); } M2 M1 OnHello

ActorRef Actor - Can access an actor via a handle called as ActorRef. - Can send a message to a remote actor with ActorRef. State M3 M2

M1 Behavior ActorRef ActorRef greeter // create actor IActorRef greeter = system.ActorOf("greeter");

_count M3 M2 M1 OnHello // send message greeter.Tell(new Hello("World")); Hello(World)

greeter Actor hierarchy - Actor creates child actors. - Parent actor handles an exception which child actor throw Actor Actor Resume|Restart| Stop|Escalate Actor Actor

Exception Actor hierarchy class Worker : UntypedActor { IActorRef counterService = Context.ActorOf("counter"); override SupervisorStrategy SupervisorStrategy() { return new OneForOneStrategy(ex => { if (ex is ServiceUnavailableException) return Directive.Stop; return Directive.Escalate; }); }

Remote - Send a message to remote actors. (Location transparency) NodeA Actor State M3 M2

M1 Behavior - Create an actor on a remote node. ActorRef NodeA:Actor NodeB Remote // server

using (var system = ActorSystem.Create("MyServer", config)) { system.ActorOf("greeter"); ... } // client using (var system = ActorSystem.Create("MyClient", config)) { IActorRef greeter = system.ActorSelection( "akka.tcp://[email protected]:8080/user/greeter"); greeter.Tell(new Hello("World")); } Cluster NodeA

NodeB Beyond remote Membership management - Gossip protocol is used. (No SPOF/B) - Roles for every nodes NodeC Cluster utility - Singleton, Sharding, Distributed Pub/Sub. Cluster class SimpleCluster : UntypedActor {

Cluster Cluster = Cluster.Get(System); NodeA B override void OnReceive(object message) { var up = message as ClusterEvent.MemberUp; if (up != null) { Print("Up: {0}", up.Member); } } C

NodeB A NodeC A B C Addition to Akka.NET. More things to be done for building online game server.

Discovery / Table NodeA NodeB Interfac e Client Actor State Sync

? DB Akka.Interfaced https://github.com/SaladLab/Akka.Interfaced NodeA NodeB Interfac e Actor

Akka.Interfaced Terse and readable code - No message class - Message handler has a form of interface method. - Message sending has a form of calling a method. - Influenced by Orleans and WCF Contract. No type error for implement and using actor. - Compile-time check for correct message handling. - Compile-time check for correct message sending. Akka.Interfaced IHello

M3 M2 greeter greeter _count _count M1

M3 M2 M1 OnHello IHello .Hello IHello Hello()

greeter IHello.Hello() greeter Akka.NET style // define message class Hello { public string Name; } class HelloResult { public string Say; } // implement actor class HelloActor : ReceiveActor { public HelloActor() { Receive(m => {

Sender.Tell(new HelloResult($"Hello {m.Name}!")}); }); // use actor var result = await actorRef.Ask(new Hello("World")); Print(result.Say); Akka.Interfaced style // define interface interface IHello : IInterfacedActor { Task SayHello(string name); } // implement actor class HelloActor : InterfacedActor, IHello { async Task IHello.SayHello(string name) {

return $"Hello {name}!"; } // use actor Print(await helloRef.SayHello("World")); Akka.Interfaced.SlimSocket https://github.com/SaladLab/Akka.Interfaced.SlimSocket NodeA NodeB Interfac e

Client Actor Akka.Interfaced.SlimSocket No client role in Akka - Everyone can send any messages to any actors. Access control - Client can only send a message in allowed interfaces to permitted actors. Supports .NET 3.5 - Implement a small part to send a message to an actor in server.

- Not necessary: Create actor, hierarchy, cluster and so Akka.Interfaced.SlimSocket Client Server Actor1 Actor1Ref Actor2Ref Actor2 ClientSession SlimSocket.Client

SlimSocket.Server protobuf/tcp Akka.Cluster.Utility https://github.com/SaladLab/Akka.Cluster.Utility Discovery / Table NodeA NodeB

Actor ? Akka.Cluster.Utility: ActorDiscovery Actor Discovery - To find interesting actor in a cluster Akka.Cluster.Utility: ActorDiscovery NodeA NodeB UserActor

Listen Notify ServiceActo r Register UserActor UserActor Listen Notify ActorDiscovery

ActorDiscovery share state Akka.Cluster.Utility: ActorDiscovery // register actor class ServiceActor { override void PreStart(){ Discovery.Tell(new Register(Self, nameof(ServiceActor))); } // discover registered actor class UserActor { override void PreStart() { Discovery.Tell(new Monitor(nameof(ServiceActor)));

} void OnActorUp(ActorUp m) { ... } void OnActorDown(ActorDown m) { ... } Akka.Cluster.Utility: DistributedActorTable Distributed Actor Table - Contains distributed actors across several nodes in a clust - Registered with unique key in a table. - Similar with sharding but more simpler one. - There is a master node. (SPOF/B) Akka.Cluster.Utility: DistributedActorTable NodeA

NodeB ActorTable ID 1 2 3 ActorRef Actor1 Actor2 Actor3

NodeC Actor1 Actor3 Actor2 Actor4 Akka.Cluster.Utility: DistributedActorTable // create table var table = System.ActorOf( Props.Create(() => new Table("Test", ...)));

// create actor on table var reply = await table.Ask(new Create(id, ...)); reply.Actor; // get actor from table var reply = await table.Ask(new Get(id)); reply.Actor; TrackableData https://github.com/SaladLab/TrackableData NodeA Client

NodeB Actor State Sync DB TrackableData Sync data in n-tier - Propagates changes to client, server and DB. Change tracking library rather than general ORM.

Supports .NET 3.5 / Unity3D Supports - Json, Protobuf - MSSQL, MySQL, postgreSQL, MongoDB, Redis Production Ready - Used in MonsterSweeperz TrackableData Client User Gold=10 Cash=20

User Gold=15 Cash=20 DB Server Snapsho t User Gold=10 Cash=20

Create Change Gold+=5 Save User Gold=15 Cash=20 User

Gold=10 Cash=20 User Gold=15 Cash=20 TrackableData interface IUserData : ITrackablePoco { string Name { get; set; } int Level { get; set; } int Gold { get; set; } } var u = new TrackableUserData();

// make changes u.Name = "Bob"; u.Level = 1; u.Gold = 10; Print(u.Tracker); // { Name:->Bob, Level:0->1, Gold:0->10 } Real-time online Tic-tac-toe Reference game for proving akka.net based game server. Features User account

- User can login with ID and password. - User data like status and achievements is persistent via D Real-time game between users - Turn game with turn-timeout - Finding an opponent with matchmaker. - When no opponent, bot will play with an user. https://github.com/SaladLab/TicTacToe Project structure Domain Domain.Tests

450 cloc GameServer 965 cloc GameServer.Tests GameClient 1141 cloc Cluster node structure Master

User User Game Game GamePai rMaker User User Game Game

User Table User Login GameBot GameBot Game Table Actor structure

Client Client Session User Login MongoDB User GamePai

rMaker Game GameBot Main tasks Login Match Making Join

Game Game Play Finish Game Login LoginActor will - Be a first actor which client can access. - Authenticate user with ID and password. - Create an user actor and bind it with client.

Client can communicate with only bound actors. - Only through bound interfaced even for bound actors. Login: Actor Login User Login Create Client Client

Session Bind User MongoDB Login: Code Client Server // client requests login to server var t1 = login.Login(id, password, observerId); yield return t1.WaitHandle;

// check account, create user actor and bind it to client async Task IUserLogin.Login(id, password) { var userId = CheckAccount(id, password); var user = CreateUserActor(userId); await UserTable.AddUserAsync(userId, user); var bindId = await ClientSession.BindAsync(user, typeof(IUser)); // client gets user actor var user = new UserRef(t1.Result); Matchmaking Behavior - Client sends a request to a pair-maker and waits for 5 secs - A pair-maker enqueues a request.

- When there are 2 users on queue, make them a pair. - When timeout, a bot is created and make a pair. Matchmaking: Actor Client Client Session User Register

GamePai rMaker Created Create Game Matchmaking: Code // client requests pairing from UserActor yield return G.User.RegisterPairing(observerId).WaitHandle; // UserActor forwards a request to GamePairMakerActor class GamePairMakerActor : ... { void RegisterPairing(userId, userName, ...) { AddToQueue(userId);

} void OnSchedule() { if (Queue.Count >= 2) { user0, user1 = Queue.Pop(2); CreateNewGame(); user0.SendNoticeToUser(gameId); user1.SendNoticeToUser(gameId); Join game Behavior - GameRoom actor will be created for every games. - User gets an ActorRef to GameRoom from a pairmaker. - User enters GameRoom and starts playing. - On entering user registers GameObserver to listen

game events. Join game: Actor Client Client Session Join Bind User

Join Game Join game: Code // client sends a join request to UserActor. var ret = G.User.JoinGame(roomId, observerId); yield return ret.WaitHandle; // UserActor forwards a request to GameActor. // when done, grant GameActor to Client as IGamePlayer. class UserActor : ... { async Task<...> IUser.JoinGame(long gameId, int observerId) { var game = await GameTable.GetAsync(gameId); await game.Join(...); var bindId = await BindAsync(game.Actor, typeof(IGamePlayer));

return ...; } Game play Behavior - Client plays game with an ActorRef to GameRoom. - Client gets game events like opponents turn and result of game from a GameObserver registered to GameRoom. Game play: Command: Actor Client

Client Session GameRef.MakeMove(2,1) MakeMove Game Game play: Command: Code // client sends a move command to GameActor class GameScene : MonoBehavior, IGameObserver { void OnBoardGridClicked(int x, int y) { _myPlayer.MakeMove(new PlacePosition(x, y));

} // GameActor proceeds the game by command public class GameActor : ... { void MakeMove(PlacePosition pos, long playerUserId) { DoGameLogic(); ... } Game play: Game events: Actor MakeMove Game Client

Client Session MakeMove GameObserver.MakeMove(2,1) Game play: Game events: Code // GameActor broadcasts game events to clients public class GameActor : ... { void MakeMove(PlacePosition pos, long playerUserId) { ... NotifyToAllObservers((id, o) => o.MakeMove(...)); }

// client consumes game events class GameScene : MonoBehavior, IGameObserver { void IGameObserver.MakeMove(...) { Board.SetMark(...); } Finish game Behavior - Game room reports on the end of game to user actor. - User actor updates users status and achievements and propagates changes to client and DB. - Destroy a game room actor. Finish game: Actor

MongoDB Client Client Session Update Update User End

End Game Kill Finish game: Code // UpdateActor updates user state when the game is over void IGameUserObserver.End(long gameId, GameResult result) { _userContext.Data.GameCount += 1; // flush changes to client and storage _userEventObserver.UserContextChange(_userContext.Tracker); MongoDbMapper.SaveAsync(_userContext.Tracker, _id); _userContext.Tracker = new TrackableUserContextTracker(); } // when no one left in GameActor, kill actor

void Leave(long userId) { NotifyToAllObservers((id, o) => o.Leave(playerId)); if (_players.Count() == 0) { Self.Tell(InterfacedPoisonPill.Instance); Conclusion Akka.NET Good to go - Akka.NET is a handy building block. - Akka.Interfaced.SlimSocket allows an Unity3D client to interact with an interfaced actor. Keep in mind that it is still young - Quite stable to use.

- But it is quite early to say matured. - When something goes wrong, it is necessary to check not only your code but also library code. Try it Tic-tac-toe - Grab sources and run it - https://github.com/SaladLab/TicTacToe Libraries - Visit project Github pages - Get libraries from NuGet and Github Releases

Thank you

Recently Viewed Presentations