本章将介绍一下Cocos2d-x
中的多媒体部分,包括:动画(Action、粒子动画、骨骼动画)、滤镜、音视频播放等等。
第一节 动画
动作
游戏的世界是一个动态的世界,无论是主角精灵还是NPC
精灵都处于不断的运动当中,甚至是背景中漂流的树叶,随风而动的小草。这些明显的或者不明显的运动构成了我们栩栩如生的游戏世界。
而精灵移动的动态效果(缩放
、闪烁
和旋转
等),它们每一种效果都可以看成是精灵的一个Action
(动作)。从技术上来说,Action
的本质就是改变某个图形对象的位置,角度,大小等属性。
Action
和之前介绍的Node
等显示对象不同,Cocos2d-x
的动作类Action
并不是一个在屏幕中显示的对象,动作必须要依托于Node
类及它的子类的实例才能发挥作用。
Action
类的继承关系如下图所示:

Action
类是所有动作类的基类,我们后面将要学习到的所有动作类都是它的子类。而且Cocos2d-x
提供的动作,并不是只有Sprite
可以使用,只要是Node
对象都是可以进行动作操作的。
从上图可以看出,Action
类有四个子类:
- FiniteTimeAction:有限次动作执行类。即按时间顺序执行一系列动作,执行完后动作结束。
- Speed:调整实体(节点)的执行速度。
- Follow:可以使节点跟随指定的另一个节点移动。
其中FiniteTimeAction
又有两个子类:延时动作和瞬时动作。
- ActionInterval(延时动作):执行需要一定的时间(或者说一个过程,如5秒内移动到指定位置、3秒内旋转360度等)。
- ActionInstanse(瞬时动作):跟ActionInterval主要区别是没有执行过程,动作瞬间就执行完成了(如让Node隐藏和显现等)。
范例1:在显示对象上运行一个动作很简单。1
2
3
4
5
6
7
8function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
-- 用0.3秒的时间,将node从当前位置移动到100, 200
node:runAction(cc.MoveTo:create(1.3, cc.p(100, 200)))
end
语句解释:
- 本范例中的runAction方法用来执行一个动作,它定义在Node类中,因此Node的各个子类都可以直接调用该方法。
- MoveTo的具体语法后面会介绍,现在只需要知道它代表一个移动动作,也就是说可以让执行它的Node对象移动到指定的位置上。
范例2:如果连续调用多次runAction()
,可以实现并列执行多个动作,例如,移动的同时旋转。1
2
3
4
5
6
7
8
9function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
node:runAction(cc.MoveTo:create(1.3, cc.p(100, 200)))
-- 1.3秒内旋转360度
node:runAction(cc.RotateBy:create(1.3, 360))
end
语句解释:
- RotateBy的具体语法后面会介绍,现在只需要知道它代表一个旋转动作,也就是说可以让执行它的Node对象旋转指定的度数。
范例3:查询显示对象上当前有多少个动作在执行。1
2
3
4
5
6
7
8
9
10function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
node:runAction(cc.MoveTo:create(1.3, cc.p(100, 200)))
node:runAction(cc.RotateBy:create(1.3, 360))
-- 输出2
print(node:getNumberOfRunningActions())
end
动作在执行时间结束后,会自动停止。但我们也可以在需要的时候停止正在执行的动作。
范例4:停止Action。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23local action = cc.MoveTo:create(1.3, cc.p(100, 200))
node:runAction(action)
-- 在需要停止的时候
action:stop()
-- 或者写为
node:stopAction(action)
-- 通过 tag 停止指定的动作
local action = cc.MoveTo:create(1.3, cc.p(100, 200))
action:setTag(1)
node:runAction(action)
-- 在需要停止的时候
node:stopActionByTag(1)
-- 或者写为
local action = node:getActionByTag(1)
action:stop()
-- 停止所有正在执行的动作
local action1 = cc.MoveTo:create(1.3, cc.p(100, 200))
local action2 = cc.RotateBy:create(1.3, 360)
node:runAction(action1)
node:runAction(action2)
node:stopAllActions()
本节参考阅读:
ActionInterval
根据官网的类结构图可以看出,ActionInterval
的子类有很多,本节将会详细介绍开发中常用的几种Action
。
范例1:MoveTo与MoveBy。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
node:runAction(cc.MoveBy:create(1.3, cc.p(100, 200)))
end
语句解释:
- MoveTo表示“移动”,它会将Node在指定的时间内,从当前位置移动到指定的位置。
- MoveBy的用法与MoveTo完全一致,不同的是,MoveBy是将Node从当前位置开始,在x和y轴方向上偏移指定位置。
范例2:ScaleTo与ScaleBy。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
-- 让Node在1.5秒内宽度和高度放大到2倍
node:runAction(cc.ScaleTo:create(1.5, 2, 2))
end
语句解释:
- 与MoveTo、MoveBy的用法类似。
- 构造方法:create(持续时间, x轴放大倍数, y轴放大倍数)。
范例3:RotateTo与RotateBy。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
-- 让Node在1.5秒内顺时针旋转180度
node:runAction(cc.RotateTo:create(1.5, 180))
end
语句解释:
- 构造方法:create(持续时间, 旋转的度数)
- 对于RotateTo来说:
- 度数为正数,则顺时针方向旋转,反之逆时针方向。
- 每跨度180度,则按照相反的方向旋转,如设置旋转190度,则最终看到的效果,只会逆时针旋转170度(即360-190)。
- 对于RotateBy来说:
- 度数为正数,则顺时针方向旋转,反之逆时针方向。
- 设置的度数就是真正旋转的度数。 如设置360度,则节点就会顺时针旋转360度。
范例4:JumpTo与JumpBy。1
2
3
4
5
6
7
8
9
10
11
12
13
14function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
-- 让Node在1.5秒内从当前位置跳向(300, 300)的位置,每次跳跃高度为50,一共跳跃5次。
node:runAction(cc.JumpTo:create(1.5, cc.p(300, 300), 50, 5))
-- 向x方向跳动300像素,跳跃高度为50,跳跃次数为4。
-- node:runAction(cc.JumpBy:create(1.5, cc.p(300, 0), 50, 4))
-- 原地跳动,高度80,次数为4。
-- node:runAction(cc.JumpBy:create(1.5, cc.p(0, 0), 80, 4))
end
语句解释:
- 构造方法:create(持续时间, 目标位置, 跳跃高度, 跳跃次数)
- 如果目标位置和节点当前所在的位置不一致,则节点会“跳跃的同时并移动”到目标位置。
范例5:BezierTo与BezierBy。1
2
3
4
5
6-- 当游戏需要做抛物线效果时,会使用到贝塞尔曲线。
-- 参考阅读:
-- http://zh.wikipedia.org/wiki/%E8%B2%9D%E8%8C%B2%E6%9B%B2%E7%B7%9A
-- http://bbs.9ria.com/thread-216954-1-1.html
-- http://bbs.csdn.net/topics/390358020
范例6:Blink、FadeIn、FadeOut。1
2
3
4
5
6
7
8
9
10-- 让Node在1秒内闪烁2次
node:runAction(cc.Blink:create(1, 2))
-- 先设置node为完全透明
node:setOpacity(0)
-- 然后再执行动作,让Node在1秒内变为完全不透明
node:runAction(cc.FadeIn:create(1))
-- 让Node在1秒内变为完全透明
node:runAction(cc.FadeOut:create(1))
范例7:TintTo与TintBy。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
-- 让Node在2秒内与指定的颜色值进行混合,即对Node进行调色。后面三个参数分别表示要上色的rgb值
node:runAction(cc.TintTo:create(2, 255, 0, 255))
end
语句解释:
- 构造方法:create(持续时间, r, g, b)
范例8:Sequence与reverse。1
2
3
4
5
6
7
8
9function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local action1 = cc.MoveBy:create(1, cc.p(80, 80))
local action2 = action1:reverse()
node:runAction(cc.Sequence:create(action1, action2))
end
语句解释:
- 使用Sequence用来创建一个sequence,在sequence中定义的Action会按照先后顺序依次被执行,即前一个没执行完之前,后一个不会被启动。
- reverse可以创建出一个反转的action,即该action会从原来action的终点状态向起点状态执行。
- Sequence也有reverse方法。
范例9:Repeat与RepeatForever。1
2
3
4
5
6
7
8
9function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local action1 = cc.MoveBy:create(1, cc.p(80, 80))
local action2 = action1:reverse()
node:runAction(cc.Repeat:create(cc.Sequence:create(action1, action2), 3))
end
语句解释:
- Repeat用来重复执行一个Action(可以是普通的action也可以是sequence)。
- 构造方法:Repeat:create(action, 重复次数)
- 而RepeatForever可以用来无限执行一个action。构造方法:RepeatForever:create(action)
范例10:运行多个动作。1
2
3
4
5
6
7
8
9
10
11function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.MoveBy:create(1, cc.p(80, 80))
actions[2] = actions[1]:reverse()
actions[3] = cc.JumpBy:create(1, cc.p(0, 0), 50, 3)
node:runAction(cc.RepeatForever:create(cc.Sequence:create(actions)))
end
语句解释:
- 使用Lua中的表来创建一个数组,然后将多个动作放入数组中,最后使用这个数组来创建一个Sequence对象。
- Sequence中的各个动作按照它们在数组中的顺序来执行。
范例11:DelayTime。1
2
3
4
5
6
7
8
9
10
11
12function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.MoveBy:create(1, cc.p(80, 80))
actions[2] = actions[1]:reverse()
-- 等待2秒
actions[3] = cc.DelayTime:create(1)
node:runAction(cc.RepeatForever:create(cc.Sequence:create(actions)))
end
语句解释:
- 构造方法:create(等待时间)。
范例12:Spawn。1
2
3
4
5
6
7
8
9
10
11
12function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.MoveBy:create(1, cc.p(40, 0))
actions[2] = cc.MoveBy:create(1, cc.p(0, -60))
actions[3] = cc.JumpBy:create(2, cc.p(0, 0), 50, 4)
actions[4] = cc.RotateBy:create(2, 720)
node:runAction(cc.RepeatForever:create(cc.Spawn:create(actions)))
end
语句解释:
- Spawn和Sequence相反,它会同时执行其内的所有Action,而不是依次执行。
范例13:OrbitCamera。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
node:runAction(cc.RepeatForever:create(cc.OrbitCamera:create(1, 1, 0, 0, 180, 0, 0)))
end
语句解释:
- ActionCamera及其子类(OrbitCamera)封装了摄像机相关的动作。
- OrbitCamera使用球坐标系围绕节点中心旋转摄像机的视角。简单的说,它可以用3d的方式360度无死角的观察节点,以产生节点被旋转的效果,而Rotate只可以平面方式旋转节点。
- 构造方法:create(旋转的时间、起始半径、半径差、起始z角、旋转z角差、起始x角、旋转x角差)
- OrbitCamera通常用来实现翻牌效果。
范例14:创建副本。1
2
3
4
5
6
7
8
9
10
11function MainScene:ctor()
local node = display.newSprite("icon.png", display.cx+50,display.cy+50)
local node2 = display.newSprite("icon.png", display.cx, display.cy)
self:addChild(node)
self:addChild(node2)
local action1 = cc.RepeatForever:create(cc.OrbitCamera:create(1, 1, 0, 0, 180, 0, 0))
local action2 = action1:clone()
node:runAction(action1)
node2:runAction(action2)
end
语句解释:
- 在Cocos2dx中,经常需要将一个action施加到多个Sprite上面,以达到相同的效果。
- 但是一个action对应不能直接施加到多个Sprite上面去,只有通过调用action的clone()方法创建一个副本。
范例15:TargetedAction。1
2
3
4
5
6
7
8
9
10
11
12function MainScene:ctor()
local node = display.newSprite("icon.png", display.cx+50,display.cy+50)
local node2 = display.newSprite("icon.png", display.cx, display.cy)
self:addChild(node)
self:addChild(node2)
local action1 = cc.JumpBy:create(1, cc.p(0,0), 50, 3)
local actions = {}
actions[1] = action1
actions[2] = cc.TargetedAction:create(node, action1:clone())
node2:runAction(cc.RepeatForever:create(cc.Sequence:create(actions)))
end
语句解释:
- 通常默认的动作执行对象是调用runAction的对象,而TargetedAction可以改变动作执行对象。
- 在本范例中,node2跳跃完毕后,node会接着跳跃。
速度控制
接下来介绍如何使用ActionEase
类及其子类进行动画速度控制。
所谓的动画速度控制就是控制动画在什么时候播放的快一些,什么时候播放的慢一些。但这只是改变了Action
在某一时刻的运动速度,并没有改变总体时间。如果整个动作会持续5s
,那么最终整个Action
播放完毕的时间仍然是5s
。
ActionEase
的子类按照速度控制的类型,可以分成三类:
- In动作:开始的时候加速。 比如:CCEaseIn、CCEaseSineIn等。
- Out动作:快结束的时候加速。 比如:CCEaseOut、CCEaseSineOut等。
- InOut动作:开始和快结束的时候加速。 比如:CCEaseInOut等。
范例1:线性变化。1
2
3
4
5
6
7
8
9
10function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.EaseIn:create(cc.MoveBy:create(1, cc.p(300, 0)), 1.5)
actions[2] = cc.EaseOut:create(cc.MoveBy:create(1, cc.p(-300, 0)), 1)
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- EaseIn:由慢至快(速度线性变化),在开始时慢。
- EaseOut:由快至慢,后来慢。
- EaseInOut:由慢至快再由快至慢,开始时和后来慢。
- 创建动作时,第二个参数表示速率, 决定速度变化的快慢。设置为1则保持正常速度,值越大越慢振幅越小。
范例2:指数变化。1
2
3
4
5
6
7
8
9
10function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.EaseExponentialIn:create(cc.MoveBy:create(1, cc.p(300, 0)))
actions[2] = cc.EaseExponentialOut:create(cc.MoveBy:create(1, cc.p(-300, 0)))
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- EaseExponentialIn:由慢至极快(速度指数级变化)。
- EaseExponentialOut:由极快至慢。
- EaseExponentialInOut:由慢至极快再由极快至慢。
- 构造指数变化的动作时,只需要指定一个参数即可。
范例3:正弦变化。1
2
3
4
5
6
7
8
9
10function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.EaseSineIn:create(cc.MoveBy:create(1, cc.p(300, 0)))
actions[2] = cc.EaseSineOut:create(cc.MoveBy:create(1, cc.p(-300, 0)))
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- EaseSineIn:由慢至快(速度正弦变化)。
- EaseSineOut:由快至慢。
- EaseSineInOut:由慢至快再由快至慢。
范例4:弹性变化。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
node:runAction(cc.EaseElasticIn:create(cc.RotateBy:create(1.5, 180)))
end
语句解释:
- EaseElasticIn:先左右弹动一下,然后再播放动作。
- EaseElasticOut:先播放动作,在结束的时候,弹动一下。
- EaseElasticInOut:开始和快结束的时候,都会弹动一下。
- 动作的效果类似于弹了一下一根绷紧的橡皮筋。
范例5:跳跃变化。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
node:runAction(cc.EaseBounceOut:create(cc.MoveBy:create(1, cc.p(300, 0))))
end
语句解释:
- 可以理解为一个乒乓球从空中落地之后在地上弹跳的情况。
范例6:回退变化。1
2
3
4
5
6
7function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
node:runAction(cc.EaseBackIn:create(cc.MoveBy:create(1, cc.p(300, 0))))
end
语句解释:
- EaseBackIn:先后撤一下,然后再播放动作。
- EaseElasticOut:先播放动作,动作会超过指定的值一点,然后再还原为指定值。
- EaseElasticInOut:开始和快结束的时候,都会后撤一下。
本节参考阅读:
ActionInstanse
范例1:Place。1
2
3
4
5
6
7
8
9
10
11function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.DelayTime:create(2)
actions[2] = cc.Place:create(cc.p(200,200))
actions[3] = cc.MoveBy:create(1, cc.p(300, 0))
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- 构造方法:create(位置)。
- Place用来将节点放置指定位置,它与修改节点的position属性相同。
- 使用Place动作与直接设置结点的position属性的区别在于它可以作为Action被放入到一个Sequence中。
范例2:Show、Hide。1
2
3
4
5
6
7
8
9
10
11
12function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.DelayTime:create(2)
actions[2] = cc.Hide:create()
actions[3] = cc.DelayTime:create(2)
actions[4] = cc.Show:create()
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- Hide用来隐藏结点。Show用来显示节点。
范例3:FlipX、FlipY。1
2
3
4
5
6
7
8
9
10
11
12function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.DelayTime:create(2)
actions[2] = cc.FlipX:create(true)
actions[3] = cc.DelayTime:create(2)
actions[4] = cc.FlipY:create(true)
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- FlipX设置是否在水平方向上,反转节点。
- FlipY设置是否在垂直方向上,反转结点。
范例4:CallFunc。1
2
3
4
5
6
7
8
9
10
11
12
13function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local function f(p1)
print(p1 == node)
end
local actions = {}
actions[1] = cc.DelayTime:create(2)
actions[2] = cc.CallFunc:create(f)
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- CallFunc动作,用来调用一个函数,同时会将运行当前Action的Node作为参数传递过去。
范例5:ToggleVisibility。1
2
3
4
5
6
7
8
9
10
11
12
13function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local actions = {}
actions[1] = cc.MoveBy:create(1, cc.p(200, 0))
actions[2] = cc.ToggleVisibility:create()
actions[3] = cc.MoveBy:create(1, cc.p(0, 300))
actions[4] = actions[2]
actions[5] = cc.MoveBy:create(1, cc.p(-200, 0))
node:runAction(cc.Sequence:create(actions))
end
语句解释:
- ToggleVisibility用来反向设置节点是否可见,即如果当前节点可见,则执行此Action后,会变为不可见。
Follow
Follow可以使节点跟随指定的另一个节点移动。
范例1:Follow。1
2
3
4
5
6
7
8
9
10
11
12function MainScene:ctor()
local map = display.newSprite("background.png")
local node = display.newSprite("icon.png")
node:center()
map:center()
map:setScale(3)
self:addChild(map)
self:addChild(node)
map:runAction(cc.Follow:create(node))
node:runAction(cc.RepeatForever:create(cc.MoveBy:create(1, cc.p(100,0))))
end
语句解释:
- Follow动作,可以让一个节点跟随另一个节点做位移,本范例中让map跟随node。
- 它有两个静态方法,后者可以设置一个跟随范围,离开范围就不再跟随。
- Follow:create (Node pFollowedNode)
- Follow:create (Node pFollowedNode, rect)
- Follow经常用来设置layer跟随sprite,可以实现类似摄像机跟拍的效果。
Speed
动画是游戏的必然要素之一,在整个游戏过程中,又有着加速、减速动画的需求。以塔防为例子,布塔的时候希望能够将游戏减速,布好塔后,则希望能将游戏加速;当某个怪被冰冻后,移动速度减缓,而其他怪的移动速度不变。
范例1:Speed。1
2
3
4
5
6
7
8
9
10
11
12
13
14function MainScene:ctor()
local node = display.newSprite("icon.png")
node:center()
self:addChild(node)
local speed = cc.Speed:create(cc.RotateBy:create(5, 360), 1)
node:runAction(speed)
self:performWithDelay(function ()
speed:setSpeed(0.3)
end, 1)
self:performWithDelay(function ()
speed:setSpeed(1)
end, 3)
end
语句解释:
- Speed用来设置动作的执行速度,构造方法:create(动作, 速率)。
- 其中“速率”指执行的倍数,即如果一个动作需要5秒完成,设置Speed的第二个参数为2后,只需要2.5秒即可完成。
- 你可以在Speed执行的中途,通过调用Speed的setSpeed方法来动态调节播放速度,以此来达到慢镜头的效果。当把speed设置为0时Action将会暂停执行。
粒子动画
引言
Cocos2d-x
引擎提供了强大的粒子系统,它在模仿自然现象、物理现象及空间扭曲上具备得天独厚的优势,为我们实现一些真实自然而又带有随机性的特效(如爆炸、烟花、水流)提供了方便。
尽管如此,它众多的粒子属性还是着实让人头疼。
因为如果要想自己编码写出炫丽的粒子效果,这里有太多的属性需要手动设置和调节。不管是对新手还是资深的老油条程序员来说,都存在不同程度的不便性。
第二节 暂停更新
现在是2015.1.8日,我此刻所在的公司目前唯一的一款游戏是基于quick
的2.2.5
的版本开发的,而截止至今天,quick
现在已经发展到了quick3.3 final
版。
从现在的情况来看,公司短期内似乎并不打算再研发另一款游戏,所以为了不让自己所学的东西变得毫无用处,笔者决定在2015
年农历春节来临之前,对自己今年所学到的知识做一个总结,把原来针对quick2.2
所写的教程改为现在的针对quick3.3 rc1
版本的,并将它放到博客中。
从实际开发的角度来看,只掌握这六篇文章所介绍的知识那是远远不够的,由于时间关系(也是因为自己没研究透,也不想去研究了)游戏开发中会涉及到的滤镜
、媒体播放
、状态机
、数据存储
、网络请求
、瓦片地图
、粒子动画
、骨骼动画
、3D精灵
等等技术都没有写出来。但掌握了这六篇文章所介绍的知识后,也算是在游戏开发上面入门了,之后所要学的虽然有很多,但是兵来将挡,没有学不会的技术。
笔者下一步的打算是,把精力暂时放回到Android
开发上面,之后会陆续发布Android
相关的博文,以后也许还会继续回来做游戏开发。
最后,在此特别感谢廖大、蓝大的quick
团队,为我们游戏开发者做出无私奉献,也感谢那些无私奉献的博主们,前人为我们铺平了道路,我能做的就是让后来者能再少走点弯路。