You are here:
独立开发者:新手做2D手游该用哪些工具
【GameLook专稿,转载请注明出处】
GameLook报道/随着全球手游行业规模将突破250亿美元,越来越多的开发者开始进入手游研发领域,而作为一名菜鸟,很多时候,如果没有其他开发者的建议,我们会走很多弯路,最近独立工作室Sheado.net公司的Chad Ata在博客中分享了他们从一开始进入手游研发到如今四年多以来积累的经验,希望可以给新入行者提供一些帮助。以下是GameLook编译的博客内容:
一开始进入游戏研发领域的时候,你很难知道该选择什么工具、什么程序语言以及哪些框架,你会面临许许多多的选择和建议,我和我的团队总能发现其他游戏公司的经验是有用的,所以这里提供一些我们的经验,希望给做手游的新手们一些帮助。
初入手游行业
虽然在其他行业有过10多年的编程和策划经验,但当我开始做手游研发的时候,依然觉得自己是一只菜鸟。我们的第一个游戏Furdiburb(宠物冒险游戏)最初是在2009年开始研发的,当时是专门为Android而做的。对于毫无游戏研发经验的我们来说,Java是唯一可用到编程语言。作为一个2D游戏,我们(非常不明智)没有使用OpenGL就开始非硬件加速图形开发。随着Furdi受到了更多人的欢迎,我们的游戏项目也得到更多的注意,最终我们遇到了性能和移植问题。如果我们此前学习了其他开发者的经验,很多问题都是可以完全避免的。
找到更好的方法
在完成了Furdiburb的研发,并且使用playn缓慢的把游戏移植到了iOS平台之后,我们决定开始第二款游戏(Eras of Alchemy)的研发。我们当时希望摆脱Java语言,找到可以广泛使用的跨平台研发工具,最好是未来还可以支持主机游戏平台。
随后我们开始了搜索,我用了将近一个月的时间对框架、工具以及引擎进行对比,最后,我和我的团队非常满意新的研发方式,直到现在也非常不错。这里我不会说为什么要选择这些,框架是经常变化的,一年前选择它的理由可能到现在就已经不再是考虑的主要因素了。所以,这篇文章的其余部分只会对我们选择某个工具的原因进行简单的描述。以下就是我们当初选择并对比了一个月之后的结果:
我们所列举的都是在研发我们游戏的时候用到的,而且只是我们做游戏的方式而已,很多工具和框架的结合也是非常完美的,需要开发者们自行发现。
我们选择的所有架构,要么是开源的,要么就是有开放的代码。这是非常好的,因为我们在必要的时候可以进行紧急修改和优化。
Cocos2d-x:我们所有的跨平台研发都是使用开源Cocos2d-x框架完成,在Eras Of Alchemy的研发过程中,我们使用了版本2,我们的下一个游戏正在使用的是版本3,进行了大幅度的API以及性能提升。整体来说,Cocos2d-x的2D表现非常好,而且文件管理非常方便。
Spine:我们使用Spine做了动画,所以我们加入了C语言为基础的Cocos2d-x插件。
Box2D:我们还没有发布一款使用该引擎的游戏,但我们的下一款游戏A Quiver Of Crows将会使用,我们目前研发就使用了这个工具。
SQLite:Cocos2d-x本身也提供数据存储方案,但我们更喜欢使用SQLite,因为它的读写速度和表现更好。
C++:Cocos2d-x支持多种语言编程,但我们选择了C++,因为我们觉得目前该语言是最合适做跨平台研发的。
C:你经常会想要加入一些开源的API,其中有一些就是C语言编程的。
Java:我们依然需要用到Java,但也只是在Android平台做游戏内IAP、广告以及特殊系统功能的时候。语言之间的切换可以通过JNI来完成。
Objective-C:选择它的理由和Java一样,我们使用Objective-C是为了使用iOS系统的特定功能。
Scripting:这包括程序化脚本和其他脚本语言,所有的开发者们都要时不时的写脚本,但我们却很少会谈论这个问题。这个问题是非常容易的,但如果你和我一样而且由于不常使用而不记得一些语法的话,这个工作有是非常耗时间的,我们要给维护代码、自动音频转换以及纹理打包写脚本。
Xcode:所有人都告诉我们说Xcode非常好,所以我进行了尝试,而且我不得不说的是,这是目前我最喜欢的开发环境。这里我并不想说太多具体的原因,因为我不想引发集成开发环境(IDE)争论,我们使用Xcode做跨平台研发,也为苹果平台做专门的编程。
Eclipse:对于IDE来说慢的可怕,但我看来却非常好用。我们用Eclipse做了所有的Java和Android研发,包括适配和修复bug。Android目前在推Android Studio而不是Eclipse,但我们没有那么多的时间,也没有什么特别的理由去转换到新的工具。
Visual Studio:也是个非常优秀的IDE,我们用它来做左右和微软相关的编程、Bug修复以及适配。
即便你的团队只有一个人,你也应该使用版本控制。所有人都会犯错,而且任何一次大改都可能导致游戏神秘的死亡。我们的团队只有3个人,因此从第一天做手游开始,版本控制就是非常必要的。目前有非常多的方案可以选择,但我们使用的有以下几个:
SVN:我个人喜欢SVN,因为可以做到所有我需要的功能,比如合并、同步、恢复等等,但学习起来比较困难。
Git:我们使用的很多开源框架都使用Git。我们使用Git就是为了保持与框架同步,当需要的时候可以进行快速修复。
很明显,你做游戏是需要使用电脑的。最初所有的研发都是在Linux机器上完成的。但我们开始了iOS平台的研发之后,用两三台电脑变得效率非常低,所以我们买了一些iMac,而且我们都非常喜欢用它来做游戏研发。幸运的是,OSX的很多指令与Linux相同,所以我们的很多脚本都没有做改变。
Adobe CS:相信这个没有人觉得奇怪,我们的美术师最常使用的是Illustrator和Photoshop。
Spine:非常推荐这个工具制作骨骼动画,和传统的帧到帧动画相比,骨骼动画可以节约硬盘空间,还可以节约大量的研发时间,提供强大的功能,比如动画混合、蒙皮技术以及网格变形。
Texture Packer:你或许会想要把图片进行打包获得更大的图像以获得更好的游戏表现。我们选择Texture Packer来完成这项工作,而且我们还使用它的指令功能进行自动化打包处理。
关卡编辑器
我们使用的关卡编辑工具包括:
没有编辑器:如果可以不用的话,我们绝不会使用编辑器。我们可以用代码解决,这听起来非常疯狂,而且有点浪费时间,但如果你的团队非常小的话,有时候为了节约时间可以不必为了一次性的任务专门用代码写一个编辑器。
定制化编辑器:有时候我们写了一个非常不好用的游戏内编辑器来做图形或者关卡,我的意思是未经优化的,恐怕也只有我们会这么做。
R.U.B.E:对于我们的下一个游戏,我们在使用R.U.B.E(Really Useful Box2D Editor)之前,几乎自己研发了一个游戏内编辑器,这个非常强悍的工具节约了我们大量的时间,但如果我们决定要做关卡编辑器的话,我们必须自己研发。
音乐和视频
Ffmpeg:我个人非常喜欢ffmpeg,这个工具非常好用,我们通常使用脚本用它把我们的视频变成各个平台需要的格式。
Cakewalk Sonar:这是个非常强悍的音乐制作软件,一开始的学习会比较困难。
GArritan Personal Orchestra:如果你想给自己的游戏加入管弦乐,Garritan可以带来非常高质量的音乐,我们通常和Sonar混合使用。
Audacity:一个非常不错的视频编辑和录制工具。
以下2个是我们已经不再使用的工具,但可能对于新手来说依然具有推荐意义:
Anvil Studio:如果你熟悉乐器而且乐意学一些音乐知识,并且想要做MIDI格式的音乐,这是个非常不错的软件。
Linux Multaimedia Studio:这是个非常不错而且简单的软件,可以制作非常不错的音乐,而且不需要你阅读很多的音乐知识。
目前做游戏的工具非常多,以上的这些工具只是我们在做2D游戏的时候选择的工具,目前为止,我们对这些工具非常满意,我们最新的游戏发布到了iOS、Android和Windows Phone平台。我们还打算在下一款游戏发布的时候,用同样的工具把游戏扩展到PC、Mac以及Linux平台。
& 2017 . All rights reserved.自制Unity小游戏TankHero-2D(1)制作主角坦克
我在做这样一个坦克游戏,是仿照()这个游戏制作的。仅为学习Unity之用。图片大部分是自己画的,少数是从网上搜来的。您可以到我的github页面()上得到工程源码。
本篇主要记录制作主角坦克(TankHero)的一些重点。
2D游戏布局
如上图所示,红色矩形围起来的是主角坦克,白色的一圈是围墙,坦克和围墙在同一平面上。地面背景放到离摄像机最远的后方。这样,在2D摄像机下看起来是这样的:
坦克本身由底座(Base)和炮塔(Head)两部分组成。当然,在2D世界,其实就是两个扁平的贴图。在2D摄像机下是这样的:
(PS:上图中的绿色矩形框是Box Collider 2D,忽略即可)
为了保证炮塔始终显示在底座上方,我们要让炮塔稍微靠近一点摄像机。如下图所示,炮塔和底座两张贴图是分隔开的。
坦克的运动
坦克的运动包括:上下左右平移;底座旋转;炮塔旋转。其中平移时会同样地移动底座和炮塔,所以用最上层的TankHero负责。底座和炮塔的旋转我们要求两者互不干涉,所以TankHead和TankBase放在同一层,并且分别负责各自的旋转。
坦克的移动十分容易。玩家在纵横方向的按键情况就是坦克的移动方向,速度由程序员指定,再乘上时间就好了。
void Update () {
var h = Input.GetAxis ("Horizontal");
var v = Input.GetAxis ("Vertical");
if (Mathf.Abs(h) & Quaternion.kEpsilon || Mathf.Abs(v) & Quaternion.kEpsilon)
Move (h, v);
void Move(float h, float v)
var moveVector = new Vector3 (h, v, 0);
moveVector.Normalize ();
this.transform.position += moveVector * speed * Time.deltaT
底座应该朝向移动的方向,即上文的&moveVector&。这里用&Quaternion.Slerp&使底座平滑地转向&moveVector&。
void Update () {
var h = Input.GetAxis ("Horizontal");
var v = Input.GetAxis ("Vertical");
if (Mathf.Abs(h) & Quaternion.kEpsilon || Mathf.Abs(v) & Quaternion.kEpsilon)
this.targetAngle = Mathf.Atan2(v, h) * Mathf.Rad2D
//Debug.Log("target angle: " + targetAngle);
this.transform.rotation = Quaternion.Slerp (
this.transform.rotation,
Quaternion.Euler (0, 0, targetAngle),
rotationSpeed * Time.deltaTime);
炮塔要指向鼠标(即目标)所在的位置,所以从炮塔到鼠标的向量就是炮塔的方向。
注意炮塔不是围绕自身的中心旋转的,这个旋转点需要根据坦克的形状来指定。所以这里要用&transform.RotateAround&来进行旋转。
void Update () {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray, out hit))
var p = hit.
var y = p.y - this.transform.position.y;
var x = p.x - this.transform.position.x;
if (Mathf.Abs(y) & Quaternion.kEpsilon || Mathf.Abs(x) & Quaternion.kEpsilon)
this.targetAngle = Mathf.Atan2(y, x) * Mathf.Rad2D
var angle = this.targetAngle - this.transform.rotation.eulerAngles.z;
this.transform.RotateAround (this.rotationCenter.position, new Vector3 (0, 0, 1), angle);
其实这不算是运动了,不过放在这一节也还算紧凑。
坦克移动的时候,我希望车轮下下图所示这样,显得很生动:
我的思路是用4张图片表现车轮滚动的效果,让TankBase负责循环显示这4张图片。
当然,脚本可以处理任意多张图片的循环播放。其关键就是依次将各个BaseSprite的&renderer.enabled&字段设为&true&。
public float interval = 10;
public List&GameObject&
private int current = 0;
private float passedInterval = 0;
// Use this for initialization
void Start () {
if (wheels != null && wheels.Count & 0)
{ wheels[0].renderer.enabled = true; }
for (int i = 1; i & wheels.C i++)
wheels[i].renderer.enabled = false;
// Update is called once per frame
void Update () {
if (wheels == null || wheels.Count & 2) { return; }
var h = Input.GetAxis ("Horizontal");
var v = Input.GetAxis ("Vertical");
if (Mathf.Abs(h) & Quaternion.kEpsilon || Mathf.Abs(v) & Quaternion.kEpsilon)
passedInterval += Time.deltaTime * 100;
//Debug.Log (passedInterval);
if (passedInterval &= interval)
if (current == wheels.Count - 1) { current = 0; }
else { current++; }
wheels[current].renderer.enabled = true;
wheels[tmp].renderer.enabled = false;
passedInterval = 0;
这个游戏中,TankHero能够发射多种炮弹,所以需要有多种武器,每种武器发射一种炮弹。因此炮塔充当了武器管理员的角色,而不是武器本身。一种武器决定了它发射的炮弹的速度、威力等信息。这段话是武器系统的关键。
发射炮弹这种事,典型的方法是用&Instantiate&。这就需要在场景中持有一个现成的炮弹。如下图所示:
这个炮弹要永远存在,还不能被摄像机看到,所以我们把它放到之前说的地面背景的更后面。
你注意到图中的炮弹中心有个比较小的绿色的圈,这个圈是Circle Collider 2D,是用来产生碰撞的。我刻意把这个Collider调到这么小,是为了避免在坦克刚刚发射出炮弹时,炮弹与自身产生碰撞(即自己开炮瞬间打了自己)。
同时,在上图中我用***圈圈出了那个BulletPosition的gameobject,这是专门用来指定炮弹产生点的,也是为了避免炮弹刚刚发射出来就把自己给打了。
注意,带2D的Collider似乎有这样的问题:无论在Z方向上是否在同一Z平面,都能引发碰撞事件。所以,那个永生的炮弹,虽然藏到地面背景后方去了,却仍旧可能与游戏中的其它物体发生碰撞(然后就会爆炸消失被Destroy掉,之后就无法再用Instantiate来创建炮弹了)。为了避免它的&Destroy&,我们需要将它和其它炮弹区别开来,所以就必须给炮弹对象添加一个&undying&字段,让&undying&为true的炮弹在触发了碰撞事件时也不爆炸消失。
摄像机随主角移动
我希望地图能够大一点,所以一屏肯定放不下。所以需要摄像机随主角坦克的移动而移动。这个很容易,不断跟随主角坦克就行了。
public float catchingSpeed = 1;
private Transform tankH
void Awake()
this.tankHero = GameObject.FindGameObjectWithTag (Tags.hero).
void Update () {
var targetPosition = new Vector3 (this.tankHero.position.x, this.tankHero.position.y, this.transform.position.z);
this.transform.position = Vector3.Lerp (this.transform.position, targetPosition, Time.deltaTime * this.catchingSpeed);
注意这里将&catchingSpeed&调低一些,会产生摄像机延迟跟随主角坦克的现象。我很喜欢这种跟随的感觉,柔和不生硬,而且还解决了后文遇到的一个问题。
自定义鼠标样式
我希望鼠标在游戏中显示为下图所示的样子,很带感。
方法有两种。
Default cursor
一是在File & Build Settings & Player Settings打开的Inspector面板中设置Default Cursor。
这个方法有点问题,首先在build之后的exe中你可能发现鼠标彻底消失了,既没有原始图标也没有自定义图标,其次在你修改了自定义图标之后,可能会显示成一个很奇怪的图标,最后,这样自定义的图标,其清晰度大打折扣,其size也是固定的。
所以我推荐另一种方法,即用脚本实现。
典型的实现方式是这样的,在主摄像机上添加一个TargetCusor.cs的脚本(脚本名无所谓),编写代码如下:
//3D贴图是Material,2D贴图是Texture
public Texture CurosrT
void OnGUI() { //
渲染GUI和处理GUI时调用。
if (CurosrTexture != null) {
// 计算图片左上角的坐标
float left = Input.mousePosition.x - CurosrTexture.width / 2;
float top = Screen.height - Input.mousePosition.y - CurosrTexture.height / 2;
GUI.DrawTexture(new Rect(left, top, CurosrTexture.width, CurosrTexture.height), CurosrTexture);
在Inspector面板中指定你的图标即可。
限制坦克和炮弹的活动范围是必须的。这里我暂且简单地制作一个正方形围墙。
这个围墙由四个quad组成。绿色的线条是Box Collider 2D组件。围墙的功能就是把撞上它的东西(坦克、炮弹等)弹回去。这里不得不用一个&Dictionary&Collider2D, Vector3&&字典记录撞到围墙的物体在碰撞瞬间的位置,因为之后要将物体弹回这个位置。
1 public class PushBackToField : MonoBehaviour {
Dictionary&Collider2D, Vector3& initialPositionD// = new Dictionary&Collider, Vector3&();
void Awake()
initialPositionDict = new Dictionary&Collider2D, Vector3& ();
void OnTriggerEnter2D(Collider2D other)
if (initialPositionDict.ContainsKey(other))
initialPositionDict[other] = other.transform.
initialPositionDict.Add(other, other.transform.position);
void OnTriggerStay2D(Collider2D other)
Push (other);
void OnTriggerExit2D(Collider2D other)
if (initialPositionDict.ContainsKey(other))
initialPositionDict.Remove(other);
void Push(Collider2D other)
Vector3 initialPosition = Vector3.
if (initialPositionDict.ContainsKey(other))
initialPosition = initialPositionDict[other];
Debug.LogError(string.Format("{0} should have been added to the dict.", other.gameObject.name));
if ((initialPosition - other.transform.position).magnitude & 0.001f)
//Debug.Log("lerp push");
other.transform.position = Vector3.Lerp(other.transform.position, Vector3.zero, Time.deltaTime);
//Debug.Log("sudden push");
other.transform.position = initialP
这个脚本对上下左右四个围墙都适用,以后有了别的形状的围墙,也仍然适用。这也是它的优点之一。
说到这个围墙的反弹,就涉及摄像机跟随的一个问题。实际上,围墙反弹时,如果玩家持续撞击围墙,会使玩家坦克产生快速的震动。此时摄像机也就跟着快速震动,这很影响体验。上文里将跟随速度设置得比较低时,这种震动就不会影响到摄像机。这是因为,摄像机反应慢,震动速度快,不等摄像机需要向左跟随,就又要向右跟随了,所以摄像机基本上就在原地不动了。
将上文的&catchingSpeed&调大一些,再持续去撞墙,你就会明白了。
忘了说,要添加一个线光源,不然场景会很暗淡。下图就是没有添加光源的样子。
加上光源就成了这样:
想显示上图所示的文字?用Unity最近推出的uGUI还是很舒服的(也可能是因为我没有学过nGUI等等的UI系统吧)。
点击Text后,会增加3个对象,Canvas,Text和EventSystem。
给Text对象添加一个DrawMouseInfo.cs的组件(名字无所谓)。
1 public class DrawMouseInfo : MonoBehaviour {
void Awake()
guiText = this.GetComponent&Text& ();
void Update () {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray, out hit))
guiText.text = string.Format ("input: {0} mouse: {1} | {2}", Input.mousePosition, hit.point, hit.transform.gameObject.name);
guiText.text = string.Format ("input: {0} mouse: {1} | {2}", Input.mousePosition, "null", "null");
uGUI对象是可以在Scene视图里拖动的,只不过你要先找到它。
它的位置很奇葩,如上图所示,整个地图在它的Canvas脚下都很渺小。
快速自制贴图资源
本项目中的坦克、子弹、光标、背景图都是本人制作的,制作工具你猜猜?是PPT。
坦克底座是SmartArt图形里的。
轮子只是设置了一下渐变填充。
炮塔的圆形,把底座的圆形缩小一点就是。炮塔的炮管,是&形状&里的箭头,删掉凸起的尖的部分,调整一下锚点长短就OK。
背景用的是&纹理填充&,看到第二行第一个了没?
准星,用的是SmartArt里的&分离射线&。把四个箭头留下,其它内容删除。再把箭头的尾部顶点删除,左右交换位置,上下交换位置,上个色就成了。
还可以吧?
您可以到我的github页面()上得到工程源码。
请多多指教~
阅读(...) 评论()