这第一期是个考反应的游戏,看看效果图便能瞬间明白主要玩法:
首先需要准备游戏素材。对喜欢亲自动(zhe)手(teng)的我来说不是个事儿,我使用了PhotoShop+数位板自行绘制。没有数位板的同学只用PhotoShop也可以做到。
我绘制的素材和原版有些不同,具体如下:
所绘制素材文件
这里将咱亲手绘制的资源奉上:
网盘链接:https://pan.baidu.com/share/init?surl=f-crQYmgBbaRtO4CN_FqVA
提取码:1lq6
还是得说一句:请不要用于商业用途哦!
将准备好的游戏物体导入Unity引擎,下面开始正式游戏制作。
首先我们制作游戏菜单,也是游戏的第一个场景。
从预览图中应该能看出来:本游戏的一大特征,是活泼生动的动画效果。如果缺少这些效果那么游戏的魅力可以说是减少了一大半。
这些动画效果都是可以通过Unity的Animator实现的,这部分我用的都是2D图片精灵(Sprite),用UGUI也可以实现,这里需要分别需要两个动画状态机以及两个脚本:
第一个动画状态机:拼图表情动画状态机只需要添加一个默认的缩放动画即可,这样一来在游戏中该游戏物体就会一直自己缩放,不用我们操心(动画在Unity的动画窗口录制)。
气泡精灵锚点预设
这样一来同一个状态机可以给四个气泡重复使用,该状态机有两个状态,一个是默认状态一个是气泡放大的状态,分别是两个只有一帧的动画,这两个帧分别是Scale(0,0,0)和Scale(1,1,1),用一个bool类型的数值控制他们的状态转换,利用状态转换的自动补间动画实现我们需要的效果。
气泡精灵的录制动画
在表情游戏物体上添加一个控制鼠标滑过事件的脚本,当鼠标进入游戏物体时使气泡转为放大状态并修改游戏物体的贴图,否则气泡为默认状态,游戏贴图也是默认贴图。
private void OnMouseEnter()
{
isTouch = true;
}
private void OnMouseExit()
{
isTouch = false;
}
private void BubbleMove()
{
if (isTouch)
{
myBubbleAnimator.SetBool("touch", true);
gameObject.GetComponent
}
else
{
myBubbleAnimator.SetBool("touch", false);
gameObject.GetComponent
}
}
菜单中的四个拼图的效果基本一致,但只有第一个拼图点击后会跳转到游戏场景。这里我将以上游戏物体复制了三份,并分别放置到正确的位置更改为对应的气泡图片,再另外新建了一个控制鼠标点击事件的脚本,触发场景跳转,同学们如果不喜欢,也可以重新写一个单独控制第一个表情的脚本。
到这里我们已经将一个看起来很酷的游戏菜单场景制作完毕。
接下来我们制作游戏中的第二个场景:游戏进行中的场景,由于游戏中有镜头抖动的效果,所以只能使用Sprite作为游戏物体。
场景中的其他图片不用多说,场景中的拼图分为左边的拼图和右边的拼图,左边叫玩家右边叫敌人好像不合适,就叫目标拼图吧。
玩家拼图的制作非常简单,只要添加控制根据按键切换赋值的脚本即可。
目标拼图是最好玩的,你以为我要用代码写拼图的运动和碰撞吗?不,录制一个从右往左的动画,在动画的最后一帧加入事件,该事件调用GameManager的碰撞触发方法就可以了,因为目的地的坐标不同,两个类型的目标拼图要分别录制动画。
GIF
目标拼图预制体制作
目标拼图有许多颜色,但是我又懒得画很多个颜色的各种拼图,看到这里你一定发现了目标拼图的预制体是一个灰色带阴影的图,通过实验我在这里用了两个图层(外图层是拼图的外框)通过对内图层的颜色更改实现不同颜色的目标拼图。
GIF
其实这也是我画的第一个素材,之后我哭着翻出了压箱底的数位板。
说到核心游戏物体就不得不说核心游戏玩法,整个游戏的核心玩法是用左右按钮(这里设置为A键和D键)改变玩家拼图的状态以接住屏幕右边移动到玩家拼图面前的目标拼图,如果接不到就游戏结束,本文主要是对游戏效果进行复现,并不对玩法进行改进。
那么问题是我们如何判断有没有接到。这个问题很简单,用一些数值类型枚举值就可以搞定,但是我选择用对象。
所以同学们这段如果理解不了可以不学,直接用枚举值就好,好奇为啥这样说的可以接着看一看。
这里画了个不太规范的类图来展示游戏中的脚本关系。
声明一个拼图类和三个不同拼图子类,这几个类不继承MonoBehaviour,每个子类中重写父类中的碰撞判断方法,该方法返回一个布尔值,通过与GameManager中的静态字段对比来返回结果,这里以左方向拼图为例:
public class LeftPuzzle : Puzzle
{
public override bool PuzzleIsTure(Puzzle puzzle)
{
return puzzle == GameManager.Instance.rightPuzzle;
}
}
另外三个脚本(玩家、目标拼图、GameManager)分别持有拼图对象,回忆上文,目标拼图的最后一帧会调用一个方法,也就是图中目标拼图持有的“动画帧触发”,该方法调用整个游戏逻辑中的核心方法——“拼图碰撞触发方法”
public void PuzzleTrig(GameObject gameObject)
{
Puzzle thePuzzle = gameObject.GetComponent
//回收go
if (thePuzzle == leftPuzzle)
{
leftPuzzleList.Add(gameObject);
}
else if (thePuzzle == rightPuzzle)
{
rightPuzzleList.Add(gameObject);
}
//判断对错
if (player.myPuzzle.PuzzleIsTure(thePuzzle))
{
AddScore();
UpdateScoreText();
winEffect.Play();
SetPlayerFaceShow(false, true, false);
scoreAnimator.SetTrigger("getscore");
}
else
{
StopAllCoroutines();
SpacePuzzle(puzzleList);
dieEffect.Play();
SetPlayerFaceShow(false, false, true);
gameOver.Play("GameOver");
}
StartCoroutine(CameraShake());
}
用GameManager脚本管理整个游戏的主要逻辑,将这个脚本设置为单例模式,
public class GameManager : MonoBehaviour{
public static GameManager Instance;
private void Awake()
{
Instance = this;
}}
并声明三个拼图对象作为整个游戏所有拼图所持有的对象,这三个对象分别是默认拼图、左拼图和右拼图(默认拼图很寂寞,因为只有一个拼图要他),当拼图碰撞时只要调用玩家拼图持有的拼图对象的碰撞判断方法即可。
后面写拼图预制体的时候就遇到问题了,两个预制体必须分别写脚本,因为使用的是对象来标识拼图,不能在Inspector面板中分别设置值(如果是数值就很简单了),但问题不大,多写两个脚本就好了,这样避免了很多if判断,比较舒服。
接下来我们来制作游戏中的动态效果,看图分析,游戏中的动画有玩家拼图上的表情动画和左下角的得分上的动画以及游戏结束的破碎动画,除此之外还有两个粒子特效,加上每一次拼图碰撞伴随着的镜头抖动,都是很简单的东西,组合起来有不简单的效果。
原版是用眼睛放大缩小的状态变化来表现方块“未碰撞”“碰到对的”“碰到错的”这三个状态,用Animator可以简单搞定,为什么要特地提一嘴?因为我给自己挖了个坑。
我画了三个表情。并且,第一个表情的眼睛是分两张图的,第二个表情眼睛是要放大的,第三个表情是可以不动的,我尝试使用Animator管理他们,得到了一个鬼畜的方块,然后我一拍大腿,写了一个方法搞定了。
我设计的表情素材
用显示隐藏游戏物体进行玩家的表情状态变换
所有表情的动画默认播放就行。我真佩服我自己。
这个游戏物体是整个场景目前唯一使用到UGUI的地方,并且使用的是Unity新加入的TextMeshPro-Text,这里使用的是默认字体,感兴趣的同学可以自行深入了解。
之所以在这里使用新组件主要原因当然也是因为动画,该组件支持许多原版Text没有的操作,比如“自动匹配大小”就是当前需要使用的功能,将“AutoSize”勾选并将字体最大值设置到足够大,这样一来录制动画就非常容易了。
该物体有两个状态,默认状态是普通的放大缩小动画,得分时的动画比较特别,这里用图片展示。需要注意这里动图里面有个错误就是没有设置中心点,导致动画记录了中心点移动,这也是开始菜单的气泡动画强调的注意点,而我等到游戏做完写文章的时候才注意到。
细心的同学还会发现,游戏中的字体是有投影的,这个用新版组件是否能够做到呢?其实和组件没什么关系,复制一份Text调整颜色和位置,让他们双宿双飞共同进退就好了。
GIF
GIF
当拼图碰在一起时玩家按了错误的按键(或者没有按按键),这时候需要播放死亡动画,该动画通过调节图片的透明度和颜色完成,这里我用了UGUI的Image,理论上用Sprite应该也能实现,聪明的同学一定能自己做出来。
图包里有对应的贴图,拖拽到粒子的TextureSheetAnimation上,Shape设置为Circle,然后调整其他数值把两个粒子特效做出来就行了,这里我也没法说,毕竟我都是试出来的。
在GameManager写一个控制镜头抖动的协程,在碰撞触发方法调用他就是了,要什么自行车,没什么好说的。
public IEnumerator CameraShake()
{
Vector3 OrigPosition = shakeCameraTransform.localPosition;
float ElapsedTime = 0.0f;
while (ElapsedTime < shakeTime)
{
Vector3 RandomPoint = OrigPosition + Random.insideUnitSphere * shakeAmount;
shakeCameraTransform.localPosition = Vector3.Lerp(shakeCameraTransform.localPosition, RandomPoint,
Time.deltaTime * shakeSpeed);
yield return null;
ElapsedTime += Time.deltaTime;
}
shakeCameraTransform.localPosition = OrigPosition;
}
每一个动画和特效都非常的简单,但是综合起来却能让人觉得效果特别炫,这正是这个游戏真正值得我们学习的地方。原游戏还有音效加持,效果更好,美术上的加持也是必不可少的。
最后简单制作游戏的结束场景,这个场景里的游戏物体全都是UGUI,最终成绩的显示动画和上一个场景的动画类似,由于Game Manager是单例模式,所以存储在该脚本的“得分”在游戏结束的场景中也能被调用哦~
新的东西是按钮动画效果——新建按钮之后将组件的“Transition”设置为“Animation”,再点击“AutoGenerateAnimation”,这样Unity会自动帮你生成该按钮的状态机,再在对应的状态中录制两个不同的动画(默认和鼠标悬停两个状态有不同动画),就可以完成了。
Ctrl+D复制上文制作按钮,换个贴图再各自放到合适的位置,哦嚯!场景搭建完毕!游戏做完了?不!我们还要把三个场景连接起来,在对应的地方写上场景跳转:SceneManager.LoadScene("目标场景名称")。
但是场景跳转的时候,似乎只是屏幕一黑,没有可爱的拼图开合动画啊,这个动画当然也是要自己做呀。
当我们做好了一个过渡动画之后又会发现,在不同的场景中,过渡动画结束后要做的事情都有些微的不同,那么我们要每个场景都写一个脚本,做三套动画的游戏物体吗?当然不!这时候就是委托大展身手的时候啦。
public delegate void SceneEvent();
public SceneEvent SceneOpenEvent;
public SceneEvent SceneCloseEvent;
public void CloseGo()
{
SceneCloseEvent?.Invoke();
}
public void OpenGo()
{
SceneOpenEvent?.Invoke();
}
这样一来这个游戏就真正完成啦,是不是简单而不枯燥,感觉自己又变强了呢~
下面开始技术总结(敲黑板)
第一、镜头有抖动的场景背景图要画大一点。
第二、动画的帧数只要12帧就可以流畅播放,但是部分动画效果会不好,这里设置的不太好。
第三、用动画帧事件实现跳转前播放动画比用协程实现更灵活方便,但是如果状态机过渡动画没有走到设置事件的帧就会不触发。
第四、得分动画本来是想在游戏进行场景做预制体在游戏结束场景中复用的,但是问题很大,重新做了,说明在制作原型的时候考虑的不到位。
第五、原游戏场景跳转做了异步跳转,但是我没有。
该游戏源码已上传到Github,总结完毕:https://github.com/peiyl/Puzzling