Skip to content

Commit bee3e4e

Browse files
authored
feat(attributes): 增加玩家属性同步协议 (#84)
- 新增 PlayerAttributeEntry / NotifyPlayerAttributeSync / NotifyPlayerAttributeChanged 协议消息 - 新增 PlayerAttributeSyncBuilder 构建完整快照与增量变更 - 登录成功后推送属性完整快照,最终值变化时推送增量同步 - 服务端权威单向推送,客户端仅消费用于显示 - 新增 AttributeSyncSelfCheck 覆盖快照与增量构建 Closes GFX-128
1 parent e830392 commit bee3e4e

8 files changed

Lines changed: 333 additions & 16 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using GameFrameX.Apps.Player.Attribute;
4+
using GameFrameX.Apps.Player.Attribute.Entity;
5+
using GameFrameX.Hotfix.Logic.Player.Attribute;
6+
using GameFrameX.Proto.Proto;
7+
8+
namespace GameFrameX.Apps.SelfCheck;
9+
10+
internal static class AttributeSyncSelfCheck
11+
{
12+
public static void Run()
13+
{
14+
// 增量构建
15+
var changed = PlayerAttributeSyncBuilder.BuildChanged(AttributeType.Life, 1480L);
16+
AssertEqual((int)AttributeType.Life, changed.Type, "BuildChanged.Type");
17+
AssertEqual(1480L, changed.Value, "BuildChanged.Value");
18+
19+
// 快照构建
20+
var state = new PlayerAttributeState();
21+
state.Values[(int)AttributeType.LifeBase] = 1000L;
22+
state.Values[(int)AttributeType.LifeAdd] = 200L;
23+
state.Values[(int)AttributeType.LifePct] = 1500L;
24+
state.Values[(int)AttributeType.Life] = 1480L;
25+
state.Values[(int)AttributeType.PhysicalAttackBase] = 50L;
26+
state.Values[(int)AttributeType.PhysicalAttack] = 50L;
27+
28+
var snapshot = PlayerAttributeSyncBuilder.BuildSnapshot(state);
29+
// 最终值槽固定 9 个:Life / PhysicalAttack / MagicAttack / PhysicalDefense / MagicDefense / Critical / CriticalDamage / Precision / Block
30+
AssertEqual(9, snapshot.Attributes.Count, "BuildSnapshot.Count");
31+
32+
var lifeEntry = snapshot.Attributes.Find(e => e.Type == (int)AttributeType.Life);
33+
if (lifeEntry == null)
34+
{
35+
throw new InvalidOperationException("BuildSnapshot 缺少 Life 条目");
36+
}
37+
38+
AssertEqual(1480L, lifeEntry.Value, "BuildSnapshot.Life.Value");
39+
AssertEqual(1000L, lifeEntry.Base, "BuildSnapshot.Life.Base");
40+
AssertEqual(200L, lifeEntry.Add, "BuildSnapshot.Life.Add");
41+
AssertEqual(1500L, lifeEntry.Pct, "BuildSnapshot.Life.Pct");
42+
43+
// 缺失派生槽默认 0
44+
var physicalAttackEntry = snapshot.Attributes.Find(e => e.Type == (int)AttributeType.PhysicalAttack);
45+
if (physicalAttackEntry == null)
46+
{
47+
throw new InvalidOperationException("BuildSnapshot 缺少 PhysicalAttack 条目");
48+
}
49+
50+
AssertEqual(50L, physicalAttackEntry.Value, "BuildSnapshot.PhysicalAttack.Value");
51+
AssertEqual(50L, physicalAttackEntry.Base, "BuildSnapshot.PhysicalAttack.Base");
52+
AssertEqual(0L, physicalAttackEntry.Add, "BuildSnapshot.PhysicalAttack.Add");
53+
54+
// 空状态快照仍包含全部最终值槽(值为 0)
55+
var emptySnapshot = PlayerAttributeSyncBuilder.BuildSnapshot(new PlayerAttributeState());
56+
AssertEqual(9, emptySnapshot.Attributes.Count, "BuildSnapshot.Empty.Count");
57+
}
58+
59+
private static void AssertEqual<T>(T expected, T actual, string name)
60+
{
61+
if (!EqualityComparer<T>.Default.Equals(expected, actual))
62+
{
63+
throw new InvalidOperationException(name + " 自检失败,expected=" + expected + ", actual=" + actual + "。");
64+
}
65+
}
66+
}

GameFrameX.Apps.SelfCheck/GameFrameX.Apps.SelfCheck.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
<Compile Include="..\GameFrameX.Apps\Player\Attribute\*.cs" Link="Player\Attribute\%(Filename)%(Extension)" />
1313
<Compile Include="..\GameFrameX.Apps\Player\Attribute\Entity\PlayerAttributeState.cs" Link="Player\Attribute\Entity\PlayerAttributeState.cs" />
1414
<Compile Include="..\GameFrameX.Hotfix\Logic\Player\Attribute\PlayerAttributeMutation.cs" Link="Player\Attribute\PlayerAttributeMutation.cs" />
15+
<Compile Include="..\GameFrameX.Hotfix\Logic\Player\Attribute\PlayerAttributeSyncBuilder.cs" Link="Player\Attribute\PlayerAttributeSyncBuilder.cs" />
1516
<Compile Include="..\GameFrameX.Hotfix\Logic\Player\Attribute\PlayerInitialAttributeDefaults.cs" Link="Player\Attribute\PlayerInitialAttributeDefaults.cs" />
1617
</ItemGroup>
1718

19+
<ItemGroup>
20+
<ProjectReference Include="..\GameFrameX.Proto\GameFrameX.Proto.csproj" />
21+
</ItemGroup>
22+
1823
</Project>

GameFrameX.Apps.SelfCheck/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
AttributeCoreSelfCheck.Run();
66
AttributeAgentSelfCheck.Run();
7+
AttributeSyncSelfCheck.Run();
78
Console.WriteLine("AttributeSelfCheck PASS");
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using GameFrameX.Apps.Common.Event;
2+
using GameFrameX.Apps.Common.EventData;
3+
using GameFrameX.Apps.Common.Session;
4+
using GameFrameX.Core.Abstractions.Events;
5+
6+
namespace GameFrameX.Hotfix.Logic.Player.Attribute;
7+
8+
/// <summary>
9+
/// 监听玩家最终属性变化,向在线玩家 session 推送增量同步消息。
10+
/// </summary>
11+
[Event(EventId.AttributeChanged)]
12+
internal sealed class PlayerAttributeChangedEventListener : EventListener<PlayerAttributeComponentAgent>
13+
{
14+
protected override async Task HandleEvent(PlayerAttributeComponentAgent agent, GameEventArgs gameEventArgs)
15+
{
16+
if (agent == null || !(gameEventArgs is AttributeChangedEventArgs args))
17+
{
18+
return;
19+
}
20+
21+
var session = SessionManager.GetByRoleId(args.PlayerId);
22+
if (session == null)
23+
{
24+
return;
25+
}
26+
27+
await session.WriteAsync(PlayerAttributeSyncBuilder.BuildChanged(args.AttributeType, args.NewValue));
28+
}
29+
}

GameFrameX.Hotfix/Logic/Player/Attribute/PlayerAttributeComponentAgent.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using GameFrameX.Apps.Player.Attribute.Component;
55
using GameFrameX.Apps.Player.Attribute.Entity;
66
using GameFrameX.Hotfix.Common.Events;
7+
using GameFrameX.Proto.Proto;
78

89
namespace GameFrameX.Hotfix.Logic.Player.Attribute;
910

@@ -83,6 +84,15 @@ public async Task InitializeDefaultsSilent()
8384
}
8485
}
8586

