前言
-
本篇文章主要分享 “2024年全球 MoonBit 编程创新赛 游戏赛道”参赛过程中九宫棋游戏的开发技巧和心得。以此抛砖引玉。首先介绍下 MoonBit。
-
月兔语言 MoonBit 是一个用于云计算和边缘计算的 WebAssembly 端到端的编程语言工具链。 您可以访问 https://try.moonbitlang.cn 获得 IDE 环境,无需安装任何软件,也不依赖任何服务器。生成比现有解决方案明显体积更小的 WASM 文件。并有着更高的运行时性能和先进的编译时性能。
-
比赛会用到 WASM-4 引擎,此引擎是一个框架,可以开发有趣的复古游戏,最终的编译文件可在浏览器或单片机设备(如 ESP32)上直接运行。
话不多说,直入正题
开发环境准备
- 拢共分为5步, 我们目标是以最快速度进入到游戏开发本身。因此略过目录,配置等说明,可通过官网了解更多细节。
1. 部署月兔环境
curl -fsSL https://cli.moonbitlang.cn/install/unix.sh | bash
2. 创建项目工作区
mkdir moonbit_wsam4_game
cd moonbit_wsam4_game
moon new --lib --path . --user spaceack --name mygame
3. 安装wasm4依赖
npm install -D wasm4
4. 将 wasm4绑定增加到项目中
moon update && moon add moonbitlang/wasm4
此时的代码目录如下:
5. 更新项目配置
- 使用以下代码覆盖
moon.pkg.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}}
}
- 使用以下代码覆盖
top.mbt
文件
pub fn update() -> Unit {}
pub fn start() -> Unit {}
编译及运行
- 此时一个
空
游戏代码已经写好了,我们编译并运行看看效果
# 编译
moon build --target wasm
# 运行npx wasm4 run target/wasm/release/build/mygame.wasm
- 访问 http://localhost:4444 即可查看执行效果。可见绿油油的一片。 这是WASM-4 引擎的默认配色。
游戏开发
小试牛刀
-
top.mbt
文件是游戏的主程序的入口, 简单逻辑的游戏,所有逻辑仅在此文件内编写即可。 -
依据惯例,首先我们尝试在终端打印“Hello, World”。在
start()
函数中调用wasm的trace
方法即可。它可是我们调试程序的好帮手。😊
pub fn update() -> Unit {}
pub fn start() -> Unit {
@wasm4.trace("Hello world!");
}
- 太好啦,经过再次编译运行,我们的代码起作用啦,在终端成功打印了Hello World.
设置配色
- wasm 单帧最多支持4种颜色,可以对调色板的4个颜色索引设置配色。这里我们要用到
set_palette
方法。 默认索引1为背景色。
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支持绘制矩形,椭圆, 线等基础图形。复杂的图形由基础图形构成,
可练习实现如下效果图:
代码如下:
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 简单且实用数据导向语言设计的魅力。
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 : Intmut win: String mut step: Intrng : @random.Rand
}
-
三子棋逻辑并不复杂,至多9个变量就可以描述整个状态!
-
然后我们使用 let 声明一个名为 gs 的变量, 并将其初始化为 GameStat 类型的一个新实例。
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(),}
}
定义动作(玩家落子,玩家移动,AI落子策略)
- 有了前面的状态定义,我们可以围绕这些状态参数定义一些动作。
举例1:玩家落子过程
- 地图中地图使用0表示无棋子, 1代表玩家棋子,那么我们可以写一个函数描述落子过程。
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_x
,player_y
代表玩家坐标。我们可以定义一个左移的过程。
pub fn moveLeft(self : GameStat) -> Unit {if self.player_x > 0 {self.player_x = self.player_x - 1}
}
举例3:AI落子策略
- 为了降低游戏难度,并没有使用复杂的落子策略。而是利用随机数进行随机落子。
- 需要注意,随机坐标可能会与玩家落子点坐标重合,避免坐标重合的方法由很多种,这里使用循环检测的方式生成唯一的坐标。首先随机生成两个坐标,如果坐标点位置不为空则再次调用生成坐标的函数,直到生成两个不同的坐标。
let mut flag = true
while flag {let limit = 3let 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 + 1flag = false}
}
该篇程序涉及没有复杂的算法,仅使用简单的顺序
,判断
,循环
语句和一就可实现稍稍复杂的小游戏。
正所谓:大道至简 重剑无锋 大巧不工
如果此篇文章对您有所帮助,欢迎👏Star 仓库,为我投票,您的支持是我持续更新文章的动力💪,感谢感谢 。仓库链接:https://github.com/spaceack/MoonBit-Code-JAM-2024
竞赛地址:https://tianchi.aliyun.com/competition/entrance/532262
在线试玩链接:https://moonbitlang.github.io/MoonBit-Code-JAM-2024/CS%E8%B5%8F%E9%87%91%E7%8C%8E%E6%89%8B/index.html