从零开始写一个武侠冒险游戏-3-地图生成

从零开始写一个武侠冒险游戏-3-地图生成

概述

前面两章咱们设计了角色的状态, 绘制出了角色, 而且赋予角色动做, 如今是时候为咱们的角色创造一个舞台了, 那就是游戏地图(咱们目前作的是一个2D 游戏, 所以叫地图, 若是是 3D, 则叫地形).数据库

地图生成也是游戏开发的一项基本技术, 涉及到方方面面的技能, 并且地图的数据结构要考虑到游戏里的其余景物跟角色的显示和交互, 对于整个游戏程序的效率起着决定性的影响, 不过咱们这里先解决有没有的问题, 目标不过高, 能流畅运行就能够了.数据结构

最简原型

跟咱们一贯提倡的大思路一致, 一切从简出发, 先弄个原型跑起来再说.框架

经验之谈: 不少开发过程当中的难题都是由于咱们一开始就引入了过于复杂的问题, 制定了太大的目标, 试图一开始就把方方面面都考虑到, 结果无形中就增长了难度, 不得不认可, 这种顶层设计的思路是不太符合事物发展的规律的, 也不符合生物的进化规律, 因此实现起来就比较困难, 若是咱们遵循从简单到复杂, 从原型到成品的开发思路, 就会发现开发过程变得顺利不少.dom

游戏地图原理

简单说来, 游戏地图有两个层面, 一个是显示到屏幕上的图形图像, 一个是隐藏在图像后面的数据结构, 前者是游戏跟玩家交互的界面, 后者是游戏中绘制出来的各类对象跟程序交互的接口.函数

好比玩家操纵一个游戏角色从左边一个位置走到右边一个位置, 玩家看到的是屏幕上角色的移动过程, 而程序在后面要记录玩家每时每刻的坐标, 以及该坐标在地图上对应的位置.性能

若是玩家看到地图上某个位置有一个能够操做的物体, 好比一个箱子, 玩家的角色想要靠近这个箱子而后打开它, 那么后台的地图数据库里首先要在地图的某个位置上有一个箱子, 而后再判断角色距离箱子的距离, 若是小于某个值, 那么就说明容许操做, 玩家开过箱子后, 还要把箱子的当前状态(已开启)再写回到数据库里, 等等诸如此类.动画

最简单的地图

最简单的地图就是一张事先画好的图, 角色在这张图上移来移去, 这个功能咱们在第2章就已经实现了, 可是按照这种方法实现的地图角色很难跟地图上的物体进行交互, 并且使用事先画好的图作地图还有一个问题就是若是整个游戏场景比较大的话就须要不少画预先存储到游戏中, 这样会致使较大的体积..net

因此, 咱们采起另外一种作法, 由于游戏场景中不少物体对象都是能够重复使用的, 好比树木, 岩石等等, 因此咱们能够把这些基本对象提取出来事先绘制好, 或者使用预先作好的素材, 这样咱们须要事先存储的内容就大大减小了, 而后再根据实际须要动态绘制上去, 这就是随机生成场景地图的作法.设计

刚好我以前写过一个简单的随机地图生成器, 虽然比较简陋, 不过为了减小工做量, 仍是能够拿来用用的, 固然, 直接用是不行的, 主要是以它作一个基础来进行改写.code

原型目标

首先明确一下咱们这个地图原型的基本需求点:

  • 能够灵活调整地图大小
  • 能够随机插入树木/矿物/建筑等固定物体
  • 角色能够跟地图上的这些物体交互

这是三个最基本的需求, 咱们一步一步来实现这三个需求.

格子地面地图

综合性能和实现难度方面的考虑, 咱们的地图以网格的形式进行绘制和保存, 也就是以咱们以前写好的那个随机地图生成器为基本原型, 这样一方面能够灵活控制数据表的大小, 数据表中存储的最小单位就是一个预先设定好大小的格子, 另外一方面写起来也比较简单, 还有不错的效率表现.

