从零开始写一个武侠冒险游戏-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")endfunction 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: 要拷贝图像区域的高度

更多的函数说明文档请参考:

Codea官方标准函数参考
Codea官方Wiki函数参考

绘制一个子帧

很好, 现在我们从素材中取出第一个子画面, 也就是左下角坐标为(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)...
endfunction 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

接下来就是关键 ,把所有这些子画面连续显示到屏幕上, Codeadraw() 函数每秒钟会自动执行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 thenprevTime = prevTime + 0.1k=math.fmod(i,8)i=i+1
endsprite(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 doimgs[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 doimgs[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 doimgs[i] = img:copy(table.unpack(pos[i]))
end

改写为类

为了方便使用, 另一方面也让程序主框架看起来清爽一些, 我们可以把上述实现帧动画的代码封装成一个类 Sprites, 具体来说就是把初始化的代码放在 Sprites:init() 函数中, 把实际绘制的代码放在 Sprites:draw() 函数中, 代码如下:

Sprites = class() function Sprites:init(x,y,img,pos)self.x = xself.y = yself.index = 1self.img = imgself.imgs = {}self.pos = posself.i=0self.k=1self.q=0self.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]))endprint(#self.imgs)
endfunction Sprites:draw()-- 确定每帧子画面在屏幕上停留的时间if ElapsedTime > self.prevTime + 0.1 thenself.prevTime = self.prevTime + 0.1    self.k = math.fmod(self.i,#self.imgs) self.i = self.i + 1      endself.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)endfunction draw()background(39, 44, 39, 255)m:draw()-- m1:draw()
end

现在的代码

最新版本的代码

-- 帧动画对象类Sprites = class() function Sprites:init(x,y,img,pos)self.x = xself.y = y-- self.index = 1self.img = imgself.imgs = {}self.pos = posself.i=0self.k=1self.q=0self.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]))endendfunction Sprites:deal()---[[ 对原图进行预处理,把背景修改为透明,现存问题:角色内部有白色也会被去掉local v = 255for x=1,self.img.width dofor 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 thenif r == v and g == v and b == v and a == v thenself.img:set(x,y,r,g,b,0)endendend--]]
endfunction Sprites:draw()-- 确定每帧子画面在屏幕上停留的时间if ElapsedTime > self.prevTime + 0.08 thenself.prevTime = self.prevTime + 0.08 self.k = math.fmod(self.i,#self.imgs) self.i = self.i + 1-- self.x = self.x + 1    endself.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,43pos1 = {{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,1024pos2 = {{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)
endfunction 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 dofor x=1,imgs[i].width dofor y =1, imgs[i].height dor,g,b,a = imgs[i]:get(x,y)if r == 255 and g == 255 and b == 255 thenimgs[i]:set(x,y,r,g,b,0)endendendend--]]

等等, 为什么不直接对整个素材图像进行处理呢, 这样还可以少一个循环, 如下:

    ---[[ 对原图进行预处理,把背景修改为透明for x=1,img.width dofor y =1, img.height do-- 取出所有像素的颜色值r,g,b,a = img:get(x,y)-- if r >= 205 and g >= 205 and b >= 205 thenif r == 255 and g == 255 and b == 255 thenimg:set(x,y,r,g,b,0)endendend--]]

这两段代码最终效果是一样的, 不过后一种效率更高, 因为它集中处理整副素材图, 少了一重循环.

因为这段代码只需要执行一次即可, 所以我们把它放在 setup() 函数中, 看看效果, 果然感觉好多了, 虽然边缘部分看起来清除得不是那么好.

最终我们会把这个函数整合到帧动画类中, 作为一个方法, 代码如下:

