WASM-4 是一款使用 WebAssembly 实现的复古风格游戏机。凹语言作为国内首个面向 WebAssembly 设计的通用编程语言在 syscall/wasm4
内置标准库对 WASM4 平台提供了支持,从而为使用凹语言开发小游戏的用户提供最佳体验。
我们以一个简单的乒乓球游戏作为例子,看看如何开发WASM4游戏。
配置环境
安装凹语言 v0.15 以上的版本,或者通过以下Go命令安装最新的wa命令行:
$ go install wa-lang.org/wa[@master](https://my.oschina.net/u/48054)
然后通过以下命令创建一个hello新目录工程:
$ wa init -wasm4
$ tree hello/
hello/
├── README.md
├── src
│ └── main.wa
└── wa.mod
2 directories, 3 files
命令行环境进入hello目录后,输入wa run
可以在浏览器打开查看效果。
程序整体骨架
直接修改src/main.wa
文件:
import (
"math/rand"
"strconv"
"syscall/wasm4"
)
const (
width = 5
height = 15
ballSize = 5
screenSize = int(wasm4.SCREEN_SIZE)
)
// 玩家1(右边): 上下方向键
// 玩家2(左边): ED键对应上下键, 左右方向键盘控制
global game = NewPongGame(true) // 双人游戏
#wa:export update
func Update {
game.Input()
game.Update()
game.Draw()
}
Update函数会以每秒60帧的频率被调用,其中分布出来游戏的输入、更新游戏状态并显示。
定义游戏对象
在对象中保存的游戏状态:
// 游戏的状态
type PongGame :struct {
isMultiplayer: bool // 多人游戏
ballX: int // 球的水平位置
ballY: int // 球的竖直位置
dirX: int // 球的方向
dirY: int // 球的方向
y1: int // 左边挡板位置
y2: int // 右边挡板位置
score1: int // 玩家分数
score2: int // 玩家分数
}
// 构建一个新游戏对象
func NewPongGame(enableMultiplayer: bool) => *PongGame {
return &PongGame{
isMultiplayer: enableMultiplayer,
ballX: screenSize / 2,
ballY: screenSize / 2,
dirX: 1,
dirY: 1,
y1: screenSize / 2,
y2: screenSize / 2,
score1: 0,
score2: 0,
}
}
主要是乒乓球、挡板等位置和方向信息。
处理输入键
通过不同方向键盘分别控制2个挡板的移动。
func PongGame.Input {
// 第1个玩家
if pad := wasm4.GetGamePad1(); pad&wasm4.BUTTON_UP != 0 && this.y1 > 0 {
this.y1 -= 2
} else if pad&wasm4.BUTTON_DOWN != 0 && this.y1+height < screenSize {
this.y1 += 2
}
// 第2个玩家或机器人
if this.isMultiplayer {
// 左右方向键盘控制
if pad := wasm4.GetGamePad1(); pad&wasm4.BUTTON_LEFT != 0 && this.y2 > 0 {
this.y2 -= 2
} else if pad&wasm4.BUTTON_RIGHT != 0 && this.y2+height < screenSize {
this.y2 += 2
}
if pad := wasm4.GetGamePad2(); pad&wasm4.BUTTON_UP != 0 && this.y2 > 0 {
this.y2 -= 2
} else if pad&wasm4.BUTTON_DOWN != 0 && this.y2+height < screenSize {
this.y2 += 2
}
} else {
this.y2 = this.ballY // 自动对齐到接球位置(TODO: 失误机制)
}
}
根据键盘更新挡板的位置信息。
更新游戏的状态
每秒钟60帧的速度更新状态:
func PongGame.Update {
// 更新球的方向
if dirNow := this.paddleCollision(); dirNow != 0 {
wasm4.Tone(2000, 5, 100, wasm4.TONE_PULSE2|wasm4.TONE_MODE2)
if rand.Int()%2 != 0 {
this.dirX = dirNow
this.dirY = -1
} else {
this.dirX = dirNow
this.dirY = 1
}
}
// 更新球的位置
this.ballX += this.dirX
this.ballY += this.dirY
// 检查球是否反弹
if this.ballY > screenSize || this.ballY < 0 {
wasm4.Tone(2000, 5, 100, wasm4.TONE_PULSE2|wasm4.TONE_MODE2)
this.dirY = -this.dirY
}
// 判断得分
if this.ballX <= 0 || this.ballX > screenSize {
wasm4.Tone(1000, 5, 100, wasm4.TONE_PULSE2|wasm4.TONE_MODE2)
if this.ballX <= 0 { // 左边玩家失球
this.score2 += 1
} else if this.ballX > screenSize {
this.score1 += 1 // 右边玩家失球
}
// 重置球位置
this.ballX = screenSize / 2
this.ballY = screenSize / 2
this.dirX = -this.dirX
}
}
同时判断失球和得分情况。以下是碰撞判断:
func PongGame.paddleCollision => int {
if this.ballX < width &&
this.ballY < this.y2+height &&
this.ballY+ballSize > this.y2 {
return 1
}
if this.ballX+ballSize > screenSize-width &&
this.ballY < this.y1+height &&
this.ballY+ballSize > this.y1 {
return -1
}
return 0
}
球碰到和超出边界表示失球得分。
如何画乒乓球和挡板
WASM4 的调色板寄存器一次只能存储 4 种颜色,可以通过更改这一寄存器来引入新的颜色。以下是WASM4默认的配色表:
WASM4内置的绘图函数不直接访问这个颜色表寄存器,而是访问同样能够存储4个颜色的 DRAW_COLORS寄存器来指定对应的颜色表索引。可以通过wasm4.SetDrawColors
函数完成。
绘制场景的代码:
func PongGame.Draw {
wasm4.SetDrawColors(0, 4)
wasm4.SetDrawColors(1, 0)
wasm4.Text(strconv.Itoa(this.score1), 85, 0)
wasm4.Text(strconv.Itoa(this.score2), 70, 0)
wasm4.Rect(screenSize/2, 0, 2, screenSize)
wasm4.SetDrawColors(0, 2)
wasm4.SetDrawColors(1, 3)
wasm4.Oval(this.ballX, this.ballY, ballSize, ballSize)
wasm4.Rect(0, this.y2, width, height)
wasm4.Rect(screenSize-width, this.y1, width, height)
}
有此乒乓球游戏就完成了。
完整代码
完整代码大约150行:https://gitee.com/wa-lang/wa/tree/master/waroot/examples/w4-pong
如果你也是游戏爱好者,也可以试试用凹语言开发自己的游戏了。