首先肯定咱们的初始化参数和数据结构, 用这个函数来实现:

function initParams()
    print("Simple Map Sample!!")
    textMode(CORNER)
    spriteMode(CORNER)

    --[[
    gridCount:网格数目,范围:1~100,例如,设为3则生成3*3的地图,设为100,则生成100*100的地图。
    scaleX:单位网格大小比例,范围:1~100,该值越小,则单位网格越小;该值越大,则单位网格越大。
    scaleY:同上,若与scaleX相同则单位网格是正方形格子。
    plantSeed:植物生成概率,范围:大于4的数,该值越小,生成的植物越多;该值越大,生成的植物越少。
    minerialSeed:矿物生成概率,范围:大于3的数,该值越小,生成的矿物越多;该值越大,生成的矿物越少。
    --]]
    gridCount = 50
    scaleX = 50
    scaleY = 50
    plantSeed = 20.0
    minerialSeed = 50.0

    -- 根据地图大小申请图像
    local w,h = (gridCount+1)*scaleX, (gridCount+1)*scaleY
    imgMap = image(w,h)

    -- 整个地图使用的全局数据表
    mapTable = {}

    -- 设置物体名称
    tree1,tree2,tree3 = "松树", "杨树", "小草"    
    mine1,mine2 = "铁矿", "铜矿"

    -- 设置物体图像
    imgTree1 = readImage("Planet Cute:Tree Short")
    imgTree2 = readImage("Planet Cute:Tree Tall")
    imgTree3 = readImage("Platformer Art:Grass")
    imgMine1 = readImage("Platformer Art:Mushroom")
    imgMine2 = readImage("Small World:Treasure")

    -- 存放物体: 名称,图像
    itemTable = {[tree1]=imgTree1,[tree2]=imgTree2,[tree3]=imgTree3,[mine1]=imgMine1,[mine2]=imgMine2}

    -- 3*3 
    mapTable = {{pos=vec2(1,1),plant=nil,mineral=mine1},{pos=vec2(1,2),plant=nil,mineral=nil},
                {pos=vec2(1,3),plant=tree3,mineral=nil},{pos=vec2(2,1),plant=tree1,mineral=nil},
                {pos=vec2(2,2),plant=tree2,mineral=mine2},{pos=vec2(2,3),plant=nil,mineral=nil},
                {pos=vec2(3,1),plant=nil,mineral=nil},{pos=vec2(3,2),plant=nil,mineral=mine2},
                {pos=vec2(3,3),plant=tree3,mineral=nil}}

end

接下来是绘制地面单位格子的函数, 如今是在每一个格子上绘制一个矩形, 参数 position 是一个二维向量, 形如 vec(1,2) 则表示该格子位于第1行, 第2列, 代码以下:

-- 绘制单位格子地面
function drawUnitGround(position)
    local x,y = scaleX * position.x, scaleY * position.y
    pushMatrix()
    stroke(99, 94, 94, 255)
    -- 网格线宽度
    strokeWidth(1)
    -- 地面颜色
    fill(5,155,40,255)
    -- fill(5,155,240,255)
    rect(x,y,scaleX,scaleY)
    popMatrix()
end

用这两个函数来调用它:

-- 新建地图数据表, 插入地图上每一个格子里的物体数据
function createMapTable()
    for i=1,gridCount,1 do
        for j=1,gridCount,1 do
            mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(mapTable, mapItem)
        end
    end
    updateMap()
end

-- 更新地图
function updateMap()
    setContext(imgMap)   
    for i = 1,gridCount*gridCount,1 do
        local pos = mapTable[i].pos
        -- 绘制地面
        drawUnitGround(pos)
    end
    setContext()
end

-- 绘制地图
function drawMap() 
    -- 绘制地图
    sprite(imgMap,-scaleX,-scaleY)
end

最基本原型的完整代码

下面咱们把实现这个最基本原型的完整代码列出来:

-- MapSample

