MGPIC案例分享|零基础早鸟教程:8小时使用 wasm4 开发井子棋小游戏!

本文转载自 CSDN:

blog.csdn.net/a541972321?...

作者:Spaceack

缘起

我于10月9日在天池大数据科研平台发现了此次比赛。虽然对比赛很感兴趣,但面对新生编程语言和陌生的开发引擎,难免令人心生畏惧。但可爱的月兔形象充满了活力,对我还是蛮有吸引力。因此我想试一试,至少了解一下。得益于完善的官方教程和博客案例引导,我惊喜的发现月兔简洁的语法和富有表现力的数据结构很容易上手。由于井子棋逻辑较为简单,我仅花了约半天时间,便完成了井子棋小游戏的开发。正所谓"天下事有难易乎? 为之,则难者亦易矣;

本篇文章主要分享 "2024年全球 MoonBit 编程创新赛 游戏赛道"参赛过程中九宫棋游戏的开发技巧和心得。以此抛砖引玉。首先介绍下 MoonBit。

MoonBit 简介

  • 月兔 语言 MoonBit 是一个AI原生的用于云计算和边缘计算的 WebAssembly 端到端的编程语言工具链。 您可以访问 try.moonbitlang.cn 获得 IDE 环境,无需安装任何软件,也不依赖任何服务器。生成比现有解决方案明显体积更小 的 WASM 文件。并有着更高的运行时性能 和先进的编译时性能

  • 比赛会用到 WASM-4 引擎,此引擎是一个框架,可以开发有趣的复古游戏,最终的编译文件可在浏览器或单片机设备(如 ESP32)上直接运行。

(咱话不多说,直入正题)

开发环境准备

  • 简单快速,拢共分为5步。我们目标是以最快速度进入到游戏开发本身,因此略过目录。命令参数,配置等说明,更多细节可通过官网了解。

1. 部署月兔环境

bash 复制代码
curl -fsSL https://cli.moonbitlang.cn/install/unix.sh | bash

2. 创建项目工作区

bash 复制代码
mkdir moonbit_wsam4_game
cd moonbit_wsam4_game
moon new --lib --path . --user spaceack --name mygame

3. 安装wasm4依赖

bash 复制代码
npm install -D wasm4 

4. 将 wasm4绑定增加到项目中

bash 复制代码
moon update && moon add moonbitlang/wasm4

此时的代码目录如下:

5. 更新项目配置

  1. 使用以下代码覆盖 moon.pkg.json文件
json 复制代码
{
  // "is-main": true,
  "import": ["moonbitlang/wasm4"],
  "link": {
    "wasm-gc": {
      "exports": ["start", "update"],
      "import-memory": {
        "module": "env",
        "name": "memory"
      }
    },
    "wasm": {
      "exports": ["start", "update"],
      "import-memory": {
        "module": "env",
        "name": "memory"
      },
      "heap-start-address": 6590
    }
  }
}
  1. 使用以下代码覆盖top.mbt文件
rust 复制代码
pub fn update() -> Unit {
  
}
pub fn start() -> Unit {

}

编译及运行

  • 此时一个游戏代码已经写好了,我们编译并运行看看效果
bash 复制代码
# 编译
moon build --target wasm
bash 复制代码
# 运行
 npx wasm4 run target/wasm/release/build/mygame.wasm
  • 访问 http://localhost:4444 即可查看执行效果。可见绿油油的一片。 这是WASM-4 引擎的默认配色。

游戏开发

小试牛刀

  • top.mbt文件是游戏的主程序的入口, 简单逻辑的游戏,所有逻辑仅在此文件内编写即可。

  • 依据惯例,首先我们尝试在终端打印"Hello, World"。在start()函数中调用wasm的trace方法即可。它可是我们调试程序的好帮手。😊

js 复制代码
pub fn update() -> Unit {
  
}
pub fn start() -> Unit {
@wasm4.trace("Hello world!");
}
  • 太好啦😊,经过再次编译运行,我们的代码起作用啦,在终端成功打印了Hello World.

设置配色

  • wasm 单帧最多支持4种颜色,可以对调色板的4个颜色索引设置配色。这里我们要用到set_palette方法。 默认索引1为背景色。
