弹幕shooter(I)

本文开始,Alexar带大家一起开发一款弹幕射击游戏,随着游戏的开发,对love2d中的一些实用技能和代码习惯进行讲解。

弹幕shooter(I)

前言

开始之前,我要说的是,案例教程,与之前的基础教程最大的差异就是,它是为了理清思路的教程,笔者不会在代码完成后,再去根据代码写教程,而是在每一篇代码开始之前,先把思路解释给各位,以便各位在自己写项目的时候,知道自己面对一个比较复杂的项目时从何入手,又如何把大问题分解为小问题,再通过解决方案来实现。因此,在这个系列的教程中,不会对各种算法过多的解释,因为基本的东西已经在基础教程说过了,而是告诉大家,面对一个问题,如何分析,为什么这么分析,解决的思路是什么。最终的代码往往十分简单,但更重要的是背后的东西, 希望大家形成自己的解决思路,好了,我们现在开始进入正题。

定题

我们这次已经确定了要做一个纵版弹幕的射击游戏,就不需要我们大费周章的去想游戏创意了。我们有很多现成的弹幕游戏的标杆,我们本次主要模仿一下雷电。雷电算是弹幕中系统最为简单的一个游戏了。

游戏元素

雷电的游戏元素很纯粹,没有商店、升级等概念,所以很多问题不必考虑。游戏外围的只有一个积分榜,用一个存档即可实现。剩下的就是游戏内容了,我们来梳理一下有哪些。
玩家控制飞机,用键盘或者鼠标控制,不能转向永远面向屏幕上方,移动有比较小的加速动作,通过升级来改变武器。
一般杂鱼敌机,移动方式各异,可以转向,并可以自动开火,行为方式比较有规律,有一些补给飞机爆炸后能够提供升级道具。
boss飞机,个头一般比较大,火力比较猛,有不止一套的行为方式,不过是随机抽取或按顺序进行。爆炸后提供额外奖励。
版面,用固定版面,背景画面移动即可,不必设置镜头,
碰撞,因为物体移动速度都比较高,也不需要物理模拟,用aabb就行了。
绘图,简单的像素风格绘图,和帧动画即可。
界面,用户界面很简单,显示用户名,血条,大杀器(bomb),升级,分数,credit数就差不多了。
故事,可以仿造雷电弄点简单的故事情节和对话,头像+对话框的形式。不过这个是后话。

用到的库

从游戏元素上看,我们可能会用到下面的库,不过也不一定,看实际需要:
util 个人代码片段 我习惯每个项目都加上
gooi ui库,当然用suit也不错,暂时先用这个。
bump aabb碰撞库最舒服的一个了
animation 自己写的帧动画库,用anim8也行
autobatch batch库,可以提高帧率
delay 自己写的延迟库,因为飞机的刷新有可能使用时间。
tween 缓动库,一些飞机的移动用缓动比较方便。
trail 尾迹库,暂时放在这里,有可能对一些特效有用。
sound 声音库,自己封装的一个库,主要是为了解决重复音效单独建立声音对象的问题。

游戏对象

暂时想到的游戏对象类和继承关系。
base 在游戏沙盒中建立objects表,base主要提供对象的创建(加入表)和销毁,静态的位置,以及图片和动画的更新绘制方法。基本的移动方法。
collidable 继承base,使其具备碰撞功能。
bullet 继承collidable,具有子弹相关的属性和方法。
plane 继承collidable,具有飞机相关属性和方法。
各种npc,继承plane,提供特殊的飞行方式,火控方式和子弹,掉落,破坏动画等。
player,继承palne, 提供飞机的控制,火控,与其他的碰撞行为等。
obstacle,继承collidable。也可以与bullet合并。
item,继承自collidable,与bullet类似,比如武器升级或炸弹等。
effect, 继承base,比如爆炸动画等。

游戏场景

模仿街机的模式,有下面几种场景
menu场景 开始游戏、继续游戏、生存模式、高分榜、帮助几个选项
game场景 游戏主场景,每个关卡一个场景。
高分榜 在gameover后可以输入名字,并在加入高分榜。
帮助 基本操作,道具图例等。

本章目标

建立项目目录,复制有用的库,配置基本的main和conf文件。建立基本循环和base对象。

核心代码

以后在这个阶段仅列出笔者认为比较重要的核心代码。
conf文件

1
2
3
4
5
6
7
8
9
function love.conf(t)
io.stdout:setvbuf("no") --之前说过,可以让一些ide流式的把lua输出放到console里。
t.identity = "raiden" --指定存档文件夹
-- t.version = "0.10.2" --指定版本,如果不符会弹出警告框,然并卵
t.console = false --如果为真会阻止某些ide输出
t.window.title = "raiden" --标题
t.window.width = 500 --纵版射击嘛所以搞个纵版的窗体,不过不是标准窗体
t.window.height = 800
end

conf文件没有太多要讲的,对于窗体及分辨率的问题,我们后面再说。

main文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require "lib/autobatch" --自动精灵批处理库
require "lib/util" --个人代码片都
bump=require "lib/bump" --2d碰撞库
class=require "lib/middleclass" --类库
gamestate= require "lib/hump/gamestate" --游戏场景库
Tween= require "lib/tween" --缓动库
delay= require "lib/delay" --延迟库
Animation = require "lib/animation" --帧动画库
playSound = require "lib/sound" --声音库
res = require "scr.resloader" --资源导入脚本
require "lib/gooi" --UI库
--DEBUG = true --debug标签,查看debug信息用。
function love.load()
love.graphics.setFont(res.font)
gameState={}
for _,name in ipairs(love.filesystem.getDirectoryItems("scene")) do
gameState[name:sub(1,-5)]=require("scene."..name:sub(1,-5))
end
gamestate.registerEvents()
gamestate.switch(gameState.test)
end
function love.update(dt) delay:update(dt) end
function love.mousereleased(x, y, button) gooi.released() end
function love.mousepressed(x, y, button) gooi.pressed() end
function love.textinput(text) gooi.textinput(text) end
function love.keypressed(key) gooi.keypressed(key) end

对于main文件来讲,我们要说的是,一般而言,个人喜好所有的全局变量都放在这里。当然,你要知道即时是全局变量,实际上也只是__G下的变量。
引入库的规范,一般而言,单例模式(也就是返回一个表或者函数的),我们使用小写首字母。类模式(返回类对象的),以首字母大写作为承载的变量。一些库会自动注册些全局变量,这个我们就没法控制了。
然后,我们在load回调里放入了一个标准的游戏场景导入,然后把入口给了test场景。
剩下的就是一些ui的绑定了,没有什么太多的内容。

test场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local scene = gamestate.new()
game = require "scr/game"
editor = require "scr/editor"
function scene:enter()
game:init()
editor:init()
end
function scene:leave()
game = nil
end
function scene:update(dt)
game:update(dt)
end
function scene:draw()
game:draw()
end
return scene

一个很标准的场景文件。一般我不太喜欢把游戏逻辑也写到场景文件里。而是单独建立一个沙盒game。game是个全局变量,只是因为这样做比较方便,当然你可以用局域变量,在文件头部去require一下,也是一样的效果。editor是编辑器,暂时我们留着以后再说。
当场景离开的时候,释放game,也就是释放了game下的所有资源。

本章的代码就先到这里,下一章,我们来构思一下游戏对象的架构,以及如何来控制他们。