提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作游戏第二个BOSS燥郁的毛里克
- 1.导入素材和制作相关动画
- 1.5处理玩家受到战吼相关行为逻辑处理
- 2.制作相应的行为控制和生命系统管理
- 3.制作战斗场景和战斗门
- 4.制作BOSS死亡行为
- 总结
前言
hello大家好久没见,之所以隔了这么久才更新并不是因为我又放弃了这个项目,而是接下来要制作的工作太忙碌了,每次我都花了很长的时间解决完一个部分,然后就没力气打开CSDN写文章就直接睡觉去了,现在终于有时间整理下我这半个月都做了什么内容
废话少说,接下来我将介绍我做的第二个BOSS苍蝇之母,因为我上一期制作的苍蝇之母反响还不错,大晚上的还有一百来人在看,那么直接乘胜追击制作第二个BOSS燥郁的毛里克。
另外,我的Github已经更新了,想要查看最新的内容话请到我的Github主页下载工程吧:
GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!
一、制作游戏第二个BOSS燥郁的毛里克
1.导入素材和制作相关动画
还是先制作好tk2dsprite和tk2dspriteanimator吧:
可能你已经注意到了,提供的tk2dsprite有毛里克的不同躯干,没错,我们要做的就是类似于骨骼动画一样的,每一个部分处理对应部分的行为,在这里我们分为四个部分:分别是左右手臂,嘴巴,身体,先来制作隐藏状态下的动画:
然后是手臂:
身体方面的动画:
头部动画:
还有就是作为整体的跳跃动画:
作为整体的喷射子弹动画:
作为整体的战吼动画:
死亡动画;
1.5处理玩家受到战吼相关行为逻辑处理
可能你看到这标题不知道我想表达什么,其实这也是我自己打这个BOSS的时候突然发现的,就是玩家在面临强大的BOSS时,会被BOSS的气场给震住,被迫失去控制的朝向BOSS,然后BOSS发射阵阵战吼冲击波,其实你看到下图小骑士的动画你就明白是什么了:
然后给小骑士一个playmakerFSM叫“Roar Lock”来实现相关行为:
初始阶段找到特效文件夹和里面需要的特效:
通过Tag名字叫 Roar的判断是否进入下一个状态
判断是否允许进入Roar行为:
玩家锁定状态下,发送事件,同时RelinquishControl取消控制以及AffectedByGravity受到重力影响,停止播放其他动画,使玩家朝向敌人
自定义playmakerFSM脚本:
using System;
using UnityEngine;namespace HutongGames.PlayMaker.Actions
{[ActionCategory(ActionCategory.Logic)][Tooltip("Tests if all the given Bool Variables are are equal to thier Bool States.")]public class BoolTestMulti : FsmStateAction{[RequiredField][UIHint(UIHint.Variable)][Tooltip("This must be the same number used for Bool States.")]public FsmBool[] boolVariables;[RequiredField][Tooltip("This must be the same number used for Bool Variables.")]public FsmBool[] boolStates;public FsmEvent trueEvent;public FsmEvent falseEvent;[UIHint(UIHint.Variable)]public FsmBool storeResult;public bool everyFrame;public override void Reset(){boolVariables = null;boolStates = null;trueEvent = null;falseEvent = null;storeResult = null;everyFrame = false;}public override void OnEnter(){DoAllTrue();if (!everyFrame){Finish();}}public override void OnUpdate(){DoAllTrue();}private void DoAllTrue(){if (boolVariables.Length == 0 || boolStates.Length == 0){return;}if (boolVariables.Length != boolStates.Length){return;}bool flag = true;for (int i = 0; i < boolVariables.Length; i++){if(boolVariables[i].Value != boolStates[i].Value){flag = false;break;}}storeResult.Value = flag;if (flag){Fsm.Event(trueEvent);return;}Fsm.Event(falseEvent);}}
}
看看是否要翻转玩家X方向:
处理粒子系统相关:
判断玩家是否在地面:
在空中锁定:
在地面被锁定:
等待发送ROAR EXIT事件:玩家重新获得输入和动画控制
取消粒子播放效果:回到Detect状态中。
2.制作相应的行为控制和生命系统管理
终于来到我们最爱的处理BOSS相应行为的时候,但在此之前还是把该要的组件导入来:
然后再来看看它有什么子对象,首先自然是它的身体部件了,首先介绍头部:
然后是左右臂:
还有一个子对象Attack Range,给左右两只手检测玩家是否进入攻击范围的:
右手同理:
接着是身体部分:
Boss的警戒范围:Alert Range New
然后是一些粒子系统:
喷射效果:
一个简单的playmakerFSM,叫你PLAY的时候你再play:
我们先来看看毛里克的头部是怎么控制行为的,其实也很简单,就是不停的喷子弹就完事了,
这个特别说明的变量是喷射速度,你可以根据情况自己调:
初始化阶段就是获得玩家,获得自己孩子和父母
初始化完整后就进入待苏醒阶段:
苏醒阶段:设置攻击间隔为0.3-0.6秒之间
喷射准备阶段:播放动画,等一下会
检测玩家位置:
开喷!
这里有个预制体叫做Shot Mawlek No Drip,也就是毛里克头部发射的子弹,
记得添加DamageHero.cs脚本,这样它才能伤害到玩家
这里有一个新脚本叫:EnemyBullet.cs,代码段如下:主要分为Trigger碰到玩家,以及Collision碰到Terrian层级的墙壁和地面的行为
using System.Collections;
using UnityEngine;[RequireComponent(typeof(Rigidbody2D))]
public class EnemyBullet : MonoBehaviour
{public float scaleMin = 1.15f;public float scaleMax = 1.45f;private float scale;[Space]public float stretchFactor = 1.2f;public float stretchMinX = 0.75f;public float stretchMaxY = 1.75f;[Space]public AudioSource audioSourcePrefab;public AudioEvent impactSound;private bool active;private Rigidbody2D body;private tk2dSpriteAnimator anim;private Collider2D col;private void Awake(){body = GetComponent<Rigidbody2D>();anim = GetComponent<tk2dSpriteAnimator>();col = GetComponent<Collider2D>();}private void OnEnable(){active = true;scale = Random.Range(scaleMin, scaleMax);col.enabled = true;body.isKinematic = false;body.velocity = Vector2.zero;body.angularVelocity = 0f;anim.Play("Idle");}private void Update(){if (active){float rotation = Random.Range(body.velocity.y,body.velocity.x) * 57.295776f;transform.SetRotation2D(rotation);float num = 1f - body.velocity.magnitude * stretchFactor * 0.01f;float num2 = 1f + body.velocity.magnitude * stretchFactor * 0.01f;if (num2 < stretchMinX){num2 = stretchMinX;}if (num > stretchMaxY){num = stretchMaxY;}num *= scale;num2 *= scale;transform.localScale = new Vector3(num2, num, transform.localScale.z);}}private void OnCollisionEnter2D(Collision2D collision){if (active){active = false;StartCoroutine(Collision(collision.GetSafeContact().Normal, true));}}private void OnTriggerEnter2D(Collider2D collision){if(active && collision.tag == "HeroBox"){active = false;StartCoroutine(Collision(Vector2.zero, false));}}public void OrbitShieldHit(Transform shield){if (active){active = false;Vector2 normal = transform.position - shield.position;normal.Normalize();StartCoroutine(Collision(normal, true));}}private IEnumerator Collision(Vector2 normal, bool doRotation){transform.localScale = new Vector3(scale, scale, transform.localScale.z);body.isKinematic = true;body.velocity = Vector2.zero;body.angularVelocity = 0f;tk2dSpriteAnimationClip impactAnim = anim.GetClipByName("Impact");anim.Play(impactAnim);if (!doRotation || (normal.y >= 0.75f && Mathf.Abs(normal.x) < 0.5f)){transform.SetRotation2D(0f);}else if (normal.y <= 0.75f && Mathf.Abs(normal.x) < 0.5f){transform.SetRotation2D(180f);}else if (normal.x >= 0.75f && Mathf.Abs(normal.y) < 0.5f){transform.SetRotation2D(270f);}else if (normal.x <= 0.75f && Mathf.Abs(normal.y) < 0.5f){transform.SetRotation2D(90f);}impactSound.SpawnAndPlayOneShot(audioSourcePrefab, transform.position);yield return null;col.enabled = false;yield return new WaitForSeconds((impactAnim.frames.Length - 1) / impactAnim.fps);Destroy(gameObject);//TODO:}}
我们把它的tk2dSprite和tk2dSpriteAnimator完成一下吧:
最后再加个子对象,给它一点亮光:
然后我们来处理它的身体Dummy行为:没啥好说的,就是攻击的时候身体flash发光一下
所以它需要SpriteFlash.cs脚本。
然后是它的左右手行为处理:这里我以左手为例
我们通过SetProperty开启左右手臂的MeshRenderer
在待苏醒阶段我们肯定要关闭它的Collider2d,否则你待苏醒阶段突然给玩家扣了一滴血玩家肯定要喷你的
判断是否进入攻击距离:
准备攻击:
攻击阶段:这时候就到了开启Collider2d的时候了
然后再攻击冷却阶段再关上collider2d
准备下一次攻击,回到Idle状态。
然后右手臂也同理,我就不贴出来了。
处理完各个部分的行为逻辑,接下来就是整体的逻辑行为处理了:
看到下面这两个变量你可能会好奇,我头部不是已经设置了Shot Speed了吗,怎么这里还有,这是因为头部的那个只能一次喷一颗子弹,而这个是作为整体开大招喷半个屏幕的子弹的,两者还是有差别的。
初始化阶段:找自己,找孩子,停止自己移动,播放动画
休眠阶段:播放动画,如果是神居里的BOSS,不用等WAKE事件直接苏醒,否则就要等到Alert Range New发送WAKE事件
回到Alert Range New,和上一期讲到的一样,还是等到Trigger2d检测到后发送WAKE事件给父对象
苏醒阶段:给Battle Scene发送事件START,假身Dummy播放动画
苏醒跳跃阶段: 给正Y轴一个向上的速度
苏醒在空中阶段:改变Z轴大小,检测是否落地面
苏醒落地阶段:
介绍BOSS阶段,但我还没做到这里,所以直接跳过
苏醒战吼阶段:向小骑士发送事件ROAR ENTER,设置Roar Object为自己
战吼结束阶段:向小骑士发送事件ROAR EXIT
音乐起:
相关代码如下:
using System;
using UnityEngine;
using UnityEngine.Audio;[CreateAssetMenu(fileName = "MusicCue", menuName = "Hollow Knight/Music Cue", order = 1000)]
public class MusicCue : ScriptableObject
{[SerializeField] private string originalMusicEventName;[SerializeField] private int originalMusicTrackNumber;[SerializeField] private AudioMixerSnapshot snapshot;[SerializeField][ArrayForEnum(typeof(MusicChannels))]private MusicCue.MusicChannelInfo[] channelInfos;[SerializeField] private MusicCue.Alternative[] alternatives;public string OriginalMusicEventName{get{return originalMusicEventName;}}public int OriginalMusicTrackNumber{get{return originalMusicTrackNumber;}}public AudioMixerSnapshot Snapshot{get{return snapshot;}}public MusicChannelInfo GetChanelInfo(MusicChannels channel){if (channel < MusicChannels.Main || channel >= (MusicChannels)channelInfos.Length){return null;}return channelInfos[(int)channel];}public MusicCue ResolveAlternatives(PlayerData playerData){if (alternatives != null){int i = 0;while (i < alternatives.Length){MusicCue.Alternative alternative = alternatives[i];if (playerData.GetBool(alternative.PlayerDataBoolKey)){MusicCue cue = alternative.Cue;if (!(cue != null)){return null;}return cue.ResolveAlternatives(playerData);}else{i++;}}}return this;}[Serializable]public class MusicChannelInfo{[SerializeField] private AudioClip clip;[SerializeField] private MusicChannelSync sync;public AudioClip Clip{get{return clip;}}public bool IsEnabled{get{return clip != null;}}public bool IsSyncRequired{get{if(sync == MusicChannelSync.Implicit){return clip != null;}return sync == MusicChannelSync.ExplicitOn;}}}[Serializable]public struct Alternative{public string PlayerDataBoolKey;public MusicCue Cue;}
}
using UnityEngine;
using System;namespace HutongGames.PlayMaker.Actions
{[ActionCategory(ActionCategory.Audio)][ActionTarget(typeof(MusicCue), "musicCue", false)][Tooltip("Plays music cues.")]public class ApplyMusicCue : FsmStateAction{[Tooltip("Music cue to play.")][ObjectType(typeof(MusicCue))]public FsmObject musicCue;[Tooltip("Delay before starting transition")]public FsmFloat delayTime;[Tooltip("Transition duration.")]public FsmFloat transitionTime;public override void Reset(){musicCue = null;delayTime = 0f;transitionTime = 0f;}public override void OnEnter(){MusicCue x = musicCue.Value as MusicCue;GameManager instance = GameManager.instance;if (!(x == null)){if (instance == null){Debug.LogErrorFormat(Owner, "Failed to play music cue, because the game manager is not ready", Array.Empty<object>());}else{instance.AudioManager.ApplyMusicCue(x, delayTime.Value, transitionTime.Value, false);}}Finish();}}}
然后假身利用完了就设置为空白的动画 ,此时就从整体变成各个分部,自身设置为Body Idle动画
等个2到3秒钟准备开大招:
那么到底要到什么时候才能进入开大招阶段呢?你总不可能一边左右手攻击一边飞起来吧,所以我打算让它们三分之二同时满足条件,也就是active同时激活的时候才进入开大招阶段
关闭头部collider,同时经典二选一:
作为整体拥有两种攻击行为,一是吐半个屏幕的子弹,还有一种是跳到玩家头上,这里我先介绍前一种:
又是判断是否重复执行同一种攻击行为多次,和判断玩家位置
设置好假身大小朝向:
喷半个屏幕行为:这里的SLEEP事件是发送给是自己的其他分体部件的playmakerFSM上的,就是当整体在执行行为的时候其他部分是不能动的
喷射子弹
大招结束后的冷却时间:
这里我给了四分之一的概率是否重复释放大招
重复执行大招:
否则的话回到Start状态重新 WAKE身体的各个部件,然后从此整体又变成了各个部件
然后再来看看跳跃攻击行为:
判断玩家方向:
超级大跳行为状态:
在空中行为状态:Collision判断是否落地
落地行为状态:
然后还要回到起跳点的位置:
然后就是二段跳阶段:我们在前面记录的第一次起跳前x方向的位置,然后我们就故技重施重新调回去就好了,别忘了来点粒子效果和音效啥的
至此我们完成了一个完整的BOSS相应的行为控制
3.制作战斗场景和战斗门
我觉得战斗门就不用我多说了,因为我上一期已经讲过了,直接预制体一拖然后设置好位置就OK,
我们重点是制作战斗场景,首先创建同名,设置好tag,创建playmakerFSM名字叫:Battle Control
这个heart piece就是游戏的面具碎片,由于我还没做到所以先空着不管:
如果是已经击败过的BOSS就进入activate状态,生成面具碎片以及效果boss房的camera lock
战斗开始后,设置好Camera lock和敌人数量为1,还有就是关门
战斗结束后设置为激活状态,等个五秒多:
结束后记得开门
4.制作BOSS死亡行为
最后我们还要制作BOSS的尸体:
首先我们来制作BOSS尸体爆炸后变成一堆躯干:
还有一些躯干掉落后的橙汁蒸汽之类的:
我们先隐藏好上述的躯干,回到主体当中,需要rb2d和box2d:同是别忘了设置好layer
它的playmaker也很简单,跟我们上期讲到苍蝇之母一样:
告诉battle Scene战斗结束的事件:BATTLE END
初始化生成好波澜:
冒气Steam阶段和上面的差不多:
准备阶段:
最后阶段:boss尸体爆炸,生成躯干和果冻:
果冻就是这个orange Glob:
我们先把它的tk2dsprite和tk2dspriteanimator做好:
这里为什么有两个collider2d呢?因为我要一个来判断果冻是否落地,另一个判断玩家是否用骨钉攻击它:
这个是用来判断有无敌人踩到它:踩到了就执行动画wobbleAnim
攻击它后的效果:
我们来写一个脚本名字就叫:
GlobControl.cs
using System.Collections;
using UnityEngine;[RequireComponent(typeof(Collider2D))]
public class GlobControl : MonoBehaviour
{public Renderer rend;[Space]public float minScale = 0.6f;public float maxScale = 1.6f;[Space]public string landAnim = "Glob Land";public string wobbleAnim = "Glob Wobble";public string breakAnim = "Glob Break";[Space]public AudioSource audioPlayerPrefab;public AudioEvent breakSound;public Color bloodColorOverride = new Color(1f, 0.537f, 0.188f);[Space]public GameObject splatChild;[Space]public Collider2D groundCollider;private bool landed;private bool broken;private tk2dSpriteAnimator anim;private void Awake(){anim = GetComponent<tk2dSpriteAnimator>();}private void OnEnable(){float num = Random.Range(minScale, maxScale);transform.localScale = new Vector3(num, num, 1f);if (splatChild){splatChild.SetActive(false);}landed = false;broken = false;}private void Start(){CollisionEnterEvent collision = GetComponent<CollisionEnterEvent>();if (collision){collision.OnCollisionEnteredDirectional += delegate (CollisionEnterEvent.Direction direction, Collision2D col){if (!landed){if(direction == CollisionEnterEvent.Direction.Bottom){landed = true;collision.doCollisionStay = false;if (CheckForGround()) //检测是否碰到地面{anim.Play(landAnim);return;}StartCoroutine(Break());return;}else{collision.doCollisionStay = true;}}};}TriggerEnterEvent componentInChildren = GetComponentInChildren<TriggerEnterEvent>();if (componentInChildren){componentInChildren.OnTriggerEntered += delegate (Collider2D col, GameObject sender){if (!landed || broken){return;}if (col.gameObject.layer == LayerMask.NameToLayer("Enemies")){anim.Play(wobbleAnim); //检测如果敌人碰到了执行动画wobbleAnim}};}}private void OnTriggerEnter2D(Collider2D col){if (!landed || broken){return;}if (col.tag == "Nail Attack") //如果是骨钉攻击,执行break函数{StartCoroutine(Break());return;}if (col.tag == "HeroBox") //如果是玩家碰到了执行wobble的动画{anim.Play(wobbleAnim);}}private IEnumerator Break(){broken = true;breakSound.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);GlobalPrefabDefaults.Instance.SpawnBlood(transform.position, 4, 5, 5f, 20f, 80f, 100f, new Color?(bloodColorOverride));if (splatChild){splatChild.SetActive(true); //生成一些效果和子对象splatChild}yield return anim.PlayAnimWait(breakAnim);if (rend){rend.enabled = false;}yield break;}private bool CheckForGround(){if (!groundCollider){return true;}Vector2 vector = groundCollider.bounds.min;Vector2 vector2 = groundCollider.bounds.max;float num = vector2.y - vector.y;vector.y = vector2.y;vector.x += 0.1f;vector2.x -= 0.1f;RaycastHit2D raycastHit2D = Physics2D.Raycast(vector, Vector2.down, num + 0.25f, LayerMask.GetMask("Terrain"));RaycastHit2D raycastHit2D2 = Physics2D.Raycast(vector2, Vector2.down, num + 0.25f, LayerMask.GetMask("Terrain"));return raycastHit2D.collider != null && raycastHit2D2.collider != null;}}
别忘了回到编辑器添加好参数:
总结
至此我们完成了制作的第二个BOSS燥郁的毛里克,就在这里展示一下成果吧:
(顺便提一下我把血量设置太高了没打过,只能自己打二周目了)
这里我的spit speed设置太低了,因为我后面改了一下游戏的重力大小,所以喷不远,你们记得根据实际情况来填参数:
右边的爪子也没有问题:
这里没生成躯干是因为不知道为什么我的相机不会渲染的sprite-default的material了,所以没办法只能换个material:
OK终于做完一期了,刚好最近有空整理一下最近做的内容。