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...

相关推荐
Moonbit1 天前
MoonBit 双周报 Vol.59:新增编译器常量支持,改进未使用警告,支持跨包函数导入...多个关键技术持续优化中!
编程语言
神经星星1 天前
【TVM 教程】外部张量函数
人工智能·开源·编程语言
邓校长的编程课堂7 天前
少儿编程进入义务教育课程:培养信息科技素养的新政策解读
科技·编程语言·少儿编程·信息学竞赛·科技特长生·义务教育
Moonbit15 天前
MoonBit 双周报 Vol.58:原生后端支持、多行字符串插值、json.inspect 功能等多项关键特性取得显著进展!
编程语言
小尤笔记1 个月前
Python操作系统的6个自动化脚本
数据库·python·自动化·php·编程语言·python入门
神经星星1 个月前
【TVM 教程】使用 Relay Visualizer 可视化 Relay
人工智能·机器学习·编程语言
GoppViper2 个月前
golang学习笔记17——golang使用go-kit框架搭建微服务详解
笔记·后端·学习·微服务·golang·编程语言·go-kit
GoppViper2 个月前
golang学习笔记12——Go 语言内存管理详解
笔记·后端·学习·golang·编程语言·内存优化·golang内存管理
GoppViper2 个月前
golang学习笔记11——Go 语言的并发与同步实现详解
笔记·后端·学习·golang·编程语言·goroutine·golang并发