-- 初始化地图参数
function initParams()
    print("地图初始化开始...")
    textMode(CORNER)
    spriteMode(CORNER)

    --[[ 参数说明:
    gridCount:网格数目,范围:1~100,例如,设为3则生成3*3的地图,设为100,则生成100*100的地图。
    scaleX:单位网格大小比例,范围:1~100,该值越小,则单位网格越小;该值越大,则单位网格越大。
    scaleY:同上,若与scaleX相同则单位网格是正方形格子。
    plantSeed:植物生成概率,范围:大于4的数,该值越小,生成的植物越多;该值越大,生成的植物越少。
    minerialSeed:矿物生成概率,范围:大于3的数,该值越小,生成的矿物越多;该值越大,生成的矿物越少。
    --]]
    gridCount = 50
    scaleX = 50
    scaleY = 50
    plantSeed = 20.0
    minerialSeed = 50.0

    -- 根据地图大小申请图像
    local w,h = (gridCount+1)*scaleX, (gridCount+1)*scaleY
    imgMap = image(w,h)

    -- 整个地图使用的全局数据表
    mapTable = {}

    -- 设置物体名称
    tree1,tree2,tree3 = "松树", "杨树", "小草"    
    mine1,mine2 = "铁矿", "铜矿"

    -- 设置物体图像
    imgTree1 = readImage("Planet Cute:Tree Short")
    imgTree2 = readImage("Planet Cute:Tree Tall")
    imgTree3 = readImage("Platformer Art:Grass")
    imgMine1 = readImage("Platformer Art:Mushroom")
    imgMine2 = readImage("Small World:Treasure")

    -- 存放物体: 名称,图像
    itemTable = {[tree1]=imgTree1,[tree2]=imgTree2,[tree3]=imgTree3,[mine1]=imgMine1,[mine2]=imgMine2}

    -- 3*3 
    mapTable = {{pos=vec2(1,1),plant=nil,mineral=mine1},{pos=vec2(1,2),plant=nil,mineral=nil},
                {pos=vec2(1,3),plant=tree3,mineral=nil},{pos=vec2(2,1),plant=tree1,mineral=nil},
                {pos=vec2(2,2),plant=tree2,mineral=mine2},{pos=vec2(2,3),plant=nil,mineral=nil},
                {pos=vec2(3,1),plant=nil,mineral=nil},{pos=vec2(3,2),plant=nil,mineral=mine2},
                {pos=vec2(3,3),plant=tree3,mineral=nil}}

end

-- 新建地图数据表, 插入地图上每一个格子里的物体数据
function createMapTable()
    for i=1,gridCount,1 do
        for j=1,gridCount,1 do
            mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(mapTable, mapItem)
        end
    end
    updateMap()
end

-- 跟据地图数据表, 刷新地图
function updateMap()
    setContext(imgMap)   
    for i = 1,gridCount*gridCount,1 do
        local pos = mapTable[i].pos
        -- 绘制地面
        drawUnitGround(pos)
    end
    setContext()
end

-- 绘制单位格子地面
function drawUnitGround(position)
    local x,y = scaleX * position.x, scaleY * position.y
    pushMatrix()
    stroke(99, 94, 94, 255)
    -- 网格线宽度
    strokeWidth(1)
    -- 地面颜色
    fill(5,155,40,255)
    -- fill(5,155,240,255)
    rect(x,y,scaleX,scaleY)
    popMatrix()
end

-- 游戏主程序框架
function setup()
    displayMode(OVERLAY)

    initParams()
end

function draw()
    background(40, 40, 50)    

    -- 绘制地图
    drawMap()
end

看看截图:

只有地面的地图原型

很好, 基本的格子地图写好了, 接着咱们来解决在格子地图上随机插入树木/矿物/建筑等固定物体的功能.

插入物体

由于咱们已经在设计数据表时就考虑到了要插入固定物体, 因此如今须要作的就是写几个相关的函数, 首先是两个随机选取物体名字的函数:

