C# 从事件驱动编程到实现协程版本Actor模型
概述
本文主要理解从事件驱动编程到实现完整版本的Actor模型,再将Actor模型进化到最终的协程版 Actor 模型,最后实现一个高性能游戏服务器基本架构。
事件驱动编程 or 轮询
轮询是 CPU 主动查,事件驱动是「状态变化时,主动通知 CPU」,CPU 在没事件的时候,线程是挂起 / 休眠 / 空闲状态,此时该线程的 CPU 占用率≈0%,这是最关键的点。
轮训:
- 轮询:CPU 主动问 → 「状态变了吗?变了吗?变了吗?变了吗?变了吗?」
- 一种是纯 while (true) 无休眠轮询,CPU 直接飙满 1-2 个核心(性能杀手)
- 另一种是加了 Thread.Sleep 的轮询,看似 CPU 降了,但有致命缺点:延迟 + CPU 浪费并存,而且 Sleep 的时间不好把控
事件驱动
- 事件驱动:状态 主动喊 → 「我变了!快来处理!」
- CPU 占用率≈0% (最核心的)
- 所有需要「持续监控某个状态 / 数据 / 硬件 / 消息」的场景,都应该用事件驱动替换轮询。
- 由于状态改变立即通知所以响应实时无延迟
- 代码解耦, 「状态监控」和「业务逻辑」完全分离,事件定义、事件触发、事件处理三者独立;
- 支持多事件 / 多订阅
- 线程安全:C# 原生event关键字天然支持线程安全,无需手动加锁处理并发。
事件驱动的核心逻辑是:
- 定义一个
事件 (Event),这个事件代表「目标状态发生变化 / 目标条件满足」; - 为这个事件
绑定处理方法 (回调函数),也就是「状态变化后要执行的业务逻辑」; - 当且仅当「目标状态真的发生变化」时,
主动触发这个事件,此时才执行绑定的业务逻辑; - 无事件发生时,执行线程处于「休眠 / 空闲」状态,此时该线程的
CPU 占用率 = 0%!
C# 事件驱动的 3 种主流实现方案(按场景选择)
方案 1:原生event事件(推荐,90% 场景用这个)
适用场景:同步场景、本地状态监控、类与类之间的通信、硬件状态变更、消息通知等绝大多数业务;
核心关键字:delegate(委托) + event(事件)。
方案 2:异步回调(Callback)- 替代「异步轮询」
很多时候你需要轮询的是异步操作的结果(比如:网络请求是否完成、文件是否写入成功、数据库查询是否返回),此时用「异步回调」替代「异步轮询」,本质也是事件驱动。
核心思想:异步操作完成后,主动调用回调方法,而不是循环判断「异步操作是否完成」。
方案 3:基于Task/ValueTask的异步事件(.NET 5+ 推荐)
结合async/await的异步事件,是高并发场景的最优解,完全无阻塞,CPU 占用率依然≈0%,适合:微服务、分布式、高并发接口、消息队列等场景。
游戏服务器中哪些用到了事件驱动编程
C# 游戏服务端是「事件驱动编程」的极致、重度应用场景,游戏服务端 99% 的核心核心逻辑,都是基于事件驱动实现的,几乎没有任何核心模块会用轮询(轮询在游戏服务端里是「大忌」,用了必出性能问题)。
- 【最核心】网络通信层 - 游戏服务端的「生命线」(100% 事件驱动)
- 【业务核心】玩家行为 / 游戏内交互 - 100% 事件驱动
- 高频玩家行为事件(全部事件驱动,无轮询)
- 游戏服务端会封装一个轻量级事件总线(EventBus),统一管理所有业务事件,这是游戏服务端的标配
- 【游戏世界】场景 / 怪物 / 副本 核心事件驱动
- 游戏的「大世界」本身就是一个事件驱动的生态,游戏世界里的所有环境变化、NPC / 怪物行为、副本逻辑,全部都是事件驱动,这部分是游戏的「玩法核心」
- 【性能核心】定时器 / 心跳 / 定时任务 - 事件驱动替代轮询定时器(必用优化)
- C# 游戏服务端里,所有的定时任务、心跳检测、倒计时、刷新逻辑,全部都是用 事件驱动的定时器 实现,核心就是:定时器到点后「主动触发事件」,无任务时 CPU 完全空闲,CPU 占用≈0%。
游戏中这么多事件难道都要单独实现?
面对 几百 / 几千 / 上万种玩家操作类型,✅ 正确姿势:只需要「1 个统一的全局事件总线」+「一套消息 ID 映射规则」,全程只需要订阅 1 次,就能完美承接所有操作,零冗余、高性能、极致解耦;
游戏中, 所有的时间基本都是 基于玩家操作 + 时间 触发事件。玩家操作可以看成不同的网络数据包。
「一个全局事件总线 + 一套消息 ID 映射规则 + 分模块的业务处理器」
- 只定义
1 个统一的全局事件,承载所有玩家的所有操作; - 用
数字 ID(Opcode)区分几千种不同的操作; - 按业务逻辑
分模块处理不同 ID 段的操作(比如战斗类、角色类、社交类); - 全程
只需要订阅 1 次事件,永久生效,新增操作零改动核心代码。
/// <summary>
/// 游戏操作消息ID定义(Opcode)- 几千个操作都在这里定义
/// 行业标准:按业务模块分段,一目了然,无限扩展
/// </summary>
public class GameOpCode
{
#region 战斗模块 1000-1999 (几百个普攻+技能都在这里)
public const int Player_Move = 1001; // 玩家移动
public const int Player_Attack = 1002; // 玩家普攻
public const int Player_Skill_1 = 1003; // 技能1
public const int Player_Skill_2 = 1004; // 技能2
// ... 一直到 Player_Skill_1000 = 2002
public const int Player_Revive = 1999; // 玩家复活
#endregion
#region 角色模块 2000-2999
public const int Player_PickItem = 2001; // 拾取装备
public const int Player_WearEquip = 2002; // 穿戴装备
public const int Player_Strengthen = 2003; // 装备强化
// ... 几百个角色操作
#endregion
#region 社交模块 3000-3999
public const int Player_Chat = 3001; // 世界聊天
public const int Player_Team = 3002; // 创建组队
public const int Player_Trade = 3003; // 发起交易
// ... 几百个社交操作
#endregion
// 后续新增任何操作,只需要在这里加一行常量即可!
}
Actor模型 + 事件驱动
正确的设计方案是:事件驱动(消息产生) + Actor 邮箱队列(消息排队) + 单线程串行处理(消息消费) —— 两者深度融合,不是二选一!事件驱动负责「解耦 Actor 之间的通信」,邮箱队列 + 单线程负责「保证 Actor 内部数据一致性」,这也是所有商业游戏的标准实现、工业级 Actor 模型的标准答案。
游戏行业的标准 Actor 模型,核心就三个不可动摇的原则,这三个原则组合在一起,从根上杜绝了所有线程安全问题、数据一致性问题,且全程无锁、极致性能,这也是 Actor 模型能成为高并发游戏服务端标配的核心原因:
- ✔
原则 1:每个 Actor 拥有【独立的私有邮箱队列 (Message MailBox)】- 所有对该 Actor 的交互请求 / 事件 / 消息,都不会直接触发回调,而是被封装成一个「消息对象」,投递到该 Actor 的邮箱队列中排队;
- 邮箱队列是先进先出 (FIFO) 的,保证消息的处理顺序和投递顺序一致;
- 队列是 Actor私有的,只有自己能访问,其他 Actor 绝对不能操作,天然隔离。
- ✔
原则 2:每个 Actor 执行【单线程串行消费】- Actor 初始化时,会启动一个线程:死循环从邮箱队列中取消息,取到消息就串行执行消息的处理逻辑,处理完一个再处理下一个;
- 核心精髓:Actor 内部的所有业务逻辑(扣血、扣体力、属性修改),永远只在这一个线程中执行,绝对不会有第二个线程进入 Actor 内部;
- 这个特性直接让「线程安全问题」消失,因为没有并发,就没有竞争!
- ✔
原则 3:Actor 的【消息驱动 + 完全自治】,无任何跨线程直接调用- Actor 的所有行为,都由「邮箱队列中的消息」驱动,无消息时,工作线程处于
休眠状态,CPU 占用≈0%(不是轮询的空转!); - Actor 的属性修改、状态更新、业务逻辑,永远只有自己的工作线程能执行,绝对禁止任何外部线程直接调用 / 修改,完全自治;
- Actor 之间的通信,永远是「发送消息到对方邮箱」,而不是直接调用方法 / 触发事件回调。
- Actor 的所有行为,都由「邮箱队列中的消息」驱动,无消息时,工作线程处于
在原则2中, 每个 actor都有一个线程来专门消费邮箱中的消息, 这里的线程根据总体actor的多少可以依次升级:
- Actor少(几百个内), 可以使用线程完成
- Actor多(几千), 用「线程池 Task」代替独立线程
- Actor海量(上万或更多), 用「async/await协程」优化 Task
在原则3中, 邮箱队列中无消息时, actor的工作协程会进入休眠状态, 通常情况下 线程/task/async,await 这些异步多线程操作状态都是自动转化的, 比如由空闲->工作, 由工作->等待。这里我们就需要在actor邮箱无消息时 await 手动设置异步的状态, 从而手动控制actor的休眠等。
- 最优解:while(true) + await挂起 无限循环消费
System.Threading.Tasks.Sources让你能够自定义 ValueTask 和 ValueTask的“信号源”(Source)。本例中可以手动设置协程的挂起/运行 等状态.
「Actor模型」极简源码
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks.Sources;
namespace ActorServer;
internal class Program
{
static async Task Main(string[] args)
{
// ✅ .NET10 线程池最优配置 - 游戏服务端黄金公式,必须在启动时执行
ConfigureThreadPool();
// 测试:创建2个玩家Actor,模拟攻击行为
using var player1 = new PlayerActor("player_001", "萧炎");
using var player2 = new PlayerActor("player_002", "林动");
// 模拟高并发攻击:1000次攻击消息入队
Parallel.For(0, 1000, i => player1.Attack(player2.ActorId));
await player2.WaitAllMessagesProcessedAsync();
Console.WriteLine($"测试完成,玩家2最终血量: {player2.Hp}");
Console.WriteLine("Actor模型运行正常,零GC零锁零并发风险");
// 运行性能测试【可选】
// await RunBenchmarks();
}
/// <summary>
/// .NET10 线程池最优配置 (多核CPU极致利用,无上下文切换)
/// </summary>
private static void ConfigureThreadPool()
{
var cpuCore = Environment.ProcessorCount;
ThreadPool.SetMinThreads(cpuCore * 2, cpuCore * 10);
ThreadPool.SetMaxThreads(cpuCore * 4, cpuCore * 20);
}
}
/// <summary>
/// 核心消息体 - 只读值类型 零GC分配 栈内存存储
/// </summary>
public readonly struct ActorEventArgs
{
public string SourceActorId { get; init; }
public string TargetActorId { get; init; }
public int EventOpCode { get; init; }
public object? AttachData { get; init; } // ✅ 新增:消息附加数据,传递复杂参数,零GC
}
/// <summary>
/// 扣血消息体 - 继承扩展,同样零GC
/// </summary>
public readonly struct ActorHpReduceEventArgs
{
public string SourceActorId { get; init; }
public string TargetActorId { get; init; }
public int EventOpCode { get; init; }
public int ReduceValue { get; init; }
}
/// <summary>
/// 事件指令码定义
/// </summary>
public static class ActorEventOpCode
{
public const int Hp_Reduce = 10001;
}
/// <summary>
/// 全局事件总线 - 多线程安全发布,无锁设计
/// </summary>
public static class ActorEventBus
{
public delegate void ActorEventHandler(ActorEventArgs args);
public static event ActorEventHandler? OnActorEvent;
public static void PublishEvent(ActorEventArgs args)
{
OnActorEvent?.Invoke(args);
}
public static void PublishHpReduceEvent(ActorHpReduceEventArgs args)
{
PublishEvent(new ActorEventArgs
{
SourceActorId = args.SourceActorId,
TargetActorId = args.TargetActorId,
EventOpCode = args.EventOpCode,
AttachData = args
});
}
}
/// <summary>
/// .NET10 Actor基类 终极优化版
/// ✅ 单协程+while(true)串行消费 ✅ SingleWaiterAutoResetEvent零GC唤醒 ✅ ConcurrentQueue批量消费
/// ✅ readonly struct零GC消息 ✅ ValueTask零分配协程 ✅ 杜绝无效唤醒 ✅ 优雅销毁 ✅ 异常隔离
/// </summary>
public abstract class BaseActor : IDisposable
{
#region 核心组件
public string ActorId { get; }
protected bool IsAlive { get; private set; } = true;
private readonly ConcurrentQueue<ActorEventArgs> _mailBoxQueue = new();
private readonly SingleWaiterAutoResetEvent _waitEvent = new(); // 核心异步信号,零GC
private Task? _messageConsumeTask; // 唯一消费协程
#endregion
#region Actor核心属性(串行修改,绝对线程安全)
private int _hp = 100;
public int Hp
{
get => _hp;
protected set => _hp = Math.Max(0, value);
}
private int _stamina = 100;
public int Stamina
{
get => _stamina;
protected set => _stamina = Math.Max(0, value);
}
#endregion
protected BaseActor(string actorId)
{
ActorId = actorId ?? throw new ArgumentNullException(nameof(actorId));
ActorEventBus.OnActorEvent += EnqueueEvent;
_messageConsumeTask = ConsumeMessageLoopAsync(); // 启动唯一消费协程,终身仅一次
}
/// <summary>
/// 消息入队 - 线程安全,杜绝无效唤醒,零锁,极致轻量
/// </summary>
private void EnqueueEvent(ActorEventArgs args)
{
if (!IsAlive || args.TargetActorId != ActorId) return;
var needWake = _mailBoxQueue.IsEmpty;
_mailBoxQueue.Enqueue(args);
if (needWake) _waitEvent.Signal(); // 仅队空时唤醒,杜绝99%无效唤醒
}
/// <summary>
/// 核心:唯一消费协程,while(true)串行消费,零并发,零GC,CPU空转0%
/// </summary>
private async Task ConsumeMessageLoopAsync()
{
while (IsAlive)
{
try
{
// 批量消费所有可用消息,最大化吞吐,减少循环开销
while (_mailBoxQueue.TryDequeue(out var msg))
{
await HandleActorEventAsync(msg);
}
// 无消息时协程挂起,释放线程,CPU=0%,零GC
if (IsAlive) await _waitEvent.WaitAsync();
}
catch (Exception ex)
{
// 异常隔离:单条消息报错不终止协程,生产级必备
Console.WriteLine($"【{ActorId}】消息处理异常: {ex.Message}");
}
}
}
/// <summary>
/// 业务消息分发 - 子类可重写扩展
/// </summary>
protected virtual async ValueTask HandleActorEventAsync(ActorEventArgs args)
{
if (args.EventOpCode == ActorEventOpCode.Hp_Reduce)
{
await HandleHpReduceAsync(args);
}
}
/// <summary>
/// 扣血业务逻辑 - 模拟真实游戏场景:同步计算+异步IO(存档/广播)
/// </summary>
private async ValueTask HandleHpReduceAsync(ActorEventArgs args)
{
var oldHp = Hp;
if (args.AttachData is ActorHpReduceEventArgs hpArgs)
{
Hp -= hpArgs.ReduceValue; // 使用消息体的真实扣血数值
}
Console.WriteLine($"攻击后,hp: {Hp}");
// 模拟异步IO:数据库存档/Redis写入/网络广播,零阻塞释放线程
await SaveHpToDbAsync(ActorId, Hp);
await BroadcastHpChangeAsync(ActorId, oldHp, Hp);
}
#region 模拟业务异步IO(可替换为真实业务代码)
protected async ValueTask SaveHpToDbAsync(string actorId, int hp)
{
await ValueTask.CompletedTask; // 真实场景替换为 await DbContext.SaveChangesAsync()
}
protected async ValueTask BroadcastHpChangeAsync(string actorId, int oldHp, int newHp)
{
await ValueTask.CompletedTask; // 真实场景替换为 await Network.BroadcastAsync()
}
#endregion
/// <summary>
/// 对外发送扣血消息
/// </summary>
public void Attack(string targetActorId)
{
ActorEventBus.PublishHpReduceEvent(new ActorHpReduceEventArgs
{
SourceActorId = ActorId,
TargetActorId = targetActorId,
EventOpCode = ActorEventOpCode.Hp_Reduce,
ReduceValue = 10
});
}
#region ✅ 核心新增:生产环境必备 - 等待消息队列消费完成(解决异步时序问题,零GC/零锁)
/// <summary>
/// 等待消息队列中所有消息处理完成,异步无阻塞,零性能损耗
/// 适用场景:测试、业务结算、存档前等待处理完所有消息
/// </summary>
// ✅ 可靠等待消息消费完成:零GC、无死锁、必等处理完,测试/生产通用
public async ValueTask WaitAllMessagesProcessedAsync(TimeSpan timeout = default)
{
timeout = timeout == default ? TimeSpan.FromSeconds(5) : timeout;
var cts = new CancellationTokenSource(timeout);
while (!_mailBoxQueue.IsEmpty && !cts.Token.IsCancellationRequested)
{
await Task.Yield();
}
if (cts.Token.IsCancellationRequested)
{
throw new TimeoutException($"等待消息处理超时({timeout.TotalSeconds}s)");
}
}
#endregion
/// <summary>
/// 优雅销毁Actor,释放资源,退出协程,无内存泄漏
/// </summary>
public void Dispose()
{
IsAlive = false;
ActorEventBus.OnActorEvent -= EnqueueEvent;
_waitEvent.Signal(); // 唤醒挂起的协程,让循环正常退出
_mailBoxQueue.Clear();
GC.SuppressFinalize(this);
}
}
/// <summary>
/// 玩家Actor子类,业务层扩展,完全复用基类核心逻辑
/// </summary>
public class PlayerActor : BaseActor
{
public string PlayerName { get; set; }
public PlayerActor(string actorId, string playerName) : base(actorId)
{
PlayerName = playerName;
}
}
/// <summary>
/// 修复阻塞版:零GC 零锁 单等待者 自动重置 异步事件
/// 解决核心问题:WaitAsync分支缺失、Version绑定错误、Signal唤醒时序
/// </summary>
public class SingleWaiterAutoResetEvent : IValueTaskSource
{
// 状态机:0=空闲(无等待/无信号) 1=等待中(协程挂起) 2=有信号(未消费)
private const int STATE_IDLE = 0;
private const int STATE_WAITING = 1;
private const int STATE_SIGNALED = 2;
private int _state = STATE_IDLE;
private ManualResetValueTaskSourceCore<bool> _waitSource;
private short _version; // 手动管理Version,避免续体错乱
public SingleWaiterAutoResetEvent()
{
_waitSource = new ManualResetValueTaskSourceCore<bool>
{
RunContinuationsAsynchronously = true
};
_version = 0;
}
/// <summary>
/// 等待信号(修复核心:补全IDLE分支,正确初始化ValueTask)
/// </summary>
public ValueTask WaitAsync()
{
// 原子操作:尝试从 空闲 → 等待中
int oldState = Interlocked.CompareExchange(ref _state, STATE_WAITING, STATE_IDLE);
if (oldState == STATE_SIGNALED)
{
// 场景1:已有待消费信号 → 直接完成任务,重置为空闲
Interlocked.Exchange(ref _state, STATE_IDLE);
_waitSource.SetResult(true);
return new ValueTask(this, _version);
}
if (oldState == STATE_IDLE)
{
// 场景2:首次等待 → 初始化ValueTaskSource,返回绑定Version的ValueTask
_waitSource.Reset(); // 确保重置,避免旧状态干扰
_version++; // 版本号递增,防止续体复用
return new ValueTask(this, _version);
}
// 场景3:并发等待(违反单等待者约束)
ThrowConcurrencyViolation();
return default; // 不可达,仅消除编译警告
}
/// <summary>
/// 发送信号(修复核心:正确唤醒等待中的协程,保留信号时序)
/// </summary>
public void Signal()
{
while (true)
{
int currentState = Volatile.Read(ref _state);
// 状态已为有信号 → 无需操作
if (currentState == STATE_SIGNALED)
return;
// 尝试原子切换状态:
// - 空闲 → 有信号
// - 等待中 → 空闲(并唤醒)
int newState = currentState == STATE_IDLE ? STATE_SIGNALED : STATE_IDLE;
int oldState = Interlocked.CompareExchange(ref _state, newState, currentState);
// CAS成功,退出循环
if (oldState == currentState)
{
// 旧状态是等待中 → 唤醒协程
if (oldState == STATE_WAITING)
{
_waitSource.SetResult(true);
}
break;
}
}
}
/// <summary>
/// 获取结果(修复核心:时序调整,先重置Source再重置状态)
/// </summary>
public void GetResult(short token)
{
try
{
// 检查版本号,防止非法调用
if (token != _version)
ThrowVersionMismatch();
// 获取结果(若未完成会抛异常,符合ValueTask语义)
_waitSource.GetResult(token);
}
finally
{
// 重置ValueTaskSource(先重置Source,再重置状态)
_waitSource.Reset();
// 自动重置为空闲状态
Volatile.Write(ref _state, STATE_IDLE);
}
}
#region IValueTaskSource接口实现(严格按规范)
public ValueTaskSourceStatus GetStatus(short token)
{
if (token != _version)
ThrowVersionMismatch();
return _waitSource.GetStatus(token);
}
public void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
{
if (token != _version)
ThrowVersionMismatch();
_waitSource.OnCompleted(continuation, state, token, flags);
}
#endregion
#region 异常辅助方法
private static void ThrowConcurrencyViolation()
=> throw new InvalidOperationException("仅支持单协程WaitAsync!禁止并发调用");
private static void ThrowVersionMismatch()
=> throw new InvalidOperationException("ValueTask版本不匹配,禁止重复调用GetResult");
#endregion
}
对于同步队列可以使用 BlockingCollection<T> 是 .NET 框架中的高级集合类,专为多线程协作设计,核心特性包括:线程安全、阻塞操作、容量控制、取消支持
/// <summary>
/// .NET10 Actor基类 【十万Actor终极最优版】
/// ✅ 核心:有界BlockingCollection(容量1000) 内存可控永不溢出 + 原生阻塞唤醒零冗余
/// ✅ 零GC | 零锁 | 零内存泄漏 | 单协程串行消费 | 异步让出CPU | 优雅销毁
/// ✅ 十万Actor适配:内存固定可控,性能稳定,无OOM风险,游戏服务端百万并发标配
/// </summary>
public abstract class BaseActor : IDisposable
{
#region 核心组件 - 十万Actor黄金配置:有界队列 容量=1000
public string ActorId { get; }
protected bool IsAlive { get; private set; } = true;
private const int MAILBOX_CAPACITY = 1000; // 黄金容量阈值
private readonly BlockingCollection<ActorEventArgs> _mailBoxQueue = new(MAILBOX_CAPACITY);
private Task? _messageConsumeTask;
#endregion
#region 异步让出CPU 最优配置 (游戏服务端黄金值)
private const int YIELD_THRESHOLD = 200; // 每次消费最多个
private int _processedCount = 0; // 当前循环已消费多个个
#endregion
protected BaseActor(string actorId)
{
ActorId = actorId ?? throw new ArgumentNullException(nameof(actorId));
ActorEventBus.OnActorEvent += EnqueueEvent;
_messageConsumeTask = ConsumeMessageLoopAsync();
}
/// <summary>
/// 消息入队:有界队列自动限流,生产者良性阻塞,零内存堆积
/// </summary>
private void EnqueueEvent(ActorEventArgs args)
{
if (!IsAlive || args.TargetActorId != ActorId || _mailBoxQueue.IsAddingCompleted)
return;
_mailBoxQueue.Add(args); // 队列满则自动阻塞生产者,完美削峰填谷
}
/// <summary>
/// 核心消费循环:GetConsumingEnumerable 异步友好,零伪唤醒,零冗余
/// </summary>
private async Task ConsumeMessageLoopAsync()
{
foreach (var msgArgs in _mailBoxQueue.GetConsumingEnumerable()) // 核心
{
if (!IsAlive) break;
try
{
await HandleActorEventAsync(msgArgs);
_processedCount++;
// 黄金组合:异步让出CPU,多Actor线程调度均衡,零GC零开销
if (_processedCount >= YIELD_THRESHOLD)
{
await ValueTask.Yield();
_processedCount = 0;
}
}
catch (Exception ex)
{
Console.WriteLine($"【{ActorId}】消息处理异常: {ex.Message}");
}
}
}
}
关于多线程中,避免轮询的一些知识点
c# 在线程、task、async/await 中,各是用什么控制其状态,例如:我需要在 一个while (true){}中控制其挂起状态,cpu=0%,也可以随时唤醒,避免轮询。
线程(Thread)中的控制
对于传统线程,我们可以使用ManualResetEvent、AutoResetEvent、Semaphore等同步原语来阻塞线程,使其挂起(不占用CPU),并在需要时唤醒。
Task中的控制
Task通常用于线程池,我们也可以使用类似的同步原语,但更推荐使用异步的等待方式。
async/await中的控制
在async/await中,我们通常使用Task.Delay、Task.Yield等,但更常见的是使用异步信号量,如SemaphoreSlim、AsyncManualResetEvent(非内置,需自定义)等。
各方案对比表
| 场景 | 推荐方案 | CPU占用 | 内存分配 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 线程暂停 | ManualResetEvent |
0% | 低 | 简单 | WinForms/WPF后台线程 |
| Task暂停 | SemaphoreSlim |
0% | 中 | 中等 | 线程池任务控制 |
| async/await暂停 | AsyncManualResetEvent(第三方库) |
0% | 中 | 中等 | ASP.NET Core、异步业务 |
| 高频暂停恢复 | ManualResetValueTaskSourceCore |
0% | 极低 | 高 | 游戏循环、高性能库 |
| 简单场景 | TaskCompletionSource |
0% | 中 | 简单 | 简单异步控制 |
ManualResetValueTaskSourceCore 详解
ManualResetValueTaskSourceCore通过手动设置状态来控制异步操作的完成,常用于高性能场景或封装现有异步模式。
在以上 async/await暂停 方案中,就是利用特性,当邮箱中无数据时设置其休眠,当邮箱中进入数据后设置其唤醒,从而达到了CPU≈0%的性能。
关键特性包括:
核心方法有
GetStatus(获取操作状态,参数为 token,返回 ValueTaskSourceStatus 枚举)、OnCompleted(注册延续操作,参数包括 continuation、state、token 和 ValueTaskSourceOnCompletedFlags)、Reset(重置结构以准备下一次操作)、SetResult(成功完成操作并设置结果)SetException(以异常完成操作,参数为 Exception);
属性方面,
RunContinuationsAsynchronously控制延续执行方式(true 强制异步,false 允许同步),Version提供操作版本号用于同步。
游戏中buff处理
在现代游戏服务器中,Buff系统通常采用事件驱动 + 定时器 + 状态缓存的组合方案,既能保证实时性,又能支持大规模并发。
核心思想:将N个定时器的问题转化为N个时间槽中事件分发的问题,通过批量处理大幅提升性能。
时间轮(Time Wheel)是一种高效调度定时任务的算法,核心思想是将时间划分为固定槽位(Slot),通过指针周期性移动触发任务执行,时间复杂度为O(1),显著优于传统堆结构(O(nlogn))。
每个Actor独立定时器
如果每个Actor都有自己的定时器,那么一个游戏服务器中有十几万个定时器将带来灾难性的性能问题。
- 内存开销:每个定时器约 80-100 字节
- 线程池压力:定时器回调使用线程池
- 上下文切换:大量定时器导致频繁线程切换
- 内存开销高,cpu开销高
集中式时间轮(Time Wheel)
单时间轮服务所有Actor
- 只有一个全局定时器
- 所有十几万Actor共享同一个时间轮
- 事件批量处理,减少上下文切换
- 中大规模,内存开低,cpu开销中
分层时间轮
将时间轮按精度分成:秒级别,分钟级别,小时级别的时间轮,根据时间选择合适的时间轮即可。
- 多个定时器,分组
- 内存开销低,cpu开销低(因为分钟和小时的触发低)
- 适合长短时混合的场景,最好长短时数量差不多的场景
分片时间轮
将Actor分组处理,每组Actor采用集中式时间轮方式管理,例如:将10万Actor分成10组,每个定时器处理1万个Actor的buff,这样合理分组后就可以处理超大规模buff问题了。
- 适合超大规模,集中式时间轮容易出现瓶颈
- 内存开销中比单个高,cpu开销低(因为少了)
- 可以将 分片时间轮+分层时间轮 混合处理,其中一个分片采用小时级别,一个分片采用分钟级别,其他几个分片采用秒级别,这样更灵活更节省资源。
非玩家角色的逻辑AI处理
经典的MMO游戏AI设计问题。
在基于Actor模型的游戏服务器中,为了避免每个Actor都使用独立的定时器(以及由此带来的性能问题),通常会将需要定时执行的逻辑(如仇恨判断、追击、游荡、攻击等)抽取到专门的系统(或称为管理器、服务)中集中处理。这些系统使用少量的定时器(例如一个或多个时间轮)来驱动,处理大量怪物的AI逻辑。
这里提出一种相对简单且高效的设计:
- 使用一个中心化的AI管理器(AIManager),它负责管理所有被激活的怪物AI。
- 怪物被激活的条件:当玩家移动时,检查玩家周围一定范围内的怪物,将这些怪物注册到AIManager中。
- AIManager使用一个定时器(例如每秒10次)来更新所有已激活怪物的AI(包括仇恨判断、移动、攻击等)。
- 怪物在未被激活时,不进行任何AI计算。
- 怪物被激活后,如果在接下来的一段时间内没有玩家在附近,则将其从AIManager中移除(休眠)。
几种实现方式与对比
| 实现方式 | 核心特点 | 适用场景 |
|---|---|---|
| 怪物自轮询模式 | 怪物Actor内置心跳,自己触发AI决策与状态变更 | 中小型游戏,逻辑简单,减少消息传递开销 |
| 定时器+Actor+邮箱 | 决策与执行分离,异步消息,线程安全 | 大型MMORPG、高并发游戏服务器,追求低延迟与可扩展性 |
| 事件驱动+状态机 | 由外部事件(如玩家进入视野)触发AI决策,而非定时轮询 | 对实时性要求极高的动作游戏,减少无效计算 |
| 混合模式 | 关键行为(攻击)用定时器,次要行为(游荡)用事件触发 | 平衡性能与实时性,常见于MOBA/ARPG游戏 |
定时器驱动AI决策
- 为怪物注册专门的定时任务(如 100ms/200ms 心跳),由全局定时器系统(如时间轮)统一管理
- 定时触发 AI 逻辑:仇恨判断(更新仇恨列表、选择目标)、追击逻辑(路径计算、移动决策)、游荡行为(随机移动、巡逻点切换)、攻击判定(攻击间隔检查、技能选择)
- 决策结果不直接修改怪物状态,而是生成状态变更指令(如ChangeState(Chase)、AttackTarget(Player123))
邮箱消息传递指令
- 每个怪物作为独立 Actor,拥有专属邮箱(消息队列)
- AI 决策模块将状态变更指令封装为消息,发送到怪物 Actor 的邮箱
- 消息异步处理,避免共享状态竞争,保证线程安全
Actor 执行状态变更
- 怪物 Actor 从邮箱中取出消息并按序处理
- 执行最终状态变更:更新移动目标、播放动画、发起攻击、同步状态到客户端等
- 状态变更完成后,可反馈结果给 AI 决策模块,形成闭环
「定时器+Actor+邮箱」 极简实现源码
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace GameMonsterAI
{
// ===================== 核心枚举定义 - 状态/消息类型 =====================
/// <summary>
/// 怪物AI核心状态(对应业务行为)
/// </summary>
public enum MonsterState
{
Idle = 0, // 空闲
Wander = 1, // 游荡/巡逻
Chase = 2, // 追击目标
Attack = 3 // 攻击目标
}
/// <summary>
/// 邮箱消息类型(AI决策结果的指令载体)
/// </summary>
public enum MonsterMsgType
{
UpdateHate, // 更新仇恨列表
DoWander, // 执行游荡行为
DoChase, // 执行追击行为
DoAttack, // 执行攻击行为
ResetIdle // 重置为空闲状态
}
// ===================== 数据实体 - 消息/仇恨目标 =====================
/// <summary>
/// 怪物Actor的【邮箱消息实体】- 所有状态变更都通过该消息传递,线程安全
/// </summary>
public class MonsterMailMsg
{
public MonsterMsgType MsgType { get; set; }
public int TargetPlayerId { get; set; } // 仇恨目标玩家ID
public float HateValue { get; set; } // 仇恨值
public (float X, float Y) TargetPos { get; set; } // 目标坐标
public (float X, float Y) WanderPos { get; set; } // 游荡目标坐标
}
/// <summary>
/// 仇恨目标实体 - 存储玩家仇恨相关数据
/// </summary>
public class HateTarget
{
public int PlayerId { get; set; }
public float HateValue { get; set; }
public (float X, float Y) PlayerPos { get; set; }
}
// ===================== 核心核心 - 怪物Actor类(带专属邮箱) =====================
/// <summary>
/// 怪物Actor【核心类】
/// 1. 每个怪物是一个独立Actor实例
/// 2. 拥有【专属线程安全的邮箱(消息队列)】,所有状态变更指令都投递到邮箱
/// 3. 独立的消息消费线程,异步执行状态变更,无锁竞争
/// 4. 只做【状态执行】,不做AI决策,完美解耦
/// </summary>
public class MonsterActor
{
// 基础属性
public int MonsterId { get; private set; }
public (float X, float Y) CurrentPos { get; private set; }
public MonsterState CurrentState { get; private set; } = MonsterState.Idle;
public float AttackRange { get; private set; } = 2.0f; // 攻击范围
public float ChaseRange { get; private set; } = 10.0f; // 追击范围
public float AttackCd { get; private set; } = 1.5f; // 攻击CD(秒)
public float LastAttackTime { get; private set; } = 0; // 上次攻击时间
// Actor核心:【专属邮箱】- 线程安全的消息队列,所有AI决策指令都发往这里
private readonly ConcurrentQueue<MonsterMailMsg> _mailBox = new();
// 仇恨列表 - Actor内部维护的核心数据
private readonly ConcurrentDictionary<int, HateTarget> _hateTargets = new();
// 消息消费线程的停止信号
private readonly CancellationTokenSource _cts = new();
// 随机数-游荡坐标生成
private readonly Random _random = new();
public MonsterActor(int monsterId, float initX, float initY)
{
MonsterId = monsterId;
CurrentPos = (initX, initY);
// 启动Actor的【邮箱消息消费线程】- 持续消费消息,执行状态变更
Task.Run(ProcessMailBoxAsync, _cts.Token);
}
#region 对外API - 投递消息到邮箱(AI决策模块调用)
/// <summary>
/// 投递消息到怪物的专属邮箱 - 外部唯一的状态变更入口
/// </summary>
public void SendMailToActor(MonsterMailMsg msg)
{
_mailBox.Enqueue(msg);
Console.WriteLine($"【怪物{MonsterId}】邮箱收到消息:{msg.MsgType},当前邮箱消息数:{_mailBox.Count}");
}
#endregion
#region 核心内部逻辑 - 消费邮箱消息,执行最终的状态变更
/// <summary>
/// 异步消费邮箱消息 - Actor的核心工作方法
/// 所有AI决策的结果,最终都在这里执行状态变更,无任何决策逻辑
/// </summary>
private async Task ProcessMailBoxAsync()
{
while (!_cts.Token.IsCancellationRequested)
{
if (_mailBox.TryDequeue(out var msg))
{
// 根据消息类型执行对应的状态变更
switch (msg.MsgType)
{
case MonsterMsgType.UpdateHate:
UpdateHateList(msg);
break;
case MonsterMsgType.DoWander:
DoWanderAction(msg.WanderPos);
break;
case MonsterMsgType.DoChase:
DoChaseAction(msg.TargetPlayerId, msg.TargetPos);
break;
case MonsterMsgType.DoAttack:
DoAttackAction(msg.TargetPlayerId);
break;
case MonsterMsgType.ResetIdle:
ResetToIdle();
break;
}
}
await Task.Delay(10); // 降低CPU占用,不影响消息处理实时性
}
}
#endregion
#region 状态变更具体实现 - 纯执行逻辑,无决策
// 更新仇恨列表(玩家攻击/靠近都会触发仇恨值增减)
private void UpdateHateList(MonsterMailMsg msg)
{
if (_hateTargets.ContainsKey(msg.TargetPlayerId))
{
_hateTargets[msg.TargetPlayerId].HateValue += msg.HateValue;
_hateTargets[msg.TargetPlayerId].PlayerPos = msg.TargetPos;
}
else
{
_hateTargets.TryAdd(msg.TargetPlayerId, new HateTarget
{
PlayerId = msg.TargetPlayerId,
HateValue = msg.HateValue,
PlayerPos = msg.TargetPos
});
}
Console.WriteLine($"【怪物{MonsterId}】仇恨更新:玩家{msg.TargetPlayerId},仇恨值={msg.HateValue},当前仇恨目标数={_hateTargets.Count}");
}
// 执行游荡行为 - 随机移动、状态变更
private void DoWanderAction((float X, float Y) wanderPos)
{
if (CurrentState != MonsterState.Wander)
{
CurrentState = MonsterState.Wander;
Console.WriteLine($"【怪物{MonsterId}】状态变更:{MonsterState.Idle} → {MonsterState.Wander}");
}
CurrentPos = wanderPos;
Console.WriteLine($"【怪物{MonsterId}】游荡到坐标:X={CurrentPos.X:F1}, Y={CurrentPos.Y:F1}");
}
// 执行追击行为 - 向目标移动、状态变更
private void DoChaseAction(int targetPlayerId, (float X, float Y) targetPos)
{
if (CurrentState != MonsterState.Chase)
{
CurrentState = MonsterState.Chase;
Console.WriteLine($"【怪物{MonsterId}】状态变更:{CurrentState} → {MonsterState.Chase},追击目标:玩家{targetPlayerId}");
}
// 模拟向目标移动(实际项目替换为寻路逻辑)
CurrentPos = ((CurrentPos.X + targetPos.X) / 2, (CurrentPos.Y + targetPos.Y) / 2);
Console.WriteLine($"【怪物{MonsterId}】追击移动到:X={CurrentPos.X:F1}, Y={CurrentPos.Y:F1},目标坐标:X={targetPos.X:F1}, Y={targetPos.Y:F1}");
}
// 执行攻击行为 - 攻击判定、CD校验、状态变更
private void DoAttackAction(int targetPlayerId)
{
var now = (float)DateTime.Now.TimeOfDay.TotalSeconds;
if (now - LastAttackTime < AttackCd)
{
Console.WriteLine($"【怪物{MonsterId}】攻击CD中,剩余:{(AttackCd - (now - LastAttackTime)):F1}秒");
return;
}
if (CurrentState != MonsterState.Attack)
{
CurrentState = MonsterState.Attack;
Console.WriteLine($"【怪物{MonsterId}】状态变更:{CurrentState} → {MonsterState.Attack},攻击目标:玩家{targetPlayerId}");
}
LastAttackTime = now;
Console.WriteLine($"【怪物{MonsterId}】攻击命中!玩家{targetPlayerId} 受到伤害,攻击CD重置为{AttackCd}秒");
}
// 重置为空闲状态
private void ResetToIdle()
{
if (CurrentState != MonsterState.Idle)
{
Console.WriteLine($"【怪物{MonsterId}】状态变更:{CurrentState} → {MonsterState.Idle},仇恨清空");
CurrentState = MonsterState.Idle;
_hateTargets.Clear();
}
}
#endregion
#region 对外提供状态查询(AI决策模块调用,只读,无修改)
/// <summary>
/// 获取当前仇恨列表(供AI决策层做仇恨判断,只读)
/// </summary>
public ConcurrentDictionary<int, HateTarget> GetHateTargets() => _hateTargets;
/// <summary>
/// 获取当前状态(供AI决策层判断,只读)
/// </summary>
public MonsterState GetCurrentState() => CurrentState;
/// <summary>
/// 获取当前坐标(供AI决策层计算,只读)
/// </summary>
public (float X, float Y) GetCurrentPos() => CurrentPos;
#endregion
/// <summary>
/// 生成随机游荡坐标(AI决策层调用)
/// </summary>
public (float X, float Y) GetRandomWanderPos()
{
return (CurrentPos.X + _random.Next(-3, 4), CurrentPos.Y + _random.Next(-3, 4));
}
/// <summary>
/// 停止Actor
/// </summary>
public void Stop()
{
_cts.Cancel();
}
}
// ===================== 独立AI决策模块(核心定时器驱动) =====================
/// <summary>
/// 怪物AI决策中心【纯决策层】
/// ✅ 无任何状态,无任何Actor状态修改逻辑
/// ✅ 仅通过【定时器】定时触发决策计算
/// ✅ 决策结果仅做一件事:封装成消息 → 投递到怪物Actor的邮箱
/// ✅ 完美解耦:决策层与执行层完全分离,可独立扩展/修改AI逻辑
/// </summary>
public static class MonsterAIDecisionCenter
{
private static Timer _timerHateChase; // 仇恨判断+追击决策 定时器 (200ms - 中等优先级)
private static Timer _timerAttack; // 攻击决策 定时器 (100ms - 最高优先级,攻击需要最高实时性)
private static Timer _timerWander; // 游荡决策 定时器 (500ms - 最低优先级,节省CPU)
private static MonsterActor _monster; // 关联的怪物Actor
/// <summary>
/// 启动AI决策定时器(核心入口)
/// 分层频率设计:游戏性能优化的核心最佳实践
/// </summary>
public static void StartAIDecisionTimer(MonsterActor monster)
{
_monster = monster;
// 100ms 定时器:攻击决策 → 最高优先级,攻击需要精准判定
_timerAttack = new Timer(DoAttackDecision, null, 0, 100);
// 200ms 定时器:仇恨判断+追击决策 → 中等优先级,仇恨更新/追击范围判定
_timerHateChase = new Timer(DoHateChaseDecision, null, 0, 200);
// 500ms 定时器:游荡决策 → 最低优先级,非战斗状态,降低CPU消耗
_timerWander = new Timer(DoWanderDecision, null, 0, 500);
}
/// <summary>
/// 决策逻辑1:仇恨判断 + 追击决策 (200ms)
/// 核心:筛选最高仇恨目标、判断是否超出追击范围、是否需要追击/重置空闲
/// </summary>
private static void DoHateChaseDecision(object state)
{
var hateTargets = _monster.GetHateTargets();
if (hateTargets.Count == 0) return;
// 仇恨判断核心逻辑:筛选【仇恨值最高】的玩家作为目标(游戏通用仇恨规则)
HateTarget topHateTarget = null;
foreach (var target in hateTargets.Values)
{
if (topHateTarget == null || target.HateValue > topHateTarget.HateValue)
{
topHateTarget = target;
}
}
if (topHateTarget == null || topHateTarget.HateValue < 10) // 仇恨阈值:低于10不追击
{
_monster.SendMailToActor(new MonsterMailMsg { MsgType = MonsterMsgType.ResetIdle });
return;
}
// 计算与目标的距离,判断是否在追击范围内
var monsterPos = _monster.GetCurrentPos();
var distance = MathF.Sqrt(MathF.Pow(topHateTarget.PlayerPos.X - monsterPos.X, 2) +
MathF.Pow(topHateTarget.PlayerPos.Y - monsterPos.Y, 2));
if (distance > _monster.ChaseRange)
{
_monster.SendMailToActor(new MonsterMailMsg { MsgType = MonsterMsgType.ResetIdle });
}
else if (distance > _monster.AttackRange)
{
// 超出攻击范围 → 执行追击
_monster.SendMailToActor(new MonsterMailMsg
{
MsgType = MonsterMsgType.DoChase,
TargetPlayerId = topHateTarget.PlayerId,
TargetPos = topHateTarget.PlayerPos
});
}
}
/// <summary>
/// 决策逻辑2:攻击决策 (100ms) - 最高优先级
/// 核心:判断是否有仇恨目标、是否在攻击范围内、是否满足攻击CD → 投递攻击消息
/// </summary>
private static void DoAttackDecision(object state)
{
var hateTargets = _monster.GetHateTargets();
if (hateTargets.Count == 0) return;
var topHateTarget = hateTargets.Values.MaxBy(t => t.HateValue);
var monsterPos = _monster.GetCurrentPos();
var distance = MathF.Sqrt(MathF.Pow(topHateTarget.PlayerPos.X - monsterPos.X, 2) +
MathF.Pow(topHateTarget.PlayerPos.Y - monsterPos.Y, 2));
// 核心攻击判定:在攻击范围内 + 仇恨值达标
if (distance <= _monster.AttackRange && topHateTarget.HateValue >= 10)
{
_monster.SendMailToActor(new MonsterMailMsg
{
MsgType = MonsterMsgType.DoAttack,
TargetPlayerId = topHateTarget.PlayerId
});
}
}
/// <summary>
/// 决策逻辑3:游荡决策 (500ms) - 最低优先级
/// 核心:只有【空闲状态】才会触发游荡,有仇恨目标时自动停止 → 投递游荡消息
/// </summary>
private static void DoWanderDecision(object state)
{
if (_monster.GetCurrentState() == MonsterState.Idle && _monster.GetHateTargets().Count == 0)
{
_monster.SendMailToActor(new MonsterMailMsg
{
MsgType = MonsterMsgType.DoWander,
WanderPos = _monster.GetRandomWanderPos()
});
}
}
/// <summary>
/// 停止所有定时器
/// </summary>
public static void StopAllTimer()
{
_timerAttack?.Dispose();
_timerHateChase?.Dispose();
_timerWander?.Dispose();
}
}
// ===================== 程序入口 - 测试运行 =====================
internal class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("===== .NET 10 怪物AI启动 (Actor+定时器+邮箱消息) =====");
Console.WriteLine("核心逻辑:定时器驱动AI决策 → 邮箱消息传递指令 → Actor执行状态变更\n");
// 1. 创建怪物Actor实例(ID=1,初始坐标X=0,Y=0)
var monster = new MonsterActor(1, 0, 0);
// 2. 启动AI决策定时器(核心:定时器开始驱动所有AI逻辑)
MonsterAIDecisionCenter.StartAIDecisionTimer(monster);
// 3. 模拟业务场景:玩家1001进入怪物视野,攻击怪物 → 产生仇恨值
Console.WriteLine("\n【模拟事件】玩家1001攻击怪物,添加仇恨值...");
monster.SendMailToActor(new MonsterMailMsg
{
MsgType = MonsterMsgType.UpdateHate,
TargetPlayerId = 1001,
HateValue = 50,
TargetPos = (3, 2) // 玩家初始坐标
});
// 4. 模拟:3秒后玩家1001移动坐标,持续产生仇恨
await Task.Delay(3000);
Console.WriteLine("\n【模拟事件】玩家1001移动并再次攻击,追加仇恨值...");
monster.SendMailToActor(new MonsterMailMsg
{
MsgType = MonsterMsgType.UpdateHate,
TargetPlayerId = 1001,
HateValue = 20,
TargetPos = (1, 1) // 玩家靠近坐标
});
// 5. 模拟:8秒后玩家远离,超出追击范围
await Task.Delay(5000);
Console.WriteLine("\n【模拟事件】玩家1001远离怪物,超出追击范围...");
monster.SendMailToActor(new MonsterMailMsg
{
MsgType = MonsterMsgType.UpdateHate,
TargetPlayerId = 1001,
HateValue = 0,
TargetPos = (20, 20) // 玩家远离坐标
});
// 6. 运行一段时间后停止
await Task.Delay(3000);
Console.WriteLine("\n===== 怪物AI停止 =====");
MonsterAIDecisionCenter.StopAllTimer();
monster.Stop();
Console.ReadLine();
}
}
}
事件驱动 + 状态机
「定时器轮询 + Actor + 邮箱消息」 方案,和 「事件驱动 + 有限状态机(FSM)」 方案,是游戏怪物AI的两大主流最优方案,二者可以互补、甚至混合使用,完全匹配非玩家角色的AI需求(仇恨判断、怪物追击、怪物游荡、怪物攻击等)。
| 定时器轮询 + Actor + 邮箱消息 | 事件驱动 + 有限状态机(FSM) |
|---|---|
| 核心模式:主动轮询 → 定时器每隔固定时间,不管有没有变化,都主动计算AI逻辑 | 核心模式:被动响应 → 完全抛弃定时器,只有发生「具体游戏事件」时才触发AI决策,无事件则零计算、零CPU消耗 |
| 行为管控:通过「邮箱消息指令」驱动状态变更,状态流转分散在消息处理中 | 行为管控:通过「状态机」统一管控所有状态,状态的切换、进入、退出、行为逻辑全部标准化、集中化管理 |
| 核心优点:逻辑简单、实时性稳定、适合高并发批量怪物、状态变更解耦彻底 | 核心优点:极致性能(无无效轮询)、状态流转绝对可控、逻辑闭环清晰、适合对性能要求极高/怪物数量极多的大型游戏 |
| 核心缺点:存在「无效轮询」(比如怪物游荡时,定时器依然在跑,哪怕没有任何玩家)、CPU消耗相对高一点 | 核心缺点:需要提前梳理所有游戏事件、状态切换规则要提前定义,前期设计成本稍高 |
「事件驱动 + 状态机」 极简实现源码
using System;
using System.Collections.Generic;
using System.Linq;
namespace GameMonsterAI_EventFSM
{
// ===================== 核心枚举定义(状态+事件,完美匹配你的需求) =====================
public enum MonsterState
{
Idle = 0, // 空闲
Wander = 1, // 游荡/巡逻
Chase = 2, // 追击目标
Attack = 3 // 攻击目标
}
public enum GameEventType
{
PlayerEnterVision, // 玩家进入怪物视野
PlayerLeaveVision, // 玩家离开怪物视野
PlayerAttackMonster, // 玩家攻击怪物【仇恨核心事件】
PlayerMove, // 玩家移动【追击核心事件】
MonsterOutChaseRange, // 怪物超出追击范围【复位核心事件】
None
}
// ===================== 数据实体 =====================
public class HateTarget
{
public int PlayerId { get; set; }
public float HateValue { get; set; }
public (float X, float Y) PlayerPos { get; set; }
}
public class GameEvent
{
public GameEventType EventType { get; set; }
public int PlayerId { get; set; }
public float HateValue { get; set; }
public (float X, float Y) TargetPos { get; set; }
}
// ===================== 【核心】有限状态机(FSM) 基类 =====================
public abstract class BaseState
{
protected Monster Monster;
public MonsterState StateType { get; protected set; }
public BaseState(Monster monster, MonsterState stateType)
{
Monster = monster;
StateType = stateType;
}
public abstract void OnEnter();
public abstract void OnExecute();
public abstract void OnExit();
}
// ===================== 怪物4大状态具体实现 =====================
/// <summary>
/// 空闲状态 - 修复逻辑BUG:无仇恨→游荡,有仇恨→追击(正确逻辑)
/// </summary>
public class IdleState : BaseState
{
public IdleState(Monster monster) : base(monster, MonsterState.Idle) { }
public override void OnEnter()
{
Console.WriteLine($"【怪物{Monster.Id}】-> 进入状态:{MonsterState.Idle} | 仇恨清空、无目标、回到出生点");
Monster.CurrentPos = Monster.BornPos;
Monster.HateTargets.Clear();
}
public override void OnExecute()
{
// ✅ 修复逻辑BUG:无仇恨目标 → 切换游荡;有仇恨目标 → 切换追击
if (Monster.HateTargets.Count == 0)
{
Monster.StateMachine.ChangeState(MonsterState.Wander);
}
else if (Monster.GetTopHateTarget() != null)
{
Monster.StateMachine.ChangeState(MonsterState.Chase);
}
}
public override void OnExit()
{
Console.WriteLine($"【怪物{Monster.Id}】<- 退出状态:{MonsterState.Idle}");
}
}
public class WanderState : BaseState
{
private readonly Random _random = new Random();
private (float X, float Y) _wanderTargetPos;
public WanderState(Monster monster) : base(monster, MonsterState.Wander) { }
public override void OnEnter()
{
Console.WriteLine($"【怪物{Monster.Id}】-> 进入状态:{MonsterState.Wander} | 开始随机游荡");
_wanderTargetPos = GetRandomWanderPos();
}
public override void OnExecute()
{
// 游荡移动逻辑
Monster.CurrentPos = (
MathF.Round((Monster.CurrentPos.X + _wanderTargetPos.X) / 2, 1),
MathF.Round((Monster.CurrentPos.Y + _wanderTargetPos.Y) / 2, 1)
);
Console.WriteLine($"【怪物{Monster.Id}】游荡中 → 坐标:({Monster.CurrentPos.X},{Monster.CurrentPos.Y})");
// 有仇恨目标 立即切换追击
if (Monster.HateTargets.Count > 0 && Monster.GetTopHateTarget() != null)
{
Monster.StateMachine.ChangeState(MonsterState.Chase);
}
// 到达游荡点 生成新目标
else if (MathF.Abs(Monster.CurrentPos.X - _wanderTargetPos.X) < 0.5f &&
MathF.Abs(Monster.CurrentPos.Y - _wanderTargetPos.Y) < 0.5f)
{
_wanderTargetPos = GetRandomWanderPos();
}
}
public override void OnExit()
{
Console.WriteLine($"【怪物{Monster.Id}】<- 退出状态:{MonsterState.Wander}");
}
private (float X, float Y) GetRandomWanderPos()
{
return (
Monster.BornPos.X + _random.Next(-3, 4),
Monster.BornPos.Y + _random.Next(-3, 4)
);
}
}
public class ChaseState : BaseState
{
private HateTarget _target;
public ChaseState(Monster monster) : base(monster, MonsterState.Chase) { }
public override void OnEnter()
{
_target = Monster.GetTopHateTarget();
Console.WriteLine($"【怪物{Monster.Id}】-> 进入状态:{MonsterState.Chase} | 锁定仇恨目标:玩家{_target.PlayerId},仇恨值:{_target.HateValue}");
}
public override void OnExecute()
{
if (_target == null)
{
Monster.StateMachine.ChangeState(MonsterState.Idle);
return;
}
var distance = Monster.CalcDistance(Monster.CurrentPos, _target.PlayerPos);
Monster.CurrentPos = (
MathF.Round((Monster.CurrentPos.X + _target.PlayerPos.X) / 2, 1),
MathF.Round((Monster.CurrentPos.Y + _target.PlayerPos.Y) / 2, 1)
);
Console.WriteLine($"【怪物{Monster.Id}】追击玩家{_target.PlayerId} → 坐标:({Monster.CurrentPos.X},{Monster.CurrentPos.Y}) | 距离目标:{distance:F1}m");
if (distance <= Monster.AttackRange)
Monster.StateMachine.ChangeState(MonsterState.Attack);
else if (distance > Monster.ChaseRange)
Monster.StateMachine.ChangeState(MonsterState.Idle);
_target = Monster.GetTopHateTarget();
}
public override void OnExit()
{
Console.WriteLine($"【怪物{Monster.Id}】<- 退出状态:{MonsterState.Chase}");
}
}
public class AttackState : BaseState
{
private HateTarget _target;
private float _lastAttackTime;
public AttackState(Monster monster) : base(monster, MonsterState.Attack) { }
public override void OnEnter()
{
_target = Monster.GetTopHateTarget();
_lastAttackTime = 0;
Console.WriteLine($"【怪物{Monster.Id}】-> 进入状态:{MonsterState.Attack} | 攻击目标:玩家{_target.PlayerId},攻击范围:{Monster.AttackRange}m");
}
public override void OnExecute()
{
if (_target == null)
{
Monster.StateMachine.ChangeState(MonsterState.Idle);
return;
}
var distance = Monster.CalcDistance(Monster.CurrentPos, _target.PlayerPos);
var now = (float)DateTime.Now.TimeOfDay.TotalSeconds;
if (distance > Monster.AttackRange)
Monster.StateMachine.ChangeState(MonsterState.Chase);
else if (distance > Monster.ChaseRange)
Monster.StateMachine.ChangeState(MonsterState.Idle);
else if (now - _lastAttackTime >= Monster.AttackCD)
{
Console.WriteLine($"【怪物{Monster.Id}】攻击命中玩家{_target.PlayerId}!造成伤害 | 攻击CD:{Monster.AttackCD}s");
_lastAttackTime = now;
}
else
{
Console.WriteLine($"【怪物{Monster.Id}】攻击CD中 → 剩余:{(Monster.AttackCD - (now - _lastAttackTime)):F1}s");
}
}
public override void OnExit()
{
Console.WriteLine($"【怪物{Monster.Id}】<- 退出状态:{MonsterState.Attack}");
}
}
// ===================== 【核心修复】状态机管理器 - 补全 RegisterState 方法 =====================
public class MonsterStateMachine
{
private readonly Monster _monster;
private readonly Dictionary<MonsterState, BaseState> _states = new Dictionary<MonsterState, BaseState>();
private BaseState _currentState;
public MonsterStateMachine(Monster monster)
{
_monster = monster;
// 注册怪物的4个核心状态
RegisterState(new IdleState(_monster));
RegisterState(new WanderState(_monster));
RegisterState(new ChaseState(_monster));
RegisterState(new AttackState(_monster));
}
// ✅ 完整实现:注册状态的核心方法
private void RegisterState(BaseState state)
{
if (!_states.ContainsKey(state.StateType))
{
_states.Add(state.StateType, state);
}
}
// 状态切换核心方法
public void ChangeState(MonsterState newStateType)
{
if (_currentState != null && _currentState.StateType == newStateType) return;
_currentState?.OnExit();
_currentState = _states[newStateType];
_currentState.OnEnter();
}
public void ExecuteCurrentState()
{
_currentState?.OnExecute();
}
public MonsterState GetCurrentState() => _currentState?.StateType ?? MonsterState.Idle;
}
// ===================== 怪物主类(事件驱动 + 状态机 整合) =====================
public class Monster
{
public int Id { get; set; }
public (float X, float Y) BornPos { get; set; }
public (float X, float Y) CurrentPos { get; set; }
public float AttackRange { get; set; } = 2.0f;
public float ChaseRange { get; set; } = 10.0f;
public float AttackCD { get; set; } = 1.5f;
public Dictionary<int, HateTarget> HateTargets { get; set; } = new();
public MonsterStateMachine StateMachine { get; set; }
public Monster(int id, float bornX, float bornY)
{
Id = id;
BornPos = (bornX, bornY);
CurrentPos = BornPos;
StateMachine = new MonsterStateMachine(this);
StateMachine.ChangeState(MonsterState.Idle);
}
// 事件驱动唯一入口
public void HandleGameEvent(GameEvent gameEvent)
{
switch (gameEvent.EventType)
{
case GameEventType.PlayerAttackMonster:
UpdateHateTarget(gameEvent.PlayerId, gameEvent.HateValue, gameEvent.TargetPos);
break;
case GameEventType.PlayerMove:
UpdateTargetPos(gameEvent.PlayerId, gameEvent.TargetPos);
break;
case GameEventType.PlayerLeaveVision:
HateTargets.Remove(gameEvent.PlayerId);
break;
case GameEventType.MonsterOutChaseRange:
HateTargets.Clear();
break;
case GameEventType.PlayerEnterVision:
AddHateTarget(gameEvent.PlayerId, 5, gameEvent.TargetPos);
break;
}
StateMachine.ExecuteCurrentState();
}
#region 仇恨核心逻辑
public void AddHateTarget(int playerId, float hateValue, (float X, float Y) pos)
{
if (!HateTargets.ContainsKey(playerId))
{
HateTargets.Add(playerId, new HateTarget { PlayerId = playerId, HateValue = hateValue, PlayerPos = pos });
}
}
public void UpdateHateTarget(int playerId, float hateValue, (float X, float Y) pos)
{
if (HateTargets.ContainsKey(playerId))
{
HateTargets[playerId].HateValue += hateValue;
HateTargets[playerId].PlayerPos = pos;
}
else
{
AddHateTarget(playerId, hateValue, pos);
}
Console.WriteLine($"【怪物{Id}】仇恨更新 → 玩家{playerId},当前仇恨值:{HateTargets[playerId].HateValue}");
}
public void UpdateTargetPos(int playerId, (float X, float Y) pos)
{
if (HateTargets.ContainsKey(playerId))
{
HateTargets[playerId].PlayerPos = pos;
}
}
public HateTarget GetTopHateTarget()
{
return HateTargets.Count == 0 ? null : HateTargets.Values.OrderByDescending(t => t.HateValue).First();
}
#endregion
public float CalcDistance((float X, float Y) a, (float X, float Y) b)
{
return MathF.Sqrt(MathF.Pow(b.X - a.X, 2) + MathF.Pow(b.Y - a.Y, 2));
}
}
// ===================== 程序入口 - 测试运行 =====================
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("===== .NET10 怪物AI启动【事件驱动 + 有限状态机(FSM)】 =====");
Console.WriteLine("核心逻辑:游戏事件触发AI决策 → 状态机统一管控状态 → 执行怪物行为");
Console.WriteLine("核心特性:零定时器、零轮询、零无效计算、状态流转绝对可控\n");
var monster = new Monster(1, 0, 0);
Console.WriteLine("===== 模拟游戏事件开始 =====");
// 事件1:玩家1001进入视野 → 初始化仇恨
monster.HandleGameEvent(new GameEvent
{
EventType = GameEventType.PlayerEnterVision,
PlayerId = 1001,
TargetPos = (3, 2)
});
System.Threading.Thread.Sleep(1000);
// 事件2:玩家攻击怪物 → 仇恨叠加,切换追击
monster.HandleGameEvent(new GameEvent
{
EventType = GameEventType.PlayerAttackMonster,
PlayerId = 1001,
HateValue = 50,
TargetPos = (3, 2)
});
System.Threading.Thread.Sleep(2000);
// 事件3:玩家移动靠近 → 进入攻击范围,切换攻击
monster.HandleGameEvent(new GameEvent
{
EventType = GameEventType.PlayerMove,
PlayerId = 1001,
TargetPos = (1, 1)
});
System.Threading.Thread.Sleep(3000);
// 事件4:玩家远离 → 超出范围,复位空闲
monster.HandleGameEvent(new GameEvent
{
EventType = GameEventType.PlayerMove,
PlayerId = 1001,
TargetPos = (20, 20)
});
monster.HandleGameEvent(new GameEvent
{
EventType = GameEventType.MonsterOutChaseRange,
PlayerId = 1001
});
Console.WriteLine("\n===== 模拟游戏事件结束 =====");
Console.WriteLine($"【最终状态】怪物{monster.Id} 当前状态:{monster.StateMachine.GetCurrentState()}");
Console.ReadLine();
}
}
}
扩展阅读
略
最后更新于 2026-01-06 01:23:19 并被添加「」标签,已有 73 位童鞋阅读过。
本站使用「署名 4.0 国际」创作共享协议,可自由转载、引用,但需署名作者且注明文章出处
此处评论已关闭