87+
/// <summary>
88+
/// 构建当前玩家属性的完整快照消息,用于登录后或重连时同步给客户端。
89+
/// </summary>
90+
/// <returns>属性快照消息。</returns>
91+
public NotifyPlayerAttributeSync BuildSyncSnapshot()
92+
{
93+
return PlayerAttributeSyncBuilder.BuildSnapshot(OwnerComponent.State);
94+
}
95+
8696
private async Task Set(AttributeType attributeType, long value, bool silent)
8797
{
8898
var result = PlayerAttributeMutation.ApplyValue(OwnerComponent.State.Values, attributeType, value, silent);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using GameFrameX.Apps.Player.Attribute;
3+
using GameFrameX.Apps.Player.Attribute.Entity;
4+
using GameFrameX.Proto.Proto;
5+
6+
namespace GameFrameX.Hotfix.Logic.Player.Attribute;
7+
8+
/// <summary>
9+
/// 玩家属性同步消息构建器。把玩家属性状态和最终值变化转换为面向客户端的同步消息。
10+
/// </summary>
11+
public static class PlayerAttributeSyncBuilder
12+
{
13+
/// <summary>
14+
/// 根据玩家属性状态构建完整属性快照消息,包含全部最终值槽及其 Base/Add/Pct 调试字段。
15+
/// </summary>
16+
/// <param name="state">玩家属性状态。</param>
17+
/// <returns>属性快照消息。</returns>
18+
public static NotifyPlayerAttributeSync BuildSnapshot(PlayerAttributeState state)
19+
{
20+
if (state == null)
21+
{
22+
throw new ArgumentNullException(nameof(state));
23+
}
24+
25+
var message = new NotifyPlayerAttributeSync();
26+
foreach (AttributeType attributeType in Enum.GetValues(typeof(AttributeType)))
27+
{
28+
if (attributeType == AttributeType.None)
29+
{
30+
continue;
31+
}
32+
33+
if (!AttributeCore.IsFinalAttribute(attributeType))
34+
{
35+
continue;
36+
}
37+
38+
message.Attributes.Add(BuildEntry(state, attributeType));
39+
}
40+
41+
return message;
42+
}
43+
44+
/// <summary>
45+
/// 根据最终属性变化构建增量同步消息。
46+
/// </summary>
47+
/// <param name="attributeType">发生变化的最终属性编号。</param>
48+
/// <param name="newValue">新的最终值。</param>
49+
/// <returns>属性增量消息。</returns>
50+
public static NotifyPlayerAttributeChanged BuildChanged(AttributeType attributeType, long newValue)
51+
{
52+
return new NotifyPlayerAttributeChanged
53+
{
54+
Type = (int)attributeType,
55+
Value = newValue,
56+
};
57+
}
58+
59+
private static PlayerAttributeEntry BuildEntry(PlayerAttributeState state, AttributeType finalAttribute)
60+
{
61+
return new PlayerAttributeEntry
62+
{
63+
Type = (int)finalAttribute,
64+
Value = state.GetValue(finalAttribute),
65+
Base = state.GetValue(AttributeCore.GetSlotAttribute(finalAttribute, AttributeSlotKind.Base)),
66+
Add = state.GetValue(AttributeCore.GetSlotAttribute(finalAttribute, AttributeSlotKind.Add)),
67+
Pct = state.GetValue(AttributeCore.GetSlotAttribute(finalAttribute, AttributeSlotKind.Pct)),
68+
};
69+
}
70+
}

GameFrameX.Hotfix/Logic/Player/Login/PlayerComponentAgent.cs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@
3030
// ==========================================================================================
3131

3232

33-
using GameFrameX.Apps.Common.Session;
34-
using GameFrameX.Apps.Player.Player.Component;
35-
using GameFrameX.Apps.Player.Player.Entity;
36-
using GameFrameX.Hotfix.Logic.Game.Room;
37-
using GameFrameX.Hotfix.Logic.Player.Attribute;
38-
using GameFrameX.Hotfix.Logic.Server;
33+
using GameFrameX.Apps.Common.Session;
34+
using GameFrameX.Apps.Player.Player.Component;
35+
using GameFrameX.Apps.Player.Player.Entity;
36+
using GameFrameX.Hotfix.Logic.Game.Room;
37+
using GameFrameX.Hotfix.Logic.Player.Attribute;
38+
using GameFrameX.Hotfix.Logic.Server;
3939

4040
namespace GameFrameX.Hotfix.Logic.Player.Login;
4141

@@ -71,16 +71,19 @@ public async Task OnPlayerLogin(INetWorkChannel workChannel, PlayerState playerS
7171
Level = playerState.Level,
7272
State = playerState.State,
7373
Avatar = playerState.Avatar,
74-
};
75-
74+
};
75+
7676
var attributeComponentAgent = await ActorManager.GetComponentAgent<PlayerAttributeComponentAgent>(playerState.Id);
7777
await attributeComponentAgent.InitializeDefaultsSilent();
7878

