Skip to content

玩家系统(Player System) ​

管理游戏中的玩家槽位、用户连接、队伍关系和控制权。

定义位置:GameCore.PlayerAndUsers

核心概念 ​

Player vs User ​

概念类型生命周期用途
Player槽位游戏全程存在控制单位、队伍关系、指令权限
User平台用户连接时绑定,断线后可能变化持久化数据(CloudData)、显示名
  • Player.Id(int)= 临时槽位索引,每局不同
  • User.UserId(long)= 平台持久 ID,用于 CloudData

AI 玩家没有 User(Player.User == null)。

Player 类 ​

属性 ​

属性类型端说明
Idint双端槽位 ID
TeamTeam双端(服务端可写)所属队伍
IsNeutralbool双端(服务端可写)中立标志
ControllerSettingControllerType双端配置的控制器类型(来自 GameDataPlayerSettings)
ControllerControllerType双端实际控制器类型
MainUnitUnit?双端(服务端可写)主控单位
UnitsIReadOnlyCollection<Unit>双端拥有的所有单位
SceneScene双端当前场景
UserUser?双端绑定的平台用户;AI 玩家为 null
IsOnlinebool服务端Connected 或 Unresponsive 状态;AI 始终在线
IsConnectionUnresponsivebool服务端当前连接是否处于暂时无响应状态
SlotControllerSlotController服务端PlayerController(人类)或 AIController(AI)

MainUnit 绑定与移动能力无关:未启用 UpdateFlags.Walkable 或 AllowMover 的静态单位也可以作为主控单位,用于默认镜头跟随。移动摇杆和移动指令仍要求单位具备对应的移动能力。设置无效单位时会清空绑定并记录警告;单位应属于该玩家,并与玩家处于同一场景。

静态成员 ​

成员类型说明
AllPlayersstatic List<Player>所有玩家
DefaultPlayerstatic Player系统玩家(Id = 0)
LocalPlayerstatic Player本地玩家(仅客户端)
GetById(int id)static Player?按 ID 查找

方法 ​

csharp
// 关系查询
PlayerRelationShip GetRelationShip(Player other);
bool CanControlTo(Entity entity, bool isRequest = false);

// 属性
TValue? GetProperty<TValue>(PropertyPlayer property);
void SetProperty<TValue>(PropertyPlayer property, TValue value);    // 服务端
void AddProperty(PropertyPlayer property, Fixed value);              // 服务端
TValue? GetUIProperty<TValue>(PropertyPlayerUI property);

// 场景(服务端)
bool SwitchScene(Scene scene, bool withMainUnit = false);

// 控制权共享(服务端)
void SetShareControlTo(Player player, bool on);
bool IsShareControlTo(Player player);
void SetShareSightTo(Player player, bool on);
bool IsShareSightTo(Player player);

// 消息广播(服务端)
static void BroadcastClientMessage<T>(ref T protocol, Func<Player, bool>? filter = null);

切场景与主控绑定 ​

SwitchScene(scene) 默认 withMainUnit = false,只请求切换玩家当前视图,不负责携带现有英雄。需要框架携带当前主控时可用 SwitchScene(scene, true);若项目已通过 hero.MoveTo(...) 显式迁移英雄,则使用 SwitchScene(scene, false),在服务端玩家与英雄同场后重新设置 MainUnit。绑定前核对原玩家槽位与英雄归属,避免因时序问题误换角色或玩家。

返回 true 不代表客户端地图、英雄副本和主控都已就绪。客户端应结合场景加载状态、本地英雄副本及 Player.LocalPlayer.MainUnit 确认完成;其他玩家的 Scene 在客户端不一定可靠,不能用于判断远端玩家是否迁移成功。迁移顺序、库存校验、失败恢复及双端验收见 携带已有英雄跨场景。

客户端事件 ​

csharp
event Action<bool>? OnNeutralChanged;
event Action<Team>? OnTeamChanged;
event Action<Unit?>? OnMainUnitChanged;

User 类 ​

属性 ​

属性类型端说明
UserIdlong双端平台持久 ID
Namestring?双端显示名(需先 Fetch)
NameFetchStatusUserNameFetchStatus服务端NotStarted/InProgress/Succeeded/Failed/NotFound

方法 ​

csharp
// 获取平台用户名(服务端)
async Task<bool> FetchNameAsync();
static async Task<UserCloudDataResult> FetchNameAsyncForAllLivingUsers();

