第5章 游戏对象

本章内容:从我们教程的第一章开始,我们就接触到了游戏对象。游戏对象就像一个集合,包含了游戏的各种数据。而本章我们的道路有了分叉,我们将了解一些OOP(面向对象编程)和EOP(面向组件编程)之间的那点事。

游戏对象

面向对象编程

关于OOP的资料,请自行百度。OOP的核心概念是“类”,可以说一切都是从类衍生出来的,比如各种库都是类,而你如果要使用它,就必须从类中实例化一个实例出来使用。类的核心就是继承。我们可以复用/重写从父类继承的各种方法。这就提升了代码效率。比如,车是一个基类,而赛车是其子类,赛车自然而然的继承了车的前进,后退,转向等方法,只是车的配置不同而已。
lua本身并不提供“类”的概念,也更不要求“一切皆类”(累)。但是lua的metatable的特性,使得做一个类变得十分容易。我们举一个简单的例子。

1
2
3
4
5
6
person = {
name = "unknown",
say = function(self)
print("my name is "..self.name)
end
}

这个是个简单的对象。它不具备任何拓展和继承能力。我们再看下面代码。

1
2
3
4
5
6
7
8
9
function person.new(name)
local new = {}
setmetatable(new,person)
person.__index = person --这里一定要注意,index方法要用在metatable上,而非实例上。
return new
end
local Alexar = person.new("Alexar")
Alexar:say()

上面就是一个最简单的lua类编写过程,其中person是类,而Alexar是其一个实例。关于lua类的其他内容,请自行百度。

面向组件编程

面向对象编程可以说在程序领域,几乎是一手遮天的,每天敲的都是各种class加{},不过在游戏领域,面向组件式编程也比较流行。面向组件式编程的核心在于,任何游戏对象仅仅是数据,不含任何方法。而游戏组件是一系列方法,我们需要把游戏对象传入组件,才能实现其作用。一般,还有一个组件控制器,来控制游戏对象向相应组件的注册,删除以及遍历。
实际上,我们之前几章都是用的组件式的模式写的。我们再来举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function translate(object)
object.vx = object.vx + object.ax
object.vy = object.vy + object.ay
object.vrot = object.vrot + object.arot
object.x = object.x + object.vx
object.y = object.y + object.vy
object.rot = object.rot + object.vrot
end
local ball1 = {
... --不再详细写了
}
local ball2 = {
... --不再详细写了
}
translate(ball1)
translate(ball2)

上面translate是一个组件,实际上unity的组件也差不多是这么玩的。实际情形要再复杂写。

对象式和组件式的选择。

对象式的优点在于方便继承,逻辑条理比较清晰。缺点在于,数据和方法混搭,不容易存储;容易产生一些临时性的数据;不太适合编辑器;
组件式的优点在于复用方便,便于统一协调,除了游戏对象的数据,不会有额外的输出产生,比较安全可靠;十分方便的导出和导入数据,配合编辑器使用很合适;缺点在于,逻辑上稍微有些复杂,不符合人思维的习惯,需要单独配置组件控制系统。组件注册、删除比较麻烦;
当然还有一种是混合了两种方法的,游戏对象还是用类的形式,而对象的方法直接来自组件方法。然后就不需要单独的组件控制,仅仅使用类实例控制即可。不过这种方法,往往被两种编程模式爱好者所不齿,指责代码不规范。不过,关键在于好用就行。

单例模式

lua的oop中,并不需要所谓的单例模式,因为你在创建类的时候,只要不写new方法,仅仅使用一个表盛装这个单例就行了。因为我们并不是真正的oop编程。

游戏对象的复制

对于oop来讲,直接实例化就行了。这个是最容易的,因为类本身就是模板。而对于组件式的话,需要复制一个表。关于表的复制,这里不多说。有几个小技巧。
对于序列表

1
copy = {unpack(tab)}

对于一般的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function table.copy(source,copyto,ifcopyfunction)
copyto=copyto or {}
for k, v in pairs(source or {}) do
if type(v) == "table" then
copyto[k] = table.copy(v,copyto[k])
elseif type(v) == "function" then
if ifcopyfunction then
copyto[k] = v
end
else
copyto[k] = v
end
end
return copyto
end

