非网格类Factorio游戏建筑系统:菜单驱动类实例化与职责划分问询
针对Factorio类游戏建筑系统职责划分的优化建议
首先,你当前Game.BuyBuilding(Building building)的设计确实存在职责混淆和逻辑漏洞——先创建建筑再校验资源的顺序是反的,不仅会造成不必要的对象实例化浪费,还把本该属于Game的"构建决策"逻辑提前泄露到了UI或其他调用方。下面是这类模拟建造游戏中通用的职责划分实践,结合你的代码给出具体优化方案:
1. 明确三类核心模块的职责边界
- UI类:只负责用户交互与展示:
- 遍历
BuildInstruction.BuildInstructions的分类树,渲染菜单 - 捕获用户的选择(比如选中了Stockpile的BuildInstruction)、输入(比如点击的世界坐标、选中的两个连接点)
- 将用户的"建造请求"(包含BuildInstruction和必要参数)传递给Game,不做任何业务逻辑校验,也不创建Building/Connection对象
- 遍历
- Game类:负责业务规则校验与流程控制:
- 校验玩家的资金、建造条件(比如是否有足够空间、是否满足前置科技)
- 若校验通过,调用BuildInstruction创建建筑/连接对象,再委托World处理实际的世界添加逻辑
- 处理建造成功/失败的反馈(比如扣钱、给UI返回提示)
- World类:负责世界状态的维护:
- 管理所有实体的位置、碰撞、更新逻辑
- 提供添加/移除实体的原子操作(比如
AddWorldObject)
2. 重构BuildInstruction与Game的交互逻辑
第一步:给BuildInstruction补充必要的元数据
让BuildInstruction携带建造所需的核心信息(比如成本),方便Game做校验:
public abstract class BuildInstruction { // 新增:建造该建筑/连接所需的资金 public int Cost { get; protected set; } public string Name { get; protected set; } // 保留原有的分类树初始化逻辑... } public class BuildingBuildInstruction : BuildInstruction { Func<PointF, Building> BuildFunction; // 构造时传入成本与名称 public BuildingBuildInstruction(string name, int cost, Func<PointF, Building> buildFunction) { Name = name; Cost = cost; BuildFunction = buildFunction; } public Building Build(PointF worldPos) => BuildFunction(worldPos); } public class ConnectionBuildInstruction : BuildInstruction { Func<IConnectable, IConnectable, Connection> BuildFunction; public ConnectionBuildInstruction(string name, int cost, Func<IConnectable, IConnectable, Connection> buildFunction) { Name = name; Cost = cost; BuildFunction = buildFunction; } public Connection Build(IConnectable start, IConnectable end) => BuildFunction(start, end); }
第二步:修改Game类的方法,接收BuildInstruction而非已构建的对象
让Game主导从校验到构建的完整流程:
public class Game { World World = new World(); public int PlayerMoney { get; private set; } = 1000; // 示例玩家资金 // 处理建筑建造请求 public bool BuyBuilding(BuildingBuildInstruction instruction, PointF worldPos) { // 1. 先校验资金 if (PlayerMoney < instruction.Cost) { // 通知UI:资金不足 return false; } // 2. 校验世界建造条件(比如是否有碰撞) var tempBuilding = instruction.Build(worldPos); if (!World.CanPlaceObject(tempBuilding)) { // 通知UI:无法放置(比如空间不足) return false; } // 3. 所有校验通过,执行建造流程 PlayerMoney -= instruction.Cost; if (ConstructBuilding(tempBuilding)) { // 通知UI:建造成功 return true; } else { // 异常情况:回滚资金 PlayerMoney += instruction.Cost; return false; } } // 处理连接建造请求(比如电线、管道) public bool BuyConnection(ConnectionBuildInstruction instruction, IConnectable start, IConnectable end) { if (PlayerMoney < instruction.Cost) { return false; } var connection = instruction.Build(start, end); if (World.AddWorldObject(connection)) { PlayerMoney -= instruction.Cost; return true; } return false; } bool ConstructBuilding(Building building) { return World.AddWorldObject(new ConstructionSite(building)); } }
第三步:UI类的调用逻辑示例
UI只需要传递用户选择的BuildInstruction和输入参数:
public class BuildMenuUI { Game _game; // 用户选中了Stockpile的BuildInstruction,点击了世界坐标 public void OnBuildingSelected(BuildingBuildInstruction instruction, PointF clickPos) { bool success = _game.BuyBuilding(instruction, clickPos); if (success) { ShowMessage($"开始建造{instruction.Name}"); } else { ShowMessage("建造失败:资金不足或无法放置"); } } // 用户选中了Powerline的BuildInstruction,选中了两个连接点 public void OnConnectionSelected(ConnectionBuildInstruction instruction, IConnectable start, IConnectable end) { bool success = _game.BuyConnection(instruction, start, end); // 处理反馈... } }
3. 额外的优化建议
- 拆分ConstructionSite逻辑:可以把"建造中"的状态管理放到Game或一个单独的ConstructionManager类中,World只负责存储实体,不处理建造进度逻辑,保持职责单一。
- 抽象建造校验逻辑:如果未来有更多建造条件(比如科技解锁、电力需求),可以给BuildInstruction添加
CanBuild(Game game, World world, params object[] args)抽象方法,让每个具体的BuildInstruction自己实现校验逻辑,Game只做统一调用。 - 缓存临时对象:如果担心
tempBuilding的创建开销,可以在BuildInstruction中添加Preview(PointF worldPos)方法,返回一个轻量的预览对象用于碰撞检测,避免创建完整的Building实例。
内容的提问来源于stack exchange,提问作者AntonJH
相关产品推荐
相关产品推荐