rust 复制代码
pub fn start() -> Unit {
  @wasm4.trace("Hello world!");
  @wasm4.set_palette(1, @wasm4.rgb(0x282e30)) // 暗岩灰
  @wasm4.set_palette(2, @wasm4.rgb(0xaa337f)) // 陈玫红 Maximum Red Purple
  @wasm4.set_palette(3, @wasm4.rgb(0xd4392e)) // 茜红
  @wasm4.set_palette(4, @wasm4.rgb(0x898f92)) // 
}
  • 太好啦,经过再次编译运行,我们的代码起作用啦,背景色成功变为了暗岩灰。

绘制图形

  • wasm 界面的长和宽是固定的 160*160像素
  • 以左上为坐标0点(第三象限)
  • wasm支持绘制矩形,椭圆, 线等基础图形。复杂的图形由基础图形构成, 可练习实现如下效果图: 代码如下:
rust 复制代码
pub fn update() -> Unit {
  @wasm4.set_draw_colors(2) // 选用调色板索引为2的颜色,即茜红 
  @wasm4.rect(0, 0, 80, 80) // 用茜红绘制一个矩阵 四个参数分别为第三象限的 x, y, width, heigh
  @wasm4.set_draw_colors(4) 
  @wasm4.line(0, 0, 80, 80)   //一条斜线 从左上到中间(叠加到矩形之上)
  @wasm4.line(0, 80, 160, 80) // 中间一条横线 (叠加到矩形之上)
  @wasm4.line(80, 0, 80, 160) // 中间一条竖	线 (叠加到矩形之上)
  @wasm4.set_draw_colors(3) 
  @wasm4.oval(80, 80, 80,80 ) // 圆 第四象限
} 
pub fn start() -> Unit {
  @wasm4.trace("Hello world!");
  @wasm4.set_palette(1, @wasm4.rgb(0xaa337f)) // 陈玫红 Maximum Red Purple
  @wasm4.set_palette(2, @wasm4.rgb(0xd4392e)) // 茜红
  @wasm4.set_palette(3, @wasm4.rgb(0xffffff)) // 白
  @wasm4.set_palette(4, @wasm4.rgb(0x282e30)) // 暗岩灰
}

游戏状态

  • 一般游戏可以抽象为一个状态机,总共有开始游戏中结束三种基本状态。那么我们需要构建一个状态结构来描绘游戏的状态信息。
  • 以井子棋游戏为例。我们用枚举类型描绘三种状态。并定义一个状态结构来存储状态信息。
  • 这里我们能领略到 MoonBit 简单且实用数据导向语言设计的魅力。
rust 复制代码
enum GameState {
  GSInit // 初始状态
  GSGameStart // 游戏中
  GSGameEnd // 游戏结束
}

struct GameStat {
   mut game_state : GameState 
   mut score : UInt // 胜利得分
   mut times: UInt //游戏次数
   gamemap : FixedArray[FixedArray[Char]] //地图
   mut player_x : Int 
   mut player_y : Int
   mut win: String 
   mut step: Int
   rng : @random.Rand
}
  • 三子棋逻辑并不复杂,至多9个变量就可以描述整个状态!

  • 然后我们使用 let 声明一个名为 gs 的变量, 并将其初始化为 GameStat 类型的一个新实例。

rust 复制代码
let gs : GameStat = GameStat::new()
pub fn GameStat::new() -> GameStat {
  let gamemap : FixedArray[FixedArray[Char]] = [
    ['0', '0', '0'],
    ['0', '0', '0'],
    ['0', '0', '0'],
  ]
  {
    game_state: GSInit,
    score: 0,
    times: 1,
    gamemap,
    player_x: 1,
    player_y: 1,
    win: "",
    step: 0,
    rng: @random.new(),
  }
}

处理玩家输入

  • 玩家输入可以有多种设备,如键盘,鼠标,游戏手柄等。这里着重介绍下游戏手柄GamePad
  • 手柄由4个方向键和2个动作键组成。

定义动作(玩家落子,玩家移动,AI落子策略)

  • 有了前面的状态定义,我们可以围绕这些状态参数定义一些动作。

举例1:玩家落子过程

  • 地图中地图使用0表示无棋子, 1代表玩家棋子,那么我们可以写一个函数描述落子过程。
rust 复制代码
pub fn put_point(self : GameStat) -> Unit {
   if self.gamemap[self.player_x][self.player_y] == '0' {
      self.gamemap[self.player_x][self.player_y] = '1'
    }
}

举例2:玩家移动过程

  • player_xplayer_y 代表玩家坐标。我们可以定义一个左移的过程。
rust 复制代码
pub fn moveLeft(self : GameStat) -> Unit {
  if self.player_x > 0 {
    self.player_x = self.player_x - 1
  }
}