79-
//加入在线玩家
80-
var serverComp = await ActorManager.GetComponentAgent<ServerComponentAgent>();
81-
await serverComp.AddOnlineRole(ActorId);
82-
83-
var roomComp = await ActorManager.GetComponentAgent<RoomComponentAgent>();
84-
await roomComp.MarkPlayerReconnected(ActorId);
85-
}
86-
}
79+
//加入在线玩家
80+
var serverComp = await ActorManager.GetComponentAgent<ServerComponentAgent>();
81+
await serverComp.AddOnlineRole(ActorId);
82+
83+
var roomComp = await ActorManager.GetComponentAgent<RoomComponentAgent>();
84+
await roomComp.MarkPlayerReconnected(ActorId);
85+
86+
// 推送玩家属性完整快照,供客户端初始化属性显示
87+
await workChannel.WriteAsync(attributeComponentAgent.BuildSyncSnapshot());
88+
}
89+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// ==========================================================================================
2+
// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
3+
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
4+
// 均受中华人民共和国及相关国际法律法规保护。
5+
// are protected by the laws of the People's Republic of China and relevant international regulations.
6+
//
7+
// 使用本项目须严格遵守相应法律法规及开源许可证之规定。
8+
// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
9+
//
10+
// 本项目采用 MIT 许可证与 Apache License 2.0 双许可证分发,
11+
// This project is dual-licensed under the MIT License and Apache License 2.0,
12+
// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
13+
// please refer to the LICENSE file in the root directory of the source code for the full license text.
14+
//
15+
// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
16+
// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
17+
// 侵犯他人合法权益等法律法规所禁止的行为!
18+
// or infringe on the legitimate rights and interests of others, as prohibited by laws and regulations!
19+
// 因基于本项目二次开发所产生的一切法律纠纷与责任,
20+
// Any legal disputes and liabilities arising from secondary development based on this project
21+
// 本项目组织与贡献者概不承担。
22+
// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
23+
//
24+
// GitHub 仓库:https://github.com/GameFrameX
25+
// GitHub Repository: https://github.com/GameFrameX
26+
// Gitee 仓库:https://gitee.com/GameFrameX
27+
// Gitee Repository: https://gitee.com/GameFrameX
28+
// 官方文档:https://gameframex.doc.alianblank.com/
29+
// Official Documentation: https://gameframex.doc.alianblank.com/
30+
// ==========================================================================================
31+
32+
using System;
33+
using ProtoBuf;
34+
using System.Collections.Generic;
35+
using GameFrameX.NetWork.Abstractions;
36+
using GameFrameX.NetWork.Messages;
37+
38+
namespace GameFrameX.Proto.Proto
39+
{
40+
/// <summary>
41+
/// 玩家属性条目,含最终值与 Base/Add/Pct 调试字段
42+
/// </summary>
43+
[ProtoContract]
44+
[System.ComponentModel.Description("玩家属性条目,含最终值与 Base/Add/Pct 调试字段")]
45+
public sealed class PlayerAttributeEntry
46+
{
47+
/// <summary>
48+
/// 属性编号(AttributeType)
49+
/// </summary>
50+
[ProtoMember(1)]
51+
[System.ComponentModel.Description("属性编号(AttributeType)")]
52+
public int Type { get; set; }
53+
54+
/// <summary>
55+
/// 最终值
56+
/// </summary>
57+
[ProtoMember(2)]
58+
[System.ComponentModel.Description("最终值")]
59+
public long Value { get; set; }
60+
61+
/// <summary>
62+
/// 基础值(调试字段)
63+
/// </summary>
64+
[ProtoMember(3)]
65+
[System.ComponentModel.Description("基础值(调试字段)")]
66+
public long Base { get; set; }
67+
68+
/// <summary>
69+
/// 加法修正(调试字段)
70+
/// </summary>
71+
[ProtoMember(4)]
72+
[System.ComponentModel.Description("加法修正(调试字段)")]
73+
public long Add { get; set; }
74+
75+
/// <summary>
76+
/// 百分比修正,10000 表示 100%(调试字段)
77+
/// </summary>
78+
[ProtoMember(5)]
79+
[System.ComponentModel.Description("百分比修正,10000 表示 100%(调试字段)")]
80+
public long Pct { get; set; }
81+
}
82+
83+
/// <summary>
84+
/// 玩家属性完整快照(服务端推送,登录后或重连时发送)
85+
/// </summary>
86+
[ProtoContract]
87+
[System.ComponentModel.Description("玩家属性完整快照(服务端推送,登录后或重连时发送)")]
88+
[MessageTypeHandler(((310) << 16) + 10)]
89+
public sealed class NotifyPlayerAttributeSync : MessageObject, INotifyMessage
90+
{
91+
/// <summary>
92+
/// 全部最终属性条目
93+
/// </summary>
94+
[ProtoMember(1)]
95+
[System.ComponentModel.Description("全部最终属性条目")]
96+
public List<PlayerAttributeEntry> Attributes { get; set; } = new List<PlayerAttributeEntry>();
97+
98+
public override void Clear()
99+
{
100+
Attributes.Clear();
101+
}
102+
}
103+
104+
/// <summary>
105+
/// 单个玩家属性最终值变化(服务端推送增量)
106+
/// </summary>
107+
[ProtoContract]
108+
[System.ComponentModel.Description("单个玩家属性最终值变化(服务端推送增量)")]
109+
[MessageTypeHandler(((310) << 16) + 11)]
110+
public sealed class NotifyPlayerAttributeChanged : MessageObject, INotifyMessage
111+
{
112+
/// <summary>
113+
/// 发生变化的最终属性编号(AttributeType)
114+
/// </summary>
115+
[ProtoMember(1)]
116+
[System.ComponentModel.Description("发生变化的最终属性编号(AttributeType)")]
117+
public int Type { get; set; }
118+
119+
/// <summary>
120+
/// 新的最终值
121+
/// </summary>
122+
[ProtoMember(2)]
123+
[System.ComponentModel.Description("新的最终值")]
124+
public long Value { get; set; }
125+
126+
public override void Clear()
127+
{
128+
Type = default;
129+
Value = default;
130+
}
131+
}
132+
133+
}

0 commit comments

Comments
 (0)