Before discussing the Actor model, we should discuss the architecture of ET. There are two architectures for game servers in order to use multi-core, single-threaded multi-process and single-process multi-threaded architecture. ET uses single-threaded multi-process architecture, while the traditional Actor model is generally single-process multi-threaded architecture, which is a major difference. The advantages and disadvantages are as follows.
- the logic needs to be single-threaded this is the same, erlang process logic is single-threaded, skynet lua virtual machine is also single-threaded. et in a process is actually equivalent to an erlang process, a skynet lua virtual machine.
- the use of single-threaded multi-process does not need to write their own set of profiler tools, you can use a lot of ready-made profiler tools, such as view memory, cpu occupation directly with top command, this point erlang and skynet need to get another set of tools.
- multi-process single-threaded architecture also has a benefit, a single physical machine and multiple physical machines is no difference, single process multi-threaded also need to consider the processing of multiple physical machines.
- multi-process single-threaded architecture is a bit of a drawback is that messages need to be serialized and deserialized across processes, taking up a bit of resources. In addition, sending network messages will have a few milliseconds delay. Generally these effects can be ignored.
The original Actor model is used for single-process multi-threaded architecture, there is a reason for this, because multi-threaded architecture developers can easily access shared variables at will, let's say a variable a, thread 1 can access, thread 2 can also access, so that both threads need to add locks when accessing the variable a, shared variables more locks everywhere, will become unmaintainable, the framework must not appear The framework must not have a situation where there are threads sharing variables everywhere. In order to ensure that the multi-threaded architecture does not go wrong, it is necessary to provide a development model that ensures easy and safe multi-threaded development. erlang's concurrency mechanism is the actor model. erlang virtual machine uses multiple threads to take advantage of multiple cores. erlang has designed a mechanism that designs its own processes on top of the virtual machine. At its simplest, each erlang process manages its own variables, and the logic of each erlang process runs on a single thread. The logic between the erlang process and the process is completely isolated, so that there are no two threads accessing the same variable and there is no multithreaded competition. The next question arises, since each erlang process has its own data and the logic is completely isolated, how should the two erlang processes communicate with each other? This is where the Actor model comes in. erlang has designed a messaging mechanism: one process can send messages to other processes, and erlang processes communicate with each other through messages. Isn't this the same message queue used by operating systems for inter-process communication? Yes, in fact, it is similar. erlang inside the process id to get the process can send messages to this process.
If the message is only sent to the process, it is still a bit inconvenient. For example, if you take an erlang process as a moba team process, and there are 10 players in the battle process, if you use erlang's actor message, the message can only be sent to the battle process, but often the message needs to be sent to a player, then erlang needs to distribute the message to the specific player again according to the player id in the message, so it actually goes around one more time. This is actually an extra detour.
According to the characteristics of its own architecture, ET does not completely copy the Actor model of erlang, but provides the Entity object-level Actor model. In ET, an Actor is an Entity object, and a MailboxComponent component attached to an Entity is an Actor. You only need to know the Entity's InstanceId to send messages to the Entity. In fact, erlang's Actor model is a special case of ET, such as giving the ET server Game.Scene as an Actor, so that it can become a process-level Actor. It only needs to know the InstanceId (ET) or the Pid (erlang) of the process to send it to the other party.
| Language | ET | Erlang | Skynet | | | ET | Erlang | Skynet | -- | :--: | :--: | :--: | | Architecture | Single-Threaded Multi-Process | Single-Process Multi-Threaded | Single-Process Multi-Threaded | | Actor | Entity | erlang process | lua virtual machine | | ActorId | Entity.InstanceId | erlang processId | service address |
For a normal Actor, we can refer to the Gate Session. map has a Unit, and the Unit holds the gate session corresponding to the player. thus, if a message in map needs to be sent to the client, it only needs to send the message to the gate session, and the gate session forwards it to the client when it receives the message. The map process sending messages to the gate session is a typical actor model. It doesn't need to know the location of the gate session, it just needs to know its InstanceId. messageHelper.cs gets an ActorMessageSender from GateSessionActorId and sends it.
// Get an ActorSenderComponent from Game.Scene, then get an ActorMessageSender by InstanceId
ActorSenderComponent actorSenderComponent = Game.Scene.GetComponent<ActorSenderComponent>();
ActorMessageSender actorMessageSender = actorSenderComponent.Get(unitGateComponent.GateSessionActorId);
// send
actorMessageSender.Send(message);
// rpc
var response = actorMessageSender.Call(message);
The question is how do you know the InstanceId of the gate session in map? This is where you need to find a way to pass it on, for example in ET, when the player is logging in to gate, the gate session hooks up a mailbox MailBoxComponent, C2G_LoginGateHandler.cs
session.AddComponent<MailBoxComponent, string>(MailboxType.GateSession);
The InstanceId of this gate session is brought into the map when the player logs into the map process, in C2G_EnterMapHandler.cs
M2G_CreateUnit createUnit = (M2G_CreateUnit)await mapSession.Call(new G2M_CreateUnit() { PlayerId = player.Id, GateSessionId = session. InstanceId });
First, the message arrives at the MailboxComponent, which has a type, and different types of mailboxes can do different processing. Currently, there are two types of mailboxes, GateSession and MessageDispatcher; GateSession mailboxes will immediately forward messages to the client when they are received, and MessageDispatcher types will again distribute the Actor messages to the specific Handler for processing. The default MailboxComponent type is MessageDispatcher. customizing a mailbox type is also very simple, inherit the IMailboxHandler interface and add the MailboxHandler tag. So why do we need to add such a feature? This feature does not exist in other actor models, and messages are generally received and distributed. The reason is that GateSession is not designed for distribution, so I added the mailbox type here. messageDispatcher has two ways of handling messages, one is to handle the messages sent by the other party, and the other is rpc messages
// To handle Send messages, you need to inherit the AMActorHandler abstract class. The first generic parameter of the abstract class is the type of the Actor, and the second parameter is the type of the message
[ActorMessageHandler(AppType.Map)]
public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test>
{
protected override ETTask Run(Unit unit, Actor_Test message)
{
Log.Debug(message.Info);
}
}
// To handle Rpc messages, you need to inherit the AMActorRpcHandler abstract class, the first generic parameter of the abstract class is the type of the Actor, the second parameter is the type of the message, and the third parameter is the type of the returned message
[ActorMessageHandler(AppType.Map)]
public class Actor_TransferHandler : AMActorRpcHandler<Unit, Actor_TransferRequest, Actor_TransferResponse>
{
protected override async ETTask Run(Unit unit, Actor_TransferRequest message, Action<Actor_TransferResponse> reply)
{
Actor_TransferResponse response = new Actor_TransferResponse();
try
{
reply(response);
}
catch (Exception e)
{
ReplyError(response, e, reply);
}
}
}
We should note that Actor messages have the potential to deadlock, such as A call to B, B call to C, and C call to A. Because MailboxComponent is essentially a message queue, it opens a concurrent process that will process one message at a time, returning ETTask to indicate that the message processing class will block MailboxComponent queue of other messages. So if there is a deadlock, we don't want a message processing to block the rest of the MailboxComponent messages, we can just open a new thread in the message processing class to handle it. For example:
[ActorMessageHandler(AppType.Map)]
public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test>
{
protected override ETTask Run(Unit unit, Actor_Test message)
{
RunAsync(unit, message).Coroutine();
}
public ETVoid RunAsync(Unit unit, Actor_Test message)
{
Log.Debug(message.Info);
}
}
For related information, you can Google the Actor deadlock problem.