注意,这里并没有进行循环检测,也就是如果你的表架构中有连接连回来,将是一个死循环哦。

游戏对象的循环

我们希望所有的游戏对象都是活的,因此,我们在每一帧都要给它一个更新的机会,就是游戏对象的update方法。而这个方法需要在love.update中让所有注册的游戏对象都调用,于是有下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- GO为一个游戏对象类
function love.load()
game = {}
game.objects = {}
for i = 1,100 do
table.insert(game.objects,GO())
end
end
function love.update()
for i = #game.objects,1 ,-1 do
local go = game.objects[i]
go:update()
if go.destroyed then table.remove(game.objects,i) end
end
end

这里有一些需要值得关注的。个人习惯把游戏的沙盒定义为一个全局的game表。我们每一个需要加入的实例都加入game.objects。在遍历的时候,需要注意的是,在遍历过程中删除正在遍历表(有序表)中的元素是十分危险的行为,因此采用逆序遍历的方式。具体原理请百度一下。

类库和组件库

类库的种类比较多,个人比较喜欢middleclass,因为它支持的功能较多,当然比较简单的有log30,hump.class等等。关于类的用法请自行参阅相关库的文本。这里以middleclass为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Class = require "middleclass"
Man = Class("man") --参数为变量名,可以任意指定
Man.test = "abc" --类属性
function Man:initialize(name) --笔者平时把这个方法替换成了init,为了方便。。。
self.name = name --初始化
self.test = self.class.test --Man.test也可以
end
function Man:say() --类方法
print("my name is "..self.name)
end
local man_a = Man("a") --类的实例化
man_a:say() --调用方法
Youth = Class("youth",Man) --继承
function Youth:say() --重写
self.super.say(self) -- 调用父类方法
print("I am young "..self.name)
end

love的组件库较少,比较全面的是tiny-ecs 。由于我个人对组件式编程并不是很擅长。这里就不多介绍了,请自行看文档。

编程时间

我们这次继续第三章的案例,对就是那个坦克,感觉缺点什么? 是的,坦克要开炮的,我们来做子弹啦。

设计阶段

我们本次要完成两个内容,一个是制作一个子弹类,并且坦克可以按其炮塔角度发射子弹。另外一个内容是把子弹加入到游戏的对象系列中方便更新。

  1. 建立子弹类
  2. 初始化子弹属性,位置为发射的炮口位置。
  3. 子弹有一个translate方法,让子弹按其角度匀速前进。
  4. 子弹需要能够被绘制,这里用圆来代替。
  5. 在全局建立一个game沙盒,把tank和子弹分别放入沙盒中。

实施阶段。

子弹类:

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
class = require "middleclass"
Bullet = class("bullet")
Bullet.fireCD = 1
Bullet.radius = 10
Bullet.speed = 5
function Bullet:init(parent)
self.parent = parent --实例化时,把坦克传进来,便于计算开炮位置。
self.rot = self.parent.cannon.rot + math.pi --这个要跟下面的旋转方法配合使用
self.x = self.parent.x + math.sin(self.rot)*self.parent.cannon.h --简单的数学方法
self.y = self.parent.x - math.cos(self.rot)*self.parent.cannon.h
self.vx = self.speed * math.sin(self.rot)
self.vy = -self.speed * math.cos(self.rot)
end
function Bullet:update(dt)
self.x = self.x + self.vx
self.y = self.y + self.vy
if self.x > 800 or self.x<0 or self.y<0 or self.y > 600 then --边界判断
self.destroyed = true
end
end
function Bullet:draw()
love.graphics.setColor(255,255,0,255)
love.graphics.circle("fill",self.x,self.y,self.radius)
end

游戏对象的加入方法这里不多说了,现在对tank对象加一个开火函数。

1
2
3
function love.mousereleased(x,y,key)
table.insert(game.objects,Bullet(tank))
end

其他都没有什么复杂的东西。都是以前学过的知识。

作业

  1. 把坦克改写为类,其中玩家控制的,是从坦克基类中继承的,额外添加一些控制坦克的函数。
  2. 加入一些障碍物,也改写成类,命名为block
  3. 为坦克,子弹,障碍绑定碰撞盒。坦克无法穿越障碍,子弹和障碍碰撞后双方均销毁。