举例3:AI落子策略

  • 为了降低游戏难度,并没有使用复杂的落子策略。而是利用随机数进行随机落子。
  • 需要注意,随机坐标可能会与玩家落子点坐标重合,避免坐标重合的方法由很多种,这里使用循环检测的方式生成唯一的坐标。首先随机生成两个坐标,如果坐标点位置不为空则再次调用生成坐标的函数,直到生成两个不同的坐标。
rust 复制代码
let mut flag = true
while flag {
  let limit = 3
  let x = self.rng.int(~limit)
  let y = self.rng.int(~limit)
  if self.gamemap[x][y] == '0' {
    self.gamemap[x][y] = '2'
    self.step = self.step + 1
    flag = false
  }
}

胜负判定算法

  • 毎落一子后,我们需要对当前棋局状态作出判定。因此我们用规则定义一个judge函数。
  • 规则如下:
    • 横向3个棋子连成一线,则胜利。
    • 纵向3个棋子连成一线,则胜利。
    • 斜向3个棋子连成一线,则胜利。
    • 棋盘上没有空位,则平局。
  • 如检查首行是否为同一玩家棋子可以对棋盘做等价性判断:
rust 复制代码
pub fn judge(self : GameStat) -> Bool {
  if (
      self.gamemap[0][0] == self.gamemap[1][0] &&
      self.gamemap[0][0] == self.gamemap[2][0] &&
      self.gamemap[0][0] == '1'
    )
}
  • 暴力判断的方法,直接遍历所有胜利条件,如果满足其中一个条件则胜利。虽然比较直观,但状态多的时候性能低下。想一想有什么方法可以优化? 欢迎在评论区留言。

图像绘制

  • 理清了游戏的整体逻辑,绘制游戏界面的部分更为简单。
  • 我们可以独立出一个界面绘制函数draw_game 此函数首先绘制制标,和记分板,棋盘,最后绘制玩家棋子。
  • 根据游戏状态,绘制对应的图形:
rust 复制代码
pub fn draw_game(self : GameStat) -> Unit {
  @wasm4.set_draw_colors(2)
  @wasm4.text("Tic-Tac-Toe", 10, 10)
  @wasm4.text("S:", 125, 90)
  @wasm4.text("T:", 125, 75)
  let win_times = self.score.to_string()
  let total_times = self.times.to_string()
  @wasm4.text(win_times, 140, 90)
  @wasm4.text(total_times, 140, 75)
  match self.game_state {
    GSInit => @wasm4.text("Press x to start", 10, 20)
    GSGameStart => @wasm4.text("Game Start", 10, 20)
    GSGameEnd => @wasm4.text("Game End", 10, 20)
  }
  if self.win == "Win" {
    @wasm4.set_draw_colors(3)
    @wasm4.text("You Win", 30, 30)
  } else if self.win == "Loss" {
    @wasm4.text("You Loss", 30, 30)
  } else if self.win == "Tie" {
    @wasm4.text("Tie", 30, 30)
  }
}

结语

该篇程序涉及没有复杂的算法,仅使用简单的顺序判断循环 语句和一就可实现稍稍复杂的小游戏。

正所谓:大道至简 重剑无锋 大巧不工

更多代码细节请见项目仓库源码。

作者正在备战第二款更具创造力的游戏,是与递归有关的游戏,敬请期待。


如果此篇文章对您有所帮助,欢迎👏Star 仓库,为我投票,您的支持是我持续更新文章的动力💪,感谢感谢 。仓库链接:github.com/spaceack/Mo...


竞赛地址:tianchi.aliyun.com/competition...

在线试玩链接:moonbitlang.github.io/MoonBit-Cod...

相关推荐
用户0595661192093 小时前
深入理解Spring Boot框架:从基础到实践
java·spring·编程语言
whysqwhw20 小时前
PlantUML 全面指南
编程语言
whysqwhw1 天前
ASCII图表及工具
编程语言
Mirageef1 天前
aardio 继承与多态
编程语言
大熊猫侯佩2 天前
Swift 中强大的 Key Paths(键路径)机制趣谈(下)
swift·编程语言·apple
Mirageef3 天前
aardio 类与对象基础
编程语言
Mirageef5 天前
aardio 自动识别验证码输入
编程语言
冒泡的肥皂6 天前
强大的ANTLR4语法解析器入门demo
后端·搜索引擎·编程语言
程序员岳焱6 天前
Java 程序员成长记(三):菜鸟入职之@Transactional「罢工」
java·后端·编程语言