TH1/Book/5.5Actor Location-ZH.md
2025-07-17 18:26:28 +08:00

77 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Actor Location
### Actor Location
Actor模型只需要知道对方的InstanceId就能发送消息十分方便但是有时候我们可能无法知道对方的InstanceId或者是一个Actor的InstanceId会发生变化。这种场景很常见比如很多游戏是分线的一个玩家可能从1线换到2线还有的游戏是分场景的一个场景一个进程玩家从场景1进入到场景2。因为做了进程迁移玩家对象的InstanceId也就变化了。ET提供了给这类对象发送消息的机制叫做Actor Location机制。其原理比较简单
1. 因为InstanceId是变化的对象的Entity.Id是不变的所以我们首先可以想到使用Entity.Id来发送actor消息
2. 提供一个位置进程(Location Server)Actor对象可以将自己的Entity.Id跟InstanceId作为kv存到位置进程中。发送Actor消息前先去位置进程查询到Actor对象的InstanceId再发送actor消息。
3. Actor对象在一个进程创建时或者迁移到一个新的进程时都需要把自己的Id跟InstanceId注册到Location Server上去
4. 因为Actor对象是可以迁移的消息发过去有可能Actor已经迁移到其它进程上去了所以发送Actor Location消息需要提供一种可靠机制
5. ActorLocationSender提供两种方法Send跟CallSend一个消息也需要接受者返回一个消息只有收到返回消息才会发送下一个消息。
6. Actor对象如果迁移走了这时会返回Actor不存在的错误发送者收到这个错误会等待1秒然后重新去获取Actor的InstanceId然后重新发送目前会尝试5次5次过后抛出异常报告错误
7. ActorLocationSender发送消息不会每次都去查询Location Server因为对象迁移毕竟比较少见只有第一次去查询之后缓存InstanceId以后发送失败再重新查询。
8. Actor对象在迁移过程中有可能其它进程发送过来消息这时会发生错误所以location server提供了一种Lock的机制。对象在传送前删掉在本进程的信息然后在location server上加上锁一旦锁上后其它的对该key的请求会进行队列。
9. 传送前因为对方删除了本进程的actor所以其它进程会发送失败这时候他们会进行重试。重试的时候会重新请求location server这时候会发现被锁了于是一直等待
10. 传送完成后要unlock location server上的锁并且更新新的地址然后响应其它的location请求。其它发给这个actor的请求继续进行下去。
注意Actor模型是纯粹的服务端消息通信机制跟客户端是没什么关系的很多用ET的新人看到ET客户端消息也有Actor接口以为这是客户端跟服务端通信的机制其实不是的。ET客户端使用这个Actor完全是因为Gate需要对客户端消息进行转发我们可以正好利用服务端actor模型来进行转发所以客户端有些消息也是继承了actor的接口。假如我们客户端不使用actor接口会怎么样呢比如Frame_ClickMap这个消息
```protobuf
message Frame_ClickMap // IActorLocationMessage
{
int64 ActorId = 93;
int64 Id = 94;
float X = 1;
float Y = 2;
float Z = 3;
}
```
我们可能就不需要ActorId这个字段消息发送到Gategate看到是Frame_ClickMap消息它需要转发给Map上的Unit转发还好办gate可以从session中获取对应的map的unit的位置然后转发问题来了Frame_ClickMap消息到了mapmap怎么知道消息需要给哪个对象呢这时候有几种设计
1. 在转发的底层协议中带上unit的Id需要比较复杂的底层协议支持。
2. 用一个消息对Frame_ClickMap消息包装一下包装的消息带上Unit的Id用消息包装意味着更大的消耗增加GC。
个人感觉这两种都很差不好用而且就算分发给unit对象处理了怎么解决消息重入的问题呢unit对象仍然需要挂上一个消息处理队列然后收到消息扔到队列里面。这不跟actor模型重复了吗目前ET在客户端发给unit的消息做了个设计消息做成actor消息gate收到发现是actor消息直接发到对应的actor上解决的可以说很漂亮。其实客户端仍然是使用session.send跟call发送消息发送的时候也不知道消息是actor消息只有到了gategate才进行了判断参考OuterMessageDispatcher.cs
### Actor Location消息的处理
ActorLocation消息发送
```csharp
// 从Game.Scene上获取ActorLocationSenderComponent然后通过Entity.Id获取ActorLocationSender
ActorLocationSender actorLocationSender = Game.Scene.GetComponent<ActorLocationSenderComponent>().Get(unitId);
// 通过ActorLocationSender来发送消息
actorLocationSender.Send(actorLocationMessage);
// 发送Rpc消息
IResponse response = await actorLocationSender.Call(actorLocationRequest);
```
ActorLocation消息的处理跟Actor消息几乎一样不同的是继承的两个抽象类不同注意actorlocation的抽象类多了个Location
```csharp
// 处理send过来的消息 需要继承AMActorLocationHandler抽象类抽象类第一个泛型参数是Actor的类型第二个参数是消息的类型
[ActorMessageHandler(AppType.Map)]
public class Frame_ClickMapHandler : AMActorLocationHandler<Unit, Frame_ClickMap>
{
protected override ETTask Run(Unit unit, Frame_ClickMap message)
{
Vector3 target = new Vector3(message.X, message.Y, message.Z);
unit.GetComponent<UnitPathComponent>().MoveTo(target).Coroutine();
}
}
// 处理Rpc消息, 需要继承AMActorRpcHandler抽象类抽象类第一个泛型参数是Actor的类型第二个参数是消息的类型第三个参数是返回消息的类型
[ActorMessageHandler(AppType.Map)]
public class C2M_TestActorRequestHandler : AMActorLocationRpcHandler<Unit, C2M_TestActorRequest, M2C_TestActorResponse>
{
protected override async ETTask Run(Unit unit, C2M_TestActorRequest message, Action<M2C_TestActorResponse> reply)
{
reply(new M2C_TestActorResponse(){Info = "actor rpc response"});
await ETTask.CompletedTask;
}
}
```
### ET的actor跟actor location的比喻
中国有很多城市进程城市中有很多人entity对象居住每个人都有身份证号码Entity.Id。一个人每到一个市都需要办理居住证分配到唯一的居住证号码InstanceId居住证号码的格式是2个字节市编号+4个字节时间+2个字节递增。身份证号码是永远不会变化的但是居住证号码每到一个城市都变化的。
现在有个中国邮政actor。假设小明要发信给女朋友小红
1. 小红为了收信自己必须挂载一个邮箱MailboxComponent小红收到消息就会处理。注意这里处理是一个个进行处理的。有可能小红会同时收到很多人的信。但是她必须一封一封的信看比方说小明跟小宝都发了信给小红小红先收到小明的信再收到了小宝的信。小红先读小明的信小明信中让小红给外婆打个电话产生协程再给自己回信注意这期间小红也不能读下一封信必须打完电话后才能读小宝的信。当然小红自己可以选择不处理完成就开始读小宝的信做法是小红开一个新的协程来处理小明的信。
2. 假设小明知道小红的居住证号码那么邮政actor可以根据居住证号码头两位找到小红居住的城市进程然后再根据小红的居住证编号找到小红把消息投递到小红的邮箱MailboxComponent中。这种是最简单的原生的actor模型
3. ET还支持了一套actor location机制。假设小明不知道小红的居住证号码但是他知道小红的身份证号码怎么办呢邮政开发了一套高级邮政actor location想了一个办法如果一个人经常搬家它还想收到信那他到一个新的城市都必须把自己的居住证跟身份证上报到中央政府location server这样高级邮政能够通过身份证号码来发送邮件。方法就是去中央政府拿到小红的居住证号码再利用actor机制发送。
4. 假设小红之前在广州市,小明用小红的身份证给小红发信件了。 高级邮政获取了小红的居住证号码,给小红发信。发信的这个过程中,小红搬家了,从广州搬到了深圳,这时小红在中央政府上报了自己新的居住证。 高级邮政的信送到到广州的时候发现,小红不在广州。那么高级邮政会再次去中央政府获取小红的居住证,重新发送,有可能成功有可能再次失败,这个过程会重复几次,如果一直不成功则告诉小明,信件发送失败了。
5. 高级邮政发信比较贵,而且人搬家的次数并不多,一般小明用高级邮政发信后会记住小红的居住证,下次再发的时候直接用居住证发信,发送失败了再使用高级邮政发信。
6. 高级邮政的信都是有回执的,有两种回执,一种回执没有内容,只表示小红收到了信,一种回执带了小红的回信。小明在发信的时候可以选择使用哪种回执形式。小明给小红不能同时发送两封信,必须等小红的回执到了,小明才能继续发信。