本章代码

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class = require "assets/middleclass"
Bullet = class("bullet")
Bullet.fireCD = 0.2
Bullet.radius = 10
Bullet.speed = 5
function Bullet:init(parent)
self.parent = parent --实例化时,把坦克传进来,便于计算开炮位置。
self.rot = self.parent.cannon.rot + math.pi
self.x = self.parent.x + math.sin(self.rot)*self.parent.cannon.h/2 --简单的数学方法
self.y = self.parent.y - math.cos(self.rot)*self.parent.cannon.h/2
self.vx = self.speed * math.sin(self.rot)
self.vy = -self.speed * math.cos(self.rot)
end
function Bullet:update(dt)
self.x = self.x + self.vx
self.y = self.y + self.vy
if self.x > 800 or self.x<0 or self.y<0 or self.y > 600 then --边界判断
self.destroyed = true
end
end
function Bullet:draw()
love.graphics.setColor(255,255,0,255)
love.graphics.circle("fill",self.x,self.y,self.radius)
end
function initTank()
tank = {
x = 400, --放到屏幕中心
y = 300,
w = 60,
h = 100,
speed = 1,
rot = 0,
cannon = {
w = 10,
h = 50,
radius = 20
},
fireCD = Bullet.fireCD,
fireTimer = 0
}
target = {
x = 0,
y = 0
}
end
function keyControl()
local down = love.keyboard.isDown --方便书写,而且会加快一些速度
if down("a") then
tank.rot = tank.rot - 0.1
elseif down("d") then
tank.rot = tank.rot + 0.1
elseif down("w") then
tank.x = tank.x + tank.speed*math.sin(tank.rot) --速度直接叠加,就不加入vx变量了
tank.y = tank.y - tank.speed*math.cos(tank.rot)
elseif down("s") then
tank.x = tank.x - tank.speed*math.sin(tank.rot) --倒车
tank.y = tank.y + tank.speed*math.cos(tank.rot)
end
end
function getRot(x1,y1,x2,y2)
if x1==x2 and y1==y2 then return 0 end
local angle=math.atan((x2-x1)/(y2-y1))
if y1-y2<0 then angle=angle-math.pi end
if angle>0 then angle=angle-2*math.pi end
return -angle
end
function mouseControl(dt)
target.x,target.y = love.mouse.getPosition()
local rot = getRot(target.x,target.y,tank.x,tank.y)
tank.cannon.rot = rot --大炮的角度为坦克与鼠标连线的角度
tank.fireTimer = tank.fireTimer - dt --这里的开火计时器是十分常用的一种方法,需要学会
if love.mouse.isDown(1) and tank.fireTimer < 0 then
tank.fireTimer = tank.fireCD
table.insert(game.objects,Bullet(tank))
end
end
function updateBullets()
for i = #game.objects,1 ,-1 do
local go = game.objects[i]
go:update()
if go.destroyed then table.remove(game.objects,i) end
end
end
function drawTank()
--车身
love.graphics.push()
love.graphics.translate(tank.x,tank.y)
love.graphics.rotate(tank.rot)
love.graphics.setColor(128,128,128)
love.graphics.rectangle("fill",-tank.w/2,-tank.h/2,tank.w,tank.h) --以0,0为中心
love.graphics.pop()
--炮塔
love.graphics.push()
love.graphics.translate(tank.x,tank.y)
love.graphics.rotate(tank.cannon.rot)
love.graphics.setColor(0,255,0)
love.graphics.circle("fill",0,0,tank.cannon.radius)
love.graphics.setColor(0,255,255)
love.graphics.rectangle("fill",-tank.cannon.w/2,0,tank.cannon.w,tank.cannon.h)
love.graphics.pop()
--激光
love.graphics.setColor(255,0,0)
love.graphics.line(tank.x,tank.y,target.x,target.y)
end
function drawBullets()
for i,v in ipairs(game.objects) do
v:draw()
end
end
function love.load()
game = {}
game.objects = {}
initTank()
end
function love.update(dt)
keyControl()
mouseControl(dt)
updateBullets()
end
function love.draw()
drawTank()
drawBullets()
end