素材说明
素材即游戏中需要的图片、音乐等等。
图片素材主要是行走图的处理,本作中使用「序列帧」动画制作行走图。
素材规格
图片在游戏中叫做 Sprite(精灵),一般都是单张图片,也可以是多张图片合成在一起,合成在一起的图片叫做 Sheet(精灵表/图集)。
RPG Maker 行走图教程:行走图教程
RPG Maker 系列的行走图示例:
一般都是用专门的打包工具合成这样的图集。
图集打包工具:TexturePacker 官方网站
图集
通常的游戏开发引擎都可以读取图集。
使用打包工具合成一张图集之后还会得到一个文本文件:
文本记录了一个 JSON 格式的字符串,用来保存偏移位置等等图片的信息。
游戏开发引擎可以读取这个文本从而实现图片的切割。
图集的优点
游戏引擎有个 DrawCall
(绘图次数),Draw Call 就是 CPU 调用图形编程接口,比如 DirectX 或 OpenGL 来命令 GPU 进行渲染的操作。
简单地说这个值越低越好,数值越高表示要画图的次数越多,这样游戏的性能就会下降。
使用图集可以降低 DrawCall
(具体的原理还未深究)。
但是一次性加载一张图片和每次画图都要加载单张图片相比,效率肯定高得多。
除了提高画图的性能,还有一个优点就是方便开发者调用。
如果是单张图片开发者不使用脚本读取的话,就要一张张拖到组件里;但如果使用图集打包之后,只要拖一次。如果使用脚本动态读取,图集只要加载一次,而单张图片却要加载 N 次,不管是性能还是操作上,图集都要优于单张图片。
图集的缺点
因为图片打包在一起,所以修改其中一张就得重新打包。
而且如果图集里包含了不需要用到的图片就白白加载了多余的资源。
格式标准
为了避免改图必须重新全部打包的问题,我们决定图集只包含单个动作。
攻击动作:
倒地动作:
像这样每一个动作都是一张图集,如果要改动其中一个动作就不必全部重新打包了。
图集切割
使用 TexturePacker 将行走图打包成单张图片:
可以使用 Unity 内置的分割程序将图片切分成 4 等分。
图集插件
参考教程:Unity3D-图集制作插件TexturePacker中文教程
(作者:Chinarcsdn)
Unity 商店提供了一款可以读取 TexturePacker 切割数据的插件:TexturePacker Import
从商店把资源添加到账户里,接着导入到游戏项目中:
导入插件的时候,插件内的 Example(范例)是没什么用的,可以不勾上。
第一次导入的时候,插件会查找本地的文件,如果有图集就会自动加载,这个过程比较漫长(如果本地图片多的话)。
趁这个时间,打开 TexturePacker,然后重新合成一份 unity 支持的图集类型:
打包完成后,有一个合并的图像文件和一个 .tpsheet
后缀的文件:
这个文件是图集的配置,可以用文本文档打开:
将这两个文件一起拷贝粘贴到 Unity 工程里:
可以看到,行走图已经自动被分割好了!这样就不用手动切图啦!
插件规范
在导入插件后发现目录出现了一个文件夹:
直接放在根目录明显不美观而且也不规范。
创建一个 Plugins
文件夹用来存放插件:
虽然插件放在哪 Unity 都会自动加载,但是为了统一管理,添加一个用来保存插件的文件夹是最好的。
动画制作
行走图切割完成之后,如果使用 Unity 自带的动画程序就比较麻烦了。
每一个动画都要添加状态机,并且还要给每一个角色节点添加动画组件,总体的工程量非常大。
所以这里要在框架里面实现一个动画系统。
加载行走图
行走图切割完后放在 Resources 文件夹备用。
新建框架类 Spriter
,添加一个读取切割好的图片的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| using System.Collections; using System.Collections.Generic; using UnityEngine;
namespace FR { public class Spriter : MonoBehaviour { public static Sprite[] LoadSprites(string path) { Sprite[] sprites = Resources.LoadAll<Sprite>(path);
return sprites; } } }
|
这里我重新修改了所有框架类的命名空间,全部只套一层 FR,不再区分更细的目录。
行走图动画
行走图的动画可以用 Unity 的动画系统实现,也可以自己手动实现,每个游戏都不一样,因此不应该放在框架中处理,而是要放到游戏本身的逻辑当中。
添加一个地图角色父类 AbstractCharacter
,该方法继承框架的 Character
,并且提供了载入行走图以及显示动画的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| using System.Collections; using System.Collections.Generic; using UnityEngine; using FR;
namespace Refactor { public abstract class AbstractCharacter : Character { protected bool isWalking; protected Dictionary<string, Sprite[]> animateSprites;
protected int animateIndex; protected float walkingInterval = 0.15f; protected float spriteAnimateTime;
protected void LoadAnimateSprites(string characterName) { animateSprites = new Dictionary<string, Sprite[]>();
string[] fields = new string[] { "walk", "idle" }; string basePath = "Sprites/Character/" + characterName + "/";
foreach (var field in fields) { var items = Spriter.LoadSprites(basePath + field);
if (items.Length != 0) { animateSprites.Add(field, items); } } }
protected virtual void AnimateMonitor() { WalkingAnimate(); IdleAnimate(); }
private void SetCurrentAnimateSprite(string key) { if (animateSprites.ContainsKey(key) == false) { Debug.Log("没有找到对应的动画文件:" + key); return; }
spriteAnimateTime = Time.time + walkingInterval;
animateIndex++;
if (animateIndex >= animateSprites[key].Length) { animateIndex = 0; }
var sprite = animateSprites[key][animateIndex]; SetSprite(sprite); }
protected virtual void IdleAnimate() { if (isWalking == false && Time.time > spriteAnimateTime) { SetCurrentAnimateSprite("idle"); } }
protected virtual void WalkingAnimate() { if (isWalking && Time.time > spriteAnimateTime) { SetCurrentAnimateSprite("walk"); } }
protected virtual void InputMonitor() { } protected virtual void InputHandle() { }
protected void Update() { InputMonitor(); }
protected void FixedUpdate() { WalkingAnimate(); IdleAnimate(); InputHandle(); } } }
|
上面的脚本会自动加载角色的图集文件:walk
(行走) 和 idle
(待机)。
需要在 Resources 添加好对应的图片,以后所有的角色都遵循这个标准,即包含一个行走图和待机图。
InputMonitor
(输入监听器) 和 InputHandle
(输入处理器)可以在子类重写。
玩家控制的角色类 PlayerCharacter
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| using System.Collections; using System.Collections.Generic; using UnityEngine; using FR;
namespace Refactor { public class PlayerCharacter : AbstractCharacter { public float moveSpeed = 3.8f;
private Rigidbody2D rb; private float horizontal;
private Vector3 originalLocalScale;
protected override void InitCharacterAction() { LoadAnimateSprites("ace");
rb = GetComponent<Rigidbody2D>(); originalLocalScale = transform.localScale; }
protected override void InputMonitor() { horizontal = Input.GetAxisRaw("Horizontal"); }
protected override void InputHandle() { MoveHandle(); }
protected void MoveHandle() { rb.velocity = new Vector2(horizontal * moveSpeed, 0);
if (horizontal != 0) { isWalking = true; float scaleX = horizontal > 0 ? -1 * originalLocalScale.x : originalLocalScale.x; SetLocalScale(scaleX); } else { if (isWalking == true) { animateIndex = 0; }
isWalking = false; } }
} }
|
NPC 的角色类 NonePlayerCharacter
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| using System.Collections; using System.Collections.Generic; using UnityEngine;
using FR;
namespace Refactor { public class NonePlayerCharacter : AbstractCharacter { protected override void InitCharacterAction() { } } }
|
目前还未开始实现 NPC 角色,因此留空备用。
角色类的新设计
与前一篇相比,角色基类的改动比较大。
最大的不同是节点的创建,在前一篇中是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| // 定义一个字典用于保存场景中的NPC节点 private Dictionary<string, GameObject> npcs = new Dictionary<string, GameObject>();
public GameObject CreateNPC(FR_Data.CharacterData data) { GameObject obj = new GameObject();
obj.name = data.name; SpriteRenderer spriteRenderer = obj.AddComponent<SpriteRenderer>(); spriteRenderer.sprite = Resources.Load<Sprite>(data.walkingGraphPath); spriteRenderer.sortingOrder = data.sorting;
npcs.Add(data.name, obj);
return obj; }
|
上述脚本通过代码动态创建一个 NPC 节点。
但是我突然想到,NPC 节点有体型的差异,比如场景中如果出现一只 BOSS,那么它的体型可能比玩家大好几倍,这样的话碰撞器的范围就难以用脚本的方法控制了。因为素材是存在多余的透明区域的,而脚本只能读取到图片的大小,要计算出不透明的宽高就比较麻烦了;所以我决定还是创建一个角色预制体,角色的预制体不需要添加逻辑脚本,而只是一个没有“芯片”的机器人,每一个 NPC 的逻辑都不同,只要给它们植入“控制芯片”就可以了。
角色预制体相当于是角色的模型,只差给它们“注入灵魂”。