-- 随机生成植物
function randomPlant()
    local seed = math.random(1.0, plantSeed)
    local result = nil

    if seed >= 1 and seed < 2 then result = tree1
    elseif seed >= 2 and seed < 3 then result = tree2
    elseif seed >= 3 and seed < 4 then result = tree3
    elseif seed >= 4 and seed <= plantSeed then result = nil end

    -- 返回随机选取的物体名字
    return result
end

-- 随机生成矿物
function randomMinerial()
    local seed = math.random(1.0, minerialSeed)
    local result = nil

    if seed >= 1 and seed < 2 then result = mine1
    elseif seed >= 2 and seed < 3 then result = mine2
    elseif seed >= 3 and seed <= minerialSeed then result = nil end

    -- 返回随机选取的物体名字
    return result
end

而后增长两个绘制函数, 来绘制出物体的图像:

-- 绘制单位格子内的植物
function drawUnitTree(position,plant)
    local x,y = scaleX * position.x, scaleY * position.y
    pushMatrix()
    -- 绘制植物图像
    sprite(itemTable[plant], x, y, scaleX*6/10,scaleY)

    --fill(100,100,200,255)
    --text(plant,x,y)
    popMatrix()
end

-- 绘制单位格子内的矿物
function drawUnitMineral(position,mineral)
    local x,y = scaleX * position.x, scaleY * position.y
    pushMatrix()
    -- 绘制矿物图像
    sprite(itemTable[mineral], x+scaleX/2, y, scaleX/2, scaleX/2)

    --fill(100,100,200,255)
    --text(mineral,x+scaleX/2,y)
    popMatrix()
end

最后须要修改函数 createMapTable()updateMap(), 在其中增长对 plantmineral 的处理, 修改后的代码以下:

-- 新建地图数据表, 插入地图上每一个格子里的物体数据, 目前为 plant  和 mineral 为空
function createMapTable()
    --local mapTable = {}
    for i=1,gridCount,1 do
        for j=1,gridCount,1 do
            mapItem = {pos=vec2(i,j), plant=randomPlant(), mineral=randomMinerial()}
            --mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(mapTable, mapItem)
        end
    end
    updateMap()
end

-- 跟据地图数据表, 刷新地图
function updateMap()
    setContext(imgMap)   
    for i = 1,gridCount*gridCount,1 do
        local pos = mapTable[i].pos
        local plant = mapTable[i].plant
        local mineral = mapTable[i].mineral
        -- 绘制地面
        drawUnitGround(pos)
        -- 绘制植物和矿物
        if plant ~= nil then drawUnitTree(pos, plant) end
        if mineral ~= nil then drawUnitMineral(pos, mineral) end
    end
    setContext()
end

很是好, 第二个基本目标也完成了, 截个图:

插入植物矿物的完整地图

看看如今的截图效果, 是否是感受咱们的原型正在一步步走向完善? 紧接着就要想办法实现角色跟地图上物体的交互了, 想作到这一点, 首先须要创建角色跟地图在地图数据表中的数据关联.

创建角色跟地图的关联

如今地图绘制好了, 角色也能够自由地在地图上活动了, 不过这只是咱们看到的表面现象, 实际在隐藏于屏幕后面的程序代码中, 角色的位置跟地图的坐标(方格)并无创建任何关联.

例如, 角色在地图上看到一棵树, 他想要对这棵树作一些动做(观察/浇水/砍伐 等)进行交互, 若是角色选择了砍伐树, 那么最终树被砍倒以后咱们还须要更新地图数据表, 把对应位置的树的图片更换成树根, 而实现角色跟树的交互, 就须要根据角色位置坐标跟树的位置坐标进行判断.

咱们知道树的位置坐标已经保存在地图的数据表中了, 可是角色的坐标跟地图的数据表尚未任何关系, 由于角色常常移动, 因此咱们能够写一个函数, 根据角色的屏幕像素点坐标来计算所处的地图方格坐标, 代码以下:

-- 根据像素坐标值计算所处网格的 i,j 值
function where(x,y)
    local i = math.ceil((x+scaleX) / scaleX)
    local j = math.ceil((y+scaleY) / scaleY)
    return i,j
end

有了这个函数, 咱们只要把角色当前位置的像素点坐标输入, 就能够获得它所处网格的坐标, 这样就把角色跟地图从数据层面创建了关联. 后续就能够方便地经过这个接口来处理他们之间的交互了.

为方便后续代码维护, 咱们要把上述代码改写为一个地图生成类, 改写后的完整代码以下:

-- MapSample

Maps = class()

function Maps:init()
    --[[
    gridCount:网格数目,范围:1~100,例如,设为3则生成3*3的地图,设为100,则生成100*100的地图。
    scaleX:单位网格大小比例,范围:1~100,该值越小,则单位网格越小;该值越大,则单位网格越大。
    scaleY:同上,若与scaleX相同则单位网格是正方形格子。
    plantSeed:植物生成概率,范围:大于4的数,该值越小,生成的植物越多;该值越大,生成的植物越少。
    minerialSeed:矿物生成概率,范围:大于3的数,该值越小,生成的矿物越多;该值越大,生成的矿物越少。
    --]]
    self.gridCount = 50
    self.scaleX = 50
    self.scaleY = 50
    self.plantSeed = 20.0
    self.minerialSeed = 50.0

    -- 根据地图大小申请图像
    local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY
    self.imgMap = image(w,h)

    -- 整个地图使用的全局数据表
    self.mapTable = {}

    -- 设置物体名称
    tree1,tree2,tree3 = "松树", "杨树", "小草"    
    mine1,mine2 = "铁矿", "铜矿"

    -- 设置物体图像
    imgTree1 = readImage("Planet Cute:Tree Short")
    imgTree2 = readImage("Planet Cute:Tree Tall")
    imgTree3 = readImage("Platformer Art:Grass")
    imgMine1 = readImage("Platformer Art:Mushroom")
    imgMine2 = readImage("Small World:Treasure")

    -- 存放物体: 名称,图像
    self.itemTable = {[tree1]=imgTree1,[tree2]=imgTree2,[tree3]=imgTree3,[mine1]=imgMine1,[mine2]=imgMine2}

    -- 尺寸为 3*3 的数据表示例
    self.mapTable = {{pos=vec2(1,1),plant=nil,mineral=mine1},{pos=vec2(1,2),plant=nil,mineral=nil},
                {pos=vec2(1,3),plant=tree3,mineral=nil},{pos=vec2(2,1),plant=tree1,mineral=nil},
                {pos=vec2(2,2),plant=tree2,mineral=mine2},{pos=vec2(2,3),plant=nil,mineral=nil},
                {pos=vec2(3,1),plant=nil,mineral=nil},{pos=vec2(3,2),plant=nil,mineral=mine2},
                {pos=vec2(3,3),plant=tree3,mineral=nil}}

    print("地图初始化开始...")
    -- 根据初始参数值新建地图
    self:createMapTable()
    print("OK, 地图初始化完成! ")
end

-- 新建地图数据表, 插入地图上每一个格子里的物体数据
function Maps:createMapTable()
    --local mapTable = {}
    for i=1,self.gridCount,1 do
        for j=1,self.gridCount,1 do
            self.mapItem = {pos=vec2(i,j), plant=self:randomPlant(), mineral=self:randomMinerial()}
            --self.mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(self.mapTable, self.mapItem)
        end
    end
    self:updateMap()
end

-- 根据地图数据表, 刷新地图
function Maps:updateMap()
    setContext(self.imgMap)   
    for i = 1,self.gridCount*self.gridCount,1 do
        local pos = self.mapTable[i].pos
        local plant = self.mapTable[i].plant
        local mineral = self.mapTable[i].mineral
        -- 绘制地面
        self:drawGround(pos)
        -- 绘制植物和矿物
        if plant ~= nil then self:drawTree(pos, plant) end
        if mineral ~= nil then self:drawMineral(pos, mineral) end
    end
    setContext()