function Sprites:deal()---[[ 对原图进行预处理,把背景修改为透明for x=1,self.img.width dofor 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 thenif r == 255 and g == 255 and b == 255 thenself.img:set(x,y,r,g,b,0)endendend
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...
endfunction 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)...
endfunction 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 = -1elseif touch.x < WIDTH/2 then s = 1end
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 thenself.prevTime = self.prevTime + 0.08 self.k = math.fmod(self.i,#self.imgs) self.i = self.i + 1self.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).md2 directories, 26 files
Air:Write-A-Adventure-Game-From-Zero admin$ 

转载于:https://www.cnblogs.com/freeblues/p/5732478.html

从零开始写一个武侠冒险游戏-2-帧动画相关推荐

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

    2019独角兽企业重金招聘Python工程师标准>>> 从零开始写一个武侠冒险游戏-3-地图生成 概述 前面两章我们设计了角色的状态, 绘制出了角色, 并且赋予角色动作, 现在是时候 ...

  2. 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)

    从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...

  3. 从零开始写一个武侠冒险游戏-8-用GPU提升性能(3)

    从零开始写一个武侠冒险游戏-8-用GPU提升性能(3) ----解决因绘制雷达图导致的帧速下降问题 作者:FreeBlues 修订记录 2016.06.23 初稿完成. 2016.08.07 增加对 ...

  4. 如何搭建python框架_从零开始:写一个简单的Python框架

    原标题:从零开始:写一个简单的Python框架 Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 你为什么想搭建一个Web框架?我想有下面几个原因: 有一个 ...

  5. mysql c测试程序_Linux平台下从零开始写一个C语言访问MySQL的测试程序

    Linux 平台下从零开始写一个 C 语言访问 MySQL 的测试程序 2010-8-20 Hu Dennis Chengdu 前置条件: (1) Linux 已经安装好 mysql 数据库: (2) ...

  6. dotnet 从零开始写一个人工智能 从一个神经元开始

    现在小伙伴说的人工智能都是弱智能,可以基于神经网络来做.而神经网络是有多层网络,每一层网络都有多个神经元.那么最简单的神经网络就是只有一层,而这一层只有一个神经元,也就是整个神经网络只是有一个神经元. ...

  7. 用python从零开始写一个注册机(新手也能操作)-前言

    今天开始带领大家从零开始写一个网站的账号注册机,达到批量注册的目的. 涉及到的相关知识包含: python的基本使用 playwright库的相关用法 验证码的识别 欢迎大家关注.

  8. 从零开始写一个抖音App——Apt代码生成技术、gradle插件开发与protocol协议

    1.讨论--总结前两周评论中有意义的讨论并给予我的解答 2.mvps代码生成原理--将上周的 mvps 架构的代码生成原理进行解析 3.开发一款gradle插件--从 mvps 的代码引出 gradl ...

  9. 从零开始写一个图像处理程序之四(Prewitt 算子分析)

    上一篇: 从零开始写一个图像处理程序之三(卷积原理简介)_星空_MAX的博客-CSDN博客 具体看看卷积核为什么能够得到图像信息呢 先看Prewitt算子的结构 分别过滤出来横向边缘和纵向边缘: 拿G ...

最新文章

  1. windows10+Python3.7安装dlib库进行面部标志识别
  2. 恭喜CocoStudio 1.5和Mac版本发布
  3. C/S和B/S两种架构区别与优缺点分析
  4. DL之DCGNN:基于TF利用DCGAN实现在MNIST数据集上训练生成新样本
  5. 软件系统架构~软件架构概念
  6. 河神,不用砍死那个天秤座的男孩了
  7. 95-270-019-源码-指标监测-常用监控指标
  8. NYOJ-区域赛系列一多边形划分(贪心)
  9. VB.NET 教程_04_高级教程
  10. 香槟分校计算机研究生专业,伊利诺伊大学香槟分校计算机科学专业各大方向介绍...
  11. STL源码剖析(四):容器(6)Rb_tree
  12. linux+创建一个v文件共享,win10与Ubantu双系统:Linux下开启FTP服务器与创建无线热点(实现文件共享)...
  13. 导出excel poi
  14. C语言用循环写出新年祝福语图案,如何用C语言写新年祝福
  15. conda 克隆环境及导入新环境/conda环境移植
  16. 网闸——物理隔离网闸常见技术问题解答
  17. scada组态开源java_RapidScada免费开源Scada组态软件系列教程6-二次开发
  18. violinplot如何看懂_如何看懂电工图纸
  19. 单反相机手动拍摄技巧
  20. clickhouse-jdbc-bridge

热门文章

  1. Bespin Global武文广:企业数字化转型进入深水区,Cloud IT成为云MSP发展新方向
  2. Dalvik与ART的GC调试
  3. 超200家上市企业布局!从千余条备案信息看区块链产业
  4. excel表格 固定表头 表格主体滚动 动画制作
  5. bluestacks 爬虫_如何将Android Studio连接到Bluestacks
  6. android 隐藏功能介绍,安卓手机隐藏的功能介绍,有没有你不知道的?
  7. 图像变换——向前映射和向后映射
  8. 基于HTML开发外观漂亮大气的APP下载页源码分享
  9. netty实现多协议,多编解码器
  10. 小结:PCB板材品质,适用范围