美文网首页
第5章 游戏对象

第5章 游戏对象

作者: 我和我的火柴 | 来源:发表于2021-04-10 12:10 被阅读0次

    本章内容:从我们教程的第一章开始,我们就接触到了游戏对象。游戏对象就像一个集合,包含了游戏的各种数据。而本章我们的道路有了分叉,我们将了解一些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 。由于我个人对组件式编程并不是很擅长。这里就不多介绍了,请自行看文档。

    编程时间

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

    设计阶段

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

    1. 建立子弹类
    2. 初始化子弹属性,位置为发射的炮口位置。
    3. 子弹有一个translate方法,让子弹按其角度匀速前进。
    4. 子弹需要能够被绘制,这里用圆来代替。
    5. 在全局建立一个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
    

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

    作业

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

    相关文章

      网友评论

          本文标题:第5章 游戏对象

          本文链接:https://www.haomeiwen.com/subject/autnkltx.html