本章内容:从我们教程的第一章开始,我们就接触到了游戏对象。游戏对象就像一个集合,包含了游戏的各种数据。而本章我们的道路有了分叉,我们将了解一些OOP(面向对象编程)和EOP(面向组件编程)之间的那点事。
[toc]
游戏对象
面向对象编程
关于OOP的资料,请自行百度。OOP的核心概念是“类”,可以说一切都是从类衍生出来的,比如各种库都是类,而你如果要使用它,就必须从类中实例化一个实例出来使用。类的核心就是继承。我们可以复用/重写从父类继承的各种方法。这就提升了代码效率。比如,车是一个基类,而赛车是其子类,赛车自然而然的继承了车的前进,后退,转向等方法,只是车的配置不同而已。
lua本身并不提供“类”的概念,也更不要求“一切皆类”(累)。但是lua的metatable的特性,使得做一个类变得十分容易。我们举一个简单的例子。
person = {
name = "unknown",
say = function(self)
print("my name is "..self.name)
end
}
这个是个简单的对象。它不具备任何拓展和继承能力。我们再看下面代码。
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加{},不过在游戏领域,面向组件式编程也比较流行。面向组件式编程的核心在于,任何游戏对象仅仅是数据,不含任何方法。而游戏组件是一系列方法,我们需要把游戏对象传入组件,才能实现其作用。一般,还有一个组件控制器,来控制游戏对象向相应组件的注册,删除以及遍历。
实际上,我们之前几章都是用的组件式的模式写的。我们再来举个例子:
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来讲,直接实例化就行了。这个是最容易的,因为类本身就是模板。而对于组件式的话,需要复制一个表。关于表的复制,这里不多说。有几个小技巧。
对于序列表
copy = {unpack(tab)}
对于一般的表
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中让所有注册的游戏对象都调用,于是有下面代码。
-- 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为例。
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 。由于我个人对组件式编程并不是很擅长。这里就不多介绍了,请自行看文档。
编程时间
我们这次继续第三章的案例,对就是那个坦克,感觉缺点什么? 是的,坦克要开炮的,我们来做子弹啦。
设计阶段
我们本次要完成两个内容,一个是制作一个子弹类,并且坦克可以按其炮塔角度发射子弹。另外一个内容是把子弹加入到游戏的对象系列中方便更新。
- 建立子弹类
- 初始化子弹属性,位置为发射的炮口位置。
- 子弹有一个translate方法,让子弹按其角度匀速前进。
- 子弹需要能够被绘制,这里用圆来代替。
- 在全局建立一个game沙盒,把tank和子弹分别放入沙盒中。
实施阶段。
子弹类:
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对象加一个开火函数。
function love.mousereleased(x,y,key)
table.insert(game.objects,Bullet(tank))
end
其他都没有什么复杂的东西。都是以前学过的知识。
作业
- 把坦克改写为类,其中玩家控制的,是从坦克基类中继承的,额外添加一些控制坦克的函数。
- 加入一些障碍物,也改写成类,命名为block
- 为坦克,子弹,障碍绑定碰撞盒。坦克无法穿越障碍,子弹和障碍碰撞后双方均销毁。
本章代码
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
网友评论