Wireworld Simulator Using the Raylib: Part2¶
继续上一部分。
这部分主要实现 无限画布 的功能,包含视角移动、视图缩放、网格自动拓展。
项目地址:github.com/13m0n4de/wireworld
修复网格外绘制造成的问题 ¶
某些情况下,窗口大小会大于网格大小,鼠标在网格外点击会导致 grid
下标溢出。
限制范围:
diff --git a/main.c b/main.c
index 828e1c8..f86f289 100644
--- a/main.c
+++ b/main.c
@@ -252,8 +252,12 @@ void HandleUserInput(void) {
!CheckCollisionPointRec(mousePosition, playButtonRect) &&
!CheckCollisionPointRec(mousePosition, nextButtonRect) &&
!CheckCollisionPointRec(mousePosition, indicatorRect)) {
- grid[mouseYGridPos][mouseXGridPos] = selectCellType;
- DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
+ if ((mouseXGridPos >= 0 && mouseXGridPos < cols) &&
+ (mouseYGridPos >= 0 && mouseYGridPos < rows)) {
+ grid[mouseYGridPos][mouseXGridPos] = selectCellType;
+ DrawCell(mouseXGridPos, mouseYGridPos,
+ GetCellColor(selectCellType));
+ }
}
DrawCellLines(mouseXGridPos, mouseYGridPos, WHITE);
使用 calloc 初始化网格 ¶
使用 calloc
可以在分配内存的同时将内存初始化为 0
,也正好就是 EMPTY
,省去额外遍历初始化的过程。
void InitGrid(void) {
grid.cells = RL_CALLOC(grid.rows, sizeof(Cell*));
for (int y = 0; y < grid.rows; y++) {
grid.cells[y] = RL_CALLOC(grid.cols, sizeof(Cell));
}
}
视角移动 ¶
无限画布的前置功能,按住鼠标右键拖动画布。
试了几个方案,最后还是用了 2D 摄像头,通过移动摄像头位置模拟画布拖动,这种方法不会太过复杂。
Camera2D camera = {0};
camera.zoom = 1.0f;
if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
Vector2 delta = GetMouseDelta();
delta = Vector2Scale(delta, -1.0f / camera.zoom);
camera.target = Vector2Add(camera.target, delta);
}
这时网格的绘制逻辑需要放在 2D 模式中(BeginMode2D
和 EndMode2D
之间
由于 HandleUserInput
函数需要在鼠标左击时绘制「细胞」以及在鼠标悬停时绘制「预览边框
BeginMode2D(camera);
float frameTime = GetFrameTime();
elapsedTime += frameTime;
if (isPlaying && elapsedTime >= refreshInterval) {
UpdateGrid();
elapsedTime = 0.0f;
}
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
Color cellColor = GetCellColor(grid[y][x]);
DrawCell(x, y, cellColor);
}
}
HandleUserInput();
EndMode2D();
按钮我希望保持屏幕固定位置,不会被脱拽移动,它们在结束 2D 模式后绘制:
EndMode2D();
DrawIndicators();
DrawPlayButton();
DrawNextButton();
顺带把 HandleUserInput
重构了,拆成几个函数:
void HandleUserInput(void) {
HandleCellPlacements();
HandleButtonClicks();
HandleShortcuts();
HandleCameraMovement();
}
网格拓展 ¶
策略 ¶
策略很简单,下面几张图展示了一个基本案例。
初始时相机和网格坐标一致,同为 (0, 0)
。网格大小 20 x 12,其中有一细胞位于 [9, 5]
处(第 6 行,第 10 列
当按住鼠标右键向右下角滑动时,相机向左上角移动到 (-8, -4)
,越过网格的横纵边界。
此时网格需向 X 和 Y 方向拓展一个固定的单位长度,这里设置为 8。于是网格坐标变成了 (-8, -8)
,大小变成了 28 x 20。
细胞变成 [17, 13]
,在网格中的位置变化了,但相对相机的位置没变,屏幕上不会有感知。
只在越过边界时拓展网格,同时选择一个合适的值作为每次拓展的单位长度,可以避免频繁地内存分配。
当相机越过右或下边界时,网格坐标不需要更改,只需要拓展大小。
代码 ¶
因为需要同时管理网格的坐标、长宽和细胞,所以创建 Grid
结构体用来表示网格信息:
typedef struct {
Vector2 position;
int rows;
int cols;
Cell** cells;
} Grid;
一些代码要进行相应更改,比如:
for (int y = 0; y < grid.rows; y++) {
for (int x = 0; x < grid.cols; x++) {
Color cellColor = GetCellColor(grid.cells[y][x]);
DrawCell(x, y, cellColor);
}
}
移动相机时,检查是否越过网格边界,并针对特定方向拓展网格。
void HandleCameraMovement(void) {
if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
Vector2 delta = GetMouseDelta();
delta = Vector2Scale(delta, -1.0f / camera.zoom);
camera.target = Vector2Add(camera.target, delta);
Vector2 cameraOffset = Vector2Subtract(camera.target, grid.position);
bool isOutsideX = cameraOffset.x + screenWidth >
grid.position.x + grid.cols * cellSize;
bool isOutsideY = cameraOffset.y + screenHeight >
grid.position.y + grid.rows * cellSize;
if (cameraOffset.x < 0)
ExpandGrid(LEFT);
if (cameraOffset.y < 0)
ExpandGrid(UP);
if (isOutsideX)
ExpandGrid(RIGHT);
if (isOutsideY)
ExpandGrid(DOWN);
}
}
根据拓展方向计算新的行列数,创建新的 cells
。计算偏移,将之前的细胞按照偏移放置在新 cells
中,以确保它们在屏幕上的位置不变。最后使用新的数据更新 grid 完成拓展:
const int gridIncrement = 10;
void ExpandGrid(Direction direction) {
int newRows =
grid.rows + (direction == UP || direction == DOWN ? gridIncrement : 0);
int newCols = grid.cols +
(direction == LEFT || direction == RIGHT ? gridIncrement : 0);
int xOffset = (direction == LEFT ? gridIncrement : 0);
int yOffset = (direction == UP ? gridIncrement : 0);
Cell** newCells = RL_CALLOC(newRows, sizeof(Cell*));
for (int y = 0; y < newRows; y++) {
newCells[y] = RL_CALLOC(newCols, sizeof(Cell));
}
for (int y = 0; y < grid.rows; y++) {
for (int x = 0; x < grid.cols; x++) {
newCells[y + yOffset][x + xOffset] = grid.cells[y][x];
}
}
FreeGrid();
grid.cells = newCells;
grid.rows = newRows;
grid.cols = newCols;
grid.position.x -= cellSize * xOffset;
grid.position.y -= cellSize * yOffset;
}
算得上是无限画布了。
仅绘制可见部分 ¶
网格过大时会出现明显的卡顿,因为视图外的细胞也照常绘制了。
可以通过计算当前相机视野内的网格单元格范围,只绘制这些单元格:
void DrawVisibleCells(void) {
Vector2 topLeft = GetScreenToWorld2D(Vector2Zero(), camera);
Vector2 bottomRight =
GetScreenToWorld2D((Vector2){screenWidth, screenHeight}, camera);
int startX = (int)floor((topLeft.x - grid.position.x) / cellSize);
int startY = (int)floor((topLeft.y - grid.position.y) / cellSize);
int endX = (int)ceil((bottomRight.x - grid.position.x) / cellSize);
int endY = (int)ceil((bottomRight.y - grid.position.y) / cellSize);
startX = Clamp(startX, 0, grid.cols);
startY = Clamp(startY, 0, grid.rows);
endX = Clamp(endX, 0, grid.cols);
endY = Clamp(endY, 0, grid.rows);
for (int y = startY; y < endY; y++) {
for (int x = startX; x < endX; x++) {
Color cellColor = GetCellColor(grid.cells[y][x]);
DrawCell(x, y, cellColor);
}
}
}
视图缩放 ¶
最初实验了使用相机的 zoom
字段完成缩放,但发现效果不怎么好,缩得太小时网格线会变得不清晰甚至消失,并且给许多计算引入了新变量。
所以,最终还是使用了调整细胞大小 cellSize
的方案,以此模拟“视图的缩放”。
滚动鼠标滚轮时,调整细胞大小和网格位置,确保网格会以鼠标指针所在的位置为中心进行缩放,使得鼠标指针所指的网格单元在缩放前后保持不变:
void HandleZoom(void) {
float wheel = GetMouseWheelMove();
if (wheel != 0) {
int newCellSize = cellSize + wheel * zoomSpeed;
if (newCellSize >= minCellSize && newCellSize <= maxCellSize) {
Vector2 mousePos = GetScreenToWorld2D(GetMousePosition(), camera);
Vector2 gridPos =
Vector2Divide(Vector2Subtract(mousePos, grid.position),
(Vector2){cellSize, cellSize});
cellSize = newCellSize;
grid.position = Vector2Subtract(
mousePos,
Vector2Multiply(gridPos, (Vector2){cellSize, cellSize}));
}
}
}
拓展网格在视图的移动和缩放结束后进行:
void HandleCameraMovement(void) {
if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
Vector2 delta = GetMouseDelta();
delta = Vector2Scale(delta, -1.0f / camera.zoom);
camera.target = Vector2Add(camera.target, delta);
}
}
void HandleUserInput(void) {
HandleCellPlacements();
HandleButtonClicks();
HandleShortcuts();
HandleCameraMovement();
HandleZoom();
Vector2 cameraOffset = Vector2Subtract(camera.target, grid.position);
if (cameraOffset.x < 0) {
ExpandGrid(LEFT);
}
if (cameraOffset.y < 0) {
ExpandGrid(UP);
}
if (cameraOffset.x + screenWidth > grid.position.x + grid.cols * cellSize) {
ExpandGrid(RIGHT);
}
if (cameraOffset.y + screenHeight >
grid.position.y + grid.rows * cellSize) {
ExpandGrid(DOWN);
}
}
总结 ¶
最终代码见 github.com/13m0n4de/wireworld/blob/main/main.c。
下一步计划:
- 导入导出
- 设置页面