用 mesh
改写地图类, 带来的一大好处是控制逻辑能够变得很是简单, 做为一个地图类, 最基本的控制逻辑就是显示哪一部分和地图如何卷动, 而这两点能够经过 mesh
的纹理贴图很是容易地解决, 由于在 OpenGL ES 2.0/3.0
中, 能够经过设置纹理坐标来决定如何在地图上显示纹理贴图, 而这些控制逻辑若是不用 mesh
, 本身去写, 就有些繁琐了, 不信你能够试试.git
另外咱们以前实现的地图类的地图绘制是极其简陋的, 好比地面就是一些单色的矩形块, 本章咱们将会把很小的纹理贴图素材拼接起来生成更具表现力和真实感的地面.github
基于 OpenGL ES 2.0/3.0
的纹理贴图特性, 咱们既可使用一块很小的纹理, 而后用拼图的方式把大屏幕铺满, 也可使用一块很大的超出屏幕范围的图片作纹理, 而后选择其中一个尺寸跟屏幕尺寸至关的区域来显示.编程
在本章中, 这两种方法都会用到, 前者用来生成一张大大的地图, 后者用来显示这块大地图的局部区域.数组
地图类的处理相对来讲复杂一些, 正如咱们在 概述
中提到的, 要在两个层面使用 mesh
, 第一层是用小素材纹理经过拼图的方式生成一张超过屏幕尺寸的大地图图片, 第二层是把这张大地图图片做为纹理素材, 经过纹理坐标的设置来从大地图图片素材中选择一个尺寸恰好是屏幕大小的区域, 而后把它显示在屏幕上.框架
由于咱们是前面的基础上改写, 也就是说用来生成大地图图片的代码已经写好了, 因此咱们能够选择先从简单的开始, 那就是先实现第二层面: 用大图片做为纹理贴图, 利用 mesh
的纹理坐标来实现显示小区域和地图卷动等功能.dom
具体办法就是先在初始化函数 Maps:init()
中用 mesh:addRect()
新建一个屏幕大小的矩形, 而后加载已经生成的大地图图片做为纹理贴图, 再经过设置纹理坐标 mesh:setRectTex(i, x, y, w, t)
取得对应于纹理贴图上的一块屏幕大小的区域; 而后再在 Maps:drawMap()
函数中根据角色移动来判断是否须要卷动地图, 以及若是须要卷动向哪一个方向卷动, 最后在 Maps:touched(touch)
函数中把纹理坐标的 (x, y)
跟触摸数据关联起来, 这样咱们屏幕上显示的地图就会随着角色移动到屏幕边缘而自动把新地图平移过来.函数
在初始化函数 Maps:init()
中主要是这些处理:性能
w,h
,self.imgMap
, 咱们的大地图就要绘制在这个图形对象上,self.x, self.y
, 这里把大地图的左下角坐标设为 (0,0)
,mesh
对象 self.m
,self.m
上新增一个矩形, 该矩形中心坐标为 (WIDTH/2, HEIGHT/2)
, 宽度为 WIDTH
, 高度为 HEIGHT
, 也就是一个跟屏幕同样大的矩形,self.imgMap
设为 self.m
的纹理贴图,[0,1]
, 因此须要咱们把坐标的绝对数值转换为 [0,1]
区间内的相对数值, 也就是用屏幕宽高除以大地图的宽高 local u,v = WIDTH/w, HEIGHT/h
mesh:setRectTex()
设置进去就是下面这些代码:测试
... -- 根据地图大小申请图像 local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY self.imgMap = image(w,h) -- 使用 mesh 绘制地图 -- 设置当前位置为矩形中心点的绝对数值,分别除以 w, h 能够获得相对数值 self.x, self.y = w/2-WIDTH/2, h/2-HEIGHT/2 self.m = mesh() self.mi = self.m:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT) self.m.texture = self.imgMap -- 利用纹理坐标设置显示区域,根据中心点坐标计算出左下角坐标,除以纹理宽度获得相对值,w h 使用固定值(小于1) local u,v = WIDTH/w, HEIGHT/h self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v) ...
在绘制函数 Maps:drawMap()
中要作这些处理:动画
myS
当前所在的坐标 (myS.x, myS.y)
是否是已经处于地图边缘, 若是是则开始切换地图(也就是把地图卷动过来), 切换的办法就是给地图的纹理坐标的起始点一个增量操做,self.x = self.x - WIDTH/1000
,self.x = self.x + WIDTH/1000
,self.y = self.y + HEIGHT/1000
,self.y = self.y - HEIGHT/1000
,w,h
获得位于 [0,1]
区间内的坐标的相对值,self.m:setRectTex()
的参数.代码是这些:
... -- 更新纹理贴图, --若是地图上的物体有了变化 self.m.texture = self.imgMap local w,h = self.imgMap.width, self.imgMap.height local u,v = WIDTH/w, HEIGHT/h -- 增长判断,若角色移动到边缘则切换地图:经过修改贴图坐标来实现 print(self.x,self.y) local left,right,top,bottom = WIDTH/10, WIDTH*9/10, HEIGHT/10, HEIGHT*9/10 local ss = 800 if myS.x <= left then self.x= self.x - WIDTH/ss end if myS.x >= right then self.x= self.x + WIDTH/ss end if myS.y <= bottom then self.y = self.y - HEIGHT/ss end if myS.y >= top then self.y = self.y + HEIGHT/ss end -- 根据计算获得的数据从新设置纹理坐标 self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v) ...
另外, 咱们使用了一个局部变量 local ss = 800
来控制屏幕卷动的速度, 由于考虑到玩家角色可能行走, 也可能奔跑, 而咱们这是一个武侠游戏, 可能会设置 轻功
之类的技能, 这样当角色以不一样速度运动到屏幕边缘时, 地图卷动的速度也各不相同, 看起来真实感更强一些.
补充说明一点, 为方便编程, 咱们使用的 self.x, self.y
都用了绝对数值, 可是在函数 self.m:setRectTex()
中须要的是相对数值, 因此做为参数使用时都须要除以 w, h
, 这里我在调程序的时候也犯过几回晕.
在函数 Maps:touched(touch)
中, 把触摸位置坐标 (touch.x, touch.y)
跟玩家角色坐标 (myS.x, myS.y)
创建关联, 这里这么写主要是为了方便咱们如今调试用.
代码很简单:
if touch.state == BEGAN then myS.x, myS.y = touch.x, touch.y end
另外还须要在 setup()
函数中设置一下 (myS.x, myS.y)
的初值, 让它们位于屏幕中央就能够了.
myS.x, myS.y = WIDTH/2, HEIGHT/2
完整代码以下:
-- c06-02.lua -- 主程序框架 function setup() displayMode(OVERLAY) myS = {} myS.x, myS.y = WIDTH/2, HEIGHT/2 myMap = Maps() myMap:createMapTable() end function draw() background(40, 40, 50) -- 绘制地图 myMap:drawMap() sysInfo() end function touched(touch) myMap:touched(touch) end -- 使用 mesh() 绘制地图 Maps = class() function Maps:init() self.gridCount = 100 self.scaleX = 40 self.scaleY = 40 self.plantSeed = 20.0 self.minerialSeed = 50.0 -- 根据地图大小申请图像 local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY -- print(w,h) self.imgMap = image(w,h) -- 使用 mesh 绘制地图 -- 设置当前位置为矩形中心点的绝对数值,分别除以 w, h 能够获得相对数值 self.x, self.y = (w/2-WIDTH/2), (h/2-HEIGHT/2) self.m = mesh() self.mi = self.m:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT) self.m.texture = self.imgMap -- 利用纹理坐标设置显示区域,根据中心点坐标计算出左下角坐标,除以纹理宽度获得相对值,w h 使用固定值(小于1) local u,v = WIDTH/w, HEIGHT/h self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v) -- 整个地图使用的全局数据表 self.mapTable = {} -- 设置物体名称 tree1,tree2,tree3 = "松树", "杨树", "小草" mine1,mine2 = "铁矿", "铜矿" -- 后续改用表保存物体名称 self.trees = {"松树", "杨树", "小草"} self.mines = {"铁矿", "铜矿"} -- 设置物体图像 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() end function Maps:drawMap() -- sprite(self.imgMap,-self.scaleX,-self.scaleY) -- sprite(self.imgMap,0,0) -- 更新纹理贴图, --若是地图上的物体有了变化 self.m.texture = self.imgMap local w,h = self.imgMap.width, self.imgMap.height local u,v = WIDTH/w, HEIGHT/h -- 增长判断,若角色移动到边缘则切换地图:经过修改贴图坐标来实现 -- print(self.x,self.y) local left,right,top,bottom = WIDTH/10, WIDTH*9/10, HEIGHT/10, HEIGHT*9/10 local ss = 800 if myS.x <= left then self.x= self.x - WIDTH/ss end if myS.x >= right then self.x= self.x + WIDTH/ss end if myS.y <= bottom then self.y = self.y - HEIGHT/ss end if myS.y >= top then self.y = self.y + HEIGHT/ss end -- 根据计算获得的数据从新设置纹理坐标 self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v) -- self:updateMap() self.m:draw() end function Maps:touched(touch) if touch.state == BEGAN then myS.x, myS.y = touch.x, touch.y end 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) -- myT:switchPoint(myT.taskID) end end print("OK, 地图初始化完成! ") self:updateMap() end -- 根据地图数据表, 刷新地图,比较耗时,能够考虑使用协程,每 1 秒内花 1/60 秒来执行它; -- 协程还可用来实现时间系统,气候变化,植物生长,它赋予咱们操纵游戏世界运行流程的能力(至关于控制时间变化) -- 或者不用循环,只执行改变的物体,传入网格坐标 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:touched(touch) if touch.state == BEGAN then myS.x, myS.y = touch.x, touch.y end 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
如今开始把第一层面改写为用 mesh
绘图, 也就是说以 mesh
方式来生成大地图, 具体来讲就是改写这些函数:
Maps:updateMap()
负责把全部的绘制函数整合起来, 绘制出整副地图Maps:drawGround()
负责绘制单位格子地面Maps:drawTree()
负责绘制单位格子内的植物Maps:drawMineral()
负责绘制单位格子内的矿物这里稍微麻烦一些, 由于咱们打算用小纹理贴图来拼接, 因此一旦小纹理肯定, 那么这些属性就不须要显式指定了:
self.scaleX
= 40self.scaleY
= 40它们实际上就是小纹理贴图的 宽度
和 高度
, 假设使用名为 tex
的小纹理, 那么这两个值就分别是 tex.width
和 tex.height
, 虽然咱们通常提倡使用正方形的纹理, 不过这里仍是区分了 宽度
和 高度
.
而矩形的大小, 则能够经过属性 self.gridCount = 100
来设定须要用到多少块小纹理, 这里设置的是 100
, 表示横向使用 100
块小纹理, 纵向使用 100
块小纹理.
看起来此次改写涉及的地方比较多.
这里仍是经过 mesh
的纹理贴图功能来实现, 不过跟在第一层面的用法不一样, 这里咱们会使用很小的纹理贴图, 好比大小为 50*50
像素单位, 经过纹理坐标的设置和 shader
把它们拼接起来铺满整个地图, 之因此要用到 shader
, 是由于在这里, 咱们提供纹理坐标的取值大于 [0,1]
的范围, 必须在 shader
中对纹理坐标作一个转换, 让它们从新落回到 [0,1]
的区间.
好比假设咱们程序提供的纹理坐标是 (23.4, 20.8)
, 前面的整数部分 (23, 20)
表明的都是整块的纹理图, 至关于横向有 23
个贴图, 纵向有 20
个贴图, 那么剩下的小数部分 (0.4, 0.8)
就会落在一块小纹理素材图内, 这个 (0.4, 0.8)
才是咱们真正要取的点.
咱们先从地面开始, 先新建一个名为 m1
的 mesh
, 接着在这个 mesh
上新建一个大大的矩形, 简单来讲就是跟咱们的地图同样大, 再加载一个尺寸较小的地面纹理贴图, 经过纹理坐标的设置和 shader
的处理把它以拼图的方式铺满整个矩形, 最后用函数 m1:draw()
把它绘制到 self.img
上, 不过为方便调试, 咱们先临时增长一个属性 self.img1
, 全部改写部分先在它上面绘制, 调试无误后再绘制到 self.imgMap1
上.
初始化函数 Maps:init()
中须要增长的代码
-- 使用 mesh 绘制第一层面的地图 self.m1 = mesh() self.m1.texture = readImage("Documents:3D-Wall") local tw,th = self.m1.texture.width, self.m1.texture.height local mw,mh = (self.gridCount+1)*tw, (self.gridCount+1)*th -- 临时调试用, 调试经过后删除 self.imgMap1 = image(mw, mh) -- local ws,hs = WIDTH/tw, HEIGHT/th local ws,hs = mw/tw, mh/th print(ws,hs) self.m1i = self.m1:addRect(mw/2, mh/2, mw, mh) self.m1:setRectTex(self.m1i, 1/2, 1/2, ws, hs) -- 使用拼图 shader self.m1.shader = shader(shaders["maps"].vs,shaders["maps"].fs)
由于须要修改的地方较多, 为避免引入新问题, 因此保留原来的处理, 临时增长几个函数, 专门用于调试:
-- 临时调试用 function Maps:updateMap1() setContext(self.imgMap) m1:draw() setContext() end
另外须要在增长一个专门用于拼图的 shader
, 把小块纹理图拼接起来铺满:
-- Shader shaders = { maps = { vs=[[ // 拼图着色器: 把小纹理素材拼接起来铺满整个屏幕 //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord; varying vec2 vTexCoord; varying vec4 vColor; uniform mat4 modelViewProjection; void main() { vColor = color; vTexCoord = texCoord; gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float; varying vec2 vTexCoord; varying vec4 vColor; // 纹理贴图 uniform sampler2D texture; void main() { vec4 col = texture2D(texture,vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0))); gl_FragColor = vColor * col; } ]]} }
原来咱们的 mapTable
是一个一维数组, 如今把它改成二维数组, 这样在知道一个网格的坐标 i, j
后能够很快地查找出该网格在数据表中的信息 mapTable[i][j]
, 很是方便对地图中的物体(植物/矿物)进行操做, 首先是改写地图数据表生成函数 Maps:createMapTable()
, 这里须要注意的一点是 用 Lua
的 table
实现二维数组时, 须要显示地建立每一行, 改成以下:
function Maps:createMapTable() --local mapTable = {} for i=1,self.gridCount,1 do self.mapTable[i] = {} for j=1,self.gridCount,1 do self.mapItem = {pos=vec2(i,j), plant=self:randomPlant(), mineral=self:randomMinerial()} table.insert(self.mapTable[i], self.mapItem) -- self.mapTable[i][j] = self.mapItem -- myT:switchPoint(myT.taskID) end end print("OK, 地图初始化完成! ") self:updateMap1() end
也能够这样 self.mapTable[i][j] = self.mapItem
来为数组的每一个位置赋值.
修改了数据表结构后, 不少针对数据表的相关操做也要作对应修改, 如 Maps:updateMap()
函数:
function Maps:updateMap() setContext(self.imgMap) -- 用 mesh 绘制地面 self.m1:draw() -- 用 sprite 绘制植物,矿物,建筑 for i = 1,self.gridCount,1 do for j=1,self.gridCount,1 do local pos = self.mapTable[i][j].pos local plant = self.mapTable[i][j].plant local mineral = self.mapTable[i][j].mineral -- 绘制植物和矿物 if plant ~= nil then self:drawTree(pos, plant) end if mineral ~= nil then self:drawMineral(pos, mineral) end end end setContext() end
还有其余几个函数就不一一列举了, 由于修改的地方很清晰.
这个游戏程序写了这么久了, 玩家控制的角色尚未真正对地图上的物体作过交互, 这里咱们增长几个用于操做地图上物体的函数:
首先提供一个查看对应网格信息的函数 Maps:showGridInfo()
:
function Maps:showGridInfo(i,j) local item = self.mapTable[i][j] print(item.pos, item.tree, item.mineral) if item.tree ~= nil then fill(0,255,0,255) text(item.pos.."位置处有: "..item.tree.." 和 ..", 500,200) end end
而后是一个删除物体的函数 Maps:removeMapObject()
:
function Maps:removeMapObject(i,j) local item = self.mapTable[i][j] if item.pos == vec2(i,j) then item.plant = nil item.mineral = nil end end
咱们以前写过一个根据坐标数值换算对应网格坐标的函数 ``, 如今须要改写一下, 把计算单位换成小纹理贴图的宽度和高度:
function Maps:where(x,y) local w, h = self.m1.texture.width, self.m1.texture.height local i, j = math.ceil(x/w), math.ceil(y/h) return i,j end
还存在点小问题, 精度须要提高, 后续改进.
要修改函数 Maps:drawTree()
, 原来是根据 self.scaleX, self.scaleY
和网格坐标 i, j
来计算绘制到哪一个格子上的, 如今由于地面改用 mesh
的纹理贴图绘制, 因此就要用地面纹理贴图的 width, height
来计算了.
-- 临时调试用 function Maps:drawTree(position,plant) local w, h = self.m1.texture.width, self.m1.texture.height local x, y = w * position.x, h * position.y print("tree:"..x..y) pushMatrix() -- 绘制植物图像 sprite(self.itemTable[plant],x,y,w*6/10,h) popMatrix() end
一样须要修改的还有 Maps:drawMineral()
函数:
function Maps:drawMineral(position,mineral) local w, h = self.m1.texture.width, self.m1.texture.height local x, y = w * position.x, h * position.y pushMatrix() -- 绘制矿物图像 sprite(self.itemTable[mineral], x+w/2, y , w/2, h/2) --fill(100,100,200,255) --text(mineral,x+self.scaleX/2,y) popMatrix() end
通过上面这些改动, 基本上是完成了, 不过删除地图上的物体后, 须要重绘地图, 若是把数据表 mapTable
全都遍历一遍, 至关于整副地图都重绘一遍, 显然没这个必要, 因此咱们打算只重绘那些被删除了物体的网格, 由于知道确切坐标, 因此咱们能够用这样一个函数来实现:
--局部重绘函数 function Maps:updateItem(i,j) setContext(self.imgMap) local x,y = i * self.m1.texture.width, j * self.m1.texture.height sprite(self.m1.texture, x, y) setContext() self.m.texture = self.imgMap end
-- c06-02.lua -- 主程序框架 function setup() displayMode(OVERLAY) -- 角色位置,用于调试 myS = {} myS.x, myS.y = WIDTH/2, HEIGHT/2 -- 生成地图 myMap = Maps() myMap:createMapTable() print("左下角在地图的坐标:"..myMap.x,myMap.y) local i,j = myMap:where(myMap.x,myMap.y) print("左下角对应网格坐标:"..i.." : "..j) -- print(myMap.mapTable[9][10].pos, myMap.mapTable[9][10].plant) -- 测试格子坐标计算 ss = "" end function draw() background(40, 40, 50) -- 绘制地图 myMap:drawMap() sysInfo() -- 显示点击处的格子坐标 fill(255, 0, 14, 255) -- text(ss,500,100) end function touched(touch) myMap:touched(touch) if touch.state == ENDED then c1,c2 = myMap:where(myMap.x + touch.x, myMap.y + touch.y) myMap:showGridInfo(c1,c2) myMap:removeMapObject(c1,c2) print("点击处的坐标绝对值:", (myMap.x + touch.x)/200, (myMap.y + touch.y)/200) print("c1:c2 "..c1.." : "..c2) ss = c1.." : "..c2 end end -- 系统信息: 显示FPS和内存使用状况 function sysInfo() pushStyle() fill(255, 255, 255, 255) -- 根据 DeltaTime 计算 fps, 根据 collectgarbage("count") 计算内存占用 local fps = math.floor(1/DeltaTime) local mem = math.floor(collectgarbage("count")) text("FPS: "..fps.." Mem:"..mem.." KB",650,740) popStyle() end -- 使用 mesh() 绘制地图 Maps = class() function Maps:init() self.gridCount = 20 self.scaleX = 200 self.scaleY = 200 self.plantSeed = 20.0 self.minerialSeed = 50.0 -- 根据地图大小申请图像,scaleX 可实现缩放物体 --local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY local w,h = (self.gridCount+0)*self.scaleX, (self.gridCount+0)*self.scaleY print("大地图尺寸: ",w,h) self.imgMap = image(w,h) -- 使用 mesh 绘制第一层面的地图地面 self.m1 = mesh() self.m1.texture = readImage("Documents:hm1") local tw,th = self.m1.texture.width, self.m1.texture.height local mw,mh = (self.gridCount+1)*tw, (self.gridCount+1)*th -- 临时调试用, 调试经过后删除 self.imgMap1 = image(mw, mh) -- local ws,hs = WIDTH/tw, HEIGHT/th local ws,hs = mw/tw, mh/th print("网格数目: ",ws,hs) self.m1i = self.m1:addRect(mw/2, mh/2, mw, mh) self.m1:setRectTex(self.m1i, 1/2, 1/2, ws, hs) -- 使用拼图 shader self.m1.shader = shader(shaders["maps"].vs,shaders["maps"].fs) -- 使用 mesh 绘制第二层面的地图 -- 屏幕左下角(0,0)在大地图上对应的坐标值(1488, 1616) -- 设置屏幕当前位置为矩形中心点的绝对数值,分别除以 w, h 能够获得相对数值 self.x, self.y = (w/2-WIDTH/2), (h/2-HEIGHT/2) self.m = mesh() self.mi = self.m:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT) self.m.texture = self.imgMap -- 利用纹理坐标设置显示区域,根据中心点坐标计算出左下角坐标,除以纹理宽度获得相对值,w h 使用固定值(小于1) -- 这里计算获得的是大地图中心点处的坐标,是游戏刚开始运行的坐标 local u,v = WIDTH/w, HEIGHT/h self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v) -- 整个地图使用的全局数据表 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.trees = {"松树", "杨树", "小草"} self.mines = {"铁矿", "铜矿"} -- 设置物体图像 self.items = {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} --[=[ self.itemTable = {[self.trees[1]].self.items["imgTree1"],[self.trees[2]].self.items["imgTree2"], [self.trees[3]].self.items["imgTree3"],[self.mines[1]].self.items["imgMine1"], [self.mines[3]].self.items["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() end -- 新建地图数据表, 插入地图上每一个格子里的物体数据 function Maps:createMapTable() --local mapTable = {} for i=1,self.gridCount,1 do self.mapTable[i] = {} for j=1,self.gridCount,1 do self.mapItem = {pos=vec2(i,j), plant=self:randomPlant(), mineral=self:randomMinerial()} table.insert(self.mapTable[i], self.mapItem) -- self.mapTable[i][j] = self.mapItem -- myT:switchPoint(myT.taskID) end end print("OK, 地图初始化完成! ") self:updateMap() end -- 更新整副地图:绘制地面, 绘制植物, 绘制矿物 function Maps:updateMap() setContext(self.imgMap) -- 用 mesh 绘制地面 self.m1:draw() -- 用 sprite 绘制植物,矿物,建筑 for i = 1,self.gridCount,1 do for j=1,self.gridCount,1 do local pos = self.mapTable[i][j].pos local plant = self.mapTable[i][j].plant local mineral = self.mapTable[i][j].mineral -- 绘制植物和矿物 if plant ~= nil then self:drawTree(pos, plant) end if mineral ~= nil then self:drawMineral(pos, mineral) end end end setContext() end function Maps:drawMap() -- 更新纹理贴图, --若是地图上的物体有了变化 self.m.texture = self.imgMap local w,h = self.imgMap.width, self.imgMap.height local u,v = WIDTH/w, HEIGHT/h -- 增长判断,若角色移动到边缘则切换地图:经过修改贴图坐标来实现 -- print(self.x,self.y) local left,right,top,bottom = WIDTH/10, WIDTH*9/10, HEIGHT/10, HEIGHT*9/10 local ss = 800 if myS.x <= left then self.x= self.x - WIDTH/ss end if myS.x >= right then self.x= self.x + WIDTH/ss end if myS.y <= bottom then self.y = self.y - HEIGHT/ss end if myS.y >= top then self.y = self.y + HEIGHT/ss end -- 根据计算获得的数据从新设置纹理坐标 self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v) -- self:updateMap() self.m:draw() end function Maps:touched(touch) if touch.state == BEGAN then myS.x, myS.y = touch.x, touch.y end end --局部重绘函数 function Maps:updateItem(i,j) setContext(self.imgMap) local x,y = i * self.m1.texture.width, j * self.m1.texture.height sprite(self.m1.texture, x, y) setContext() self.m.texture = self.imgMap end -- 根据像素坐标值计算所处网格的 i,j 值 function Maps:where(x,y) local w, h = self.m1.texture.width, self.m1.texture.height local i, j = math.ceil(x/w), math.ceil(y/h) return i, j end -- 角色跟地图上物体的交互 function Maps:removeMapObject(i,j) local item = self.mapTable[i][j] if item.pos == vec2(i,j) then item.plant = nil item.mineral = nil self:updateItem(i,j) end end -- 显示网格内的物体信息 function Maps:showGridInfo(i,j) local item = self.mapTable[i][j] print("showGridInfo: ", item.pos, item.tree, item.mineral) if item.tree ~= nil then fill(0,255,0,255) text(item.pos.."位置处有: ", item.tree, 500,200) end 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:drawTree(position,plant) local w, h = self.m1.texture.width, self.m1.texture.height local x,y = w * position.x, h * position.y -- print("tree:"..x.." : "..y) pushMatrix() -- 绘制植物图像 sprite(self.itemTable[plant], x, y, w*6/10, h) --fill(100,100,200,255) --text(plant,x,y) popMatrix() end -- 绘制单位格子内的矿物 function Maps:drawMineral(position,mineral) local w, h = self.m1.texture.width, self.m1.texture.height local x, y = w * position.x, h * position.y pushMatrix() -- 绘制矿物图像 sprite(self.itemTable[mineral], x+w/2, y , w/2, h/2) --fill(100,100,200,255) --text(mineral,x+self.scaleX/2,y) popMatrix() end -- Shader shaders = { maps = { vs=[[ // 拼图着色器: 把小纹理素材拼接起来铺满整个屏幕 //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord; varying vec2 vTexCoord; varying vec4 vColor; uniform mat4 modelViewProjection; void main() { vColor = color; vTexCoord = texCoord; gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float; varying vec2 vTexCoord; varying vec4 vColor; // 纹理贴图 uniform sampler2D texture; void main() { vec4 col = texture2D(texture,vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0))); gl_FragColor = vColor * col; } ]]} }
跟帧动画整合在一块儿的代码在这里: c06.lua
如今咱们能够方便地更换地面纹理贴图, 看看这两个不一样的贴图效果:
]
到目前为止, 咱们对地图类的改写基本完成, 调试经过后, 剩下的就是利用 shader
来为地图增长一些特效了.
原本打算写写下面这些特效:
下雨,下雪,雷电,迷雾,狂风
春夏秋冬四季变化
光线随时间改变明暗程度
让河流动起来
湖泊表面闪烁
用广告牌实现的树木
让地面产生动态阴影变化
搞一个立方体纹理特贴图
可是一看本章已经写了太长的篇幅了, 因此决定把这些内容放到后面单列一章, 所以本章到此结束.
本章成功实现了以下目标:
mesh
绘制地图, 用 mesh
显示地图mesh
的纹理坐标机制解决了地图自动卷动shader
.临时想到的问题, 后续解决:
Github项目地址, 源代码放在 src/
目录下, 图片素材放在 assets/
目录下, 整个项目文件结构以下:
Air:Write-A-Adventure-Game-From-Zero admin$ tree . ├── README.md ├── Vim 列编辑功能详细讲解.md ├── assets │ ├── IMG_0097.PNG │ ├── IMG_0099.JPG │ ├── IMG_0100.PNG │ ├── c04.mp4 │ ├── cat.JPG │ └── runner.png ├── src │ ├── c01.lua │ ├── c02.lua │ ├── c03.lua │ ├── c04.lua │ ├── c05.lua │ ├── c06-01.lua │ ├── c06-02.lua │ └── c06.lua ├── 从零开始写一个武侠冒险游戏-0-开发框架Codea简介.md ├── 从零开始写一个武侠冒险游戏-1-状态原型.md ├── 从零开始写一个武侠冒险游戏-2-帧动画.md ├── 从零开始写一个武侠冒险游戏-3-地图生成.md ├── 从零开始写一个武侠冒险游戏-4-第一次整合.md ├── 从零开始写一个武侠冒险游戏-5-使用协程.md ├── 从零开始写一个武侠冒险游戏-6-用GPU提高性能(1).md └── 从零开始写一个武侠冒险游戏-6-用GPU提高性能(2).md 2 directories, 24 files Air:Write-A-Adventure-Game-From-Zero admin$
从零开始写一个武侠冒险游戏-1-状态原型
从零开始写一个武侠冒险游戏-2-帧动画
从零开始写一个武侠冒险游戏-3-地图生成
从零开始写一个武侠冒险游戏-4-第一次整合
从零开始写一个武侠冒险游戏-5-使用协程
从零开始写一个武侠冒险游戏-6-用GPU提高性能(1)
从零开始写一个武侠冒险游戏-6-用GPU提高性能(2)