end

function Maps:drawMap() 
    sprite(self.imgMap,-self.scaleX,-self.scaleY)
end

-- 根据像素坐标值计算所处网格的 i,j 值
function Maps.where(x,y)
    local i = math.ceil((x+self.scaleX) / self.scaleX)
    local j = math.ceil((y+self.scaleY) / self.scaleY)
    return i,j
end

-- 随机生成植物
function Maps:randomPlant()
    local seed = math.random(1.0, self.plantSeed)
    local result = nil

    if seed >= 1 and seed < 2 then result = tree1
    elseif seed >= 2 and seed < 3 then result = tree2
    elseif seed >= 3 and seed < 4 then result = tree3
    elseif seed >= 4 and seed <= self.plantSeed then result = nil end

    return result
end

-- 随机生成矿物
function Maps:randomMinerial()
    local seed = math.random(1.0, self.minerialSeed)
    local result = nil

    if seed >= 1 and seed < 2 then result = mine1
    elseif seed >= 2 and seed < 3 then result = mine2
    elseif seed >= 3 and seed <= self.minerialSeed then result = nil end

    return result
end

function Maps:getImg(name)
    return self.itemTable[name]
end

-- 重置  
function Maps:resetMapTable()
    self.mapTable = self:createMapTable()
end

-- 绘制单位格子地面
function Maps:drawGround(position)
    local x,y = self.scaleX * position.x, self.scaleY * position.y
    pushMatrix()
    stroke(99, 94, 94, 255)
    strokeWidth(1)
    fill(5,155,40,255)
    -- fill(5,155,240,255)
    rect(x,y,self.scaleX,self.scaleY)
    --sprite("Documents:3D-Wall",x,y,scaleX,scaleY)
    popMatrix()
end

-- 绘制单位格子内的植物
function Maps:drawTree(position,plant)
    local x,y = self.scaleX * position.x, self.scaleY * position.y
    pushMatrix()
    -- 绘制植物图像
    sprite(self.itemTable[plant],x,y,self.scaleX*6/10,self.scaleY)

    --fill(100,100,200,255)
    --text(plant,x,y)
    popMatrix()
end

-- 绘制单位格子内的矿物
function Maps:drawMineral(position,mineral)
    local x,y = self.scaleX * position.x, self.scaleY * position.y
    pushMatrix()
    -- 绘制矿物图像
    sprite(self.itemTable[mineral],x+self.scaleX/2,y,self.scaleX/2,self.scaleX/2)

    --fill(100,100,200,255)
    --text(mineral,x+self.scaleX/2,y)
    popMatrix()
end

-- 游戏主程序框架
function setup()
    displayMode(OVERLAY)

    myMap = Maps()
end

function draw()
    background(40, 40, 50)    

    -- 绘制地图
    myMap:drawMap()
end

到目前为止, 咱们在地图生成原型章节的目标基本完成, 下一章咱们会尝试把 状态 , 帧动画地图生成 这三个模块整合起来, 通常来讲事物发展到 的阶段会由量变触发质变, 咱们这个程序也同样, 会在此次整合以后, 从一个个零散简陋的原型, 一跃而成一个还能看得过去的基本框架, 是否是很期待?

激动人心的新起点

事实上, 把角色屏幕位置跟地图数据表创建关联以后, 咱们的角色就真正存在于这个游戏世界中了, 它能够自由地跟地图上的每个物体进行交互, 这意味着一个全新的激动人心的开始! 到如今为止, 咱们游戏世界的基本框架已经搭建起来了, 咱们能够在这个框架上试验本身对于武侠冒险游戏的各类新想法.

全部章节连接

从零开始写一个武侠练功游戏-1-状态原型
从零开始写一个武侠练功游戏-2-帧动画
从零开始写一个武侠练功游戏-3-地图生成

相关文章
相关标签/搜索