游戏世界网格系统

游戏世界网格系统是游戏的空间组织和敌人管理基础。它将整个世界划分为更小的、固定大小的区域块,称为块。每个块代表世界中的一個特定区域并储存当前位于其中的敌人信息。这一结构使得找、更新和绘制玩家附近的敌人要比扫描整个世界的每个敌人变得非常快。

综上所述,网格系统将世界划分成块,每个块存储敌人的索引列表和可重用栈。

这一设计保证了在敌人出生、移动和死亡时,网格保持同步、紧凑和高效。

它防止了块列表无限增长,并允许其他系统(如敌人AI、生成和渲染)快速访问仅仅是玩家所需的附近敌人。

结果是在较少性能开销的情况下处理数千个活跃实体的世界管理结构可以实现。

敌人生成器

敌人生成器控制怎样和在哪里生成敌人。不是盲目地在整个世界各处创建敌人,而是在网格和玩家位置的基础上进行智能生成。

它定义了两个接近玩家的环形边界。内环防止生成敌人太接近,而外环定义了最大生成距离。

在这些环之间的空間中将敌人按随机的方式生成生成。但是,在生成之前,系统会检查网格,以确保该区域既不是已经拥挤也不是在石墙或山脉内部。

通过使用网格,生成器可以避免远处区域过度拥挤并保持运行稳定。此外,可以根据玩家距离地图中心的距离来改变生成敌人的速度和类型,从界限处的弱敌向地图深处的强敌进行交替。

绘制和更新系统

绘制和更新系统处理游戏中的每一幀的可视和逻辑更新。为提高性能,它不会处理每个敌人,而是仅仅玩家可见范围内的敌人。

每一幀,它将会调用网格中的附近函数,以获得一个玩家可见范围内的对应列表。这个列表将用来更新敌人的逻辑、动画、移动和渲染。这一方法在带有数百个敌人的大世界中显著减少了CPU负载。

随后,它会更新玩家的位置、动画和交互。最后,它会先绘制世界或背景,然后是附近的敌人,最后是玩家和UI元素,确保渲染的顺序是正确的。

这一设计使得即使数百个敌人存在在世界中,游戏也可以保持流畅,因为仅仅是玩家附近的实体在每幀都保持活跃和可见。

网格是通过三个常量来定义的,分别控制其大小和分辨率。

枚举 CHUNK_SIZE = 4

枚举 WORLD_WIDTH = 2048

枚举 WORLD_HEIGHT = 2048

每个块的大小为 4 个单位高和宽,而整个世界则在 2048 个单位的宽和高中。

从以上这些值中,网格单元格的总量可以计算出。

枚举 grid_cols, grid_rows

grid_cols = floor(WORLD_WIDTH / CHUNK_SIZE)

grid_rows = floor(WORLD_HEIGHT / CHUNK_SIZE)

因此,每个世界块被划分为 512 列和 512 行,这为组织整个世界空间中的各个实体提供了一个细致的结构。

为了高效地管理数据,网格使用两个并行的二维数据结构:空白和空白列表。

空白数据结构储存当前位于各个块中的敌人的列表(索引)。每个条目对应于世界中的一个块,并包含一列敌人的索引列表。

空白列表则储存各个块中可重用的可用栈。当一个敌人被删除时,其栈位置会归还到可用列表,在未来需要时可以随时重复使用。这样可以避免存储空间的继续暴增,保持内存使用量稳定。

每个块保留:

敌人索引的列表,指向主敌人列表中的条目,因此网格中永远不存储敌人数据,只存储轻量化的索引。

可用的块栈的列表,防止块列表如敌人生成和死亡时无限增长,保持块数组和主分类表的紧凑性。

枚举 empty, empty_freelist

empty = {}

empty_freelist = {}

for r = 1 to grid_rows do

empty = append(empty, repeat({}, grid_cols))

empty_freelist = append(empty_freelist, repeat({}, grid_cols))

end for

当游戏世界或一个关卡被重置时,网格将通过grid_init()函数调用进行清理,清除两个数据结构的内容,并将网格恢复到可重用的状态。

全局函数 grid_init()

empty = {}

empty_freelist = {}

for r = 1 to grid_rows do

empty = append(empty, repeat({}, grid_cols))

empty_freelist = append(empty_freelist, repeat({}, grid_cols))

end for

end function

网格可以通过get_chunk函数来找到一个具体位置属于哪个块。

函数get_chunk(atom x, atom y)

integer cx, cy

cx = floor(x / CHUNK_SIZE)

cy = floor(y / CHUNK_SIZE)

if cx < 1 then cx = 1 end if

if cy < 1 then cy = 1 end if

if cx > grid_cols then cx = grid_cols end if

if cy > grid_rows then cy = grid_rows end if

return {cx, cy}

end function

网格同样地会维持两个全局列表:

类表-所有激活网格项的存储空间,用于储存敌人的位置、敌人句柄和块坐标。

类自由表-一个用来存储删除或不活跃网格项的句柄列表。

数组类表, 类自由表

类表 = {}

类自由表 = {}

每个网格项在类表中储存多个属性,如世界坐标、敌人句柄、块坐标和句柄引用。

整型 grid_x = 1

整型 grid_y = 2

整型 grid_enemy_handle = 3

整型 grid_list = 4

整型 grid_emptylist = 5

整型 grid_cx = 6

整型 grid_cy = 7

整型 grid_empty_handle = 8

当一个敌人被删除时,empty_delete函数则会移除其条目,从块中释放其栈位置,并将句柄添加回列表中,等待其它被重用。

这样就避免了列表的不断长长,维持各个块的数组和主类别表的紧凑。

