使用 PHP 和 Raylib 也可以开发贪吃蛇游戏
Raylib
Raylib 是用 C 语言编写的,被定义为"一个简单易用的库,用于享受视频游戏编程"。
它提供了非常直接的函数来操作视频、音频、读取键盘、鼠标或游戏手柄等输入。它还支持 2D 和 3D 渲染。是一个相当完整的库。
这里是 Raylib 架构的概述。它期望在 Raylib 的模块之上编写游戏、引擎或工具。模块提供处理相机、纹理、文本、形状、模型、音频、数学等功能。
Raylib 的架构概述。来源:https://www.raylib.com/index.html
它没有附带引擎内容,比如复杂的碰撞检测或物理。如果需要这样的功能,需要自己构建。或者找一些现成的、准备与 Raylib 一起工作的实现。
原文链接 使用 PHP 和 Raylib 也可以开发贪吃蛇游戏
Raylib PHP 扩展
最近一个 PHP 扩展引起了关注。由 @joseph-montanez 开发的 raylib-php 扩展在不久前发布了第一个 alpha 版本。
如果需要学习如何编译和运行它,请查看仓库的 README.md 文件。在 MacOS 上,以下步骤运行良好:
bash
$ git clone git@github.com:joseph-montanez/raylib-php.git
$ cd raylib-php/
$ phpize
$ ./configure
$ make
它只在 PHP 7.4 上编译成功。所以确保有合适的 php 版本。
这个扩展旨在提供对 C 库的绑定,可以用 PHP 编写相同的游戏。
当然,由于 C 库不会提供游戏特定的功能,如物理等。所以这些功能必须在 PHP 端开发。
这个扩展还没有完成。可以查看官方仓库的 MAPPING.md 文件来了解已经完成了什么。
尽管它还没有完成,但已经相当实用了。
一个简单的贪吃蛇游戏
尽管"贪吃蛇"是一个简单的游戏,这里决定不完全实现它。主要目标是构建一个足够好的运行引擎,可以测试扩展的一些基本功能。
选择了几个需求来实现:
- 蛇必须不断移动,但可以改变方向
- 屏幕上应该有一个水果放置在随机位置
- 当蛇头碰到水果时,必须发生五件事:水果必须被销毁,蛇的身体必须增长,必须创建另一个水果,分数计数器必须增加一,蛇的速度也会增加
- 当蛇碰到窗口边缘时,它应该穿越到相反的边缘
- 应该很清楚,但玩家还需要使用输入设备(如键盘)改变蛇的方向
我选择不实现的两个非常重要的需求是:1)蛇不应该咬到自己。意思是如果由于任何原因蛇碰到自己的身体,游戏应该结束。2)蛇不能立即改变到相反的方向。所以当你向右移动时,切换到左方向需要先向上或向下。
这两个需求没有实现,因为它们纯粹是算法性的,不会给扩展本身的实验增加太多内容。
实现
这个游戏的实现有两个组件:游戏循环 和游戏状态。
游戏循环负责根据用户输入和计算更新游戏状态,然后在屏幕上绘制这个状态。为此,我创建了一个名为"GameLoop"的类。
游戏状态保存游戏的快照。它保存诸如玩家分数、水果的 x, y 坐标、蛇的 x, y 坐标以及组成其身体的所有方块。为此创建了一个"GameState"类。
下面是它们的样子。
游戏循环
GameLoop 类初始化系统,并创建一个循环,在每次迭代时执行两个步骤:更新状态和绘制状态。
所以在构造函数中,我只是初始化画布的宽度和高度,并实例化 GameState。
作为 GameState 的参数,我传递了宽度和高度除以所需的单元格大小(在我的例子中是 30 像素)。这些值表示 GameState 可以使用的最大 X 和 Y 坐标。我们稍后会检查它们。
php
// GameLoop.php
final class GameLoop
{
// ...
public function __construct(
int $width,
int $height
) {
$this->width = $width;
$this->height = $height;
// 30
$s = self::CELL_SIZE;
$this->state = new GameState(
(int) ($this->width / $s),
(int) ($this->height / $s)
);
}
// ...
}
稍后,一个名为 start() 的公共方法将生成一个窗口,设置帧率并创建一个无限循环------是的,类似于 while (true) ------它将首先触发一个私有方法 update(),然后触发方法 draw()。
php
// ...
public function start(): void
{
Window::init(
$this->width,
$this->height,
'PHP Snake'
);
Timming::setTargetFPS(60);
while (
$this->shouldStop ||
!Window::shouldClose()
) {
$this->update();
$this->draw();
}
}
// ...
update() 方法将负责更新游戏状态实例。它通过读取用户的输入(按键)并执行诸如检查碰撞等操作来做到这一点。
根据 update() 方法中完成的计算,它会触发 GameState 实例上的状态更改。
php
private function update(): void
{
$head = $this->state->snake[0];
$recSnake = new Rectangle(
(float) $head['x'],
(float) $head['y'],
1,
1,
);
$fruit = $this->state->fruit;
$recFruit = new Rectangle(
(float) $fruit['x'],
(float) $fruit['y'],
1,
1,
);
// 蛇咬到水果
if (
Collision::checkRecs(
$recSnake,
$recFruit
)
) {
$this->state->score();
}
// 控制步进速度
$now = microtime(true);
if (
$now - $this->lastStep
> (1 / $this->state->score)
) {
$this->state->step();
$this->lastStep = $now;
}
// 如有必要,更新方向
if (Key::isPressed(Key::W)) {
$this->state->direction = GameState::DIRECTION_UP;
} else if (Key::isPressed(Key::D)) {
$this->state->direction = GameState::DIRECTION_RIGHT;
} else if (Key::isPressed(Key::S)) {
$this->state->direction = GameState::DIRECTION_DOWN;
} else if (Key::isPressed(Key::A)) {
$this->state->direction = GameState::DIRECTION_LEFT;
}
}
最后是 draw() 方法。它将读取 GameState 上的属性并打印它们。应用所有比例和缩放。
我构建它的方式是,它期望 X 坐标范围从 0 到(宽度除以单元格大小),Y 坐标范围从 0 到(高度除以单元格大小)。通过将每个坐标乘以"单元格大小",我们可以得到一个足够好的缩放绘制,而不会混淆我们的状态操作和绘制。
相当简单。看起来像下面这样:
php
private function draw(): void
{
Draw::begin();
// 清除屏幕
Draw::clearBackground(
new Color(255, 255, 255, 255)
);
// 绘制水果
$x = $this->state->fruit['x'];
$y = $this->state->fruit['y'];
Draw::rectangle(
$x * self::CELL_SIZE,
$y * self::CELL_SIZE,
self::CELL_SIZE,
self::CELL_SIZE,
new Color(200, 110, 0, 255)
);
// 绘制蛇的身体
foreach (
$this->state->snake as $coords
) {
$x = $coords['x'];
$y = $coords['y'];
Draw::rectangle(
$x * self::CELL_SIZE,
$y * self::CELL_SIZE,
self::CELL_SIZE,
self::CELL_SIZE,
new Color(0,255, 0, 255)
);
}
// 绘制分数
$score = "Score: {$this->state->score}";
Text::draw(
$score,
$this->width - Text::measure($score, 12) - 10,
10,
12,
new Color(0, 255, 0, 255)
);
Draw::end();
}
我还添加了一些其他用于调试的东西,但我宁愿把它们排除在本文之外。
之后,是状态管理。这是 GameState 的责任。看看吧!
游戏状态
GameState 表示游戏中存在的所有内容。分数、玩家和水果等对象。
这意味着每当玩家必须移动或水果必须被吃掉时,这将在 GameState 内部发生。
对于蛇的身体,我决定有一个包含 (x, y) 坐标的数组。我认为数组的第一个元素(索引零)是蛇的头部。向这个数组添加更多 (x, y) 元素将增加蛇的身体大小。
然而,水果是一对 (x, y) 坐标。因为我期望每次屏幕上只有一个水果。
GameState 类的构造函数将使用随机坐标初始化这些对象。它看起来像这样:
php
// GameState.php
final class GameState
{
public function __construct(
int $maxX,
int $maxY
) {
$this->maxX = $maxX;
$this->maxY = $maxY;
$this->snake = [
$this->craftRandomCoords(),
];
$this->fruit = $this->craftRandomCoords();
}
}
为了增加蛇的身体大小,我创建了一个名为 incrementBody() 的私有方法,它应该为蛇的身体添加一个新的头部。这个新头部应该考虑蛇当前前进的方向。(左、右、上或下)
要添加新头部,我只需复制当前头部,根据当前方向更新其坐标,并将其合并到蛇的身体中,占据零索引。
php
private function incrementBody(): void
{
$newHead = $this->snake[0];
// 调整头部方向
switch ($this->direction) {
case self::DIRECTION_UP:
$newHead['y']--;
break;
case self::DIRECTION_DOWN:
$newHead['y']++;
break;
case self::DIRECTION_RIGHT:
$newHead['x']++;
break;
case self::DIRECTION_LEFT:
$newHead['x']--;
break;
}
// 在整个身体前面
// 添加新头部
$this->snake = array_merge(
[$newHead],
$this->snake
);
}
有了 incrementBody() 方法,实现 score() 方法就变得非常简单,它只是增加分数计数器和蛇的身体。此外,score() 会在随机坐标处放置一个新水果。
php
public function score(): void
{
$this->score++;
$this->incrementBody();
$this->fruit = $this->craftRandomCoords();
}
有趣的是 step() 方法,它负责移动蛇。
如果你记得很清楚,蛇移动的方式是它的头部会不断向一个方向步进,然后身体会以延迟的方式跟随它。所以如果蛇有 3 个方块作为身体大小并向下移动,需要三步才能使其完全面向左边。
我这样做的方式基本上是再次增加蛇的身体(这会在新方向上添加一个新头部)并从蛇的身体中删除最后一个元素。这样大小保持不变,头部有新的方向,在每一步中旧坐标都将被删除。
我还添加了一些从屏幕一边穿越到另一边的逻辑,你可以阅读它(我希望)。
php
public function step(): void
{
$this->incrementBody();
// 删除最后一个元素
array_pop($this->snake);
// 如有必要,使身体穿越
foreach ($this->snake as &$coords) {
if ($coords['x'] > $this->maxX - 1) {
$coords['x'] = 0;
} else if ($coords['x'] < 0) {
$coords['x'] = $this->maxX - 1;
}
if ($coords['y'] > $this->maxY - 1) {
$coords['y'] = 0;
} else if ($coords['y'] < 0) {
$coords['y'] = $this->maxY - 1;
}
}
}
将所有内容粘合在一起并实例化。我们准备好玩了!
用 PHP 开发游戏是否可行?
当然比以前更可行了。希望比明天少一些。
该扩展提供了非常酷的绑定,但仍然没有完成。如果你懂一点 C 代码,你可以通过贡献来让 PHP 游戏开发的未来变得更美好。这里有一个列表,你可以在其中找到仍需实现的函数。
PHP 默认情况下仍然是阻塞的,所以应该智能地处理繁重的 I/O。可以将此库与事件循环一起使用,或使用 Parallel 扩展的线程。可能你必须自己开发一些东西来实现这一点。
到目前为止,最让我烦恼的是用 PHP 编写的游戏的可移植性。没有简单的方法将这些游戏打包成二进制文件。所以玩家必须安装 PHP 并编译 Raylib 扩展才能玩。