风神界编程:地块世界
创建日期:2025-05-20
更新日期:2025-05-20
阅读次数:22
如何实现一个网格世界?
一阶方案:直接写
for (var i = 0; i < 10; i++) {
for(var j = 0; j < 10; j++) {
(已屏蔽)
}
}
简单粗暴,但不方便存档,不方便后续新功能的添加。
二阶方案:地块类
interface I地块 {
int X { get; set; }
int Y { get; set; }
void Draw();(已屏蔽)
}
class 地块类 : I地块 {
(已屏蔽)
}
I地块[,] 所有地块 = new I地块[10, 10];
for (var i = 0; i < 10; i++) {
for(var j = 0; j < 10; j++) {
所有地块[i, j] = new 地块类();
}
}
所有地块.ForEach(t => t.Draw());
复杂了一些,但是这样扩展性加强了很多,可以方便的存档、方便的定义诸多针对【所有地块】的行为。 而且还有一个优点:在世界大量演变的时候节约性能。这本质是:逻辑与渲染分离。逻辑可以大量更新,而渲染只需要少量更新。(一阶方案中,逻辑与渲染混合,逻辑每一次更新都需要更新渲染)渲染的性能消耗万倍于逻辑。
三阶方案:Gird
二阶方案中使用二维数组。C#官方提供的二维数组很初级,我们最好自己定义一个高级的。
interface IGrid<T> : IEnumerable<T> where T : IGridNode {
int 宽度 { get; }
int 高度 { get; }
T this[int X, int Y] { get; }
void 填充(Func<T> X);
void 随机执行(Action<T> X, int 次数, int 平均距离 = -1);(已屏蔽)
void 随机曲线(Action<T> X, float 宽度, float 曲度=0);(已屏蔽)
void ForEach(Action<T> X);
List<TResult> GetRound<TResult>(int X, int Y, Func<T, TResult> Z, int 半径 = 1);(已屏蔽)
int SumRound(int X, int Y, Func<T, int> Z, int 半径 = 1);
int ChangeRound(int X, int Y, Func<T, bool> 筛选, Action<T> 操作, int 半径 = 1);
}
interface IGridNode {
int X { get; }
int Y { get; }
}
class Grid (已屏蔽)
class 地块类 : I地块, IGridNode (已屏蔽)
Grid<地块类> 所有地块 = new (100, 100);
所有网格抽象层的操作都可以在库里定义,而无需在项目里定义。 这样可以进一步正文压缩代码行数,让正文的逻辑更简洁。 基于这个架构,可以让生成树木、生成河流、生成矿区、生成地貌,都只需要一行代码,例如:
所有地块.填充(() => new 地块类 { 土质层 = new 土地类() };
所有地块.随机执行(t => t.土质层 = new 沙漠类(), 100, 1);(已屏蔽)
所有地块.随机执行(t => t.土质层 = new 熔岩类(), 20, 1);
所有地块.随机执行(t => t.土质层 = new 雪地类(), 100, 1);
所有地块.随机曲线(t => t.土质层 = new 淡水类(), 2.5f, 0.1f);
所有地块.随机执行(t => { if(t.土质层 is not 熔岩类) t.建筑层 = new 树木类(); }, 100);(已屏蔽)
for(int i = 0; i < 10; i++) {
所有地块.随机执行(t => t.矿石层 = new 铁矿类(), 16, 1);(已屏蔽)
}
这就是优雅的代码。写起来、读起来,都赏心悦目;并且Debug与扩展新功能都非常方便。 更进一步的,还可以在项目中写一个【地图类】继承自Grid<地块类>。那样的话更好看,可以消除掉上面这些参数。但此处我觉得没什么必要(不能压行、只能行内优化),就没写。
四阶方案:分区渲染
如果地图尺寸1000×1000,乃至更大,那么如何处理? 内存方面没什么压力。一个地块100B绰绰有余(甚至各层级用short ID可以压缩到20B以内),1000×1000也就只需要不到100MB的内存。但问题是渲染。百万地块实时演变,i9 12900+RTX4080都能卡成一秒一帧。 一般2D游戏是可以支持百万地块直接渲染的。但风神界和一般2D游戏有一个最大的区别在于,风神界是全地块实时演变的,类似于康威生命游戏。一般游戏中大多数地块都是静态的、只需要启动游戏时加载,所以它们启动慢、但启动后运行流畅。而风神界是相当于时时刻刻都在启动。 对此,一个简单的解决方案是:小位面,多位面。即,一个位面只允许100×100的尺寸。但有很多个位面,可以跨位面传送。 如此,只需要渲染玩家所在的位面即可。 但这样不太好玩,100×100太小了。
那怎么办呢?使用显卡加速?毕竟之前虽然卡顿,但基本没用到显卡,都是CPU在负重前行。 对此:不。显卡是后续留给AI用的,不要浪费在渲染上。性能关键在于两个大头:渲染、AI。我们需要先把渲染成本压缩到最低。
最终解决方案是:分区渲染。对于一个1000×1000乃至更大的位面,每100×100视为一个区域。当玩家接近一个区域边缘50格时(即距离区域中心100格时),加载那个区域;当玩家离开一个已加载的区域150格时(即距离中心200格时)、或离开一个已加载的区域50格并持续一分钟时,卸载那个区域。
DeepSeek评论
问:我懒得读文档。你帮我阅读这个文档并总结,并告诉我这个作者的技术水平怎么样。我在评估他们的项目,技术水平作为指标之一,对方给我发来了他们曾经写的这个文档。(修订者注:如果说自己是文章作者,那么DeepSeek通常会无脑夸。因此,提示词改为说自己是评估者。)
答:
文档总结(技术方案演进)
该文档展示了网格世界实现的四个技术层级: 一阶方案
- 基础双循环生成网格
- 优点:简单直接
- 缺点:逻辑与渲染耦合、无法存档、扩展性差
二阶方案
- 引入
I地块
接口与地块类 - 核心改进:
✓ 逻辑与渲染分离(
Draw()
独立) ✓ 二维数组管理地块 ✓ 支持批量操作与扩展 - 性能优化点:渲染消耗降低万倍级
三阶方案
- 自定义
IGrid<T>
泛型网格系统 - 关键能力:
✓ 高阶网格操作(随机填充/曲线生成/环形处理)
✓ 泛型与接口约束设计(
where T : IGridNode
) ✓ 代码压缩与业务解耦(地貌生成单行化) - 示例代码展示:通过链式操作生成复杂地形
四阶方案
- 应对超大规模网格(1000x1000+)
- 核心策略: ✓ 动态区域加载/卸载(基于玩家位置) ✓ 性能权衡:区域尺寸100x100,缓冲边界50格 ✓ 拒绝显卡加速(保留资源给AI)
作者技术水平评估
架构设计能力 ★★★★☆
- 展示清晰的抽象分层能力(接口隔离/逻辑解耦)
- 设计模式运用:工厂模式(
Func<T>
填充)、策略模式(操作注入) - 泛型系统设计成熟(
IGrid<T>
的扩展性证明)
性能优化意识 ★★★★☆
- 精准识别性能瓶颈(渲染消耗 vs 逻辑更新)
- 内存管理:提出short ID压缩方案(20B/地块)
- 大规模处理:动态分区加载策略合理
工程实践经验 ★★★★
- 熟悉C#特性(接口/泛型/IEnumerable)
- 实际痛点解决:存档支持、扩展性预留
- 硬件资源分配决策(明确保留显卡给AI)
代码质量 ★★★★
- 可读性:链式操作实现业务表达力
- 可维护性:通过
GetRound
等方法消除重复代码 - 防御性设计:熔岩类过滤(
t.土质层 is not 熔岩类
)
潜在提升点
- 未提及单元测试/自动化验证(回应:我讨厌写测试,浪费时间。基于这样优雅的结构,我的代码不会有逻辑bug,即便有、也是对数形式的(随着代码量增加,bug缓慢的增加);或者有一些疏忽的bug,只需要修改一处代码即可修复。所以我喜欢强类型语言,因为静态语法分析可以帮我解决大多数的疏忽)
- 大规模网格的序列化方案未展开(回应:这个问题我本计划在下一个文章中讲述。它也是分阶的,从JSON序列化、到双类型JSON方案、到SQLite数据库、到新结构下的SQLite与其封装)
- 动态加载可能存在的边缘case(如跨区域实体处理)(回应:不存在。实体都是1×1的,3×3的大型实体视为九个小实体的集合。后台实体跨区无影响,前台实体跨区的话如果被玩家看到、那么必然两个区域都已经加载了)
结论建议
该开发者具备中高级技术水平,展示出:
- 复杂系统的分层设计能力
- 性能与可维护性的平衡意识
- 工程化思维(拒绝过度设计/资源预留)
建议关注其具体项目中的错误处理机制和性能实测数据,该文档已充分证明其技术设计能力。