使用Go语言编写经典游戏:太空入侵者
回忆起我最早对街机电子游戏的热爱,还得回到那个与哥哥和表兄弟们在云顶高原疯狂玩游戏的日子。那时,父母在别的娱乐设施中畅游,而我们则被允许自由地沉浸在那些闪烁着灯光、背景音乐激昂、操作杆急促摇动、按键疯狂点击的游戏世界中——有《吃豆人》、《太空入侵者》、《银河战士》、《金刚》、《青蛙过河》、《蜈蚣》等等。
作为一个程序员,创造那种魔力一直是我的梦想,尽管过程中多次失败。这次,我又一次挑战自我,重温了上世纪的经典——《太空入侵者》,使用Go语言来实现。
太空入侵者
《太空入侵者》是1978年发布的一款极其成功的街机游戏,被誉为街机游戏黄金时代的开创者之一。即使经历了街机游戏的衰退,它依然跨越了媒介,成为了视频游戏机上的常客。
游戏规则简单明了。玩家控制一台激光炮,在屏幕底部左右移动,对抗一排排向你下降的外星人。外星人来回移动,他们会向下发射飞弹,而你则依赖几座固定的防御工事进行保护。一旦外星人到达激光炮或你的所有炮台被摧毁,游戏就结束了。
看似简单,实则不易。
无引擎游戏开发
很多游戏开发都会采用某种游戏引擎或图形引擎,但这个项目与众不同。我尝试构建游戏的方式是通过逐帧绘制并快速连续显示,利用这种类似群集模拟的技术。灵感来源于iTerm2的一个小技巧,可以在终端中显示图片,进而实现了动画效果。
这样看来,《太空入侵者》其实就是一个可由用户控制的动画。
让我们一起深入代码看看是如何实现的。
图像精灵
在计算机图形学中,“精灵”是指叠加到背景上独立的对象。这种技术最初用于街机游戏,并通常由硬件生成。我们这里使用了一种流行的方法,即从单一的精灵图集中提取各个部分作为单独的精灵。
上图是sprites.png
精灵图集的一部分放大展示。
// ...
var cannonSprite = image.Rect(20, 47, 38, 59)
var cannonExplode = image.Rect(0, 47, 16, 57)
var alien1Sprite = image.Rect(0, 0, 20, 14)
// ...
每个精灵的位置由image.Rectangle
表示,例如alien1Sprite
对应于sprites.png
中的(0,0)
至(20,14)
区域。
加载图像文件并获取image.Image
对象的函数如下:
func getImage(filePath string) image.Image {
imgFile, err := os.Open(filePath)
defer imgFile.Close()
if err != nil {
fmt.Println("无法读取文件:", err)
}
img, _, err := image.Decode(imgFile)
if err != nil {
fmt.Println("无法解码文件:", err)
}
return img
}
接下来定义精灵结构体:
type Sprite struct {
size image.Rectangle // 精灵尺寸
Filter *gift.GIFT // 正常滤镜,用于绘制精灵
FilterA *gift.GIFT // 另一个滤镜,用于绘制精灵的不同形式
FilterE *gift.GIFT // 爆炸滤镜,用于绘制爆炸效果
Position image.Point // 精灵的左上角位置
Status bool // 是否存活
Points int // 被摧毁后的得分(仅适用于外星人)
}
游戏中的各种精灵如以下所示:
- 精灵的大小
- 用于绘制精灵的三个图像滤镜
- 绘制在背景上的精灵位置
- 表示精灵状态的布尔值
- 被摧毁时的得分(仅限外星人)
使用的图像滤镜来自出色的Go Image Filtering Toolkit (GIFT),但我们仅用于将精灵画到背景上。每个精灵都有一个正常滤镜、一个用于不同形状的滤镜(仅在外星人中),以及一个爆炸滤镜(用于死亡效果)。
现在我们来看看游戏中不同类型的精灵如何创建。
游戏启动
这是一款从终端启动的动画游戏,整个游戏都在终端内运行。因此,对终端的控制至关重要。我们使用了流行的termbox-go库来实现这一目标。
游戏有两个并发循环:
- 用户通过键盘(左箭头和右箭头)控制激光炮。
- 其他则是游戏循环,无论用户如何操作,包括不断下降的外星人、投下的炸弹,还有向上飞驰的激光束,其任务是消灭这些外星侵略者。
这意味着有两个并行执行的线程,也就是goroutine。
// ...
err := termbox.Init()
if err != nil {
panic(err)
}
// ...
初始化termbox-go之后,我们将键盘事件处理放在另一个goroutine中,并放入一个缓冲通道。
大部分游戏都设有开场画面,提示用户输入或点击以开始游戏,我们的游戏也一样。
// ...
startScreen := getImage("imgs/start.png")
printImage(startScreen)
start:
for {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
if ev.Ch == 's' || ...
以上只是一个简单的概述,完整项目代码提供了更详细的实现细节。通过本项目,你可以学习到如何利用Go语言的基本原理来构建一个复古风格的游戏,理解图像处理和终端交互的核心技术。如果你对游戏编程感兴趣或者想在Go语言中实践低级图形处理,这个项目无疑是一个绝佳的起点。