前言 短篇 AVG 游戏的流程已经差不多了,现在就差 cee 把游戏要用的素材提供给我,最后再导入测试就基本完成了。
所以趁这个时间,我打算把原来在 cocos creator 引擎上《名为怪物的游戏》制作进度移植到 unity 引擎。
同时花了一晚上的时间把火兔游戏的官网重建成这样一个博客,后续将会以博文的方式直播制作过程或者发布游戏预告。
(直播制作过程主要是为了防鸽……)
移植原因 首先我很喜欢 cocos creator 引擎,不仅是因为国人制作的,而且上手简单。
在制作了游戏的序章之后,发现 cocos creator 不能满足我们的要求,因为我们打算发布的是 PC 端, 而 cocos creator 主打移动游戏,比方说在游戏内调节分辨率 cocos 就不支持,还有因为 JavaScript 对文件读写什么的也有限制,要解决这些问题估计得花很多时间,但对于我们来说可以游刃有余的时间并不多了, 所以选择对单机游戏支持比较友好的 unity。
星之魔女 星之魔女是《名为怪物的游戏》中的一个怀旧向像素风 FC 游戏。
使用 cocos creator 引擎开发的画面:https://www.bilibili.com/video/BV167411L7vJ/
移植过程 文章会包含制作过程的技术相关内容,如果有兴趣的小伙伴可以了解一下。
素材导入 由于之前小游戏已经做完了,所以素材可以直接导入。
像素风的序列帧:
场景配置 新建一个 1280 * 720 的场景。
让角色动起来 现在场景有了,但角色只是一张静态图片,要让角色可以通过按键移动,就要开始编写角色控制脚本了。
新建名为 MiniGame_MoveEvent
的脚本,用来控制角色移动:
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 using System.Collections; using System.Collections.Generic; using UnityEngine; public class MiniGame_MoveEvent : MonoBehaviour { public float speed = 500f; private Vector3 left = new Vector3(-1, 1, 1); private Vector3 right = new Vector3(1, 1, 1); void FixedUpdate() { if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) { gameObject.transform.localScale = left; transform.Translate(Vector3.left * Time.deltaTime * speed); } if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) { gameObject.transform.localScale = right; transform.Translate(Vector3.right * Time.deltaTime * speed); } } }
在 unity 中加载游戏会加载所有脚本,所以脚本的名字加上 MiniGame_
来区分。
为了让角色的朝向也能改变,在按下移动键的时候,顺便改变图片的翻转。
这里可以使用 WASD 来控制移动,也可以用方向键控制移动,目前只有左右移动,把这个脚本挂在 Player 对象上即可用键盘控制角色了:
实现角色移动还可以直接用 unity 自带的物理引擎,碰撞检测阻止移动就比较方便,但是我这里选择自己写脚本逻辑,因为 FC 游戏里的一些操作是不符合物理规律的。
角色小动作 现在角色能动起来了,但是看起来就是在移动一张图片,没有游戏的感觉。
为了让角色变得“生动”,就要给角色加上动画演出效果。
unity 内置了动画系统,但是我这里也选择自己写脚本来控制。
新建一个名为 MiniGame_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 94 95 96 97 98 99 100 101 102 103 104 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public abstract class MiniGame_Character : MonoBehaviour { public Sprite[] idle; public Sprite[] hurt; public Sprite[] attack; public Sprite[] run; // 是否循环播放动画 private bool animateLoop; // 当前播放的动画 private string currentAnimate; // 当前动画index private int animateIndex; // 当前动画精灵 public Sprite[] currentAnimateSprites; // 人物行走图 private Image character; // 动画播放间隔 private float animateInterval; // 当前时间 private float time; private void Awake() { character = GetComponent<Image>(); } // 死亡回调 protected abstract void DeadCallback(); public void SetAnimate(string animate) { // 不播放重复动画 if (currentAnimate == animate) { return; } Debug.Log("切换动画:" + animate); currentAnimate = animate; animateIndex = 0; switch (animate) { case "idle": animateLoop = false; animateInterval = 0.1f; currentAnimateSprites = idle; break; case "run": animateLoop = true; animateInterval = 0.1f; currentAnimateSprites = run; break; case "attack": animateLoop = false; currentAnimateSprites = attack; break; case "hurt": animateLoop = false; currentAnimateSprites = hurt; break; } } private void PlayAnimateListerner() { if (animateLoop == false && animateIndex > currentAnimateSprites.Length - 1) { return; } if (Time.time < time) { return; } // 时间自增 time = Time.time + animateInterval; if (animateLoop == true) { if (animateIndex > currentAnimate.Length - 1) { animateIndex = 0; } } character.sprite = currentAnimateSprites[animateIndex]; animateIndex++; } private void Update() { PlayAnimateListerner(); } }
再新建一个用于玩家控制角色的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 using System.Collections; using System.Collections.Generic; using UnityEngine; public class MiniGame_Player : MiniGame_Character { protected override void DeadCallback() { // TODO 进入GameOver场景 throw new System.NotImplementedException(); } }
动画的播放逻辑很简单,就是循环播放图片,以下是主角的序列帧行走图:
跟制作动画的原理一样,就是以肉眼难以辨别的速度播放细微不同的图片,所以看起来像“动起来”一样。
为了间隔一定时间播放一张图片,这里用了一个计时器,定义下一个切换图片的时间点,比如 0.1 秒以后,如果当前时间等于 0.1 秒后,就播放下一张图片,然后切换图片的时间点等于当前时间加上 0.1s。
如果是循环播放类的图片,在图片全部播完之后,就会从第一张开始继续播放,如果是不循环的动画,就停止继续播放。
声明一个公开的方法 SetAnimate
,只要传入要播放的动画,就会自动配置对应的参数,比如当前播放的图片数组和是否循环播放以及播放间隔。
这里要加一个判断,防止重复播放相同的动画:
1 2 3 4 if (currentAnimate == animate) { return; }
如果当前已经在播这个动画了,再调用这个方法就直接返回。
抽象父类还定义了一个 DeadCallback
死亡回调方法,即当目标死亡时会做什么事情。
玩家死亡了就是 Gamover,敌人死亡了就爆金币。
现在先来做基本的动画:待机小动作和跑步动作。
要播放动画就调用 SetAnimate
方法,在 MiniGame_Character
加入代码:
1 2 3 4 private void Start() { SetAnimate("idle"); }
这样角色在进入场景的时候,就会自动播放待机动画。
把 MiniGame_Character
脚本挂在场景的 Player 节点,并且在脚本组件上把待机动画图拖进去:
进入场景就可以看到效果了:
一个简单的眨眼小动作!
同理要让角色有奔跑动画只需要在 MiniGame_MoveEvent
控制角色移动的时候播放动画即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void FixedUpdate() { if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) { player.SetAnimate("run"); gameObject.transform.localScale = left; transform.Translate(Vector3.left * Time.deltaTime * speed); } if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) { player.SetAnimate("run"); gameObject.transform.localScale = right; transform.Translate(Vector3.right * Time.deltaTime * speed); } // 判断松开按键 if (Input.GetKeyUp(KeyCode.A) || Input.GetKeyUp(KeyCode.LeftArrow) || Input.GetKeyUp(KeyCode.D) || Input.GetKeyUp(KeyCode.RightArrow) ) { player.SetAnimate("idle"); } }
这里还要判断当键盘松开的时候,要改成待机动画。
然后把奔跑的动画素材也拖到脚本组件上,演示效果:
小问题修正 这里其实还存在一个小问题,就是当按键和弹起的一瞬间,有几率出现判定失误,GetKey
方法是监听按键,不管是弹起还是按下都会触发,而 GetKeyUp
则是监听弹起事件,因此会存在监听到弹起事件的一瞬间同时也被判定为按下的情况,导致人物虽然停止移动了,但是奔跑动画却还在播放的情况。
除此之外,还有一个比较特殊的情况,就是如果玩家同时按下左方向和右方向,这样也会变得很奇怪。
为了修正上述两个问题,重新修改 MiniGame_MoveEvent
脚本:
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 using System.Collections; using System.Collections.Generic; using UnityEngine; public class MiniGame_MoveEvent : MonoBehaviour { public float speed = 400f; private MiniGame_Player player; private Vector3 left = new Vector3(-1, 1, 1); private Vector3 right = new Vector3(1, 1, 1); private void Start() { player = GetComponent<MiniGame_Player>(); } void FixedUpdate() { float raw = Input.GetAxis("Horizontal"); float moveSpeed = raw * Time.deltaTime * speed; var pos = transform.position; pos.x += moveSpeed; transform.position = pos; if (raw < 0) { player.SetAnimate("run"); gameObject.transform.localScale = left; } else if (raw > 0) { player.SetAnimate("run"); gameObject.transform.localScale = right; } else { player.SetAnimate("idle"); } } }
监听按键的方法改成 GetAxis
,这个方法会返回 -1~1的浮点数,
Horizontal
是 unity 内置的常量,对应水平方向相关的按键,即左右方向键和 WASD。
所有的常量及对应的按键参数可以在构建游戏时,Player Settings 里查看:
这些常量有一个好处就是可以支持手柄操作。
因为 GetAxis
会返回一个范围值,而且是渐进式的增加,这样还可以让角色有一个起跑短暂加速的感觉,在松开按键的时候,也会有缓冲减速效果。
修改后的演示效果:
另外,unity 还有一个 GetAxisRaw
方法,类似 GetAxis
,但是它只会返回三个值:-1、0、1。
如果使用 GetAxisRaw
方法,就没有平滑起跑的效果了,而是直接以最大的速度奔跑,改成 GetAxisRaw
后的演示效果如下:
两种效果都各有好坏,有缓冲效果感觉更加笨重,但是比较真实,以最大速度起跑操作体验更好。
这里我就采用直接最大速度开始奔跑的方案,另外,如果同时按下左右方向键,则行动会立即停止,不会因为同时按而产生奇奇怪怪的结果了。
而动画是根据按压的 raw 返回值来判断的,也就不会出现行动停止奔跑动画还在继续的情况。