using System.Collections.Concurrent; using System.Text; using Milimoe.FunGame.Core.Api.Transmittal; using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Interface; using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Base.Addons; using Milimoe.FunGame.Core.Library.Common.Architecture; using Milimoe.FunGame.Core.Library.Common.Event; using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Model; namespace Milimoe.FunGame.Core.Library.Common.Addon.Example { /// /// 建议使用一个类来存储常量,方便重用 /// public class ExampleGameModuleConstant { public static GameModuleDepend GameModuleDepend => _depends; public const string ExampleGameModule = "fungame.example.gamemodule"; public const string ExampleMap = "fungame.example.gamemap"; public const string ExampleCharacter = "fungame.example.character"; public const string ExampleSkill = "fungame.example.skill"; public const string ExampleItem = "fungame.example.item"; private static readonly string[] Maps = [ExampleMap]; private static readonly string[] Characters = [ExampleCharacter]; private static readonly string[] Skills = [ExampleSkill]; private static readonly string[] Items = [ExampleItem]; private static readonly GameModuleDepend _depends = new(Maps, Characters, Skills, Items); } /// /// GameModule 是用于客户端的模组。每个模组都有一个对应的服务器模组,可以简单理解为“一种游戏模式” /// 必须继承基类: /// 继承事件接口并实现其方法来使模组生效。例如继承: /// public class ExampleGameModule : GameModule, IGamingUpdateInfoEvent { public override string Name => ExampleGameModuleConstant.ExampleGameModule; public override string Description => "My First GameModule"; public override string Version => "1.0.0"; public override string Author => "FunGamer"; public override string DefaultMap => GameModuleDepend.MapsDepend.Length > 0 ? GameModuleDepend.MapsDepend[0] : ""; public override GameModuleDepend GameModuleDepend => ExampleGameModuleConstant.GameModuleDepend; public override RoomType RoomType => RoomType.Mix; public override int MaxUsers => 8; public override bool HideMain => false; public ExampleGameModule() { // 构造函数中可以指定模组连接到哪个模组服务器。 // 如果你使用自己的,保持默认即可:删除下面两行,并将模组服务器的名称设置为与此模组的名称相同 IsConnectToOtherServerModule = true; AssociatedServerModuleName = ExampleGameModuleConstant.ExampleGameModule; } protected Gaming? Instance; protected Room room = General.HallInstance; protected List users = []; protected Dictionary characters = []; public override void StartGame(Gaming instance, params object[] args) { Instance = instance; // 取得房间玩家等信息 GamingEventArgs eventArgs = instance.EventArgs; room = eventArgs.Room; users = eventArgs.Users; // 客户端做好准备后,等待服务器的消息通知,下面可以根据需求进一步处理 } public override void StartUI(params object[] args) { /// 如果模组不依附 类启动,或者没有UI,则不需要重写此方法 } public void GamingUpdateInfoEvent(object sender, GamingEventArgs e, Dictionary data) { // 在下方的Server示例中,服务器发来的data中,包含check字符串,因此客户端要主动发起确认连接的请求。 if (data.ContainsKey("info_type")) { // 反序列化得到指定key的值 string info_type = DataRequest.GetDictionaryJsonObject(data, "info_type") ?? ""; if (info_type == "check") { Guid token = DataRequest.GetDictionaryJsonObject(data, "connect_token"); // 发起连接确认请求 DataRequest request = Controller.NewDataRequest(GamingType.Connect); // 传递参数 request.AddRequestData("username", ((Gaming)sender).CurrentUser.Username); request.AddRequestData("connect_token", token); if (request.SendRequest() == RequestResult.Success) { string msg = request.GetResult("msg") ?? ""; Controller.WriteLine(msg); } request.Dispose(); } } } } /// /// 模组服务器:必须继承基类: /// 使用switch块分类处理 。 /// public class ExampleGameModuleServer : GameModuleServer, IHotReloadAware { /// /// 注意:服务器模组的名称必须和模组名称相同。除非你指定了 /// public override string Name => ExampleGameModuleConstant.ExampleGameModule; public override string Description => "My First GameModule"; public override string Version => "1.0.0"; public override string Author => "FunGamer"; public override string DefaultMap => GameModuleDepend.MapsDepend.Length > 0 ? GameModuleDepend.MapsDepend.First() : ""; public override GameModuleDepend GameModuleDepend => ExampleGameModuleConstant.GameModuleDepend; /// /// 创建一个工作类,接收服务器启动参数的同时,还能定义一些需要的属性 /// /// private readonly struct ModuleServerWorker(GamingObject obj) { public GamingObject GamingObject { get; } = obj; public List ConnectedUser { get; } = []; public List CharactersForPick { get; } = []; public Dictionary UserCharacters { get; } = []; public Dictionary> UserData { get; } = []; } private ConcurrentDictionary Workers { get; } = []; public override bool StartServer(GamingObject obj, params object[] args) { // 因为模组是单例的,需要为这个房间创建一个工作类接收参数,不能直接用本地变量处理 ModuleServerWorker worker = new(obj); Workers[obj.Room.Roomid] = worker; // 创建一个线程执行Test(),因为这个方法必须立即返回 TaskUtility.NewTask(async () => await Test(obj, worker)).OnError(Controller.Error); return true; } private async Task Test(GamingObject obj, ModuleServerWorker worker) { Controller.WriteLine("欢迎各位玩家进入房间 " + obj.Room.Roomid + " 。"); // 通常,我们可以对客户端的连接状态进行确认,此方法展示如何确认客户端的连接 // 有两种确认的方式,1是服务器主动确认,2是客户端发起确认 // 在FunGame项目中,建议永远使用客户端主动发起请求,因为服务器主动发起的实现难度较高 // 下面的演示基于综合的两种情况:服务器主动发送通知,客户端收到后,发起确认 // UpdateInfo是一个灵活的类型。如果发送check字符串,意味着服务器要求客户端发送确认 Dictionary data = []; data.Add("info_type", "check"); // 进阶示例:传递一个token,让客户端返回 Guid token = Guid.NewGuid(); data.Add("connect_token", token); // 我们保存到字典UserData中,这样可以方便跨方法检查变量 foreach (string username in obj.Users.Select(u => u.Username).Distinct()) { if (worker.UserData.TryGetValue(username, out Dictionary? value)) { value.Add("connect_token", token); } else { worker.UserData.Add(username, []); worker.UserData[username].Add("connect_token", token); } } await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data); // 新建一个线程等待所有玩家确认,如果超时则取消游戏,30秒 // 每200ms确认一次,不需要太频繁 await WaitForUsers(30, async () => { if (worker.ConnectedUser.Count == obj.Users.Count) { Controller.WriteLine("所有玩家都已经连接。"); return true; } return false; }, 200, async () => { Controller.WriteLine("等待玩家连接超时,放弃该局游戏!", LogLevel.Warning); await CancelGame(obj, worker, "由于等待超时,游戏已取消!"); }, async () => { // 所有玩家都连接完毕了,可以建立一个回合制游戏了 await StartGame(obj, worker); }); } public override async Task> GamingMessageHandler(IServerModel model, GamingType type, Dictionary data) { Dictionary result = []; // 获取model所在的房间工作类 ModuleServerWorker worker = Workers[model.InRoom.Roomid]; GamingObject obj = worker.GamingObject; string username = model.User.Username; switch (type) { case GamingType.Connect: { // 编写处理“连接”命令的逻辑 // 如果需要处理客户端传递的参数:获取与客户端约定好的参数key对应的值 string un = NetworkUtility.JsonDeserializeFromDictionary(data, "username") ?? ""; Guid token = NetworkUtility.JsonDeserializeFromDictionary(data, "connect_token"); if (un == username && worker.UserData.TryGetValue(username, out Dictionary? value) && value != null && (value["connect_token"]?.Equals(token) ?? false)) { worker.ConnectedUser.Add(obj.Users.Where(u => u.Username == username).First()); Controller.WriteLine(username + " 已经连接。"); } else Controller.WriteLine(username + " 确认连接失败!", LogLevel.Warning); break; } case GamingType.PickCharacter: { // 客户端选完了角色这里就要处理了 long id = NetworkUtility.JsonDeserializeFromDictionary(data, "id"); if (worker.CharactersForPick.FirstOrDefault(c => c.Id == id) is Character character) { // 如果有人选一样的,你还没有做特殊处理的话,为了防止意外,最好复制一份 worker.UserCharacters[username] = character.Copy(); } break; } case GamingType.Skill: { string e = NetworkUtility.JsonDeserializeFromDictionary(data, "event") ?? ""; if (e.Equals("SelectSkillTargets", StringComparison.CurrentCultureIgnoreCase)) { long caster = NetworkUtility.JsonDeserializeFromDictionary(data, "caster"); long[] targets = NetworkUtility.JsonDeserializeFromDictionary(data, "targets") ?? []; // 接收客户端传来的目标序号并记录 if (worker.UserData.TryGetValue(username, out Dictionary? value) && value != null) { value.Add("SkillTargets", targets); } } break; } default: await Task.Delay(1); break; } return result; } private async Task CancelGame(GamingObject obj, ModuleServerWorker worker, string reason) { // 通知所有玩家 await SendAllTextMessage(obj, reason); // 结束 SendEndGame(obj); worker.ConnectedUser.Clear(); Workers.Remove(obj.Room.Roomid, out _); } private async Task WaitForUsers(int waitSeconds, Func> waitSomething, int delay, Func onTimeout, Func onCompleted) { // 这是一个用于等待的通用辅助方法 using CancellationTokenSource cts = new(TimeSpan.FromSeconds(waitSeconds)); CancellationToken ct = cts.Token; while (!ct.IsCancellationRequested) { try { if (await waitSomething()) { await onCompleted(); return; } await Task.Delay(delay, ct); } catch (System.Exception e) when (e is not OperationCanceledException) { Controller.Error(e); await onTimeout(); return; } } // 异常和超时都走超时逻辑 await onTimeout(); } private async Task StartGame(GamingObject obj, ModuleServerWorker worker) { Dictionary characters = []; List characterPickeds = []; Dictionary data = []; // 首先,让玩家们选择角色 // 需要一个待选的角色池 // 这些角色可以从工厂中获取,比如: Character character1 = Factory.OpenFactory.GetInstance(1, "", []); worker.CharactersForPick.Add(character1); // 或者在什么地方已经有个列表?则使用复制方法 if (ModuleLoader != null && ModuleLoader.Characters.Count > 0) { CharacterModule characterModule = ModuleLoader.Characters.Values.First(); Character character2 = characterModule.Characters.Values.FirstOrDefault()?.Copy() ?? Factory.GetCharacter(); if (character2.Id > 0) { worker.CharactersForPick.Add(character2); } } // 传整个对象或者id都可以,看你心情,推荐用id,轻量,方便 data["list"] = worker.CharactersForPick.Select(c => c.Id); await SendGamingMessage(obj.All.Values, GamingType.PickCharacter, data); // 依然等待 await WaitForUsers(30, async () => { if (worker.UserCharacters.Count == obj.Users.Count) { Controller.WriteLine("所有玩家都已经完成选择。"); return true; } return false; }, 200, async () => { await CancelGame(obj, worker, "由于等待超时,游戏已取消!"); }, async () => { try { // 得到一个最终列表 List finalList = [.. worker.UserCharacters.Values]; // 这里我们可以随意对角色们进行升级和赋能 int clevel = 60; int slevel = 6; int mlevel = 8; foreach (Character c in finalList) { c.Level = clevel; c.NormalAttack.Level = mlevel; // 假设要给所有角色发一个编号为1的技能 Skill s = Factory.OpenFactory.GetInstance(1, "", []); s.Level = slevel; c.Skills.Add(s); } // 重点,创建一个战斗队列 // 注意,整个游戏中,finalList及其内部角色对象的引用始终不变,请放心使用 MixGamingQueue queue = new(finalList, (str) => { // 战斗日志可以直接通过传输信息的方式输出回客户端 _ = SendAllTextMessage(obj, str); }); // 如果你需要开启战棋地图模式 GameMap? map = GameModuleDepend.Maps.FirstOrDefault(); if (map != null) { queue.LoadGameMap(map); } // 关键,监听任何事件 // 在客户端中,通过事件可以很方便地对UI进行操作以同步界面状态,而在服务端则需要套一层网络层 //queue.TurnStartEvent += Queue_TurnStartEvent; //queue.DecideActionEvent += Queue_DecideActionEvent; //queue.SelectNormalAttackTargetsEvent += Queue_SelectNormalAttackTargetsEvent; //queue.SelectSkillEvent += Queue_SelectSkillEvent; //queue.SelectNonDirectionalSkillTargetsEvent += Queue_SelectNonDirectionalSkillTargetsEvent; //queue.SelectItemEvent += Queue_SelectItemEvent; //queue.QueueUpdatedEvent += Queue_QueueUpdatedEvent; //queue.TurnEndEvent += Queue_TurnEndEvent; // 我们示范两个事件,一是选择技能目标,需要和客户端交互的事件 queue.SelectSkillTargetsEvent += (queue, caster, skill, enemys, teammates, castRange) => { /// 如果你的逻辑都写在 里就不用这么麻烦每次都传 obj 和 worker 了。 return Queue_SelectSkillTargetsEvent(worker, caster, skill, enemys, teammates, castRange); }; // 二是角色行动完毕,需要通知客户端更新状态的事件 queue.CharacterActionTakenEvent += Queue_CharacterActionTakenEvent; // 战棋地图模式需要额外绑定的事件(如果你在map类里没有处理的话,这里还可以处理) if (queue.Map != null) { //queue.SelectTargetGridEvent += Queue_SelectTargetGridEvent; //queue.CharacterMoveEvent += Queue_CharacterMoveEvent; } queue.InitActionQueue(); // 这里我们仅演示自动化战斗,指令战斗还需要实现其他的消息处理类型/事件 // 自动化战斗时上述绑定的事件可能不会触发,参见GamingQueue的内部实现 queue.SetCharactersToAIControl(cancel: false, finalList); // 总游戏时长 double totalTime = 0; // 总死亡数 int deaths = 0; // 总回合数 int max = 999; int i = 1; while (i < max) { if (i == (max - 1)) { // 为了防止回合数超标,游戏近乎死局,可以设置一个上限,然后随便让一个人赢 await SendAllTextMessage(obj, $"=== 终局审判 ==="); Dictionary hp = []; foreach (Character c in finalList) { hp.TryAdd(c, Calculation.Round4Digits(c.HP / c.MaxHP)); } double maxhp = hp.Values.Max(); Character winner = hp.Keys.Where(c => hp[c] == maxhp).First(); await SendAllTextMessage(obj, "[ " + winner + " ] 成为了天选之人!!"); foreach (Character c in finalList.Where(c => c != winner && c.HP > 0)) { await SendAllTextMessage(obj, "[ " + winner + " ] 对 [ " + c + " ] 造成了 99999999999 点真实伤害。"); queue.DeathCalculation(winner, c); } queue.EndGameInfo(winner); break; } // 检查是否有角色可以行动 Character? characterToAct = queue.NextCharacter(); // 处理回合 if (characterToAct != null) { await SendAllTextMessage(obj, $"=== Round {i++} ==="); await SendAllTextMessage(obj, "现在是 [ " + characterToAct + " ] 的回合!"); if (queue.Queue.Count == 0) { break; } bool isGameEnd = queue.ProcessTurn(characterToAct); if (isGameEnd) { break; } queue.DisplayQueue(); } // 时间流逝,这样能知道下一个是谁可以行动 totalTime += queue.TimeLapse(); if (queue.Eliminated.Count > deaths) { deaths = queue.Eliminated.Count; } } await SendAllTextMessage(obj, "--- End ---"); await SendAllTextMessage(obj, "总游戏时长:" + Calculation.Round2Digits(totalTime)); // 赛后统计,充分利用 GamingQueue 提供的功能 await SendAllTextMessage(obj, "=== 伤害排行榜 ==="); int top = finalList.Count; int count = 1; foreach (Character character in queue.CharacterStatistics.OrderByDescending(d => d.Value.TotalDamage).Select(d => d.Key)) { StringBuilder builder = new(); CharacterStatistics stats = queue.CharacterStatistics[character]; builder.AppendLine($"{count++}. [ {character.ToStringWithLevel()} ] ({stats.Kills} / {stats.Assists})"); builder.AppendLine($"存活时长:{stats.LiveTime} / 存活回合数:{stats.LiveRound} / 行动回合数:{stats.ActionTurn} / 总计决策数:{stats.TurnDecisions} / 总计决策点:{stats.UseDecisionPoints}"); builder.AppendLine($"总计伤害:{stats.TotalDamage} / 总计物理伤害:{stats.TotalPhysicalDamage} / 总计魔法伤害:{stats.TotalMagicDamage}"); builder.AppendLine($"总承受伤害:{stats.TotalTakenDamage} / 总承受物理伤害:{stats.TotalTakenPhysicalDamage} / 总承受魔法伤害:{stats.TotalTakenMagicDamage}"); builder.Append($"每秒伤害:{stats.DamagePerSecond} / 每回合伤害:{stats.DamagePerTurn}"); await SendAllTextMessage(obj, builder.ToString()); } } catch (System.Exception e) { TXTHelper.AppendErrorLog(e.ToString()); Controller.Error(e); } finally { // 结束 SendEndGame(obj); worker.ConnectedUser.Clear(); Workers.Remove(obj.Room.Roomid, out _); } }); } private List Queue_SelectSkillTargetsEvent(ModuleServerWorker worker, Character caster, Skill skill, List enemys, List teammates, List castRange) { // 这是一个需要与客户端交互的事件,其他的选择事件与之做法相同 // SyncAwaiter是一个允许同步方法安全等待异步任务完成的工具类 return SyncAwaiter.WaitResult(RequestClientSelectSkillTargets(worker, caster, skill, enemys, teammates, castRange)); } private async Task> RequestClientSelectSkillTargets(ModuleServerWorker worker, Character caster, Skill skill, List enemys, List teammates, List castRange) { List selectTargets = []; Dictionary data = []; data.Add("event", "SelectSkillTargets"); data.Add("caster", caster.Id); data.Add("skill", skill.Id); data.Add("enemys", enemys.Select(c => c.Id)); data.Add("teammates", teammates.Select(c => c.Id)); data.Add("castRange", castRange.Select(g => g.Id)); await SendGamingMessage(_clientModels, GamingType.Skill, data); await WaitForUsers(30, async () => { string username = worker.UserCharacters.FirstOrDefault(kv => kv.Value == caster).Key; return worker.UserData.TryGetValue(username, out Dictionary? value) && value != null && value.ContainsKey("SkillTargets"); }, 200, async () => await Task.CompletedTask, async () => { string username = worker.UserCharacters.FirstOrDefault(kv => kv.Value == caster).Key; if (worker.UserData.TryGetValue(username, out Dictionary? value) && value != null && value.TryGetValue("SkillTargets", out object? value2) && value2 is long[] targets) { selectTargets.AddRange(worker.UserCharacters.Values.Where(c => targets.Contains(c.Id))); } }); return selectTargets; } private void Queue_CharacterActionTakenEvent(GamingQueue queue, Character actor, DecisionPoints dp, CharacterActionType type, RoundRecord record) { Dictionary data = []; data.Add("event", "CharacterActionTaken"); data.Add("actor", actor.Id); data.Add("dp", dp); data.Add("type", type); // 通知就行,无需等待 _ = SendGamingMessage(_clientModels, GamingType.Round, data); } private async Task SendAllTextMessage(GamingObject obj, string str) { // 工具方法,向所有人推送文本消息 Dictionary data = []; data.Add("showmessage", true); data.Add("msg", str); await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data); } protected HashSet _clientModels = []; /// /// 匿名服务器允许客户端不经过FunGameServer的登录验证就能建立一个游戏模组连接 /// 匿名服务器示例 /// /// /// /// public override bool StartAnonymousServer(IServerModel model, Dictionary data) { // 可以做验证处理(这只是个演示,具体实现只需要双方约定,收发什么都无所谓) string access_token = NetworkUtility.JsonDeserializeFromDictionary(data, "access_token") ?? ""; if (access_token == "approval_access_token") { // 接收连接匿名服务器的客户端 _clientModels.Add(model); return true; } return false; } public override void CloseAnonymousServer(IServerModel model) { // 移除客户端 _clientModels.Remove(model); } /// /// 接收并处理匿名服务器消息 /// /// /// /// public override async Task> AnonymousGameServerHandler(IServerModel model, Dictionary data) { Dictionary result = []; // 根据服务器和客户端的数据传输约定,自行处理 data,并返回。 if (data.Count > 0) { await Task.Delay(1); } result.Add("msg", "匿名服务器已经收到消息了"); return result; } /// /// 热更新示例:必须实现 接口才会被热更新模式加载这个模组 /// 如果想要实现端运行的所有模组都能热更新,那么这些模组都必须实现了这个接口(包括 等等……) /// public void OnBeforeUnload() { // 这个方法会在模组被卸载前调用,因此,这里要清理一些状态,让框架可以正确卸载模组 // 假设,这是个匿名服务器,因此它需要清理匿名连接 GamingObjects.Clear(); _ = Send(_clientModels, SocketMessageType.EndGame, Factory.GetRoom(), Factory.GetUser()); IServerModel[] models = [.. _clientModels]; foreach (IServerModel model in models) { model.NowGamingServer = null; CloseAnonymousServer(model); } } } /// /// 地图:必须继承基类: /// public class ExampleGameMap : GameMap { public override string Name => ExampleGameModuleConstant.ExampleMap; public override string Description => "My First GameMap"; public override string Version => "1.0.0"; public override string Author => "FunGamer"; public override int Length => 12; public override int Width => 12; public override int Height => 6; public override float Size => 4.0f; public override GameMap InitGamingQueue(IGamingQueue queue) { // 因为模组在模组管理器中都是单例的,所以每次游戏都需要返回一个新的地图对象给队列 GameMap map = new ExampleGameMap(); map.Load(); // 做一些绑定,以便介入游戏队列 /// 但是,传入的 queue 可能不是 ,要做类型检查 // 不使用框架的实现时,需要地图作者与游戏队列的作者做好适配 if (queue is GamingQueue gq) { gq.SelectTargetGridEvent += Gq_SelectTargetGrid; } return map; } private Grid Gq_SelectTargetGrid(GamingQueue queue, Character character, List enemys, List teammates, GameMap map, List canMoveGrids) { // 介入选择,假设这里更新界面,让玩家选择目的地 return Grid.Empty; } } /// /// 角色:必须继承基类: /// public class ExampleCharacterModule : CharacterModule { public override string Name => ExampleGameModuleConstant.ExampleCharacter; public override string Description => "My First CharacterModule"; public override string Version => "1.0.0"; public override string Author => "FunGamer"; public override Dictionary Characters { get { Dictionary dict = []; // 构建一个你想要的角色 Character c = Factory.GetCharacter(); c.Name = "Oshima"; c.FirstName = "Shiya"; c.NickName = "OSM"; c.MagicType = MagicType.PurityNatural; c.InitialHP = 30; c.InitialSTR = 20; c.InitialAGI = 10; c.InitialINT = 5; c.InitialATK = 100; c.InitialDEF = 10; dict.Add(c.Name, c); return dict; } } protected override Factory.EntityFactoryDelegate CharacterFactory() { // 上面示例用 Characters 是预定义的 // 这里的工厂模式则是根据传进来的参数定制生成角色,只要重写这个方法就能注册工厂了 return (id, name, args) => { return null; }; } public static Character CreateCharacter(long id, string name, Dictionary args) { // 注册工厂后,后续创建角色只需要这样调用 return Factory.OpenFactory.GetInstance(id, name, args); } } /// /// 技能:必须继承基类: /// public class ExampleSkillModule : SkillModule { public override string Name => ExampleGameModuleConstant.ExampleSkill; public override string Description => "My First SkillModule"; public override string Version => "1.0.0"; public override string Author => "FunGamer"; public override Dictionary Skills { get { Dictionary dict = []; /// 技能应该在新建类继承Skill实现,再自行构造并加入此列表。 /// 技能的实现示例参见: return dict; } } protected override Factory.EntityFactoryDelegate SkillFactory() { // 注册一个工厂,根据id和name,返回一个你继承实现了的类对象。所有的工厂使用方法参考 Character,都是一样的 return (id, name, args) => { return null; }; } protected override Factory.EntityFactoryDelegate EffectFactory() { return (id, name, args) => { // 以下是一个示例,实际开发中 id,name,args 怎么处置,看你心情 Skill? skill = null; if (args.TryGetValue("skill", out object? value) && value is Skill s) { skill = s; } skill ??= new OpenSkill(id, name, args); /// 如 中所说,特效需要在工厂中注册,方便重用 if (id == 1001) { return new ExampleOpenEffectExATK2(skill, args); } return null; }; } } /// /// 物品:必须继承基类: /// public class ExampleItemModule : ItemModule { public override string Name => ExampleGameModuleConstant.ExampleItem; public override string Description => "My First ItemModule"; public override string Version => "1.0.0"; public override string Author => "FunGamer"; public override Dictionary Items { get { Dictionary dict = []; /// 物品应该新建类继承Item实现,再自行构造并加入此列表。 /// 物品的实现示例参见: return dict; } } protected override Factory.EntityFactoryDelegate ItemFactory() { // 注册一个工厂,根据id和name,返回一个你继承实现了的类对象。 return (id, name, args) => { return null; }; } } }