从零开始写一个武侠冒险游戏-2-帧动画
从零开始写一个武侠冒险游戏-2-帧动画
---- 用基本绘图函数实现帧动画
- 作者:FreeBlues
- 修订记录
- 2016.06.10 初稿完成.
- 2016.08.03 增加对
XCode
项目文件的说明.
本章写作思路:
说明帧动画的原理->取得素材->把素材整个显示->截取子画面->显示子画面->讨论在循环显示函数 draw() 中让子画面逐帧显示->挖空素材背景(背景透明化)->让角色不再原地踏步(横向或纵向跑起来)->增加背景图->放大或者缩小角色(跑着跑着变瘦了-瘦身减肥效果)->背景图动起来->改变角色角度
概述
本文特点
帧动画是一种应用非常广泛的动画技术,现在我打算借助 Codea
非常友好的编程界面用最简单的例子一步步教你学会帧动画。
看过网络上不少关于帧动画的教程, 大多是通过引用这个类那个库来实现的, 看起来很复杂的样子, 很少有用基本绘图语句直接写的, 对于初学者来说, 理解帧动画还得先去理解那些类库, 无形中增加了学习难度, 本文尝试一种新的讲解方式, 全部采用 Codea
基本绘图语句来演示帧动画原理.
帧动画原理
首先介绍一下帧动画的原理: 简单说就是把所有的动画帧都集中放在一个图片上(好处是可以一次性加载到内存里), 然后依次显示每一帧, 这样连续显示起来的动画帧利用视觉暂留效应就成功地实现了帧动画.
具体来说需要这样一个图:
另外需要的就是每一个子帧在整副图片中的位置坐标和长宽, 一般会用左下角坐标和长度,宽度来确定一个子帧, 我们会用这样一个表来存储:
positon = {{x1,y,width,height}, {x2,y,width,height}, ... }
在这个数据结构中, 每副子帧的纵坐标 y
, 宽度 width
, 高度 height
都可以保持一样的值, 这样处理起来最省事.
这个素材图是每 3
副子帧完成一个动画, 它的坐标数据如下:
local x,y = 0,43
pos1 = {{0+x,0+y,32,43},{64+x,0+y,32,43},{96+x,0+y,32,43}}
不过有些帧动画素材, 为了节省那么一丁点空间, 会根据子帧中角色的实际宽度来放置, 这样就使得每副子帧的 宽度 width
不一定相同, 比如下面这个素材:
它的坐标数据就是这样的:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
{320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
再看一张大猫图:
它的坐标数据就是这样的了:
local w,h = 1024,1024
pos2 = {{0,h*3/4,w/2,h/4},{w/2,h*3/4,w/2,h/4},{0,h*2/4,w/2,h/4},{0,h*2/4,w/2,h/4},
{0,h*1/4,w/2,h/4},{w/2,h*1/4,w/2,h/4},{0,h*0/4,w/2,h/4},{0,h*0/4,w/2,h/4}}
具体实现
直接绘制整副图
明白了上述原理就好办了.
首先,我们准备好帧动画素材,接着把素材读入内存,然后试着把它直接显示到屏幕上--记住,sprite
命令是后续帧动画的基础,代码如下
function setup()
displayMode(FULLSCREEN)
-- 绘图模式选 CENTER 可以保证画面中的动画角色不会左右漂移
rectMode(CENTER)
spriteMode(CENTER)
-- 加载整副素材图
img = readImage("Documents:runner")
end
function draw()
sprite(img, x, y)
end
本文涉及的 Codea 基本绘图函数
函数 sprite(图片, x, y, width, height)
函数说明:
参数说明:
- 图片: 图片对象
- x: 左下角(或中心点)横坐标
- y: 左下角(或中心点)纵坐标
- width: 显示出来的图像的宽度
- height: 显示出来的图像的高度
函数 image:copy(x,y,width,height)
函数说明:
参数说明:
- x: 左下角横坐标
- y: 左下角纵坐标
- width: 要拷贝图像区域的宽度
- height: 要拷贝图像区域的高度
更多的函数说明文档请参考:
绘制一个子帧
很好, 现在我们从素材中取出第一个子画面, 也就是左下角坐标为(0,0)
, 宽为 110
, 高为 120
的区域:
img1 = img:copy(0,0,110,120)
现在在 draw()
函数里增加如下语句把它显示到屏幕上
sprite(img1, x, y)
绘制多幅子帧
非常好,接下来我们按照从左到右的顺序依次取出各个子画面,它们的坐标可以根据素材图像的大小进行估算,比如第二个子画面它的左下角坐标的 x 值要在第一个子画面左下角坐标 x 值的基础上加上第一个子画面的宽度 110, 高度不变, 它自己的宽度我们可以大致估算为70(根据实际情况调整), 那么如下
function setup()
...
img2 = img:copy(110, 0, 70, 120)
...
end
function draw()
...
sprite(img2, x, y)
...
end
有了这两个基础, 我们就知道怎么处理剩下的子画面了, 为了后续的操作方便,我们把所有这些子画面按顺序放到一个表中
—- 新建一个空表
images = {}
-- 分离各个子画面,按顺序存入表 imgs 中备用
imgs[1]=img:copy(0,0,110,120)
imgs[2]=img:copy(110,0,70,120)
imgs[3]=img:copy(180,0,70,120)
imgs[4]=img:copy(250,0,70,120)
imgs[5]=img:copy(320,0,105,120)
imgs[6]=img:copy(423,0,80,120)
imgs[7]=img:copy(500,0,70,120)
imgs[8]=img:copy(570,0,70,120)
然后试着在屏幕上单独显示一个子画面,检验一下我们是否成功地从素材中取得了所有的子画面, 代码如下:
function draw()
...
-- 分别显示所有子画面
sprite(imgs[1],120,200)
sprite(imgs[2],130,400)
sprite(imgs[3],120,600)
sprite(imgs[4],300,600)
sprite(imgs[5],320,400)
sprite(imgs[6],330,200)
sprite(imgs[7],620,200)
sprite(imgs[8],630,400)
end
接下来就是关键 ,把所有这些子画面连续显示到屏幕上, Codea
的 draw()
函数每秒钟会自动执行60
次, 也就是说它每 1/60
秒(0.01666...秒)会在屏幕上绘制一次.
那么我们只要在同一个位置每隔一点时间按照子帧的顺序显示一个新的子帧, 就可以形成动画效果.
这里我们使用一个全局变量 q
来索引 imgs
中的子画面, 首先在 setup()
函数中为 q
赋初值 0
q =0
接着在 draw()
函数中设置为递增, 每执行一次 draw()
, q
就会加 1
q= q+1
然后在 draw()
中依次显示 imgs[q]
,
sprite(imgs[q],120,200)
循环播放的技巧
这时点击运行就会出现一个错误, 提示数组越界, 怎么回事呢?
因为我们的 imgs
只有 8
个元素, 一旦 q
的值超过 8
就索引不到数据了, 也就是说我们需要让 imgs
的索引值保持在 1-8
的区间内.
解决办法就是用取模函数来限定数组索引的范围, 如下:
math.fmod(q, 8)
随着 q
的不断递增, 它返回的值依次为
0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7 …..
, 我们只要给它加 1
就能保证正好索引到1到8之间, 试着运行一下:
sprite(imgs[math.fmod(q, 8)+1],120,200)
这次没错了, 不过新的问题出现了, 好像跑得有些快, 如何解决呢?
控制播放速度
这是因为我们的 draw()
默认每秒执行60次, 那么我们现在的子帧有 8
副图, 按照我们上面的代码, 每执行一次 draw()
就会绘制一幅子帧, 也就是说我们的所有子帧会在 8/60
秒内就播放完.
现在我们希望能调整一下帧速率, 需要用到 Codea
提供的一个全局变量(只读) ElapsedTime
, 这个全局变量会实时返回程序执行时间, 我们用这样一个判断来实现:
用一个 prevTime
记录上一次执行时间, 在 setup()
中初始化(这时记录的是第一次执行的时间)
prevTime =0
假设我们希望子画面每隔 0.1
秒更新一次, 也就是每隔 0.1
秒, imgs
的索引值增加1
, 在 draw()
里增加这段代码:
if ElapsedTime > prevTime + 0.1 then
prevTime = prevTime + 0.1
k=math.fmod(i,8)
i=i+1
end
sprite(imgs[k+1],120,200)
看看现在的效果, 非常好, 现在的动画速度差不多是原来的 1/6
, 而且这个值可以根据需要进行调整.
代码优化
使用循环
首先是这段代码:
— 新建一个空表
images = {}
-- 分离各个子画面,按顺序存入表 imgs 中备用
imgs[1]=img:copy(0,0,110,120)
imgs[2]=img:copy(110,0,70,120)
imgs[3]=img:copy(180,0,70,120)
imgs[4]=img:copy(250,0,70,120)
imgs[5]=img:copy(320,0,105,120)
imgs[6]=img:copy(423,0,80,120)
imgs[7]=img:copy(500,0,70,120)
imgs[8]=img:copy(570,0,70,120)
我们可以把位置坐标提取出来集中放置到一个表中, 然后用循环来表示, 如下:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
{320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
for i = 1, 8 do
imgs[i]=img:copy(pos[i][1], pos[i][2], pos[i][3], pos[i][4])
end
继续用一个 table.unpack
语句来替换 pos[i][1], pos[i][2], pos[i][3], pos[i][4]
语句, 如下:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
{320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
for i = 1, 8 do
imgs[i] = img:copy(table.unpack(pos[i]))
end
不过不同的动画素材使用的子帧数目也不一定都是 8
个, 所以这里这个子帧数目可以通过 #pos
(求 pos
的长度) 来灵活设置, 所以代码如下:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
{320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
for i = 1, #pos do
imgs[i] = img:copy(table.unpack(pos[i]))
end
改写为类
为了方便使用, 另一方面也让程序主框架看起来清爽一些, 我们可以把上述实现帧动画的代码封装成一个类 Sprites
, 具体来说就是把初始化的代码放在 Sprites:init()
函数中, 把实际绘制的代码放在 Sprites:draw()
函数中, 代码如下:
Sprites = class()
function Sprites:init(x,y,img,pos)
self.x = x
self.y = y
self.index = 1
self.img = img
self.imgs = {}
self.pos = pos
self.i=0
self.k=1
self.q=0
self.prevTime =0
-- 使用循环,把各个子帧存入表中
for i=1,#self.imgs do
-- imgs[i] = img:copy(startPos[i][1],startPos[i][2],startPos[i][3],startPos[i][4])
self.imgs[i] = self.img:copy(table.unpack(self.pos[i]))
end
print(#self.imgs)
end
function Sprites:draw()
-- 确定每帧子画面在屏幕上停留的时间
if ElapsedTime > self.prevTime + 0.1 then
self.prevTime = self.prevTime + 0.1
self.k = math.fmod(self.i,#self.imgs)
self.i = self.i + 1
end
self.q=self.q+1
-- rect(800,500,120,120)
pushMatrix()
rotate(30)
-- sprite(self.imgs[self.k+1],self.i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8)
--sprite(imgs[math.fmod(q,8)+1],i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8)
sprite(self.imgs[self.k+1], self.x, self.y)
popMatrix()
-- sprite(imgs[self.index], self.x, self.y)
end
使用方法也很简单, 先在 setup()
中调用初始化函数, 然后在 draw()
中调用绘制函数:
function setup()
displayMode(FULLSCREEN)
-- 绘图模式选 CENTER 可以保证画面中的动画角色不会左右漂移
rectMode(CENTER)
spriteMode(CENTER)
fill(249, 249, 249, 255)
imgs = {}
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
{320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
img = readImage("Documents:runner")
img1 = readImage("Documents:cats")
pos1 = {{0,0,32,43},{0,0,64,43},{0,0,96,43}}
-- 初始化
m = Sprites(600,400,img,startPos)
-- m1 = Sprites(800,400,img1,pos1)
end
function draw()
background(39, 44, 39, 255)
m:draw()
-- m1:draw()
end
现在的代码
最新版本的代码
-- 帧动画对象类
Sprites = class()
function Sprites:init(x,y,img,pos)
self.x = x
self.y = y
-- self.index = 1
self.img = img
self.imgs = {}
self.pos = pos
self.i=0
self.k=1
self.q=0
self.prevTime =0
-- 处理原图,背景色变为透明
self:deal()
-- 使用循环,把各个子帧存入表中
for i=1,#self.pos do
-- imgs[i] = img:copy(pos[i][1],pos[i][2],pos[i][3],pos[i][4])
self.imgs[i] = self.img:copy(table.unpack(self.pos[i]))
end
end
function Sprites:deal()
---[[ 对原图进行预处理,把背景修改为透明,现存问题:角色内部有白色也会被去掉
local v = 255
for x=1,self.img.width do
for y =1, self.img.height do
-- 取出所有像素的颜色值
local r,g,b,a = self.img:get(x,y)
-- if r >= v and g >= v and b >= v then
if r == v and g == v and b == v and a == v then
self.img:set(x,y,r,g,b,0)
end
end
end
--]]
end
function Sprites:draw()
-- 确定每帧子画面在屏幕上停留的时间
if ElapsedTime > self.prevTime + 0.08 then
self.prevTime = self.prevTime + 0.08
self.k = math.fmod(self.i,#self.imgs)
self.i = self.i + 1
-- self.x = self.x + 1
end
self.q=self.q+1
-- rect(800,500,120,120)
pushMatrix()
-- rotate(30)
-- sprite(self.imgs[self.k+1],self.i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8)
--sprite(imgs[math.fmod(q,8)+1],i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8)
sprite(self.imgs[self.k+1], self.x, self.y,50,50)
popMatrix()
-- sprite(imgs[self.index], self.x, self.y)
end
-- Main
function setup()
displayMode(FULLSCREEN)
-- 绘图模式选 CENTER 可以保证画面中的动画角色不会左右漂移
rectMode(CENTER)
spriteMode(CENTER)
fill(249, 249, 249, 255)
imgs = {}
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
{320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
img = readImage("Documents:runner")
img1 = readImage("Documents:cats")
local x,y = 128,43
pos1 = {{0+x,0+y,32,43},{64+x,0+y,32,43},{96+x,0+y,32,43}}
img2 = readImage("Documents:catRunning")
local w,h = 1024,1024
pos2 = {{0,h*3/4,w/2,h/4},{w/2,h*3/4,w/2,h/4},{0,h*2/4,w/2,h/4},{0,h*2/4,w/2,h/4},
{0,h*1/4,w/2,h/4},{w/2,h*1/4,w/2,h/4},{0,h*0/4,w/2,h/4},{0,h*0/4,w/2,h/4}}
m = Sprites(600,400,img,pos)
m1 = Sprites(500,400,img1,pos1)
m2 = Sprites(500,200,img2,pos2)
end
function draw()
background(39, 44, 39, 255)
m:draw()
m1:draw()
m2:draw()
end
帧动画小结
帧动画是一种应用场景非常广泛的基础游戏开发技术, 游戏角色的大多数动作都是通过帧动画来实现的, 例如角色平时的移动, 无聊时的各种小动作, 以及战斗时的各种技能释放, 所以做游戏开发一定要彻底理解帧动画的原理和实现, 这样才能得心应手地把它运用在开发中.
扩展阅读
把素材背景设为透明
到目前为止, 我们的角色帧动画已经做好了, 不过看起来不是很协调, 尤其是动画角色顶着一个白色矩形框, 这是因为素材没有采用透明背景, 所以看起来感觉不太好.
不过既然背景是单一的白色, 那么我们为什么不在动画显示前把它做一个预处理? 把它的白色背景改为透明? 这里普及一下, 一般图片素材都有4个颜色通道, 分别为: r, g, b, a
, 前三个分别为红色
, 绿色
, 蓝色
, 第四个 a
就是透明度, 它们的取值范围都是 0~255
, 对于透明度来说, 0
表示透明, 255
表示不透明, 中间的值表示不同程度的透明.
那么我们的思路很简单, 把每个子画面的每个像素点都取出来,判断它是不是白色(白色的r,g,b,a值分别为255), 如果是, 我们就认为它是白色背景, 把它的 a
置为 0
,然后写回到原位置去, 如果不是背景则不做处理, 具体代码在这里:
--[[ 对每个子画面进行预处理,把背景修改为透明
for i=1,8 do
for x=1,imgs[i].width do
for y =1, imgs[i].height do
r,g,b,a = imgs[i]:get(x,y)
if r == 255 and g == 255 and b == 255 then
imgs[i]:set(x,y,r,g,b,0)
end
end
end
end
--]]
等等, 为什么不直接对整个素材图像进行处理呢, 这样还可以少一个循环, 如下:
---[[ 对原图进行预处理,把背景修改为透明
for x=1,img.width do
for y =1, img.height do
-- 取出所有像素的颜色值
r,g,b,a = img:get(x,y)
-- if r >= 205 and g >= 205 and b >= 205 then
if r == 255 and g == 255 and b == 255 then
img:set(x,y,r,g,b,0)
end
end
end
--]]
这两段代码最终效果是一样的, 不过后一种效率更高, 因为它集中处理整副素材图, 少了一重循环.
因为这段代码只需要执行一次即可, 所以我们把它放在 setup()
函数中, 看看效果, 果然感觉好多了, 虽然边缘部分看起来清除得不是那么好.
最终我们会把这个函数整合到帧动画类中, 作为一个方法, 代码如下:
function Sprites:deal()
---[[ 对原图进行预处理,把背景修改为透明
for x=1,self.img.width do
for y =1, self.img.height do
-- 取出所有像素的颜色值
local r,g,b,a = self.img:get(x,y)
-- if r >= 205 and g >= 205 and b >= 205 then
if r == 255 and g == 255 and b == 255 then
self.img:set(x,y,r,g,b,0)
end
end
end
end
可以选择把这个方法放在 Sprites:init()
中调用,
提醒
: 如果希望得到更好的图形效果, 可以用修图软件手动修改素材, 这是只是介绍一种简单的用代码处理图像的思路, 而且正式的游戏开发总是把能提前处理的步骤都尽量提前处理, 实在没办法处理的才用代码解决.
现在的效果看起来是不是又有了一些改进? 没错, 好的软件就是从一点一滴的细节改善中做出来的.
让角色平行移动
不过感觉还是有点美中不足, 首先黑黑的背景有些影响观感, 那么我们增加一个非洲大草原的背景图, 代码如下:
sprite("Documents:bgGrass",(WIDTH/2),HEIGHT/2)
效果貌似稍微好了点, 感觉还是不太对, 现在虽然看起来角色虽然在跑,可是总是在原地踏步, 游戏中的角色不能一直原地踏步不移动啊! 怎么办?
两个办法:
- 一是让背景画面平行移动起来
- 二是让角色平行移动起来
先说第一种, 让背景画面平行移动, 原理很简单, 修改这条显示背景图片的语句:
sprite("Documents:bgGrass",(WIDTH/2),HEIGHT/2)
让它的 x 坐标递减即可, 恰好我们有一个递增的变量 i 可以直接拿来用
sprite("Documents:bgGrass",(WIDTH/2-10*i),HEIGHT/2)
我们发现背景动是动起来了, 可是只要超过坐标范围就没了, 看来还得增加相关处理, 先分析一下出现这种情况的原因, 因为我们的背景图的大小是 1024*768
, 所以一旦左右移动背景超过了这个长宽范围,就没有图像了,因此,我们可以有这么几种解决思路:
- 1 预先准备一个大于屏幕尺寸的大背景图;
- 2 使用两个绘图语句,一个以
0,0
为左下角起点,另一个以1024,0
为左下角起点(因为是左右移动,所以纵坐标不需要修改),如下图所示:
这样还有个问题,假设整个背景图向左移动,那么当移动到最右边时,还会出现没有图像的情况,这时我们可以使用一种前面用过的技术,那就是对横坐标取模,让它们始终落在 0~1024
这个区间内, math.fmod(x,1024)
, 其中横坐标 x
是一个递增的量.
这样一来, 角色就可以朝各个方向移动了, 现阶段为方便测试, 我们可以这么设定:
- 点击屏幕左侧, 背景图向右平移;
- 点击屏幕右侧, 背景图向左平移.
要实现这个设定, 需要我们在 setup
中增加一个全局变量 s
, 在 draw()
中增加绘制全屏背景图的代码, 在 touched
函数中增加一段代码, 如下:
function setup()
displayMode(OVERLAY)
myStatus = Status()
-- 以下为帧动画代码
s = -1
...
end
function draw()
pushMatrix()
pushStyle()
-- spriteMode(CORNER)
rectMode(CORNER)
background(32, 29, 29, 255)
-- 增加移动的背景图: + 为右移,- 为左移
sprite("Documents:bgGrass",(WIDTH/2+10 * s * m.i)%(WIDTH),HEIGHT/2)
sprite("Documents:bgGrass",(WIDTH+10 * s * m.i)%(WIDTH),HEIGHT/2)
...
end
function touched(touch)
-- 用于测试修炼
if touch.x > WIDTH/2 and touch.state == ENDED then myStatus:update() end
-- 用于测试移动方向:点击左侧向右平移,点击右侧向左平移
if touch.x > WIDTH/2 and touch.state == ENDED then
s = -1
elseif touch.x < WIDTH/2 then
s = 1
end
end
现在游戏还不完整, 所以我们才通过一些设置好的变量来大致控制角色的移动--仅用于测试模块功能, 等后面控制系统完成, 我们会写一些触摸函数, 用它们来设置这些变量(m.i
, s
), 这样我们就可以通过触摸来精确控制角色的移动了.
再说第二种: 让角色平行移动
明白了第一种让背景平移的方法后, 第二种让角色平移的方法就更容易理解了, 也就是在Sprites:draw()
函数中动态修改角色绘制语句
sprite(self.imgs[self.k+1], self.x, self.y,50,50)`
的 self.x
值, 结合第一种方法的具体实现, 基本上就是把 self.x
模仿 self.i
的处理方式处理一下就可以了, 具体如下:
function Sprites:draw()
-- 确定每帧子画面在屏幕上停留的时间
if ElapsedTime > self.prevTime + 0.08 then
self.prevTime = self.prevTime + 0.08
self.k = math.fmod(self.i,#self.imgs)
self.i = self.i + 1
self.x = self.x + s
end
...
sprite(self.imgs[self.k+1], self.x, self.y,50,50)
end
其他都不必变(当然为了效果更明显, 可以把背景图显示改为固定位置绘制), 修改后代码如下:
-- 增加移动的背景图: + 为右移,- 为左移
-- sprite("Documents:bgGrass",(WIDTH/2+10 * s * m.i)%(WIDTH),HEIGHT/2)
-- sprite("Documents:bgGrass",(WIDTH+10 * s * m.i)%(WIDTH),HEIGHT/2)
sprite("Documents:bgGrass",WIDTH/2,HEIGHT/2)
这两种方式各有利弊, 我们可以根据实际需要进行选择.
所有章节链接
Github项目地址
Github项目地址, 源代码放在 src/
目录下, 图片素材放在 assets/
目录下, XCode
项目文件放在 MyAdventureGame
目录下, 整个项目文件结构如下:
Air:Write-A-Adventure-Game-From-Zero admin$ tree
.
├── MyAdventureGame
│ ├── Assets
│ │ ├── ...
│ ├── Libs
│ │ ├── ...
│ ├── MyAdventureGame
│ │ ├──...
│ ├── MyAdventureGame.codea
│ │ ├──...
│ ├── MyAdventureGame.xcodeproj
│ │ ├──...
│ └── libversion
├── README.md
├── Vim 列编辑功能详细讲解.md
├── assets
│ ├── ...
│ └── runner.png
├── src
│ ├── c01.lua
│ ├── c02.lua
│ ├── c03.lua
│ ├── c04.lua
│ ├── c05.lua
│ ├── c06-01.lua
│ ├── c06-02.lua
│ ├── c06-03.lua
│ └── c06.lua
├── 从零开始写一个武侠冒险游戏-0-开发框架Codea简介.md
├── 从零开始写一个武侠冒险游戏-1-状态原型.md
├── 从零开始写一个武侠冒险游戏-2-帧动画.md
├── 从零开始写一个武侠冒险游戏-3-地图生成.md
├── 从零开始写一个武侠冒险游戏-4-第一次整合.md
├── 从零开始写一个武侠冒险游戏-5-使用协程.md
├── 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1).md
├── 从零开始写一个武侠冒险游戏-6-用GPU提升性能(2).md
└── 从零开始写一个武侠冒险游戏-6-用GPU提升性能(3).md
2 directories, 26 files
Air:Write-A-Adventure-Game-From-Zero admin$