全局函数 empty_delete(integer handle)

integer cx, cy, empty_handle, enemy_id

if handle < 1 或 handle > 长度类表 then

puts(1, "empty_delete: 无效的句柄\n")

结束

end if

if 长度类表[句柄] = 0 then

puts(1, "empty_delete: 无效的句柄\n")

结束

end if

enemy_id = 类表[句柄][grid_enemy_handle]

grid_remove_enemy(enemy_id)

cx = 类表[句柄][grid_cx]

cy = 类表[句柄][grid_cy]

empty_handle = 类表[句柄][grid_empty_handle]

if cy >= 1 和 cy <= grid_rows 和 cx >= 1 和 cx <= grid_cols then

if empty_handle > 0 和 empty_handle <= 长度(empty[cy][cx]) then

empty[cy][cx][empty_handle] = {}

empty_freelist[cy][cx] = 追加(empty_freelist[cy][cx], empty_handle)

结束if

结束if

类表[句柄] = {}

类自由表 = 追加(类自由表, handle)

结束函数

当新敌人生成时,grid_new函数 则会在网格中创建一个新项,并计算出它所处的块坐标,然后就加入相应块的活跃实体列表。如果块的可用列表中有可用的栈,则会将其用于生成时,而不是新建一个新实体。

这样可以保证每个块根据需求增长,重用其可用空间,而不是随着敌人出生和死亡而无限制地增加。

全局函数 grid_new(atom x, atom y, integer enemy)

integer handle, cx, cy, empty_handle

序列块

if 长度类自由表 > 0 then

handle = 类自由表[1]

类自由表 =类自由表[2..长度(类自由表)]

else

类表 = 追加(类表, {})

handle = 长度(类表)

结束if

类表[句柄] = {x, y, enemy, {}, {}, 0, 0, 0}

块 = get_chunk(x, y)

cx = 块[1]

cy = 块[2]

类表[句柄][grid_cx] = cx

类表[句柄][grid_cy] = cy

if 长度(empty_freelist[cy][cx]) > 0 then

empty_handle = empty_freelist[cy][cx][1]

empty_freelist[cy][cx] = empty_freelist[cy][cx][2..长度(empty_freelist[cy][cx])]

else

empty[cy][cx] = 追加(empty[cy][cx], {})

empty_handle = 长度(empty[cy][cx])

结束else

empty[cy][cx][empty_handle] = {enemy}

结束if

结束函数

当 enemies 在网格中移动时,grid_update_position函数 则会检查它们是否已经进入一个不同的块。

如果是,则系统会从旧块中移除,然后将其重新插入新块并更新其存储的网格坐标。

这样就避免了位置数据过度重复或扫描,而每一幀只需要更新必要的块位置。

全局函数 grid_update_position(integer handle, atom new_x, atom new_y)

integer old_cx, old_cy, new_cx, new_cy, old_empty_handle, new_empty_handle

序列块

if handle < 1 或 handle > 长度类表 then 返回 end if

if 长度类表[句柄] = 0 then 返回 end if

old_cx =类表[句柄][grid_cx]

old_cy =类表[句柄][grid_cy]

old_empty_handle =类表[句柄][grid_empty_handle]

块 = get_chunk(new_x, new_y)

new_cx = 块[1]

new_cy = 块[2]

类表[句柄][grid_x] = new_x

类表[句柄][grid_y] = new_y

if new_cx != old_cx 或 new_cy != old_cy then

if old_cx >= 1 和 old_cx <= grid_cols 和 old_cy >= 1 和 old_cy <= grid_rows then

if old_empty_handle > 0 和 old_empty_handle <= 长度(empty[old_cy][old_cx]) then

empty[old_cy][old_cx][old_empty_handle] = {}

empty_freelist[old_cy][old_cx] = 追加(empty_freelist[old_cy][old_cx], old_empty_handle)

结束if

结束if

if 长度(empty_freelist[new_cy][new_cx]) > 0 then

new_empty_handle = empty_freelist[new_cy][new_cx][1]

empty_freelist[new_cy][new_cx] = empty_freelist[new_cy][new_cx][2..长度(empty_freelist[new_cy][new_cx])]

else

empty[new_cy][new_cx] = 追加(empty[new_cy][new_cx], {})

new_empty_handle = 长度(empty[new_cy][new_cx])

结束else

empty[new_cy][new_cx][new_empty_handle] = {类表[句柄][grid_enemy_handle]}

类表[句柄][grid_cx] = new_cx

类表[句柄][grid_cy] = new_cy

类表[句柄][grid_empty_handle] =new_empty_handle

结束if

结束if

结束函数

最后,grid_get_nearby函数则允许游戏快速检索一个给定区域内的所有敌人。

它使用网格结构来检查玩家周围的块,而不是遍历所有敌人。这一局部检查有助于大型世界场景中迅速查找附近的敌人。

全局函数 grid_get_nearby(atom x, atom y, integer range)

序列结果,块

integer cx, cy, min_cx, max_cx, min_cy, max_cy

块 = get_chunk(x, y)

cx = 块[1]

cy = 块[2]

结果 = {}

最小cx = 最大(1, cx - range)

最大cx = 最小(grid_cols, cx + range)

最小cy = 最大(1, cy - range)

最大cy = 最小(grid_rows, cy + range)

对于 yy = 最小cy 到最大cy do

对于 xx = 最小cx 到最大cx do

对于 i = 1 长度empty[yy][xx]do

如果序列(empty[yy][xx][i]) 和 长度(empty[yy][xx][i]) > 0 then

结果 = 追加(结果, empty[yy][xx][i])

结束if

结束for

结束for

结束for

返回结果

结束函数