// 事件
event Action<User, string>? OnNameFetched;  // 服务端

ControllerType ​

值说明
User人类槽位,有 User,支持连接/断线
ComputerAI 槽位,无 User,始终在线

队伍与关系 ​

Team ​

csharp
int Id;
LocalizedString? DisplayName;
static Team? GetById(int id);
Player[] GetAllPlayers();

PlayerRelationShip ​

值条件
Player同一玩家
Ally同队不同玩家
Neutral任一方 IsNeutral
Enemy不同队且不中立

用法 ​

csharp
var rel = player1.GetRelationShip(player2);
if (rel == PlayerRelationShip.Enemy)
{
    Game.Logger.LogInformation("玩家 {P1} 和 {P2} 是敌对关系", player1.Id, player2.Id);
}

玩家事件 ​

通过 Trigger<T> 订阅,在 OnGameTriggerInitialization 中注册。

服务端事件 ​

事件类型属性触发时机
EventPlayerUserConnectedPlayer, User, OldUser?用户连接
EventPlayerUserDisconnectedPlayer, User用户断线
EventPlayerConnectionUnresponsivePlayer, User连接进入 paused / 暂时失联
EventPlayerConnectionRestoredPlayer, User连接从 paused / 暂时失联恢复
EventPlayerUserActivelyQuitPlayer, User用户主动退出
EventPlayerUserAbandonReconnectPlayer, User放弃重连
EventPlayerUserNameFetchedPlayer, User, Name名称获取完成
EventPlayerStatusChangedPlayer, Status, OldStatus状态变化
EventPlayerPropertyChangePlayer, Property属性变化

EventPlayerConnectionUnresponsive / EventPlayerConnectionRestored 对应底层连接 paused / resume。常见来源包括客户端切后台、少数加载流程主动 pause,或 Host 在一段时间没有收到客户端包后自动把连接标为 paused。默认配置下,这个无包窗口通常约 10 秒;若 paused 状态继续超过地图配置 connection_timeout_when_paused,默认 120 秒,才会进入真正断线流程并触发 EventPlayerUserDisconnected。

客户端事件 ​

事件类型属性触发时机
EventPlayerMainUnitChangedPlayer, Unit?主控单位变化
EventPlayerUIPropertyChangePlayer, PropertyPlayerUIUI 属性变化

订阅示例 ​

csharp
public static void OnRegisterGameClass()
{
    Game.OnGameTriggerInitialization += OnGameTriggerInitialization;
}

#if SERVER
private static void OnGameTriggerInitialization()
{
    if (Game.GameModeLink != ScopeData.GameDataGameMode.MapGameMode) return;

    var connectTrigger = new Trigger<EventPlayerUserConnected>(OnPlayerConnected);
    connectTrigger.Register(Game.Instance);

    var disconnectTrigger = new Trigger<EventPlayerUserDisconnected>(OnPlayerDisconnected);
    disconnectTrigger.Register(Game.Instance);
}

private static void OnPlayerConnected(EventPlayerUserConnected e)
{
    Game.Logger.LogInformation("玩家 {PlayerId} 连接,用户 {UserId}", e.Player.Id, e.User.UserId);
}

private static void OnPlayerDisconnected(EventPlayerUserDisconnected e)
{
    Game.Logger.LogInformation("玩家 {PlayerId} 断线", e.Player.Id);
}
#endif

PlayerStatus ​

值说明
Open槽位开放
Connected已连接
Unresponsive连接 paused / 暂时失联
Disconnected已断线
Left已离开

常用模式 ​

遍历玩家 ​

csharp
// 所有玩家
foreach (var player in Player.AllPlayers) { ... }

// 按 ID 查找
var player = Player.GetById(playerId);

// 队友
var teammates = myPlayer.Team.GetAllPlayers();

// 在线玩家(服务端)
#if SERVER
var online = Player.AllPlayers.Where(p => p.IsOnline);
#endif

CloudData 与 User ​

csharp
#if SERVER
// 正确:使用 User.UserId
var user = player.User;
if (user != null)
{
    var result = await CloudData.ForUser(user.UserId).QueryUserDataAsync(...);
}

// 错误:不要用 Player.Id 做持久化标识
// Player.Id 是临时槽位索引,每局可能不同
#endif

控制权检查 ​

csharp
if (player.CanControlTo(unit))
{
    unit.CastAbility(abilityLink, target);
}

相关文档 ​