使用 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 扩展才能玩。

相关推荐
LYFlied35 分钟前
在AI时代,前端开发者如何构建全栈开发视野与核心竞争力
前端·人工智能·后端·ai·全栈
用户47949283569151 小时前
我只是给Typescript提个 typo PR,为什么还要签协议?
前端·后端·开源
Surpass余sheng军1 小时前
AI 时代下的网关技术选型
人工智能·经验分享·分布式·后端·学习·架构
JosieBook1 小时前
【Spring Boot】Spring Boot调用 WebService 接口的两种方式:动态调用 vs 静态调用 亲测有效
java·spring boot·后端
喵个咪2 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:基于 GORM 从零实现新服务
后端·go·orm
a努力。3 小时前
【基础数据篇】数据等价裁判:Comparer模式
java·后端
开心猴爷3 小时前
苹果App Store应用程序上架方式全面指南
后端
小飞Coding3 小时前
三种方式打 Java 可执行 JAR 包,你用对了吗?
后端
cypking3 小时前
Nuxt项目内网服务器域名代理访问故障排查
运维·服务器·php
bcbnb3 小时前
没有 Mac,如何在 Windows 上架 iOS 应用?一套可落地的工程方案
后端