使用 PHP 和 Raylib 也可以开发贪吃蛇游戏

使用 PHP 和 Raylib 也可以开发贪吃蛇游戏

Raylib

Raylib 是用 C 语言编写的,被定义为"一个简单易用的库,用于享受视频游戏编程"。

它提供了非常直接的函数来操作视频、音频、读取键盘、鼠标或游戏手柄等输入。它还支持 2D 和 3D 渲染。是一个相当完整的库。

这里是 Raylib 架构的概述。它期望在 Raylib 的模块之上编写游戏、引擎或工具。模块提供处理相机、纹理、文本、形状、模型、音频、数学等功能。

Raylib 的架构概述。来源: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 扩展才能玩。

相关推荐
codercwh2 小时前
3 分钟上手 Claude Code!API 中转站让 AI 编程效率翻倍
后端
SimonKing2 小时前
OCR告别付费!分享两款可部署的开源工具
后端
爱叫啥叫啥2 小时前
STM32从零实战:深入理解RCC时钟与按键控制LED的底层原理
后端
火山引擎开发者社区2 小时前
火山引擎 MongoDB 进化史:从扛住抖音流量洪峰到 AI 数据底座
后端
星星电灯猴2 小时前
API接口调试全攻略 Fiddler抓包工具、HTTPS配置与代理设置实战指南
后端
zwm_yy2 小时前
php8新增函数
php
007php0072 小时前
Redis面试题解析:Redis的数据过期策略
java·网络·redis·缓存·面试·职场和发展·php
Jtti2 小时前
IPv4与IPv6共存下的访问问题排查方法
开发语言·php
程序员爱钓鱼3 小时前
Python 编程实战:环境管理与依赖管理(venv / Poetry)
后端·python·trae