提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作小骑士的接触地刺复活机制
- 1.制作动画以及使用UNITY编辑器编辑
- 2.实现有攻击的地刺行为
- 3.使用代码实现小骑士接触复活功能
- 二、完善地图的可交互对象
- 总结
前言
Hello大家好久不见,隔了几天没发文章是因为这几天放假了被迫休息一下,但我还是在国庆这一天给大伙肝出来,这期的工程量也是比较大,找素材和堆叠素材到场景中花费了很多时间,但难度并不会更高,还有就是我又做上头了,本来想单独出一期接触地刺复活机制,但我做完后觉得不爽还是等做完了整个地图的可交互对象才想起来要写文章。
话不多说我们直接开始进入本期文章。
一、制作小骑士的接触地刺复活机制
1.制作动画以及使用UNITY编辑器编辑
老规矩我们还是给我们之前创建的小骑士的tk2dSprite和tk2dSpriteAnimation添加上本期要用的素材,主要是Spike Death和Spike Death Antic还有Hazard Respawn三个动画
然后我实现接触地刺复活的思想就是当小骑士碰到地刺时,隐藏小骑士的图像(也就是关闭它的MeshRenderer)然后再同一个位置生成一个小骑士死亡的Prefab叫Spike Death Prefab,然后等Prefab的动画播放完成后,再开启小骑士的图像,同时Destory掉这个prefab,这个过程会涉及到摄像机的淡入淡出,这就会让玩家不会产生一种有两个小骑士的感觉,从而从视觉上欺骗玩家,OK理解了这个思想后我们就来制作这个prefab吧:
它有个子对象是制作效果用的,但我还没找到合适的素材所以就先略过吧,你可能注意到它上面拥有一个PlaymakerFSM,我们用它来实现上述的行为。
特殊的变量和事件如下:
这里还有一个我之前没讲到的PlaymakerFSM的全局变量CameraParent,我们先去tk2dSprite创建一个PlaymakerFSM来生成它吧:
回到刚刚的Knight Death Control来,它只有两种状态:
这里调用了一个方法,是GameManager中的FreezeMoment(int),我们用它来控制游戏时间的速度
cs
public void FreezeMoment(int type)
{
if (type == 0)
{
StartCoroutine(FreezeMoment(0.01f, 0.35f, 0.1f, 0f));
}
else if (type == 1)
{
StartCoroutine(FreezeMoment(0.04f, 0.03f, 0.04f, 0f));
}
else if (type == 2)
{
StartCoroutine(FreezeMoment(0.25f, 2f, 0.25f, 0.15f));
}
else if (type == 3)
{
StartCoroutine(FreezeMoment(0.01f, 0.25f, 0.1f, 0f));
}
if (type == 4)
{
StartCoroutine(FreezeMoment(0.01f, 0.25f, 0.1f, 0f));
}
if (type == 5)
{
StartCoroutine(FreezeMoment(0.01f, 0.25f, 0.1f, 0f));
}
}
public IEnumerator FreezeMoment(float rampDownTime,float waitTime,float rampUpTime,float targetSpeed)
{
timeSlowedCount++;
yield return StartCoroutine(SetTimeScale(targetSpeed, rampDownTime));
for (float timer = 0f; timer < waitTime; timer += Time.unscaledDeltaTime)
{
yield return null;
}
yield return StartCoroutine(SetTimeScale(1f, rampUpTime));
timeSlowedCount--;
}
最后一个状态就是回收自己,当发送事件HAZARD RELOAD时就回收这个预制体即可
最后是要注意你的TK2D的动画名字一定要和playmaker里面的一样。至此我们做好了一个可以欺骗玩家的预制体 ,
2.制作有攻击的地刺行为
OK接下来我们来做一下地刺,很简单,只需要改Layer为"Hero Attack",更改碰撞箱大小并勾选isTrigger,然后给它添加上DamageHero和DamageEnemy即可,需要注意的是DamageHero的HazardType必须填2,因为我规定2为Spike地刺的伤害类型
需要注意的是,有一些CaveSpikes是有SpriteRenderer和Animator的而有一些没有,所以这里我用有没有s后缀来区分它们。
3.使用代码实现小骑士接触地刺复活功能
OK接下来就来到我们最爱的HeroController.cs代码当中,首先是在HeroControllerStates中添加两个新的状态:hazardDeath和hazardRespawning,还有HeroController中新建几个变量涉及我们刚刚创建的预制体,Hazard时期的地面检测等等。
private float hazardLandingTimer;
private float HAZARD_DEATH_CHECK_TIME = 3f;
public GameObject spikeDeathPrefab;
public GameObject acidDeathPrefab;
private float FIND_GROUND_POINT_DISTANCE = 10f;
private float FIND_GROUND_POINT_DISTANCE_EXT = 50f;
在FailSafeCheck()函数中,我们判断如果玩家在cState.hazardDeath状态下如果hazardLandingTimer计时时间超出死亡检测时间那么将重置玩家玩家的状态
cs
if (cState.hazardDeath)
{
hazardLandingTimer += Time.deltaTime;
if(hazardLandingTimer > HAZARD_DEATH_CHECK_TIME && hero_state != ActorStates.no_input)
{
ResetMotion();
AffectedByGravity(false);
SetState(ActorStates.no_input);
hazardLandingTimer = 0f;
}
else
{
hazardLandingTimer = 0f;
}
}
同样在一些判断能否进行某些动作行为中,我们也要添加上它们不能在我们hazardDeath和hazardRespawning的状态下执行,就比如:
cs
private bool CanAttack()
{
return attack_cooldown <= 0f && !cState.attacking && !cState.dashing && !cState.dead && !cState.hazardDeath && !cState.hazardRespawning && !controlReqlinquished && hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing;
}
public bool CanFocus()
{
return !gm.isPaused && hero_state != ActorStates.no_input && !cState.dashing && !cState.backDashing && (!cState.attacking || attack_time > ATTACK_RECOVERY_TIME) && !cState.recoiling && cState.onGround && !cState.recoilFrozen && !cState.hazardDeath && !cState.hazardRespawning && CanInput();
}
public bool CanCast()
{
return !gm.isPaused && !cState.dashing && hero_state != ActorStates.no_input && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) && !cState.recoiling && !cState.recoilFrozen && !cState.hazardDeath && !cState.hazardRespawning && CanInput() && preventCastByDialogueEndTimer <= 0f;
}
public bool CanDash()
{
return hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing &&
dashCooldownTimer <= 0f && !cState.dashing && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) && !cState.preventDash && (cState.onGround || !airDashed) && !cState.hazardDeath && playerData.canDash;
}
public bool CanTakeDamage()
{
return damageMode != DamageMode.NO_DAMAGE && !cState.invulnerable && !cState.recoiling && !cState.dead && !cState.hazardDeath;
}
回到我们之前创建的TakeDamage()方法中,这下我们就可以给其它的hazard类型添加相关代码了:
cs
public void TakeDamage(GameObject go,CollisionSide damageSide,int damageAmount,int hazardType)
{
bool spawnDamageEffect = true;
if (damageAmount > 0)
{
if (CanTakeDamage())
{
if (damageMode == DamageMode.HAZARD_ONLY && hazardType == 1)
{
return;
}
if (parryInvulnTimer > 0f && hazardType == 1)
{
return;
}
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
CancelAttack();
if (cState.touchingWall)
{
cState.touchingWall = false;
}
if (cState.recoilingLeft || cState.recoilingRight)
{
CancelRecoilHorizonal();
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
if (!takeNoDamage)
{
playerData.TakeHealth(damageAmount);
}
if (damageAmount > 0 && OnTakenDamage != null)
{
OnTakenDamage();
}
if (playerData.health == 0)
{
StartCoroutine(Die());
return;
}
if (hazardType == 2)
{
Debug.LogFormat("Die From Spikes");
StartCoroutine(DieFromHazard(HazardType.SPIKES, (go != null) ? go.transform.rotation.z : 0f));
return;
}
if (hazardType == 3)
{
Debug.LogFormat("Die From Acid");
StartCoroutine(DieFromHazard(HazardType.ACID,0f));
return;
}
if (hazardType == 4)
{
Debug.LogFormat("Die From Lava");
return;
}
if (hazardType == 5)
{
Debug.LogFormat("Die From Pit");
return;
}
StartCoroutine(StartRecoil(damageSide, spawnDamageEffect, damageAmount));
return;
}
else if (cState.invulnerable && !cState.hazardDeath)
{
if(hazardType == 2)
{
if (!takeNoDamage)
{
playerData.TakeHealth(damageAmount);
}
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
if(playerData.health == 0)
{
StartCoroutine(Die());
return;
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
StartCoroutine(DieFromHazard(HazardType.SPIKES, (go != null) ? go.transform.rotation.z : 0f));
return;
}
else if (hazardType == 3)
{
playerData.TakeHealth(damageAmount);
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
if (playerData.health == 0)
{
StartCoroutine(Die());
return;
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
StartCoroutine(DieFromHazard(HazardType.ACID, 0f));
return;
}
else if(hazardType == 4)
{
Debug.LogFormat("Die From Lava");
}
}
}
}
这里我们创建一个协程名字叫DieFromHazard(),以下是该协程需要执行的函数有那些:将小骑士的父对象设置为空,cState.hazardDeath = true;重置玩家行为,重置重落地计时器,不受重力影响,关闭小骑士的meshrenderer,层级设置为Ignore Raycast,并生成我们上面创建好的预制体,
执行gamemanager中的PlayerDeadFromHazard()玩家接触地刺行为函数
cs
private IEnumerator DieFromHazard(HazardType hazardType,float angle)
{
if (!cState.hazardDeath)
{
playerData.disablePause = true;
SetHeroParent(null);
SetState(ActorStates.no_input);
cState.hazardDeath = true;
ResetMotion();
ResetHardLandingTimer();
AffectedByGravity(false);
renderer.enabled = false;
gameObject.layer = 2;
if(hazardType == HazardType.SPIKES)
{
GameObject gameObject = Instantiate(spikeDeathPrefab);
gameObject.transform.position = transform.position;
FSMUtility.SetFloat(gameObject.GetComponent<PlayMakerFSM>(), "Spike Direction", angle * 57.29578f);
}
else if(hazardType == HazardType.ACID)
{
GameObject gameObject2 = Instantiate(acidDeathPrefab);
gameObject2.transform.position = transform.position;
gameObject2.transform.localScale = transform.localScale;
}
yield return null;
StartCoroutine(gm.PlayerDeadFromHazard(0f));
}
}
来到GameManager.cs中,我们在该协程中执行下列行为:执行相机淡入淡出(这个我后面做到了再来补充,所以相当于没用),还有就是广播事件"HAZARD RELOAD"给所有的PlayermakerFSM,这其中就包括我们上面创建的预制体的PlayermakerFSM,当它接收到这个事件后就会自我销毁,最后来执行HazardRespawn()
cs
public IEnumerator PlayerDeadFromHazard(float waitTime)
{
cameraCtrl.FreezeInPlace(true);
yield return new WaitForSeconds(waitTime);
cameraCtrl.FadeOut(CameraFadeType.HERO_HAZARD_DEATH);
yield return new WaitForSeconds(0.8f);
PlayMakerFSM.BroadcastEvent("HAZARD RELOAD");
HazardRespawn();
}
cs
public void HazardRespawn()
{
hazardRespawningHero = true;
cameraCtrl.ResetStartTimer();
cameraCtrl.camTarget.mode = CameraTarget.TargetMode.FOLLOW_HERO;
EnterHero(false);
}
EnterHero()方法我后面会拿来用到场景切换中,这里暂且只有处理hazardRespawn相关的代码:
cs
private void EnterHero(bool additiveGateSearch = false)
{
if (hazardRespawningHero)
{
StartCoroutine(hero_ctrl.HazardRespawn());
FinishedEnteringScene();
hazardRespawningHero = false;
return;
}
}
private void FinishedEnteringScene()
{
//TODO:s
}
小骑士触发地刺死亡的相关行为我们做完了,接下来我们来实现如何从地刺死亡后重生(或者叫复活):
回到HeroController.cs新建协程HazardRespawn(),除了重置一堆函数外,设置层级为原本的player,开启meshrenderer,决定玩家朝向,开启无敌状态的协程,摄像机相关,获取动画播放时间然后等待动画播放完成后,设置刚体状态,FinishedEnteringScene()
cs
public IEnumerator HazardRespawn()
{
cState.hazardDeath = false;
cState.onGround = true;
cState.hazardRespawning = true;
ResetMotion();
ResetHardLandingTimer();
ResetAttacks();
ResetInput();
cState.recoiling = false;
airDashed = false;
transform.SetPosition2D(FindGroundPoint(playerData.hazardRespawnLocation, true));
gameObject.layer = 9;
renderer.enabled = true;
yield return new WaitForEndOfFrame();
if (playerData.hazardRespawnFacingRight)
{
FaceRight();
}
else
{
FaceLeft();
}
if(heroInPosition != null)
{
heroInPosition(false);
}
StartCoroutine(Invulnerable(INVUL_TIME * 2f));
GameCameras.instance.cameraFadeFSM.SendEvent("RESPAWN");
float clipDuration = animCtrl.GetClipDuration("Hazard Respawn");
animCtrl.PlayClip("Hazard Respawn");
yield return new WaitForSeconds(clipDuration);
cState.hazardRespawning = false;
rb2d.interpolation = RigidbodyInterpolation2D.Interpolate;
FinishedEnteringScene(false, false);
}
首先来到HeroAnimationController.cs,我们补充玩家获取动画播放时长的方法:
cs
/// <summary>
/// 获取动画的播放时间
/// </summary>
/// <param name="clipName"></param>
/// <returns></returns>
public float GetClipDuration(string clipName)
{
if (animator == null)
{
animator = GetComponent<tk2dSpriteAnimator>();
}
tk2dSpriteAnimationClip clipByName = animator.GetClipByName(clipName);
if (clipByName == null)
{
Debug.LogError("HeroAnim: Could not find animation clip with the name " + clipName);
return -1f;
}
return clipByName.frames.Length / clipByName.fps;
}
回到HeroController.cs,获取地面位点:
cs
public Vector3 FindGroundPoint(Vector2 startPoint,bool useExtended = false)
{
float num = FIND_GROUND_POINT_DISTANCE;
if (useExtended)
{
num = FIND_GROUND_POINT_DISTANCE_EXT;
}
RaycastHit2D raycastHit2D = Physics2D.Raycast(startPoint, Vector2.down, num, LayerMask.GetMask("Terrain"));
if(raycastHit2D.collider == null)
{
Debug.LogErrorFormat("FindGroundPoint: Could not find ground point below {0}, check reference position is not too high (more than {1} tiles).", new object[]
{
startPoint.ToString(),
num
});
}
return new Vector3(raycastHit2D.point.x, raycastHit2D.point.y + col2d.bounds.extents.y - col2d.offset.y + 0.01f, transform.position.z);
}
cs
private void FinishedEnteringScene(bool setHazardMarker = true, bool preventRunBob = false)
{
playerData.disablePause = false;
SetStartingMotionState(preventRunBob);
AffectedByGravity(true);
if (setHazardMarker)
{
}
SetDamageMode(DamageMode.FULL_DAMAGE);
}
然后就是我们要确定玩家触发地刺复活后要到什么位置,这里就需要一些特殊的位置了:
我以这一篇地刺田为例子,创建好触发复活位置的游戏对象Hazard Respawn Trigger,当玩家经过并离开这个Trigger箱以后设置复活位置为这里以及玩家复活后的朝向
在此之前我们还要制作一个记录点来查看是否为地面层级
cs
using UnityEngine;
[ExecuteInEditMode]
public class HazardRespawnMarker : MonoBehaviour
{
public bool respawnFacingRight;
private float groundSenseDistance = 3f;
private Vector2 groundSenseRay = Vector2.down;
private Vector2 heroSpawnLocation;
public bool drawDebugRays;
private void Awake()
{
if (transform.parent != null && transform.parent.name.Contains("top"))
{
groundSenseDistance = 50f;
}
heroSpawnLocation = Physics2D.Raycast(transform.position, groundSenseRay, groundSenseDistance, LayerMask.GetMask("Terrain")).point;
}
private void Update()
{
if (drawDebugRays)
{
Debug.DrawRay(transform.position, groundSenseRay * groundSenseDistance, Color.green);
Debug.DrawRay(heroSpawnLocation - Vector2.right / 2f, Vector2.right, Color.green);
}
}
}
回到Hazard Respawn Trigger中我们创建一个同名脚本:请保证记录点和Trigger的点位保持一致,
cs
using UnityEngine;
public class HazardRespawnTrigger : MonoBehaviour
{
public HazardRespawnMarker respawnMarker;
private PlayerData playerData;
public bool fireOnce;
private bool inactive;
private void Awake()
{
playerData = PlayerData.instance;
if (playerData == null)
{
Debug.LogError(name + "- Player Data reference is null, please check this is being set correctly.");
}
}
private void Start()
{
if (respawnMarker == null)
{
Debug.LogWarning(name + " does not have a Hazard Respawn Marker Set");
}
}
private void OnTriggerEnter2D(Collider2D otherCollision)
{
if (!inactive && otherCollision.gameObject.layer == 9)
{
playerData.SetHazardRespawn(respawnMarker);
if (fireOnce)
{
inactive = true;
}
}
}
}
来到PlayerData.cs中,我们用两个变量来记录位置和朝向,这个是否无敌的bool类型变量后面也有用我们也一并创建了吧:
cs
[NonSerialized]
public Vector3 hazardRespawnLocation;
public bool hazardRespawnFacingRight;
public bool isInvincible ;
public void Reset()
{
SetupNewPlayerData();
}
private void SetupNewPlayerData()
{
hazardRespawnLocation = Vector3.zero;
hazardRespawnFacingRight = false;
isInvincible = false;
}
public void SetHazardRespawn(HazardRespawnMarker location)
{
hazardRespawnLocation = location.transform.position;
hazardRespawnFacingRight = location.respawnFacingRight;
Debug.LogFormat("hazardRespawnLocation =" + hazardRespawnLocation);
Debug.LogFormat("hazardRespawnFacingRight =" + hazardRespawnFacingRight);
}
这两个变量在哪里被调用了呢?当然使我们上面创建的HeroController.cs中的HazardRespawn()协程了:
cs
public IEnumerator HazardRespawn()
{
cState.hazardDeath = false;
cState.onGround = true;
cState.hazardRespawning = true;
ResetMotion();
ResetHardLandingTimer();
ResetAttacks();
ResetInput();
cState.recoiling = false;
airDashed = false;
transform.SetPosition2D(FindGroundPoint(playerData.hazardRespawnLocation, true));
gameObject.layer = 9;
renderer.enabled = true;
yield return new WaitForEndOfFrame();
if (playerData.hazardRespawnFacingRight)
{
FaceRight();
}
else
{
FaceLeft();
}
if(heroInPosition != null)
{
heroInPosition(false);
}
StartCoroutine(Invulnerable(INVUL_TIME * 2f));
GameCameras.instance.cameraFadeFSM.SendEvent("RESPAWN");
float clipDuration = animCtrl.GetClipDuration("Hazard Respawn");
animCtrl.PlayClip("Hazard Respawn");
yield return new WaitForSeconds(clipDuration);
cState.hazardRespawning = false;
rb2d.interpolation = RigidbodyInterpolation2D.Interpolate;
FinishedEnteringScene(false, false);
}
完整的HeroController.cs代码如下:
cs
using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using GlobalEnums;
using UnityEngine;
using System.Reflection;
public class HeroController : MonoBehaviour
{
public ActorStates hero_state;
public ActorStates prev_hero_state;
public bool acceptingInput = true;
public bool controlReqlinquished; //控制是否被放弃
public bool isHeroInPosition = true;
public delegate void HeroInPosition(bool forceDirect);
public event HeroInPosition heroInPosition;
public float move_input;
public float vertical_input;
public Vector2 current_velocity;
public float WALK_SPEED = 3.1f;//走路速度
public float RUN_SPEED = 5f;//跑步速度
public float JUMP_SPEED = 5f;//跳跃的速度
private NailSlash slashComponent; //决定使用哪种攻击的NailSlash
private PlayMakerFSM slashFsm;//决定使用哪种攻击的PlayMakerFSM
public NailSlash normalSlash;
public NailSlash altetnateSlash;
public NailSlash upSlash;
public NailSlash downSlash;
public PlayMakerFSM normalSlashFsm;
public PlayMakerFSM altetnateSlashFsm;
public PlayMakerFSM upSlashFsm;
public PlayMakerFSM downSlashFsm;
private bool attackQueuing; //是否开始攻击计数步骤
private int attackQueueSteps; //攻击计数步骤
private float attack_time;
private float attackDuration; //攻击状态持续时间,根据有无护符来决定
private float attack_cooldown;
private float altAttackTime; //当时间超出可按二段攻击的时间后,cstate.altattack就会为false
public float ATTACK_DURATION; //无护符时攻击状态持续时间
public float ATTACK_COOLDOWN_TIME; //攻击后冷却时间
public float ATTACK_RECOVERY_TIME; //攻击恢复时间,一旦超出这个时间就退出攻击状态
public float ALT_ATTACK_RESET; //二段攻击重置时间
private int ATTACK_QUEUE_STEPS = 5; //超过5步即可开始攻击
private float NAIL_TERRAIN_CHECK_TIME = 0.12f;
private bool drainMP; //是否正在流走MP
private float drainMP_timer; //流走MP的计时器
private float drainMP_time; //流走MP花费的时间
private float MP_drained; //已经流走的MP数量
private float focusMP_amount; //使用focus回血所需要的MP数量
private float preventCastByDialogueEndTimer;
public PlayMakerFSM spellControl;
private int jump_steps; //跳跃的步
private int jumped_steps; //已经跳跃的步
private int jumpQueueSteps; //跳跃队列的步
private bool jumpQueuing; //是否进入跳跃队列中
private int jumpReleaseQueueSteps; //释放跳跃后的步
private bool jumpReleaseQueuing; //是否进入释放跳跃队列中
private bool jumpReleaseQueueingEnabled; //是否允许进入释放跳跃队列中
public float MAX_FALL_VELOCITY; //最大下落速度(防止速度太快了)
public int JUMP_STEPS; //最大跳跃的步
public int JUMP_STEPS_MIN; //最小跳跃的步
private int JUMP_QUEUE_STEPS; //最大跳跃队列的步
private int JUMP_RELEASE_QUEUE_STEPS;//最大跳跃释放队列的步
private int dashQueueSteps;
private bool dashQueuing;
private float dashCooldownTimer; //冲刺冷却时间
private float dash_timer; //正在冲刺计数器
private float back_dash_timer; 正在后撤冲刺计数器 (标注:此行代码无用待后续开发)
private float dashLandingTimer;
private bool airDashed;//是否是在空中冲刺
public bool dashingDown;//是否正在执行向下冲刺
public PlayMakerFSM dashBurst;
public GameObject dashParticlesPrefab;//冲刺粒子效果预制体
public GameObject backDashPrefab; //后撤冲刺特效预制体 标注:此行代码无用待后续开发
private GameObject backDash;//后撤冲刺 (标注:此行代码无用待后续开发)
private GameObject dashEffect;//后撤冲刺特效生成 (标注:此行代码无用待后续开发)
public float DASH_SPEED; //冲刺时的速度
public float DASH_TIME; //冲刺时间
public float DASH_COOLDOWN; //冲刺冷却时间
public float BACK_DASH_SPEED;//后撤冲刺时的速度 (标注:此行代码无用待后续开发)
public float BACK_DASH_TIME;//后撤冲刺时间 (标注:此行代码无用待后续开发)
public float BACK_DASH_COOLDOWN; //后撤冲刺冷却时间 (标注:此行代码无用待后续开发)
public float DOWN_DASH_TIME; //向下冲刺持续时间
public float DASH_LANDING_TIME;
public int DASH_QUEUE_STEPS; //最大冲刺队列的步
public delegate void TakeDamageEvent();
public event TakeDamageEvent OnTakenDamage;
public delegate void OnDeathEvent();
public event OnDeathEvent OnDeath;
public bool takeNoDamage; //不受到伤害
public PlayMakerFSM damageEffectFSM; //负责的受伤效果playmakerFSM
public DamageMode damageMode; //受伤类型
private Coroutine takeDamageCoroutine; //受伤协程
private float parryInvulnTimer; //无敌时间
public float INVUL_TIME;//无敌时间
public float DAMAGE_FREEZE_DOWN; //受伤冻结的上半程时间
public float DAMAGE_FREEZE_WAIT; //受伤冻结切换的时间
public float DAMAGE_FREEZE_UP;//受伤冻结的下半程时间
private int recoilSteps;
private float recoilTimer; //后坐力计时器
private bool recoilLarge; //是否是更大的后坐力
private Vector2 recoilVector; //后坐力二维上的速度
public float RECOIL_HOR_VELOCITY; //后坐力X轴上的速度
public float RECOIL_HOR_VELOCITY_LONG; //后坐力X轴上更大的速度
public float RECOIL_DOWN_VELOCITY; //后坐力Y轴上的速度
public float RECOIL_HOR_STEPS; //后坐力X轴的步
public float RECOIL_DURATION; //后坐力持续时间
public float RECOIL_VELOCITY; //后坐力时的速度(是两个轴上都适用的)
public float fallTimer { get; private set; }
private float hardLandingTimer; //正在hardLanding的计时器,大于就将状态改为grounded并BackOnGround()
private float hardLandFailSafeTimer; //进入hardLand后玩家失去输入的一段时间
private bool hardLanded; //是否已经hardLand了
public float HARD_LANDING_TIME; //正在hardLanding花费的时间。
public float BIG_FALL_TIME; //判断是否是hardLanding所需要的事件,大于它就是
public GameObject hardLandingEffectPrefab;
private float prevGravityScale;
private int landingBufferSteps;
private int LANDING_BUFFER_STEPS = 5;
private bool fallRumble; //是否开启掉落时相机抖动
private float floatingBufferTimer;
private float FLOATING_CHECK_TIME = 0.18f;
private bool startWithWallslide;
private bool startWithJump;
private bool startWithFullJump;
private bool startWithDash;
private bool startWithAttack;
public GameObject softLandingEffectPrefab;
public bool touchingWall; //是否接触到墙
public bool touchingWallL; //是否接触到的墙左边
public bool touchingWallR; //是否接触到的墙右边
private float hazardLandingTimer;
private float HAZARD_DEATH_CHECK_TIME = 3f;
public GameObject spikeDeathPrefab;
public GameObject acidDeathPrefab;
private float FIND_GROUND_POINT_DISTANCE = 10f;
private float FIND_GROUND_POINT_DISTANCE_EXT = 50f;
private Rigidbody2D rb2d;
private BoxCollider2D col2d;
private GameManager gm;
public PlayerData playerData;
private InputHandler inputHandler;
public HeroControllerStates cState;
private HeroAnimationController animCtrl;
private HeroAudioController audioCtrl;
private new MeshRenderer renderer;
private InvulnerablePulse invPulse;
private SpriteFlash spriteFlash;
public PlayMakerFSM proxyFSM { get; private set; }
private static HeroController _instance;
public static HeroController instance
{
get
{
if (_instance == null)
_instance = FindObjectOfType<HeroController>();
if(_instance && Application.isPlaying)
{
DontDestroyOnLoad(_instance.gameObject);
}
return _instance;
}
}
public HeroController()
{
ATTACK_QUEUE_STEPS = 5;
NAIL_TERRAIN_CHECK_TIME = 0.12f;
JUMP_QUEUE_STEPS = 2;
JUMP_RELEASE_QUEUE_STEPS = 2;
LANDING_BUFFER_STEPS = 5;
FLOATING_CHECK_TIME = 0.18f;
}
private void Awake()
{
if(_instance == null)
{
_instance = this;
DontDestroyOnLoad(this);
}
else if(this != _instance)
{
Destroy(gameObject);
return;
}
SetupGameRefs();
}
private void SetupGameRefs()
{
if (cState == null)
cState = new HeroControllerStates();
rb2d = GetComponent<Rigidbody2D>();
col2d = GetComponent<BoxCollider2D>();
animCtrl = GetComponent<HeroAnimationController>();
audioCtrl = GetComponent<HeroAudioController>();
gm = GameManager.instance;
playerData = PlayerData.instance;
inputHandler = gm.GetComponent<InputHandler>();
renderer = GetComponent<MeshRenderer>();
invPulse = GetComponent<InvulnerablePulse>();
spriteFlash = GetComponent<SpriteFlash>();
proxyFSM = FSMUtility.LocateFSM(gameObject, "ProxyFSM");
}
private void Start()
{
heroInPosition += delegate(bool forceDirect)
{
isHeroInPosition = true;
};
playerData = PlayerData.instance;
if (dashBurst == null)
{
Debug.Log("DashBurst came up null, locating manually");
dashBurst = FSMUtility.GetFSM(transform.Find("Effects").Find("Dash Burst").gameObject);
}
if (spellControl == null)
{
Debug.Log("SpellControl came up null, locating manually");
spellControl = FSMUtility.LocateFSM(gameObject, "Spell Control");
}
if(heroInPosition != null)
{
heroInPosition(false);
}
}
private void Update()
{
current_velocity = rb2d.velocity;
FallCheck();
FailSafeCheck();
if (hero_state == ActorStates.running && !cState.dashing && !cState.backDashing && !controlReqlinquished)
{
if (cState.inWalkZone)
{
audioCtrl.StopSound(HeroSounds.FOOTSETP_RUN);
audioCtrl.PlaySound(HeroSounds.FOOTSTEP_WALK);
}
else
{
audioCtrl.StopSound(HeroSounds.FOOTSTEP_WALK);
audioCtrl.PlaySound(HeroSounds.FOOTSETP_RUN);
}
}
else
{
audioCtrl.StopSound(HeroSounds.FOOTSETP_RUN);
audioCtrl.StopSound(HeroSounds.FOOTSTEP_WALK);
}
if (hero_state == ActorStates.dash_landing)
{
dashLandingTimer += Time.deltaTime;
if (dashLandingTimer > DOWN_DASH_TIME)
{
BackOnGround();
}
}
if (hero_state == ActorStates.hard_landing)
{
hardLandingTimer += Time.deltaTime;
if (hardLandingTimer > HARD_LANDING_TIME)
{
SetState(ActorStates.grounded);
BackOnGround();
}
}
if (hero_state == ActorStates.no_input)
{
if (cState.recoiling)
{
if (recoilTimer < RECOIL_DURATION)
{
recoilTimer += Time.deltaTime;
}
else
{
CancelDamageRecoil();
if ((prev_hero_state == ActorStates.idle || prev_hero_state == ActorStates.running) && !CheckTouchingGround())
{
cState.onGround = false;
SetState(ActorStates.airborne);
}
else
{
SetState(ActorStates.previous);
}
}
}
}
else if (hero_state != ActorStates.no_input)
{
LookForInput();
if (cState.recoiling)
{
cState.recoiling = false;
AffectedByGravity(true);
}
if (cState.attacking && !cState.dashing)
{
attack_time += Time.deltaTime;
if (attack_time >= attackDuration)
{
ResetAttacks();
animCtrl.StopAttack();
}
}
if(hero_state == ActorStates.idle)
{
if(!controlReqlinquished && !gm.isPaused)
{
//TODO:
}
}
}
LookForQueueInput();
if (drainMP)
{
drainMP_timer += Time.deltaTime;
while (drainMP_timer >= drainMP_time) //drainMP_time在无护符下等于0.027
{
MP_drained += 1f;
drainMP_timer -= drainMP_time;
TakeMp(1);
if(MP_drained == focusMP_amount)
{
MP_drained -= drainMP_time;
proxyFSM.SendEvent("HeroCtrl-FocusCompleted");
}
}
}
if (attack_cooldown > 0f)
{
attack_cooldown -= Time.deltaTime;
}
if (dashCooldownTimer > 0f) //计时器在Update中-= Time.deltaTime
{
dashCooldownTimer -= Time.deltaTime;
}
preventCastByDialogueEndTimer -= Time.deltaTime;
if (parryInvulnTimer > 0f)
{
parryInvulnTimer -= Time.deltaTime;
}
}
private void FixedUpdate()
{
if(cState.recoilingLeft || cState.recoilingRight)
{
if(recoilSteps <= RECOIL_HOR_STEPS)
{
recoilSteps++;
}
else
{
CancelRecoilHorizonal();
}
}
if(hero_state == ActorStates.hard_landing || hero_state == ActorStates.dash_landing)
{
ResetMotion();
}
else if(hero_state == ActorStates.no_input)
{
if (cState.recoiling)
{
AffectedByGravity(false);
rb2d.velocity = recoilVector;
}
}
else if (hero_state != ActorStates.no_input)
{
if(hero_state == ActorStates.running)
{
if(move_input > 0f)
{
if (CheckForBump(CollisionSide.right))
{
//rb2d.velocity = new Vector2(rb2d.velocity.x, BUMP_VELOCITY);
}
}
else if (CheckForBump(CollisionSide.left))
{
//rb2d.velocity = new Vector2(rb2d.velocity.x, -BUMP_VELOCITY);
}
}
if (!cState.dashing && !cState.backDashing)
{
Move(move_input);
if (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME)
{
if (move_input > 0f && !cState.facingRight)
{
FlipSprite();
CancelAttack();
}
else if (move_input < 0f && cState.facingRight)
{
FlipSprite();
CancelAttack();
}
}
if(cState.recoilingLeft)
{
float num;
if (recoilLarge)
{
num = RECOIL_HOR_VELOCITY_LONG;
}
else
{
num = RECOIL_HOR_VELOCITY;
}
if(rb2d.velocity.x > -num)
{
rb2d.velocity = new Vector2(-num, rb2d.velocity.y);
}
else
{
rb2d.velocity = new Vector2(rb2d.velocity.x - num, rb2d.velocity.y);
}
}
if (cState.recoilingRight)
{
float num2;
if(recoilLarge)
{
num2 = RECOIL_HOR_VELOCITY_LONG;
}
else
{
num2 = RECOIL_HOR_VELOCITY;
}
if (rb2d.velocity.x < num2)
{
rb2d.velocity = new Vector2(num2, rb2d.velocity.y);
}
else
{
rb2d.velocity = new Vector2(rb2d.velocity.x + num2, rb2d.velocity.y);
}
}
}
}
if (cState.jumping) //如果cState.jumping就Jump
{
Jump();
}
if (cState.dashing)//如果cState.dashing就Dash
{
Dash();
}
//限制速度
if(rb2d.velocity.y < -MAX_FALL_VELOCITY && !controlReqlinquished )
{
rb2d.velocity = new Vector2(rb2d.velocity.x, -MAX_FALL_VELOCITY);
}
if (jumpQueuing)
{
jumpQueueSteps++;
}
if (dashQueuing) //跳跃队列开始
{
dashQueueSteps++;
}
if(attackQueuing)
{
attackQueueSteps++;
}
if (landingBufferSteps > 0)
{
landingBufferSteps--;
}
if (jumpReleaseQueueSteps > 0)
{
jumpReleaseQueueSteps--;
}
cState.wasOnGround = cState.onGround;
}
/// <summary>
/// 小骑士移动的函数
/// </summary>
/// <param name="move_direction"></param>
private void Move(float move_direction)
{
if (cState.onGround)
{
SetState(ActorStates.grounded);
}
if(acceptingInput)
{
if (cState.inWalkZone)
{
rb2d.velocity = new Vector2(move_direction * WALK_SPEED, rb2d.velocity.y);
return;
}
rb2d.velocity = new Vector2(move_direction * RUN_SPEED, rb2d.velocity.y);
}
}
private void Attack(AttackDirection attackDir)
{
if(Time.timeSinceLevelLoad - altAttackTime > ALT_ATTACK_RESET)
{
cState.altAttack = false;
}
cState.attacking = true;
attackDuration = ATTACK_DURATION;
if (attackDir == AttackDirection.normal)
{
if (!cState.altAttack)
{
slashComponent = normalSlash;
slashFsm = normalSlashFsm;
cState.altAttack = true;
}
else
{
slashComponent = altetnateSlash;
slashFsm = altetnateSlashFsm;
cState.altAttack = false;
}
}
else if (attackDir == AttackDirection.upward)
{
slashComponent = upSlash;
slashFsm = upSlashFsm;
cState.upAttacking = true;
}
else if (attackDir == AttackDirection.downward)
{
slashComponent = downSlash;
slashFsm = downSlashFsm;
cState.downAttacking = true;
}
if(attackDir == AttackDirection.normal && cState.facingRight)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 0f;
}
else if (attackDir == AttackDirection.normal && !cState.facingRight)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 180f;
}
else if (attackDir == AttackDirection.upward)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 90f;
}
else if (attackDir == AttackDirection.downward)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 270f;
}
altAttackTime = Time.timeSinceLevelLoad;
slashComponent.StartSlash();
}
private void DoAttack()
{
cState.recoiling = false;
attack_cooldown = ATTACK_COOLDOWN_TIME;
if(vertical_input > Mathf.Epsilon)
{
Attack(AttackDirection.upward);
StartCoroutine(CheckForTerrainThunk(AttackDirection.upward));
return;
}
if(vertical_input >= -Mathf.Epsilon)
{
Attack(AttackDirection.normal);
StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
return;
}
if(hero_state != ActorStates.idle && hero_state != ActorStates.running)
{
Attack(AttackDirection.downward);
StartCoroutine(CheckForTerrainThunk(AttackDirection.downward));
return;
}
Attack(AttackDirection.normal);
StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
}
private bool CanAttack()
{
return attack_cooldown <= 0f && !cState.attacking && !cState.dashing && !cState.dead && !cState.hazardDeath && !cState.hazardRespawning && !controlReqlinquished && hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing;
}
private void CancelAttack()
{
if (cState.attacking)
{
slashComponent.CancelAttack();
ResetAttacks();
}
}
private void ResetAttacks()
{
cState.attacking = false;
cState.upAttacking = false;
cState.downAttacking = false;
attack_time = 0f;
}
public void AddMPCharge(int amount)
{
int mpreverse = playerData.MPReverse;
playerData.AddMPCharge(amount);
if(playerData.MPReverse != mpreverse && gm)
{
}
}
public void SetMPCharge(int amount)
{
playerData.MPCharge = amount;
//TODO:
}
public void SoulGain()
{
int num;
if(playerData.MPCharge < playerData.maxMP)
{
num = 11;
}
else
{
num = 6;
}
int mpreverse = playerData.MPReverse;
playerData.AddMPCharge(num);
if(playerData.MPReverse != mpreverse)
{
}
}
public void TakeMp(int amount)
{
if(playerData.MPCharge > 0)
{
playerData.TakeMP(amount);
if(amount > 1)
{
}
}
}
public void AddHealth(int amount)
{
playerData.AddHealth(amount);
proxyFSM.SendEvent("HeroCtrl-Healed");
}
public void TakeHealth(int amount)
{
playerData.TakeHealth(amount);
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
}
public void MaxHealth()
{
proxyFSM.SendEvent("HeroCtrl-MaxHealth");
playerData.MaxHealth();
}
public void StartMPDrain(float time)
{
Debug.LogFormat("Start MP Drain");
drainMP = true;
drainMP_timer = 0f;
MP_drained = 0f;
drainMP_time = time; //
focusMP_amount = (float)playerData.GetInt("focusMP_amount");
}
public void StopMPDrain()
{
drainMP = false;
}
public bool CanFocus()
{
return !gm.isPaused && hero_state != ActorStates.no_input && !cState.dashing && !cState.backDashing && (!cState.attacking || attack_time > ATTACK_RECOVERY_TIME) && !cState.recoiling && cState.onGround && !cState.recoilFrozen && !cState.hazardDeath && !cState.hazardRespawning && CanInput();
}
public bool CanCast()
{
return !gm.isPaused && !cState.dashing && hero_state != ActorStates.no_input && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) && !cState.recoiling && !cState.recoilFrozen && !cState.hazardDeath && !cState.hazardRespawning && CanInput() && preventCastByDialogueEndTimer <= 0f;
}
public bool CanInput()
{
return acceptingInput;
}
public void IgnoreInput()
{
if (acceptingInput)
{
acceptingInput = false;
ResetInput();
}
}
public void AcceptInput()
{
acceptingInput = true;
}
/// <summary>
/// 放弃控制
/// </summary>
public void RelinquishControl()
{
if(!controlReqlinquished && !cState.dead)
{
ResetInput();
ResetMotion();
IgnoreInput();
controlReqlinquished = true;
ResetAttacks();
touchingWallL = false;
touchingWallR = false;
}
}
/// <summary>
/// 重新获得控制
/// </summary>
public void RegainControl()
{
AcceptInput();
hero_state = ActorStates.idle;
if(controlReqlinquished && !cState.dead)
{
AffectedByGravity(true);
SetStartingMotionState();
controlReqlinquished = false;
if (startWithWallslide)
{
cState.willHardLand = false;
cState.touchingWall = true;
if(transform.localScale.x< 0f)
{
touchingWallR = true;
return;
}
touchingWallL = true;
}
else
{
if (startWithJump)
{
HeroJumpNoEffect();
startWithJump = false;
return;
}
if (startWithFullJump)
{
HeroJump();
startWithFullJump = false;
return;
}
if (startWithDash)
{
HeroDash();
startWithDash = false;
return;
}
if (startWithAttack)
{
DoAttack();
startWithAttack = false;
return;
}
cState.touchingWall = false;
touchingWallL = false;
touchingWallR = false;
}
}
}
private void SetStartingMotionState()
{
SetStartingMotionState(false);
}
private void SetStartingMotionState(bool preventRunDip)
{
move_input = (acceptingInput || preventRunDip) ? inputHandler.inputActions.moveVector.X : 0f;
cState.touchingWall = false;
if (CheckTouchingGround())
{
cState.onGround = true;
SetState(ActorStates.grounded);
}
else
{
cState.onGround = false;
SetState(ActorStates.airborne);
}
animCtrl.UpdateState(hero_state);
}
/// <summary>
/// 小骑士跳跃的函数
/// </summary>
private void Jump()
{
if (jump_steps <= JUMP_STEPS)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, JUMP_SPEED);
jump_steps++;
jumped_steps++;
return;
}
CancelJump();
}
/// <summary>
/// 取消跳跃,这个在释放跳跃键时有用
/// </summary>
private void CancelJump()
{
cState.jumping = false;
jumpReleaseQueuing = false;
jump_steps = 0;
}
/// <summary>
/// 标注:此函数暂且不具备任何内容待后续开发
/// </summary>
private void BackDash()
{
}
/// <summary>
/// 冲刺时执行的函数
/// </summary>
private void Dash()
{
AffectedByGravity(false); //不受到重力影响
ResetHardLandingTimer();
if(dash_timer > DASH_TIME)
{
FinishedDashing();//大于则结束冲刺
return;
}
float num;
num = DASH_SPEED;
if (dashingDown)
{
rb2d.velocity = new Vector2(0f, -num);
}
else if (cState.facingRight)
{
if (CheckForBump(CollisionSide.right))
{
//rb2d.velocity = new Vector2(num, cState.onGround ? BUMP_VELOCITY : BUMP_VELOCITY_DASH);
}
else
{
rb2d.velocity = new Vector2(num, 0f); //为人物的velocity赋值DASH_SPEED
}
}
else if (CheckForBump(CollisionSide.left))
{
//rb2d.velocity = new Vector2(-num, cState.onGround ? BUMP_VELOCITY : BUMP_VELOCITY_DASH);
}
else
{
rb2d.velocity = new Vector2(-num, 0f);
}
dash_timer += Time.deltaTime;
}
private void HeroDash()
{
if (!cState.onGround)
{
airDashed = true;
}
audioCtrl.StopSound(HeroSounds.FOOTSETP_RUN);
audioCtrl.StopSound(HeroSounds.FOOTSTEP_WALK);
audioCtrl.PlaySound(HeroSounds.DASH);
cState.recoiling = false;
if (inputHandler.inputActions.right.IsPressed)
{
FaceRight();
}
else if (inputHandler.inputActions.left.IsPressed)
{
FaceLeft();
}
cState.dashing = true;
dashQueueSteps = 0;
HeroActions inputActions = inputHandler.inputActions;
if(inputActions.down.IsPressed && !cState.onGround && playerData.equippedCharm_31 && !inputActions.left.IsPressed && !inputActions.right.IsPressed)
{
dashBurst.transform.localPosition = new Vector3(-0.07f, 3.74f, 0.01f); //生成dashBurst后设置位置和旋转角
dashBurst.transform.localEulerAngles = new Vector3(0f, 0f, 90f);
dashingDown = true;
}
else
{
dashBurst.transform.localPosition = new Vector3(4.11f, -0.55f, 0.001f); //生成dashBurst后设置位置和旋转角
dashBurst.transform.localEulerAngles = new Vector3(0f, 0f, 0f);
dashingDown = false;
}
dashCooldownTimer = DASH_COOLDOWN;
dashBurst.SendEvent("PLAY"); //发送dashBurst的FSM的事件PLAY
dashParticlesPrefab.GetComponent<ParticleSystem>().enableEmission = true;
if (cState.onGround)
{
dashEffect = Instantiate(backDashPrefab, transform.position, Quaternion.identity);
dashEffect.transform.localScale = new Vector3(transform.localScale.x * -1f, transform.localScale.y, transform.localScale.z);
}
}
/// <summary>
/// 判断是否可以后撤冲刺
/// </summary>
/// <returns></returns>
public bool CanBackDash()
{
return !cState.dashing && hero_state != ActorStates.no_input && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) &&!cState.preventBackDash && !cState.backDashCooldown && !controlReqlinquished && !cState.recoilFrozen && !cState.recoiling && cState.onGround && playerData.canBackDash;
}
/// <summary>
/// 判断是否可以冲刺
/// </summary>
/// <returns></returns>
public bool CanDash()
{
return hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing &&
dashCooldownTimer <= 0f && !cState.dashing && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) && !cState.preventDash && (cState.onGround || !airDashed) && !cState.hazardDeath && playerData.canDash;
}
/// <summary>
/// 结束冲刺
/// </summary>
private void FinishedDashing()
{
CancelDash();
AffectedByGravity(true);//物体重新受到重力的影响
animCtrl.FinishedDash(); //该播放Dash To Idle动画片段了
if (cState.touchingWall && !cState.onGround)
{
if (touchingWallL)
{
}
if (touchingWallR)
{
}
}
}
/// <summary>
/// 取消冲刺,将cState.dashing设置为false后动画将不再播放
/// </summary>
public void CancelDash()
{
cState.dashing = false;
dash_timer = 0f; //重置冲刺时的计时器
AffectedByGravity(true); //物体重新受到重力的影响
if (dashParticlesPrefab.GetComponent<ParticleSystem>().enableEmission)
{
dashParticlesPrefab.GetComponent<ParticleSystem>().enableEmission = false;
}
}
private void CancelBackDash()
{
cState.backDashing = false;
back_dash_timer = 0f;
}
/// <summary>
/// 物体是否受到重力的影响
/// </summary>
/// <param name="gravityApplies"></param>
private void AffectedByGravity(bool gravityApplies)
{
float gravityScale = rb2d.gravityScale;
if(rb2d.gravityScale > Mathf.Epsilon && !gravityApplies)
{
prevGravityScale = rb2d.gravityScale;
rb2d.gravityScale = 0f;
return;
}
if(rb2d.gravityScale <= Mathf.Epsilon && gravityApplies)
{
rb2d.gravityScale = prevGravityScale;
prevGravityScale = 0f;
}
}
private void FailSafeCheck()
{
if(hero_state == ActorStates.hard_landing)
{
hardLandFailSafeTimer += Time.deltaTime;
if(hardLandFailSafeTimer > HARD_LANDING_TIME + 0.3f)
{
SetState(ActorStates.grounded);
BackOnGround();
hardLandFailSafeTimer = 0f;
}
}
else
{
hardLandFailSafeTimer = 0f;
}
if (cState.hazardDeath)
{
hazardLandingTimer += Time.deltaTime;
if(hazardLandingTimer > HAZARD_DEATH_CHECK_TIME && hero_state != ActorStates.no_input)
{
ResetMotion();
AffectedByGravity(false);
SetState(ActorStates.no_input);
hazardLandingTimer = 0f;
}
else
{
hazardLandingTimer = 0f;
}
}
if(rb2d.velocity.y == 0f && !cState.onGround && !cState.falling && !cState.jumping && !cState.dashing && hero_state != ActorStates.hard_landing && hero_state != ActorStates.no_input)
{
if (CheckTouchingGround())
{
floatingBufferTimer += Time.deltaTime;
if(floatingBufferTimer > FLOATING_CHECK_TIME)
{
if (cState.recoiling)
{
CancelDamageRecoil();
}
BackOnGround();
floatingBufferTimer = 0f;
return;
}
}
else
{
floatingBufferTimer = 0f;
}
}
}
/// <summary>
/// 进入降落状态的检查
/// </summary>
private void FallCheck()
{
//如果y轴上的速度小于-1E-06F判断是否到地面上了
if (rb2d.velocity.y <= -1E-06F)
{
if (!CheckTouchingGround())
{
cState.falling = true;
cState.onGround = false;
if(hero_state != ActorStates.no_input)
{
SetState(ActorStates.airborne);
}
fallTimer += Time.deltaTime;
if(fallTimer > BIG_FALL_TIME)
{
if (!cState.willHardLand)
{
cState.willHardLand = true;
}
if (!fallRumble)
{
StartFallRumble();
}
}
}
}
else
{
cState.falling = false;
fallTimer = 0f;
if (fallRumble)
{
CancelFallEffects();
}
}
}
private void DoHardLanding()
{
AffectedByGravity(true);
ResetInput();
SetState(ActorStates.hard_landing);
hardLanded = true;
audioCtrl.PlaySound(HeroSounds.HARD_LANDING);
Instantiate(hardLandingEffectPrefab, transform.position,Quaternion.identity);
}
public void ResetHardLandingTimer()
{
cState.willHardLand = false;
hardLandingTimer = 0f;
fallTimer = 0f;
hardLanded = false;
}
private bool ShouldHardLand(Collision2D collision)
{
return !collision.gameObject.GetComponent<NoHardLanding>() && cState.willHardLand && hero_state != ActorStates.hard_landing;
}
/// <summary>
/// 在PlaymakerFSM中被调用
/// </summary>
public void ForceHardLanding()
{
Debug.LogFormat("Force Hard Landing");
if (!cState.onGround)
{
cState.willHardLand = true;
}
}
private void ResetInput()
{
move_input = 0f;
vertical_input = 0f;
}
private void ResetMotion()
{
CancelJump();
CancelDash();
CancelBackDash();
CancelRecoilHorizonal();
rb2d.velocity = Vector2.zero;
}
/// <summary>
/// 翻转小骑士的localScale.x
/// </summary>
public void FlipSprite()
{
cState.facingRight = !cState.facingRight;
Vector3 localScale = transform.localScale;
localScale.x *= -1f;
transform.localScale = localScale;
}
public void FaceRight()
{
cState.facingRight = true;
Vector3 localScale = transform.localScale;
localScale.x = -1f;
transform.localScale = localScale;
}
public void FaceLeft()
{
cState.facingRight = false;
Vector3 localScale = transform.localScale;
localScale.x = 1f;
transform.localScale = localScale;
}
public void RecoilLeft()
{
if(!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
{
CancelDash();
recoilSteps = 0;
cState.recoilingLeft = true;
cState.recoilingRight = false;
recoilLarge = false;
rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY, rb2d.velocity.y);
}
}
public void RecoilRight()
{
if (!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
{
CancelDash();
recoilSteps = 0;
cState.recoilingLeft = false;
cState.recoilingRight = true;
recoilLarge = false;
rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY, rb2d.velocity.y);
}
}
public void RecoilLeftLong()
{
if (!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
{
CancelDash();
ResetAttacks();
recoilSteps = 0;
cState.recoilingLeft = true;
cState.recoilingRight = false;
recoilLarge = true;
rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);
}
}
public void RecoilRightLong()
{
if (!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
{
CancelDash();
ResetAttacks();
recoilSteps = 0;
cState.recoilingLeft = false;
cState.recoilingRight = true;
recoilLarge = true;
rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);
}
}
public void RecoilDown()
{
CancelJump();
if(rb2d.velocity.y > RECOIL_DOWN_VELOCITY && !controlReqlinquished)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, RECOIL_DOWN_VELOCITY);
}
}
public void CancelRecoilHorizonal()
{
cState.recoilingLeft = false;
cState.recoilingRight = false;
recoilSteps = 0;
}
public bool CanTakeDamage()
{
return damageMode != DamageMode.NO_DAMAGE && !cState.invulnerable && !cState.recoiling && !cState.dead && !cState.hazardDeath;
}
public void TakeDamage(GameObject go,CollisionSide damageSide,int damageAmount,int hazardType)
{
bool spawnDamageEffect = true;
if (damageAmount > 0)
{
if (CanTakeDamage())
{
if (damageMode == DamageMode.HAZARD_ONLY && hazardType == 1)
{
return;
}
if (parryInvulnTimer > 0f && hazardType == 1)
{
return;
}
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
CancelAttack();
if (cState.touchingWall)
{
cState.touchingWall = false;
}
if (cState.recoilingLeft || cState.recoilingRight)
{
CancelRecoilHorizonal();
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
if (!takeNoDamage)
{
playerData.TakeHealth(damageAmount);
}
if (damageAmount > 0 && OnTakenDamage != null)
{
OnTakenDamage();
}
if (playerData.health == 0)
{
StartCoroutine(Die());
return;
}
if (hazardType == 2)
{
Debug.LogFormat("Die From Spikes");
StartCoroutine(DieFromHazard(HazardType.SPIKES, (go != null) ? go.transform.rotation.z : 0f));
return;
}
if (hazardType == 3)
{
Debug.LogFormat("Die From Acid");
StartCoroutine(DieFromHazard(HazardType.ACID,0f));
return;
}
if (hazardType == 4)
{
Debug.LogFormat("Die From Lava");
return;
}
if (hazardType == 5)
{
Debug.LogFormat("Die From Pit");
return;
}
StartCoroutine(StartRecoil(damageSide, spawnDamageEffect, damageAmount));
return;
}
else if (cState.invulnerable && !cState.hazardDeath)
{
if(hazardType == 2)
{
if (!takeNoDamage)
{
playerData.TakeHealth(damageAmount);
}
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
if(playerData.health == 0)
{
StartCoroutine(Die());
return;
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
StartCoroutine(DieFromHazard(HazardType.SPIKES, (go != null) ? go.transform.rotation.z : 0f));
return;
}
else if (hazardType == 3)
{
playerData.TakeHealth(damageAmount);
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
if (playerData.health == 0)
{
StartCoroutine(Die());
return;
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
StartCoroutine(DieFromHazard(HazardType.ACID, 0f));
return;
}
else if(hazardType == 4)
{
Debug.LogFormat("Die From Lava");
}
}
}
}
private IEnumerator Die()
{
if (OnDeath != null)
{
OnDeath();
}
if (!cState.dead)
{
playerData.disablePause = true;
rb2d.velocity = Vector2.zero;
CancelRecoilHorizonal();
AffectedByGravity(false);
HeroBox.inactive = true;
rb2d.isKinematic = true;
SetState(ActorStates.no_input);
cState.dead = true;
ResetMotion();
ResetHardLandingTimer();
renderer.enabled = false;
gameObject.layer = 2;
yield return null;
}
}
private IEnumerator DieFromHazard(HazardType hazardType,float angle)
{
if (!cState.hazardDeath)
{
playerData.disablePause = true;
SetHeroParent(null);
SetState(ActorStates.no_input);
cState.hazardDeath = true;
ResetMotion();
ResetHardLandingTimer();
AffectedByGravity(false);
renderer.enabled = false;
gameObject.layer = 2;
if(hazardType == HazardType.SPIKES)
{
GameObject gameObject = Instantiate(spikeDeathPrefab);
gameObject.transform.position = transform.position;
FSMUtility.SetFloat(gameObject.GetComponent<PlayMakerFSM>(), "Spike Direction", angle * 57.29578f);
}
else if(hazardType == HazardType.ACID)
{
GameObject gameObject2 = Instantiate(acidDeathPrefab);
gameObject2.transform.position = transform.position;
gameObject2.transform.localScale = transform.localScale;
}
yield return null;
StartCoroutine(gm.PlayerDeadFromHazard(0f));
}
}
public IEnumerator HazardRespawn()
{
cState.hazardDeath = false;
cState.onGround = true;
cState.hazardRespawning = true;
ResetMotion();
ResetHardLandingTimer();
ResetAttacks();
ResetInput();
cState.recoiling = false;
airDashed = false;
transform.SetPosition2D(FindGroundPoint(playerData.hazardRespawnLocation, true));
gameObject.layer = 9;
renderer.enabled = true;
yield return new WaitForEndOfFrame();
if (playerData.hazardRespawnFacingRight)
{
FaceRight();
}
else
{
FaceLeft();
}
if(heroInPosition != null)
{
heroInPosition(false);
}
StartCoroutine(Invulnerable(INVUL_TIME * 2f));
GameCameras.instance.cameraFadeFSM.SendEvent("RESPAWN");
float clipDuration = animCtrl.GetClipDuration("Hazard Respawn");
animCtrl.PlayClip("Hazard Respawn");
yield return new WaitForSeconds(clipDuration);
cState.hazardRespawning = false;
rb2d.interpolation = RigidbodyInterpolation2D.Interpolate;
FinishedEnteringScene(false, false);
}
private void FinishedEnteringScene(bool setHazardMarker = true, bool preventRunBob = false)
{
playerData.disablePause = false;
SetStartingMotionState(preventRunBob);
AffectedByGravity(true);
if (setHazardMarker)
{
}
SetDamageMode(DamageMode.FULL_DAMAGE);
}
private IEnumerator StartRecoil(CollisionSide impactSide, bool spawnDamageEffect, int damageAmount)
{
if (!cState.recoiling)
{
playerData.disablePause = true;
ResetMotion();
AffectedByGravity(false);
if(impactSide == CollisionSide.left)
{
recoilVector = new Vector2(RECOIL_VELOCITY, RECOIL_VELOCITY * 0.5f);
if (cState.facingRight)
{
FlipSprite();
}
}
else if (impactSide == CollisionSide.right)
{
recoilVector = new Vector2(-RECOIL_VELOCITY, RECOIL_VELOCITY * 0.5f);
if (!cState.facingRight)
{
FlipSprite();
}
}
else
{
recoilVector = Vector2.zero;
}
SetState(ActorStates.no_input);
cState.recoilFrozen = true;
if (spawnDamageEffect)
{
damageEffectFSM.SendEvent("DAMAGE");
if(damageAmount > 1)
{
}
}
StartCoroutine(Invulnerable(INVUL_TIME));
yield return takeDamageCoroutine = StartCoroutine(gm.FreezeMoment(DAMAGE_FREEZE_DOWN, DAMAGE_FREEZE_WAIT, DAMAGE_FREEZE_UP, 0.0001f));
cState.recoilFrozen = false;
cState.recoiling = true;
playerData.disablePause = false;
}
}
private IEnumerator Invulnerable(float duration)
{
cState.invulnerable = true;
yield return new WaitForSeconds(DAMAGE_FREEZE_DOWN);
invPulse.StartInvulnerablePulse();
yield return new WaitForSeconds(duration);
invPulse.StopInvulnerablePulse();
cState.invulnerable = false;
cState.recoiling = false;
}
public void CancelDamageRecoil()
{
cState.recoiling = false;
recoilTimer = 0f;
ResetMotion();
AffectedByGravity(true);
}
private void LookForInput()
{
if (acceptingInput)
{
move_input = inputHandler.inputActions.moveVector.Vector.x; //获取X方向的键盘输入
vertical_input = inputHandler.inputActions.moveVector.Vector.y;//获取Y方向的键盘输入
FilterInput();//规整化
if (inputHandler.inputActions.jump.WasReleased && jumpReleaseQueueingEnabled)
{
jumpReleaseQueueSteps = JUMP_RELEASE_QUEUE_STEPS;
jumpReleaseQueuing = true;
}
if (!inputHandler.inputActions.jump.IsPressed)
{
JumpReleased();
}
if (!inputHandler.inputActions.dash.IsPressed)
{
if(cState.preventDash && !cState.dashCooldown)
{
cState.preventDash = false;
}
dashQueuing = false;
}
if (!inputHandler.inputActions.attack.IsPressed)
{
attackQueuing = false;
}
}
}
private void LookForQueueInput()
{
if (acceptingInput)
{
if (inputHandler.inputActions.jump.WasPressed)
{
if (CanJump())
{
HeroJump();
}
else
{
jumpQueueSteps = 0;
jumpQueuing = true;
}
}
if (inputHandler.inputActions.dash.WasPressed)
{
if (CanDash())
{
HeroDash();
}
else
{
dashQueueSteps = 0;
dashQueuing = true;
}
}
if(inputHandler.inputActions.attack.WasPressed)
{
if (CanAttack())
{
DoAttack();
}
else
{
attackQueueSteps = 0;
attackQueuing = true;
}
}
if (inputHandler.inputActions.jump.IsPressed)
{
if(jumpQueueSteps <= JUMP_QUEUE_STEPS && CanJump() && jumpQueuing)
{
Debug.LogFormat("Execute Hero Jump");
HeroJump();
}
}
if(inputHandler.inputActions.dash.IsPressed && dashQueueSteps <= DASH_QUEUE_STEPS && CanDash() && dashQueuing)
{
Debug.LogFormat("Start Hero Dash");
HeroDash();
}
if(inputHandler.inputActions.attack.IsPressed && attackQueueSteps <= ATTACK_QUEUE_STEPS && CanAttack() && attackQueuing)
{
Debug.LogFormat("Start Do Attack");
DoAttack();
}
}
}
/// <summary>
/// 可以跳跃吗
/// </summary>
/// <returns></returns>
private bool CanJump()
{
if(hero_state == ActorStates.no_input || hero_state == ActorStates.hard_landing || hero_state == ActorStates.dash_landing || cState.dashing || cState.backDashing || cState.jumping)
{
return false;
}
if (cState.onGround)
{
return true; //如果在地面上就return true
}
return false;
}
/// <summary>
/// 小骑士跳跃行为播放声音以及设置cstate.jumping
/// </summary>
private void HeroJump()
{
audioCtrl.PlaySound(HeroSounds.JUMP);
cState.recoiling = false;
cState.jumping = true;
jumpQueueSteps = 0;
jumped_steps = 0;
}
private void HeroJumpNoEffect()
{
audioCtrl.PlaySound(HeroSounds.JUMP);
cState.jumping = true;
jumpQueueSteps = 0;
jumped_steps = 0;
}
/// <summary>
/// 取消跳跃
/// </summary>
public void CancelHeroJump()
{
if (cState.jumping)
{
CancelJump();
if(rb2d.velocity.y > 0f)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, 0f);
}
}
}
private void JumpReleased()
{
if(rb2d.velocity.y > 0f &&jumped_steps >= JUMP_STEPS_MIN)
{
if (jumpReleaseQueueingEnabled)
{
if(jumpReleaseQueuing && jumpReleaseQueueSteps <= 0)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, 0f); //取消跳跃并且设置y轴速度为0
CancelJump();
}
}
else
{
rb2d.velocity = new Vector2(rb2d.velocity.x, 0f);
CancelJump();
}
}
jumpQueuing = false;
}
/// <summary>
/// 设置玩家的ActorState的新类型
/// </summary>
/// <param name="newState"></param>
private void SetState(ActorStates newState)
{
if(newState == ActorStates.grounded)
{
if(Mathf.Abs(move_input) > Mathf.Epsilon)
{
newState = ActorStates.running;
}
else
{
newState = ActorStates.idle;
}
}
else if(newState == ActorStates.previous)
{
newState = prev_hero_state;
}
if(newState != hero_state)
{
prev_hero_state = hero_state;
hero_state = newState;
animCtrl.UpdateState(newState);
}
}
/// <summary>
/// 回到地面上时执行的函数
/// </summary>
public void BackOnGround()
{
if(landingBufferSteps <= 0)
{
landingBufferSteps = LANDING_BUFFER_STEPS;
if(!cState.onGround && !hardLanded)
{
Instantiate(softLandingEffectPrefab, transform.position,Quaternion.identity); //TODO:
}
}
cState.falling = false;
fallTimer = 0f;
dashLandingTimer = 0f;
cState.willHardLand = false;
hardLandingTimer = 0f;
hardLanded = false;
jump_steps = 0;
SetState(ActorStates.grounded);
cState.onGround = true;
airDashed = false;
}
public bool CanInspect()
{
return !gm.isPaused && !cState.dashing && hero_state != ActorStates.no_input && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) && !cState.recoiling && !cState.hazardDeath && !cState.hazardRespawning && !cState.recoilFrozen && cState.onGround && CanInput();
}
/// <summary>
/// 开启在下落时晃动
/// </summary>
public void StartFallRumble()
{
fallRumble = true;
audioCtrl.PlaySound(HeroSounds.FALLING);
}
public void CancelFallEffects()
{
fallRumble = false;
audioCtrl.StopSound(HeroSounds.FALLING);
}
/// <summary>
/// 规整化输入
/// </summary>
private void FilterInput()
{
if (move_input > 0.3f)
{
move_input = 1f;
}
else if (move_input < -0.3f)
{
move_input = -1f;
}
else
{
move_input = 0f;
}
if (vertical_input > 0.5f)
{
vertical_input = 1f;
return;
}
if (vertical_input < -0.5f)
{
vertical_input = -1f;
return;
}
vertical_input = 0f;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.layer == LayerMask.NameToLayer("Terrain") && collision.gameObject.CompareTag("HeroWalkable") && CheckTouchingGround())
{
}
if(hero_state != ActorStates.no_input)
{
if(collision.gameObject.layer == LayerMask.NameToLayer("Terrain") || collision.gameObject.CompareTag("HeroWalkable"))
{
CollisionSide collisionSide = FindCollisionSide(collision);
//如果头顶顶到了
if (collisionSide == CollisionSide.top)
{
if (cState.jumping)
{
CancelJump();
}
}
//如果底下碰到了
if (collisionSide == CollisionSide.bottom)
{
if(ShouldHardLand(collision))
{
DoHardLanding();
}
else if(collision.gameObject.GetComponent<SteepSlope>() == null && hero_state != ActorStates.hard_landing)
{
BackOnGround();
}
if(cState.dashing && dashingDown)
{
AffectedByGravity(true);
SetState(ActorStates.dash_landing);
hardLanded = true;
return;
}
}
}
}
else if(hero_state == ActorStates.no_input)
{
}
}
private void OnCollisionStay2D(Collision2D collision)
{
if(hero_state != ActorStates.no_input && collision.gameObject.layer == LayerMask.NameToLayer("Terrain"))
{
if (collision.gameObject.GetComponent<NonSlider>() == null)
{
if (CheckStillTouchingWall(CollisionSide.left, false))
{
cState.touchingWall = true;
touchingWallL = true;
touchingWallR = false;
}
else if (CheckStillTouchingWall(CollisionSide.right, false))
{
cState.touchingWall = true;
touchingWallL = false;
touchingWallR = true;
}
else
{
cState.touchingWall = false;
touchingWallL = false;
touchingWallR = false;
}
if (CheckTouchingGround())
{
if (ShouldHardLand(collision))
{
DoHardLanding();
}
if(hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing && cState.falling)
{
BackOnGround();
return;
}
}
else if(cState.jumping || cState.falling)
{
cState.onGround = false;
SetState(ActorStates.airborne);
return;
}
}
else
{
}
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if(touchingWallL && !CheckStillTouchingWall(CollisionSide.left, false))
{
cState.touchingWall = false;
touchingWallL = false;
}
if (touchingWallR && !CheckStillTouchingWall(CollisionSide.left, false))
{
cState.touchingWall = false;
touchingWallR = false;
}
if(hero_state != ActorStates.no_input && collision.gameObject.layer == LayerMask.NameToLayer("Terrain") && !CheckTouchingGround())
{
cState.onGround = false;
SetState(ActorStates.airborne);
}
}
/// <summary>
/// 检查是否接触到地面
/// </summary>
/// <returns></returns>
public bool CheckTouchingGround()
{
Vector2 vector = new Vector2(col2d.bounds.min.x, col2d.bounds.center.y);
Vector2 vector2 = col2d.bounds.center;
Vector2 vector3 = new Vector2(col2d.bounds.max.x, col2d.bounds.center.y);
float distance = col2d.bounds.extents.y + 0.16f;
Debug.DrawRay(vector, Vector2.down, Color.yellow);
Debug.DrawRay(vector2, Vector2.down, Color.yellow);
Debug.DrawRay(vector3, Vector2.down, Color.yellow);
RaycastHit2D raycastHit2D = Physics2D.Raycast(vector, Vector2.down, distance, LayerMask.GetMask("Terrain"));
RaycastHit2D raycastHit2D2 = Physics2D.Raycast(vector2, Vector2.down, distance, LayerMask.GetMask("Terrain"));
RaycastHit2D raycastHit2D3 = Physics2D.Raycast(vector3, Vector2.down, distance, LayerMask.GetMask("Terrain"));
return raycastHit2D.collider != null || raycastHit2D2.collider != null || raycastHit2D3.collider != null;
}
/// <summary>
/// 检查是否保持着接触着墙
/// </summary>
/// <param name="side"></param>
/// <param name="checkTop"></param>
/// <returns></returns>
private bool CheckStillTouchingWall(CollisionSide side,bool checkTop = false)
{
Vector2 origin = new Vector2(col2d.bounds.min.x, col2d.bounds.max.y);
Vector2 origin2 = new Vector2(col2d.bounds.min.x, col2d.bounds.center.y);
Vector2 origin3 = new Vector2(col2d.bounds.min.x, col2d.bounds.min.y);
Vector2 origin4 = new Vector2(col2d.bounds.max.x, col2d.bounds.max.y);
Vector2 origin5 = new Vector2(col2d.bounds.max.x, col2d.bounds.center.y);
Vector2 origin6 = new Vector2(col2d.bounds.max.x, col2d.bounds.min.y);
float distance = 0.1f;
RaycastHit2D raycastHit2D = default(RaycastHit2D);
RaycastHit2D raycastHit2D2 = default(RaycastHit2D);
RaycastHit2D raycastHit2D3 = default(RaycastHit2D);
if(side == CollisionSide.left)
{
if (checkTop)
{
raycastHit2D = Physics2D.Raycast(origin, Vector2.left, distance, LayerMask.GetMask("Terrain"));
}
raycastHit2D2 = Physics2D.Raycast(origin2, Vector2.left, distance, LayerMask.GetMask("Terrain"));
raycastHit2D3 = Physics2D.Raycast(origin3, Vector2.left, distance, LayerMask.GetMask("Terrain"));
}
else
{
if(side != CollisionSide.right)
{
Debug.LogError("Invalid CollisionSide specified.");
return false;
}
if (checkTop)
{
raycastHit2D = Physics2D.Raycast(origin4, Vector2.right, distance, LayerMask.GetMask("Terrain"));
}
raycastHit2D2 = Physics2D.Raycast(origin5, Vector2.right, distance, LayerMask.GetMask("Terrain"));
raycastHit2D3 = Physics2D.Raycast(origin6, Vector2.right, distance, LayerMask.GetMask("Terrain"));
}
if(raycastHit2D2.collider != null)
{
bool flag = true;
if (raycastHit2D2.collider.isTrigger)
{
flag = false;
}
if(raycastHit2D2.collider.GetComponent<SteepSlope>() != null)
{
flag = false;
}
if (raycastHit2D2.collider.GetComponent<NonSlider>() != null)
{
flag = false;
}
if (flag)
{
return true;
}
}
if (raycastHit2D3.collider != null)
{
bool flag2 = true;
if (raycastHit2D3.collider.isTrigger)
{
flag2 = false;
}
if (raycastHit2D3.collider.GetComponent<SteepSlope>() != null)
{
flag2 = false;
}
if (raycastHit2D3.collider.GetComponent<NonSlider>() != null)
{
flag2 = false;
}
if (flag2)
{
return true;
}
}
if (checkTop && raycastHit2D.collider != null)
{
bool flag3 = true;
if (raycastHit2D.collider.isTrigger)
{
flag3 = false;
}
if (raycastHit2D.collider.GetComponent<SteepSlope>() != null)
{
flag3 = false;
}
if (raycastHit2D.collider.GetComponent<NonSlider>() != null)
{
flag3 = false;
}
if (flag3)
{
return true;
}
}
return false;
}
public IEnumerator CheckForTerrainThunk(AttackDirection attackDir)
{
bool terrainHit = false;
float thunkTimer = NAIL_TERRAIN_CHECK_TIME;
while (thunkTimer > 0.12f)
{
if (!terrainHit)
{
float num = 0.25f;
float num2;
if (attackDir == AttackDirection.normal)
{
num2 = 2f;
}
else
{
num2 = 1.5f;
}
float num3 = 1f;
//TODO:
num2 *= num3;
Vector2 size = new Vector2(0.45f, 0.45f);
Vector2 origin = new Vector2(col2d.bounds.center.x, col2d.bounds.center.y + num);
Vector2 origin2 = new Vector2(col2d.bounds.center.x, col2d.bounds.max.y);
Vector2 origin3 = new Vector2(col2d.bounds.center.x, col2d.bounds.min.y);
int layerMask = 33554432; //2的25次方,也就是Layer Soft Terrain;
RaycastHit2D raycastHit2D = default(RaycastHit2D);
if (attackDir == AttackDirection.normal)
{
if ((cState.facingRight && !cState.wallSliding) || (!cState.facingRight && !cState.wallSliding))
{
raycastHit2D = Physics2D.BoxCast(origin, size, 0f, Vector2.right, num2, layerMask);
}
else
{
raycastHit2D = Physics2D.BoxCast(origin, size, 0f, Vector2.right, num3, layerMask);
}
}
else if (attackDir == AttackDirection.upward)
{
raycastHit2D = Physics2D.BoxCast(origin2, size, 0f, Vector2.up, num2, layerMask);
}
else if (attackDir == AttackDirection.downward)
{
raycastHit2D = Physics2D.BoxCast(origin3, size, 0f, Vector2.down, num2, layerMask);
}
if (raycastHit2D.collider != null && !raycastHit2D.collider.isTrigger)
{
NonThunker component = raycastHit2D.collider.GetComponent<NonThunker>();
bool flag = !(component != null) || !component.active;
if (flag)
{
terrainHit = true;
if (attackDir == AttackDirection.normal)
{
if (cState.facingRight)
{
RecoilLeft();
}
else
{
RecoilRight();
}
}
else if (attackDir == AttackDirection.upward)
{
RecoilDown();
}
}
}
thunkTimer -= Time.deltaTime;
}
yield return null;
}
}
public bool CheckForBump(CollisionSide side)
{
float num = 0.025f;
float num2 = 0.2f;
Vector2 vector = new Vector2(col2d.bounds.min.x + num2, col2d.bounds.min.y + 0.2f);
Vector2 vector2 = new Vector2(col2d.bounds.min.x + num2, col2d.bounds.min.y - num);
Vector2 vector3 = new Vector2(col2d.bounds.max.x - num2, col2d.bounds.min.y + 0.2f);
Vector2 vector4 = new Vector2(col2d.bounds.max.x - num2, col2d.bounds.min.y - num);
float num3 = 0.32f + num2;
RaycastHit2D raycastHit2D = default(RaycastHit2D);
RaycastHit2D raycastHit2D2 = default(RaycastHit2D);
if(side == CollisionSide.left)
{
Debug.DrawLine(vector2, vector2 + Vector2.left * num3, Color.cyan, 0.15f);
Debug.DrawLine(vector, vector + Vector2.left * num3, Color.cyan, 0.15f);
raycastHit2D = Physics2D.Raycast(vector2, Vector2.left, num3, LayerMask.GetMask("Terrain"));
raycastHit2D2 = Physics2D.Raycast(vector, Vector2.left, num3, LayerMask.GetMask("Terrain"));
}
else if (side == CollisionSide.right)
{
Debug.DrawLine(vector4, vector4 + Vector2.right * num3, Color.cyan, 0.15f);
Debug.DrawLine(vector3, vector3 + Vector2.right * num3, Color.cyan, 0.15f);
raycastHit2D = Physics2D.Raycast(vector4, Vector2.right, num3, LayerMask.GetMask("Terrain"));
raycastHit2D2 = Physics2D.Raycast(vector3, Vector2.right, num3, LayerMask.GetMask("Terrain"));
}
else
{
Debug.LogError("Invalid CollisionSide specified.");
}
if(raycastHit2D2.collider != null && raycastHit2D.collider == null)
{
Vector2 vector5 = raycastHit2D2.point + new Vector2((side == CollisionSide.right) ? 0.1f : -0.1f, 1f);
RaycastHit2D raycastHit2D3 = Physics2D.Raycast(vector5, Vector2.down, 1.5f, LayerMask.GetMask("Terrain"));
Vector2 vector6 = raycastHit2D2.point + new Vector2((side == CollisionSide.right) ? -0.1f : 0.1f, 1f);
RaycastHit2D raycastHit2D4 = Physics2D.Raycast(vector6, Vector2.down, 1.5f, LayerMask.GetMask("Terrain"));
if(raycastHit2D3.collider != null)
{
Debug.DrawLine(vector5, raycastHit2D3.point, Color.cyan, 0.15f);
if (!(raycastHit2D4.collider != null))
{
return true;
}
Debug.DrawLine(vector6, raycastHit2D4.point, Color.cyan, 0.15f);
float num4 = raycastHit2D3.point.y - raycastHit2D4.point.y;
if(num4 > 0f)
{
return true;
}
}
}
return false;
}
public Vector3 FindGroundPoint(Vector2 startPoint,bool useExtended = false)
{
float num = FIND_GROUND_POINT_DISTANCE;
if (useExtended)
{
num = FIND_GROUND_POINT_DISTANCE_EXT;
}
RaycastHit2D raycastHit2D = Physics2D.Raycast(startPoint, Vector2.down, num, LayerMask.GetMask("Terrain"));
if(raycastHit2D.collider == null)
{
Debug.LogErrorFormat("FindGroundPoint: Could not find ground point below {0}, check reference position is not too high (more than {1} tiles).", new object[]
{
startPoint.ToString(),
num
});
}
return new Vector3(raycastHit2D.point.x, raycastHit2D.point.y + col2d.bounds.extents.y - col2d.offset.y + 0.01f, transform.position.z);
}
/// <summary>
/// 找到碰撞点的方向也就是上下左右
/// </summary>
/// <param name="collision"></param>
/// <returns></returns>
private CollisionSide FindCollisionSide(Collision2D collision)
{
Vector2 normal = collision.GetSafeContact().Normal ;
float x = normal.x;
float y = normal.y;
if(y >= 0.5f)
{
return CollisionSide.bottom;
}
if (y <= -0.5f)
{
return CollisionSide.top;
}
if (x < 0)
{
return CollisionSide.right;
}
if (x > 0)
{
return CollisionSide.left;
}
Debug.LogError(string.Concat(new string[]
{
"ERROR: unable to determine direction of collision - contact points at (",
normal.x.ToString(),
",",
normal.y.ToString(),
")"
}));
return CollisionSide.bottom;
}
public void SetHeroParent(Transform newParent)
{
transform.parent = newParent;
if (newParent == null)
{
DontDestroyOnLoad(gameObject);
}
}
/// <summary>
/// 设置一个新的HeroControllerStates状态
/// </summary>
/// <param name="stateName"></param>
/// <param name="value"></param>
public void SetCState(string stateName, bool value)
{
cState.SetState(stateName, value);
}
/// <summary>
/// 获取当前HeroControllerStates状态
/// </summary>
/// <param name="stateName"></param>
/// <returns></returns>
public bool GetCState(string stateName)
{
return cState.GetState(stateName);
}
public void StartAnimationControl()
{
animCtrl.StartControl();
}
public void StopAnimationControl()
{
animCtrl.StopControl();
}
public void SetDamageMode(DamageMode newDamageMode)
{
damageMode = newDamageMode;
if (newDamageMode == DamageMode.NO_DAMAGE)
{
playerData.isInvincible = true;
return;
}
playerData.isInvincible = false;
}
}
[Serializable]
public class HeroControllerStates
{
public bool facingRight;
public bool onGround;
public bool wasOnGround;
public bool attacking;
public bool altAttack;
public bool upAttacking;
public bool downAttacking;
public bool inWalkZone;
public bool jumping;
public bool falling;
public bool dashing;
public bool backDashing;
public bool touchingWall;
public bool wallSliding;
public bool willHardLand;
public bool recoilFrozen;
public bool recoiling;
public bool recoilingLeft;
public bool recoilingRight;
public bool freezeCharge;
public bool focusing;
public bool dead;
public bool hazardDeath;
public bool hazardRespawning;
public bool invulnerable;
public bool preventDash;
public bool preventBackDash;
public bool dashCooldown;
public bool backDashCooldown;
public bool isPaused;
public HeroControllerStates()
{
facingRight = false;
onGround = false;
wasOnGround = false;
attacking = false;
altAttack = false;
upAttacking = false;
downAttacking = false;
inWalkZone = false;
jumping = false;
falling = false;
dashing = false;
backDashing = false;
touchingWall = false;
wallSliding = false;
willHardLand = false;
recoilFrozen = false;
recoiling = false;
recoilingLeft = false;
recoilingRight = false;
freezeCharge = false;
focusing = false;
dead = false;
hazardDeath = false;
hazardRespawning = false;
invulnerable = false;
preventDash = false;
preventBackDash = false;
dashCooldown = false;
backDashCooldown = false;
isPaused = false;
}
/// <summary>
/// 设置一个新的状态(通常用在playmakerFSM上)
/// </summary>
/// <param name="stateName"></param>
/// <param name="value"></param>
public void SetState(string stateName, bool value)
{
FieldInfo field = GetType().GetField(stateName);
if (field != null)
{
try
{
field.SetValue(HeroController.instance.cState, value);
return;
}
catch (Exception ex)
{
string str = "Failed to set cState: ";
Exception ex2 = ex;
Debug.LogError(str + ((ex2 != null) ? ex2.ToString() : null));
return;
}
}
Debug.LogError("HeroControllerStates: Could not find bool named" + stateName + "in cState");
}
/// <summary>
/// 获取一个新的状态(通常用在playmakerFSM上)
/// </summary>
/// <param name="stateName"></param>
/// <returns></returns>
public bool GetState(string stateName)
{
FieldInfo field = GetType().GetField(stateName);
if (field != null)
{
return (bool)field.GetValue(HeroController.instance.cState);
}
Debug.LogError("HeroControllerStates: Could not find bool named" + stateName + "in cState");
return false;
}
}
其实后面还有一个二合一版本的,Hazard Respawn Trigger v2和Hazard Respawn Marker作为父子对象,这下不用担心找不到对应的Hazard Respawn Marker了!
二、完善地图的可交互对象
1.制作可攻击玩家的钟乳石行为
可交互对象是游戏中不可少的部分,如果地图只有沟槽的敌人和地刺的话玩家难免玩的会无聊,所以我们来多做几个可交互对象,首先是可以攻击玩家的钟乳石:Stalactite Hazard,如图所示添加好图像,音乐,rb2d和collider2d,以及能伤害玩家和敌人的DamageHero和DamageEnemies
头顶部分:
检测区域,注意它只对layer为Player的也就是小骑士生效!
子对象通过碰撞检测来向父对象发送事件:
cs
using System;
using UnityEngine;
public class TriggerEnterEvent : MonoBehaviour
{
public bool waitForHeroInPosition;
private bool active;
public delegate void CollisionEvent(Collider2D collider, GameObject sender);
public event CollisionEvent OnTriggerEntered;
public event CollisionEvent OnTriggerExited;
public event CollisionEvent OnTriggerStayed;
private void Start()
{
active = false;
if (!waitForHeroInPosition)
{
active = true;
return;
}
if (HeroController.instance.isHeroInPosition)
{
active = true;
return;
}
HeroController.HeroInPosition temp = null;
temp = delegate (bool forceDirect)
{
active = true;
HeroController.instance.heroInPosition -= temp;
};
HeroController.instance.heroInPosition += temp;
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (!active)
{
return;
}
if(OnTriggerEntered != null)
{
OnTriggerEntered(collision,gameObject);
}
}
private void OnTriggerStay2D(Collider2D collision)
{
if (!active)
{
return;
}
if (OnTriggerStayed != null)
{
OnTriggerStayed(collision, gameObject);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (!active)
{
return;
}
if (OnTriggerExited != null)
{
OnTriggerExited(collision, gameObject);
}
}
}
灰尘粒子系统:
下落的灰尘粒子系统:
下落的轨迹粒子系统:
可以反弹的嵌入式系统:(暂时没做)
掉落的石头粒子系统:注意!它是有碰撞Collision的,能和地面发生碰撞
我们来制作一个脚本控制它的行为:
cs
using System;
using System.Collections;
using UnityEngine;
public class StalactiteControl : MonoBehaviour
{
public GameObject top;
[Space]
public float startFallOffset = 0.1f; //开始降落时的偏移量
public GameObject startFallEffect; //开始降落时生成的效果
public AudioClip startFallSound;//开始降落时的声音
public float fallDelay = 0.25f; //延迟降落时间
[Space]
public GameObject fallEffect;
public GameObject trailEffect;
public GameObject nailStrikePrefab;
[Space]
public GameObject embeddedVersion; //嵌入式版本
public GameObject[] landEffectPrefabs; //落地后生成的效果预制体
[Space]
public float hitVelocity = 40f; //下落的速度
[Space]
public GameObject[] hitUpEffectPrefabs; //击中钟乳石后的效果预制体
public AudioClip hitSound; //击中后的声音
public GameObject hitUpRockPrefabs; //击中后的延时预制体
public int spawnMin = 10;
public int spawnMax = 12;
public int speedMin = 15;
public int speedMax = 20;
public AudioClip breakSound;
private TriggerEnterEvent trigger;
private DamageHero damageHero;
private DamageEnemies damageEnemies;
private Rigidbody2D body;
private AudioSource source;
private bool fallen;
private void Awake()
{
damageEnemies = GetComponent<DamageEnemies>();
damageHero = GetComponent<DamageHero>();
body = GetComponent<Rigidbody2D>();
source = GetComponent<AudioSource>();
}
private void Start()
{
trigger = GetComponentInChildren<TriggerEnterEvent>();
if(trigger)
{
trigger.OnTriggerEntered += HandleTriggerEnter;
}
if (damageHero)
{
damageHero.damageDealt = 0;
}
body.isKinematic = true;
if (damageEnemies)
{
damageEnemies.enabled = false;
}
}
/// <summary>
/// 订阅TriggerEnterEvent的OnTriggerEntered事件,当发生事件时执行该函数里的内容来处理碰撞检测
/// </summary>
/// <param name="collider"></param>
/// <param name="sender"></param>
private void HandleTriggerEnter(Collider2D collider, GameObject sender)
{
if (collider.tag == "Player" && Physics2D.Linecast(transform.position, collider.transform.position, LayerMask.GetMask("Terrain")).collider == null)
{
StartCoroutine(Fall(fallDelay));
trigger.OnTriggerEntered -= HandleTriggerEnter;
sender.SetActive(false);
}
}
private IEnumerator Fall(float delay)
{
if (top)
{
top.transform.SetParent(transform.parent);
}
transform.position += Vector3.down * startFallOffset;
if (startFallEffect)
{
startFallEffect.SetActive(true);
startFallEffect.transform.SetParent(transform.parent);
}
if (source && startFallSound)
{
source.PlayOneShot(startFallSound);
}
yield return new WaitForSeconds(delay);
if (fallEffect)
{
fallEffect.SetActive(true);
fallEffect.transform.SetParent(transform.parent);
}
if (trailEffect)
{
trailEffect.SetActive(true);
}
if (damageHero)
{
damageHero.damageDealt = 1;
}
if (damageEnemies)
{
damageEnemies.damageDealt = 1;
}
body.isKinematic = false;
fallen = true;
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (fallen && collision.gameObject.layer == LayerMask.NameToLayer("Terrain"))
{
body.isKinematic = true;
if (trailEffect)
{
trailEffect.transform.parent = null;
}
trailEffect.GetComponent<ParticleSystem>().Stop();
if (embeddedVersion)
{
embeddedVersion.SetActive(true);
embeddedVersion.transform.SetParent(transform.parent, true);
}
RaycastHit2D raycastHit2D = Physics2D.Raycast(transform.position, Vector2.down, 10f, LayerMask.GetMask("Terrain"));
foreach (GameObject gameObject in landEffectPrefabs)
{
Vector3 vector = new Vector3(raycastHit2D.point.x, raycastHit2D.point.y, gameObject.transform.position.z);
//TODO:Object Pool
Instantiate(gameObject, (raycastHit2D.collider != null) ? vector : transform.position, Quaternion.identity);
}
gameObject.SetActive(false);
return;
}
if (collision.tag == "Nail Attack")
{
if (!fallen)
{
StartCoroutine(Fall(0f));
}
if (damageHero)
{
damageHero.damageDealt = 0;
damageHero = null;
}
float value = PlayMakerFSM.FindFsmOnGameObject(collision.gameObject, "damages_enemy").FsmVariables.FindFsmFloat("direction").Value;
float num = 0f;
if (value < 45f)
{
num = 45f;
}
else
{
if (value < 135f)
{
GameObject[] array = hitUpEffectPrefabs;
for (int i = 0; i < array.Length; i++)
{
//TODO:
Instantiate(array[i], transform.position, Quaternion.identity);
}
FlingObjects();
if (source && breakSound)
{
AudioSource audioSource = new GameObject("StalactiteBreakEffect").AddComponent<AudioSource>();
audioSource.outputAudioMixerGroup = source.outputAudioMixerGroup;
audioSource.loop = false;
audioSource.playOnAwake = false;
audioSource.rolloffMode = source.rolloffMode;
audioSource.minDistance = source.minDistance;
audioSource.maxDistance = source.maxDistance;
audioSource.clip = breakSound;
audioSource.volume = source.volume;
audioSource.Play();
}
base.gameObject.SetActive(false);
return;
}
if (value < 225f)
{
num = -45f;
}
else if (value < 360f)
{
num = 0f;
}
}
Vector3 v = Quaternion.Euler(0f, 0f, num) * Vector3.down * hitVelocity;
body.rotation = num;
body.gravityScale = 0f;
body.velocity = v;
//TODO:
Instantiate(nailStrikePrefab, transform.position, Quaternion.identity);
if(source && hitSound)
{
source.PlayOneShot(hitSound);
}
}
}
private void FlingObjects()
{
int num = UnityEngine.Random.Range(spawnMin, speedMax + 1);
for (int i = 0; i <= num; i++)
{
GameObject gameObject = Instantiate(hitUpRockPrefabs, transform.position, transform.rotation);
Vector3 position = gameObject.transform.position;
Vector3 rotatiton2 = gameObject.transform.position;
Vector3 position3 = gameObject.transform.position;
float num2 = UnityEngine.Random.Range(speedMin, speedMax);
float num3 = UnityEngine.Random.Range(0f, 360f);
float x = num2 * Mathf.Cos(num3 * 0.017453292f);
float y = num2 * Mathf.Sin(num3 * 0.017453292f);
Vector2 velocity;
velocity.x = x;
velocity.y = y;
Rigidbody2D component = gameObject.GetComponent<Rigidbody2D>();
if (component)
{
component.velocity = velocity;
}
}
}
}
回到Unity编辑器中,我们来添加上这些参数:
2.制作水滴行为
首先呢我们来为水滴制作tk2dSprite和tk2dSpriteAnimation,然后它有四种动画如下所示:
然后就是公式化导入tk2dSPrite,audiosource,rb2d ,collider2d
创建同名脚本WaterDrip.cs:
cs
using System;
using System.Collections;
using UnityEngine;
[RequireComponent(typeof(Collider2D))]
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(tk2dSpriteAnimator))]
[RequireComponent(typeof(AudioSource))]
public class WaterDrip : MonoBehaviour
{
public float idleTimeMin = 2f;
public float idleTimeMax = 8f;
public float fallVelocity = -7f;
public RandomAudioClipTable impactAudioClipTable;
public float impactTranslation = -0.5f;
private bool impacted;
private Vector2 startPos;
private Collider2D col;
private Rigidbody2D body;
private tk2dSpriteAnimator anim;
private AudioSource source;
private void Awake()
{
col = GetComponent<Collider2D>();
body = GetComponent<Rigidbody2D>();
anim = GetComponent<tk2dSpriteAnimator>();
source = GetComponent<AudioSource>();
}
private void Start()
{
startPos = transform.position;
StartCoroutine(Drip());
}
private IEnumerator Drip()
{
for(; ; )
{
anim.Play("Idle");
body.gravityScale = 0f;
body.velocity = Vector2.zero;
col.enabled = false;
yield return new WaitForSeconds(UnityEngine.Random.Range(idleTimeMin, idleTimeMax)); //等idle状态的事件结束后执行Drip和Fall状
col.enabled = true;
yield return StartCoroutine(anim.PlayAnimWait("Drip"));
anim.Play("Fall");
body.gravityScale = 1f;
body.velocity = new Vector2(0f, fallVelocity);
impacted = false;
while (!impacted)
{
yield return null;
}
body.gravityScale = 0f;
body.velocity = Vector2.zero;
col.enabled = false;
impactAudioClipTable.PlayOneShot(source);
transform.position += new Vector3(0f, impactTranslation, 0f);
yield return StartCoroutine(anim.PlayAnimWait("Impact")); //等执行完impact状态的动画后就回到初始的位置继续循环执行
transform.position = startPos;
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
impacted = true;
}
}
添加好音响片段:
3.制作可破坏的草行为
空洞骑士的草非常特别,值得我们开一个新的Layer去记录它
一般来说,它只会跟player和HeroAttack两个layer发生碰撞
它有一个col2d和一个audiosource
它拥有三个子对象:
一个草的原始图像
一个草根,我们目前就给它制作两种动画:
一个被破坏后的粒子系统:
创建草的行为脚本:
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GrassBehaviour : MonoBehaviour
{
[Header("Animation")]
public float walkReactAmount = 1f;
public AnimationCurve walkReactCurve;
public float walkReactLength;
[Space]
public float attackReactAmount = 2f;
public AnimationCurve attackReactCurve;
public float attackReactLength;
[Space]
public string pushAnim = "Push";
private Animator animator;
[Header("Sound")]
public AudioClip[] pushSounds;
public AudioClip[] cutPushSounds;
public AudioClip[] cutSounds;
private AudioSource source;
[Header("Extra")]
public Color infectedColor = Color.white;
public bool neverInfected;
private static bool colorSet = false;
private AnimationCurve curve;
private float animLength = 2f;
private float animElapsed;
private float pushAmount = 1f;
private float pushDirection;
private bool returned = true;
private bool cutFirstFrame;
private bool isCut;
private float pushAmountError;
private Rigidbody2D player;
private Vector3 oldPlayerPos;
private SpriteRenderer[] renderers;
private static Dictionary<string, Material> sharedMaterials = new Dictionary<string, Material>();
private static int grassCount = 0;
private Material sharedMaterial;
private MaterialPropertyBlock propertyBlock;
public Material SharedMaterial
{
get
{
return sharedMaterial;
}
}
private void Awake()
{
source = GetComponent<AudioSource>();
animator = GetComponent<Animator>();
propertyBlock = new MaterialPropertyBlock();
}
private void Start()
{
if (Mathf.Abs(transform.position.z - 0.004f) > 1.8f)
{
GrassCut component = GetComponent<GrassCut>();
if (component)
{
Destroy(component);
}
Collider2D[] componentsInChildren = GetComponentsInChildren<Collider2D>();
for (int i = 0; i < componentsInChildren.Length; i++)
{
Destroy(componentsInChildren[i]);
}
}
renderers = GetComponentsInChildren<SpriteRenderer>(true);
if(renderers.Length != 0)
{
string key = renderers[0].material.name + (neverInfected ? "_neverInfected" : "");
if (sharedMaterials.ContainsKey(key))
{
sharedMaterial = sharedMaterials[key];
}
if (!sharedMaterial)
{
sharedMaterial = new Material(renderers[0].material);
sharedMaterials[key] = sharedMaterial;
}
}
if (sharedMaterial)
{
SpriteRenderer[] array = renderers;
for (int i = 0; i < array.Length; i++)
{
array[i].sharedMaterial = sharedMaterial;
}
}
if (!colorSet && !neverInfected)
{
StartCoroutine(DelayedInfectedCheck());
}
pushAmountError = Random.Range(-0.01f, 0.01f);
foreach (SpriteRenderer rend in renderers)
{
SetPushAmount(rend, pushAmountError);
}
transform.SetPositionZ(transform.position.z + Random.Range(-0.0001f, 0.0001f));
}
private void OnEnable()
{
grassCount++;
}
private void OnDisable()
{
grassCount--;
if(colorSet)
{
colorSet = false;
sharedMaterial = null;
}
if(grassCount <= 0)
{
sharedMaterials.Clear();
}
}
private void LateUpdate()
{
if (!returned)
{
float value = curve.Evaluate(animElapsed / animLength) * pushAmount * pushDirection * Mathf.Sign(transform.localScale.x) + pushAmountError;
foreach (SpriteRenderer rend in renderers)
{
SetPushAmount(rend, value);
}
if (animElapsed >= animLength)
{
returned = true;
if (animator && animator.HasParameter(pushAnim, new AnimatorControllerParameterType?(AnimatorControllerParameterType.Bool)))
{
animator.SetBool(pushAnim, false);
}
}
animElapsed += Time.deltaTime;
}
}
private void FixedUpdate()
{
if (player && returned && Mathf.Abs(player.velocity.x) >= 0.1f)
{
pushDirection = Mathf.Sign(player.velocity.x);
returned = false;
animElapsed = 0f;
pushAmount = walkReactAmount;
curve = walkReactCurve;
animLength = walkReactLength;
PlayRandomSound(isCut ? cutPushSounds : pushSounds);
if (animator)
{
if (animator.HasParameter(pushAnim, new AnimatorControllerParameterType?(AnimatorControllerParameterType.Bool)))
{
animator.SetBool(pushAnim, true);
return;
}
if (animator.HasParameter(pushAnim, new AnimatorControllerParameterType?(AnimatorControllerParameterType.Trigger)))
{
animator.SetTrigger(pushAnim);
}
}
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (returned)
{
pushDirection = Mathf.Sign(transform.position.x - collision.transform.position.x);
returned = false;
animElapsed = 0f;
if (GrassCut.ShouldCut(collision))
{
pushAmount = attackReactAmount;
curve = attackReactCurve;
animLength = attackReactLength;
PlayRandomSound(isCut ? cutPushSounds : pushSounds);
}
else
{
pushAmount = walkReactAmount;
curve = walkReactCurve;
animLength = walkReactLength;
if (collision.tag == "Player")
{
player = collision.GetComponent<Rigidbody2D>();
}
PlayRandomSound(isCut ? cutPushSounds : pushSounds);
}
if (animator && animator.HasParameter(pushAnim, null))
{
animator.SetBool(pushAnim, true);
}
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if(collision.tag == "Player")
{
player = null;
}
}
private void PlayRandomSound(AudioClip[] clips)
{
if (source && clips.Length != 0)
{
AudioClip clip = clips[Random.Range(0, clips.Length)];
source.PlayOneShot(clip);
}
}
private void SetPushAmount(Renderer rend, float value)
{
rend.GetPropertyBlock(propertyBlock);
propertyBlock.SetFloat("_PushAmount", value);
rend.SetPropertyBlock(propertyBlock);
}
private IEnumerator DelayedInfectedCheck()
{
yield return null;
if(sharedMaterial && GameObject.FindWithTag("Infected Flag"))
{
colorSet = true;
sharedMaterial.color = infectedColor;
SpriteRenderer[] array = renderers;
for (int i = 0; i < array.Length; i++)
{
array[i].sharedMaterial = sharedMaterial;
}
}
}
}
FSMActionReplacer先关代码:
cs
using System;
using UnityEngine;
public static class FSMActionReplacements
{
public static void SetMaterialColor(Component me, Color color)
{
Renderer component = me.GetComponent<Renderer>();
if (component != null)
{
component.material.color = color;
}
}
public static Directions CheckDirectionWithBrokenBehaviour(float angle)
{
if (angle < 45f)
{
return Directions.Right;
}
if (angle < 135f)
{
return Directions.Up;
}
if (angle < 225f)
{
return Directions.Left;
}
if (angle < 360f)
{
return Directions.Down;
}
return Directions.Unknown;
}
public enum Directions
{
Right,
Up,
Left,
Down,
Unknown
}
}
创建草的图像行为脚本:
cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GrassSpriteBehaviour : MonoBehaviour
{
[Header("Variables")]
public bool isWindy;
public bool noPushAnimation;
[Space]
public GameObject deathParticles;
public GameObject deathParticlesWindy;
public GameObject cutEffectPrefab;
[Space]
public AudioClip[] pushSounds;
public AudioClip[] cutSounds;
[Header("Animation State Names")]
public string idleAnimation = "Idle";
public string pushAnimation = "Push";
public string cutAnimation = "Dead";
public string idleWindyAnimation = "WindyIdle";
public string pushWindyAnimation = "WindyPush";
private bool isCut;
private bool interaction = true;
private bool visible;
private Animator animator;
private AudioSource audioSource;
public SpriteRenderer grassAlive;
public SpriteRenderer grassDead;
private void Awake()
{
animator = GetComponentInChildren<Animator>();
audioSource = GetComponent<AudioSource>();
}
private void Start()
{
if (Mathf.Abs(transform.position.z - 0.004f) > 1.8f)
{
interaction = false;
}
Init();
}
private void OnBecameVisible()
{
visible = true;
}
private void OnBecameInvisible()
{
visible = true;
}
private void Init()
{
visible = true;
animator.Play(isWindy ? idleAnimation : idleWindyAnimation);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (!isCut && interaction && visible)
{
if (GrassCut.ShouldCut(collision))
{
Debug.LogFormat("Grass Should Cut");
animator.Play(cutAnimation);
isCut = true;
if(isWindy && deathParticlesWindy)
{
deathParticlesWindy.SetActive(true);
}
else if (deathParticles)
{
deathParticles.SetActive(true);
}
if(audioSource && cutSounds.Length != 0)
{
audioSource.PlayOneShot(cutSounds[UnityEngine.Random.Range(0, cutSounds.Length)]);
}
grassAlive.enabled = false;
grassDead.enabled = true;
if (cutEffectPrefab)
{
int num = (int)Mathf.Sign(collision.transform.position.x - transform.position.x);
Vector3 position = (collision.transform.position + transform.position) / 2f;
//TODO:Object Pool
GameObject gameObject = Instantiate(cutEffectPrefab, position, Quaternion.identity);
Vector3 localScale = gameObject.transform.localScale;
localScale.x = Mathf.Abs(localScale.x) * -num;
gameObject.transform.localScale = localScale;
return;
}
}
else
{
if (!noPushAnimation)
{
animator.Play(isWindy ? pushWindyAnimation : pushAnimation);
}
if (audioSource && cutSounds.Length != 0)
{
audioSource.PlayOneShot(cutSounds[UnityEngine.Random.Range(0, cutSounds.Length)]);
}
}
}
}
}
创建判断草是否该被切割的脚本:
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GrassCut : MonoBehaviour
{
public SpriteRenderer[] disable;
public SpriteRenderer[] enable;
[Space]
public Collider2D[] disableColliders;
public Collider2D[] enableColliders;
[Space]
public GameObject particles;
public GameObject cutEffectPrefab;
private Collider2D col;
private void Awake()
{
col = GetComponent<Collider2D>();
}
public static bool ShouldCut(Collider2D collision)
{
//如果是骨钉攻击或者是锋利之影攻击
return collision.tag == "Nail Attack" || collision.tag == "Sharp Shadow";
}
}
创建好后回到Unity编辑器添加如下参数:
然后我们就可以依次创建不同样貌的草了,只需要修改小部分参数即可:
后面的我就不一一举例了,反正素材管够,最多可以生成五种不一样的草!
4.制作可破坏的门行为
可破坏的门即Tute Door,是需要玩家用骨钉去破坏的门,它的基础由图像和collider2d的trigger构成,
它的子对象中,包括当门被破坏后生成的粒子系统:
被破坏后生成的灰尘粒子系统:
UI来提示玩家攻击这个门(我还没做)
激活状态下的一个collider,当它被骨钉发送了PlaymakerFSM的HIT事件后,就设置Collider为非激活状态:
破坏后的剩余碎块:
创建一个脚本名字就叫Breakable.cs,这个脚本可以专门用来处理场景中可破坏的东西的行为:注意!它需要继承自IHitResponder接口并实现里面的方法
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Breakable : MonoBehaviour,IHitResponder
{
private Collider2D bodyCollider;
[Tooltip("Renderer which presents the undestroyed object.")]
[SerializeField] private Renderer wholeRenderer;
[Tooltip("List of child game objects which also represent the whole object.")]
[SerializeField] public GameObject[] wholeParts;
[Tooltip("List of child game objects which represent remnants that remain static after destruction.")]
[SerializeField] private GameObject[] remnantParts;
[SerializeField] private List<GameObject> debrisParts;
[SerializeField] private float angleOffset = -60f;
[Tooltip("Breakables behind this threshold are inert.")]
[SerializeField] private float inertBackgroundThreshold;
[Tooltip("Breakables in front of this threshold are inert.")]
[SerializeField] private float inertForegroundThreshold;
[Tooltip("Breakable effects are spawned at this offset.")]
[SerializeField] private Vector3 effectOffset;
[Tooltip("Prefab to spawn for audio.")]
[SerializeField] private AudioSource audioSourcePrefab;
[Tooltip("Table of audio clips to play upon break.")]
[SerializeField] private AudioEvent breakAudioEvent;
[Tooltip("Table of audio clips to play upon break.")]
[SerializeField] private RandomAudioClipTable breakAudioClipTable;
[Tooltip("Prefab to spawn when hit from a non-down angle.")]
[SerializeField] private Transform dustHitRegularPrefab;
[Tooltip("Prefab to spawn when hit from a down angle.")]
[SerializeField] private Transform dustHitDownPrefab;
[Tooltip("Prefab to spawn when hit from a down angle.")]
[SerializeField] private float flingSpeedMin;
[Tooltip("Prefab to spawn when hit from a down angle.")]
[SerializeField] private float flingSpeedMax;
[Tooltip("Strike effect prefab to spawn.")]
[SerializeField] private Transform strikeEffectPrefab;
[Tooltip("Nail hit prefab to spawn.")]
[SerializeField] private Transform nailHitEffectPrefab;
[Tooltip("Spell hit effect prefab to spawn.")]
[SerializeField] private Transform spellHitEffectPrefab;
[Tooltip("Object to send HIT event to.")]
[SerializeField] private GameObject hitEventReciever;
[Tooltip("Forward break effect to sibling FSMs.")]
[SerializeField] private bool forwardBreakEvent;
[Space]
public Probability.ProbabilityGameObject[] containingParticles;
public FlingObject[] flingObjectRegister;
private bool isBroken;
private void Awake()
{
bodyCollider = GetComponent<Collider2D>();
}
protected void Reset()
{
inertBackgroundThreshold = 1f;
inertForegroundThreshold = -1f;
effectOffset = new Vector3(0f, 0.5f, 0f);
flingSpeedMin = 10f;
flingSpeedMax = 17f;
}
protected void Start()
{
CreateAdditionalDebrisParts(debrisParts);
float z = transform.position.z;
if(z > inertForegroundThreshold || z < inertBackgroundThreshold)
{
BoxCollider2D component = GetComponent<BoxCollider2D>();
if(component != null)
{
component.enabled = false;
}
Destroy(this);
return;
}
for (int i = 0; i < remnantParts.Length; i++)
{
GameObject gameObject = remnantParts[i];
if(gameObject != null && gameObject.activeSelf)
{
gameObject.SetActive(false);
}
}
angleOffset *= Mathf.Sign(transform.localScale.x);
}
protected virtual void CreateAdditionalDebrisParts(List<GameObject> debrisParts)
{
}
public void Hit(HitInstance damageInstance)
{
if (isBroken)
{
return;
}
float impactAngle = damageInstance.Direction;
float num = damageInstance.MagnitudeMultiplier;
if(damageInstance.AttackType == AttackTypes.Spell)
{
Instantiate(spellHitEffectPrefab, base.transform.position, Quaternion.identity).SetPositionZ(0.0031f);
}
else
{
if (damageInstance.AttackType != AttackTypes.Nail && damageInstance.AttackType != AttackTypes.Generic)
{
impactAngle = 90f;
num = 1f;
}
Instantiate(strikeEffectPrefab, base.transform.position,Quaternion.identity);
Vector3 position = (damageInstance.Source.transform.position + base.transform.position) * 0.5f;
SpawnNailHitEffect(nailHitEffectPrefab, position, impactAngle);
}
int cardinalDirection = DirectionUtils.GetCardinalDirection(damageInstance.Direction);
Transform transform = dustHitRegularPrefab;
float flingAngleMin;
float flingAngleMax;
Vector3 euler;
if (cardinalDirection == 2)
{
angleOffset *= -1f;
flingAngleMin = 120f;
flingAngleMax = 160f;
euler = new Vector3(180f, 90f, 270f);
}
else if (cardinalDirection == 0)
{
flingAngleMin = 30f;
flingAngleMax = 70f;
euler = new Vector3(0f, 90f, 270f);
}
else if (cardinalDirection == 1)
{
angleOffset = 0f;
flingAngleMin = 70f;
flingAngleMax = 110f;
num *= 1.5f;
euler = new Vector3(270f, 90f, 270f);
}
else
{
angleOffset = 0f;
flingAngleMin = 160f;
flingAngleMax = 380f;
transform = dustHitDownPrefab;
euler = new Vector3(-72.5f, -180f, -180f);
}
if(transform != null)
{
Instantiate(transform, transform.position + effectOffset, Quaternion.Euler(euler));
}
Break(flingAngleMin, flingAngleMax, num);
}
private static Transform SpawnNailHitEffect(Transform nailHitEffectPrefab, Vector3 position, float impactAngle)
{
if (nailHitEffectPrefab == null)
return null;
int cardinalDirection = DirectionUtils.GetCardinalDirection(impactAngle);
float y = 1.5f;
float minInclusive;
float maxInclusive;
if (cardinalDirection == 3)
{
minInclusive = 270f;
maxInclusive = 290f;
}
else if (cardinalDirection == 1)
{
minInclusive = 70f;
maxInclusive = 110f;
}
else
{
minInclusive = 340f;
maxInclusive = 380f;
}
float x = (cardinalDirection == 2) ? -1.5f : 1.5f;
Transform transform = Instantiate(nailHitEffectPrefab,position,Quaternion.identity);
Vector3 eulerAngles = transform.eulerAngles;
eulerAngles.z = Random.Range(minInclusive, maxInclusive);
transform.eulerAngles = eulerAngles;
Vector3 localScale = transform.localScale;
localScale.x = x;
localScale.y = y;
transform.localScale = localScale;
return transform;
}
public void Break(float flingAngleMin, float flingAngleMax, float impactMultiplier)
{
if (isBroken)
return;
SetStaticPartsActivation(true);
for (int i = 0; i < debrisParts.Count; i++)
{
GameObject gameObject = debrisParts[i];
if (gameObject == null)
{
Debug.LogErrorFormat(this, "Unassigned debris part in {0}", new object[]
{
this
});
}
else
{
gameObject.SetActive(true);
gameObject.transform.SetRotationZ(gameObject.transform.localEulerAngles.z + angleOffset);
Rigidbody2D component = gameObject.GetComponent<Rigidbody2D>();
if (component != null)
{
float num = Random.Range(flingAngleMin, flingAngleMax);
Vector2 a = new Vector2(Mathf.Cos(num * 0.017453292f), Mathf.Sin(num * 0.017453292f));
float d = Random.Range(flingSpeedMin, flingSpeedMax) * impactMultiplier;
component.velocity = a * d;
}
}
}
if (containingParticles.Length != 0)
{
GameObject gameObject2 = Probability.GetRandomGameObjectByProbability(containingParticles);
if (gameObject2)
{
if (gameObject2.transform.parent != base.transform)
{
FlingObject flingObject = null;
foreach (FlingObject flingObject2 in flingObjectRegister)
{
if (flingObject2.referenceObject == gameObject2)
{
flingObject = flingObject2;
break;
}
}
if (flingObject != null)
{
flingObject.Fling(transform.position);
}
else
{
gameObject2 = Instantiate(gameObject2, transform.position, Quaternion.identity);
}
}
gameObject2.SetActive(true);
}
}
breakAudioEvent.SpawnAndPlayOneShot(audioSourcePrefab, transform.position);
breakAudioClipTable.SpawnAndPlayOneShot(audioSourcePrefab, transform.position);
if (hitEventReciever != null)
{
FSMUtility.SendEventToGameObject(hitEventReciever, "HIT", false);
}
if (forwardBreakEvent)
{
FSMUtility.SendEventToGameObject(gameObject, "BREAK", false);
}
GameObject gameObject3 = GameObject.FindGameObjectWithTag("CameraParent");
if (gameObject3 != null)
{
PlayMakerFSM playMakerFSM = PlayMakerFSM.FindFsmOnGameObject(gameObject3, "CameraShake");
if(playMakerFSM != null)
{
playMakerFSM.SendEvent("EnemyKillShake");
}
}
wholeRenderer.enabled = false;
bodyCollider.enabled = false;
isBroken = true;
}
private void SetStaticPartsActivation(bool v)
{
}
[System.Serializable]
public class FlingObject
{
public GameObject referenceObject;
[Space]
public int spawnMin;
public int spawnMax;
public float speedMin;
public float speedMax;
public float angleMin;
public float angleMax;
public Vector2 originVariation;
public FlingObject()
{
spawnMin = 25;
spawnMax = 35;
speedMin = 9f;
speedMax = 20f;
angleMin = 20f;
angleMax = 160f;
originVariation = new Vector2(0.5f, 0.5f);
}
public void Fling(Vector3 origin)
{
if (!referenceObject)
{
return;
}
int num = Random.Range(spawnMin, spawnMax + 1);
for (int i = 0; i < num; i++)
{
//TODO:Object Pool
GameObject gameObject = Instantiate(referenceObject);
if (gameObject)
{
gameObject.transform.position = origin + new Vector3(Random.Range(-originVariation.x, originVariation.x), Random.Range(-originVariation.y, originVariation.y), 0f);
float num2 = Random.Range(speedMin, speedMax);
float num3 = Random.Range(angleMin, angleMax);
float x = num2 * Mathf.Cos(num3 * 0.017453292f);
float y = num2 * Mathf.Sin(num3 * 0.017453292f);
Vector2 force = new Vector2(x, y);
Rigidbody2D component = gameObject.GetComponent<Rigidbody2D>();
if (component)
{
component.AddForce(force, ForceMode2D.Impulse);
}
}
}
}
}
}
概率生成游戏对象的脚本Probability.cs:
cs
using System;
using System.Collections.Generic;
using UnityEngine;
public class Probability
{
public static GameObject GetRandomGameObjectByProbability(ProbabilityGameObject[] array)
{
if(array.Length > 1)
{
List<ProbabilityGameObject> list = new List<ProbabilityGameObject>(array);
ProbabilityGameObject probabilityGameObject = null;
list.Sort((ProbabilityGameObject x, ProbabilityGameObject y) => x.probability.CompareTo(y.probability));
float num = 0f;
foreach (ProbabilityGameObject probabilityGameObject2 in list)
{
num += ((probabilityGameObject2.probability != 0f) ? probabilityGameObject2.probability : 1f);
}
float num2 = UnityEngine.Random.Range(0f, num);
float num3 = 0f;
foreach (ProbabilityGameObject probabilityGameObject3 in list)
{
if(num2 >= num3)
{
probabilityGameObject = probabilityGameObject3;
}
num3 += probabilityGameObject3.probability;
}
return probabilityGameObject.prefab;
}
if(array.Length == 1)
{
return array[0].prefab;
}
return null;
}
[Serializable]
public class ProbabilityGameObject
{
public GameObject prefab;
[Tooltip("If probability = 0, it will be considered 1.")]
public float probability;
public ProbabilityGameObject()
{
probability = 1f;
}
}
}
还记得我们上面创建的playmakerFSM吗?这里我们就让它作为接受的游戏对象,并向它发送HIT事件,这样就会关闭掉门的Collider了。
回到Unity编辑器中,我们来把参数都填好:
5.制作坍塌岩层行为
坍塌岩层即Collapser Tute,就是教学关卡中玩家走到一个地方突然就直接塌房了,玩家就只能被迫改道走下面,我们来实现这个行为:主要还是用预制体来实现
floor3,2,1都是堆叠素材让它看起来像正常的路面,这里我就不重复贴图了,
solid即固态的路面,别勾选isTrigger
以下都是粒子系统:
我们来用playmakerFSM实现这个坍塌岩层的完整行为,
首先是需要的事件和变量:
第一个行为状态:等一帧
然后是初始化阶段,疯狂的找孩子FindChild以及找到摄像机锁定区域CameraLockArea Collapser
然后是空闲状态,直到发现小骑士触发了射线检测机制:
然后是激烈的抖动阶段:
然后是桥梁崩塌的阶段:
cs
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.GameObject)]
[Tooltip("Set sprite renderer to active or inactive. Can only be one sprite renderer on object.")]
public class SetSpriteRenderer : FsmStateAction
{
[RequiredField]
public FsmOwnerDefault gameObject;
public FsmBool active;
public override void Reset()
{
gameObject = null;
active = null;
}
public override void OnEnter()
{
if(gameObject != null)
{
GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
if(ownerDefaultTarget != null)
{
SpriteRenderer component = ownerDefaultTarget.GetComponent<SpriteRenderer>();
if(component != null)
{
component.enabled = active.Value;
}
}
}
Finish();
}
}
}
其中涉及的自定义行为脚本如下:
cs
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.GameObject)]
[Tooltip("Activate or deactivate all children on a GameObject.")]
public class ActivateAllChildren : FsmStateAction
{
[RequiredField]
[UIHint(UIHint.Variable)]
public FsmGameObject gameObject;
public bool activate;
public override void Reset()
{
gameObject = null;
activate = false;
}
public override void OnEnter()
{
GameObject value = gameObject.Value;
if(value != null)
{
foreach (object obj in value.transform)
{
((Transform)obj).gameObject.SetActive(activate);
}
}
Finish();
}
}
}
cs
using System;
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.Audio)]
[Tooltip("Instantiate an Audio Player object and play a oneshot sound via its Audio Source.")]
public class AudioPlayerOneShot : FsmStateAction
{
[RequiredField]
[CheckForComponent(typeof(AudioSource))]
[Tooltip("The object to spawn. Select Audio Player prefab.")]
public FsmGameObject audioPlayer;
[RequiredField]
[Tooltip("Object to use as the spawn point of Audio Player")]
public FsmGameObject spawnPoint;
[CompoundArray("Audio Clips", "Audio Clip", "Weight")]
public AudioClip[] audioClips;
[HasFloatSlider(0f, 1f)]
public FsmFloat[] weights;
public FsmFloat pitchMin;
public FsmFloat pitchMax;
public FsmFloat volume;
public FsmFloat delay;
public FsmGameObject storePlayer;
private AudioSource audio;
private float timer;
public override void Reset()
{
spawnPoint = null;
audioClips = new AudioClip[3];
weights = new FsmFloat[]
{
1f,
1f,
1f
};
pitchMin = 1f;
pitchMax = 1f;
volume = 1f;
timer = 0f;
}
public override void OnEnter()
{
timer = 0f;
if(delay.Value == 0f)
{
DoPlayRandomClip();
Finish();
}
}
public override void OnUpdate()
{
if(delay.Value > 0f)
{
if(timer < delay.Value)
{
timer += Time.deltaTime;
return;
}
DoPlayRandomClip();
Finish();
}
}
private void DoPlayRandomClip()
{
if (audioClips.Length == 0)
return;
GameObject value = audioPlayer.Value;
Vector3 position = spawnPoint.Value.transform.position;
Vector3 up = Vector3.up;
//TODO:这行记得要改,因为我还没做对象池
GameObject gameObject = UnityEngine.Object.Instantiate(audioPlayer.Value, position, Quaternion.Euler(up));
audio = gameObject.GetComponent<AudioSource>();
int randomWeightIndex = ActionHelpers.GetRandomWeightedIndex(weights);
if(randomWeightIndex != -1)
{
AudioClip audioClip = audioClips[randomWeightIndex];
if(audioClip != null)
{
float pitch = UnityEngine.Random.Range(pitchMin.Value, pitchMax.Value);
audio.pitch = pitch;
audio.PlayOneShot(audioClip);
}
}
audio.volume = volume.Value;
}
public AudioPlayerOneShot()
{
pitchMin = 1f;
pitchMax = 2f;
}
}
}
cs
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.GameObject)]
[Tooltip("Spawns a random amount of chosen GameObject from global pool and fires them off in random directions.")]
public class FlingObjectsFromGlobalPool : RigidBody2dActionBase
{
[RequiredField]
[Tooltip("GameObject to spawn.")]
public FsmGameObject gameObject;
[Tooltip("GameObject to spawn at (optional).")]
public FsmGameObject spawnPoint;
[Tooltip("Position. If a Spawn Point is defined, this is used as a local offset from the Spawn Point position.")]
public FsmVector3 position;
[Tooltip("Minimum amount of objects to be spawned.")]
public FsmInt spawnMin;
[Tooltip("Maximum amount of objects to be spawned.")]
public FsmInt spawnMax;
[Tooltip("Minimum speed objects are fired at.")]
public FsmFloat speedMin;
[Tooltip("Maximum speed objects are fired at.")]
public FsmFloat speedMax;
[Tooltip("Minimum angle objects are fired at.")]
public FsmFloat angleMin;
[Tooltip("Maximum angle objects are fired at.")]
public FsmFloat angleMax;
[Tooltip("Randomises spawn points of objects within this range. Leave as 0 and all objects will spawn at same point.")]
public FsmFloat originVariationX;
public FsmFloat originVariationY;
[Tooltip("Optional: Name of FSM on object you want to send an event to after spawn")]
public FsmString FSM;
[Tooltip("Optional: Event you want to send to object after spawn")]
public FsmString FSMEvent;
private float vectorX;
private float vectorY;
private bool originAdjusted;
public override void Reset()
{
gameObject = null;
spawnPoint = null;
position = new FsmVector3
{
UseVariable = true
};
spawnMin = null;
spawnMax = null;
speedMin = null;
speedMax = null;
angleMin = null;
angleMax = null;
originVariationX = null;
originVariationY = null;
FSM = new FsmString
{
UseVariable = true
};
FSMEvent = new FsmString
{
UseVariable = true
};
}
public override void OnEnter()
{
if (gameObject.Value != null)
{
Vector3 a = Vector3.zero;
Vector3 zero = Vector3.zero;
if (spawnPoint.Value != null)
{
a = spawnPoint.Value.transform.position;
if (!position.IsNone)
{
a += position.Value;
}
}
else if (!position.IsNone)
{
a = position.Value;
}
int num = Random.Range(spawnMin.Value, spawnMax.Value + 1);
for (int i = 1; i <= num; i++)
{
//TODO:以后创造完对象池后记得替换掉
GameObject gameObject = GameObject.Instantiate(this.gameObject.Value, a, Quaternion.Euler(zero));
float x = gameObject.transform.position.x;
float y = gameObject.transform.position.y;
float z = gameObject.transform.position.z;
if (originVariationX != null)
{
x = gameObject.transform.position.x + Random.Range(-originVariationX.Value, originVariationX.Value);
originAdjusted = true;
}
if (originVariationY != null)
{
y = gameObject.transform.position.y + Random.Range(-originVariationY.Value, originVariationY.Value);
originAdjusted = true;
}
if (originAdjusted)
{
gameObject.transform.position = new Vector3(x, y, z);
}
base.CacheRigidBody2d(gameObject);
float num2 = Random.Range(speedMin.Value, speedMax.Value);
float num3 = Random.Range(angleMin.Value, angleMax.Value);
vectorX = num2 * Mathf.Cos(num3 * 0.017453292f);
vectorY = num2 * Mathf.Sin(num3 * 0.017453292f);
Vector2 velocity;
velocity.x = vectorX;
velocity.y = vectorY;
rb2d.velocity = velocity;
if (!FSM.IsNone)
{
FSMUtility.LocateFSM(gameObject, FSM.Value).SendEvent(FSMEvent.Value);
}
}
}
Finish();
}
}
}
最后是强制玩家进入willHardLand状态:
来到HeroController.cs中创建这个方法
cs
public void ForceHardLanding()
{
Debug.LogFormat("Force Hard Landing");
if (!cState.onGround)
{
cState.willHardLand = true;
}
}
还有就是如果已经是激活状态了(也就是玩家已经体验过一次崩塌桥梁)就进入这个Activated状态:
最后我们还要创建一个mask蒙版,让玩家觉得他在走一个正常的路上,然后当踩到桥梁后瞬间崩塌且让蒙版变成不激活状态:
当trigger2D Event检测到游戏对象为player 的时候执行HIT事件进入Fade状态
Fade状态就是渐变式的让蒙版消失(改变颜色的alpha值)
自定义playmakerFSM行为脚本如下:
cs
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.iTween)]
[Tooltip("Changes a GameObject's opacity over time.")]
public class iTweenFadeTo : iTweenFsmAction
{
[RequiredField]
public FsmOwnerDefault gameObject;
[Tooltip("iTween ID. If set you can use iTween Stop action to stop it by its id.")]
public FsmString id;
[Tooltip("The end alpha value of the animation.")]
public FsmFloat alpha;
[Tooltip("Whether or not to include children of this GameObject. True by default.")]
public FsmBool includeChildren;
[Tooltip("Which color of a shader to use. Uses '_Color' by default.")]
public FsmString namedValueColor;
[Tooltip("The time in seconds the animation will take to complete.")]
public FsmFloat time;
[Tooltip("The time in seconds the animation will wait before beginning.")]
public FsmFloat delay;
[Tooltip("The shape of the easing curve applied to the animation.")]
public iTween.EaseType easeType = iTween.EaseType.linear;
[Tooltip("The type of loop to apply once the animation has completed.")]
public iTween.LoopType loopType;
public override void Reset()
{
base.Reset();
id = new FsmString
{
UseVariable = true
};
alpha = 0f;
includeChildren = true;
namedValueColor = "_Color";
time = 1f;
delay = 0f;
}
public override void OnEnter()
{
OnEnteriTween(gameObject);
if(loopType != iTween.LoopType.none)
{
IsLoop(true);
}
DoiTween();
}
public override void OnExit()
{
base.OnExitiTween(gameObject);
}
private void DoiTween()
{
GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
if (ownerDefaultTarget)
return;
itweenType = "fade";
iTween.FadeTo(ownerDefaultTarget, iTween.Hash(new object[]
{
"name",
id.IsNone ? "" : id.Value,
"alpha",
alpha.Value,
"includechildren",
includeChildren.IsNone || includeChildren.Value,
"NamedValueColor",
namedValueColor.Value,
"time",
time.Value,
"delay",
delay.IsNone ? 0f : delay.Value,
"easetype",
easeType,
"looptype",
loopType,
"oncomplete",
"iTweenOnComplete",
"oncompleteparams",
itweenID,
"onstart",
"iTweenOnStart",
"onstartparams",
itweenID,
"ignoretimescale",
!realTime.IsNone && realTime.Value
}));
}
}
}
6.制作生命水的基本行为
老规矩先制作tk2dSprite和tk2dSpriteAnimator:
回到编辑器中添加好上述两个以外还有boxCollider2d和audiosource
光照haze:
生命水灯的基础:
生命水灯的帽子,注意它是有rb2d和poly2d的
被击破后的溅射效果:
我们来创建同名脚本HealthCocoon.cs:
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthCocoon : MonoBehaviour
{
[Header("Behaviour")]
public GameObject[] slashEffects;
public GameObject[] spellEffects;
public Vector3 effectOrigin = new Vector3(0f, 0.8f, 0f);
[Space]
public FlingPrefab[] flingPrefabs;
[Space]
public GameObject[] enableChildren;
public GameObject[] disableChildren;
public Collider2D[] disableColliders;
[Space]
public Rigidbody2D cap;
public float capHitForce = 10f;
[Space]
public AudioClip deathSound;
[Header("Animation")]
public string idleAnimation = "Cocoon Idle";
public string sweatAnimation = "Cocoon Sweat";
public AudioClip moveSound;
public float waitMin = 2f;
public float waitMax = 6f;
private Coroutine animRoutine;
private AudioSource source;
private tk2dSpriteAnimator animator;
private bool activated;
private void Awake()
{
source = GetComponent<AudioSource>();
animator = GetComponent<tk2dSpriteAnimator>();
}
private void Start()
{
animRoutine = StartCoroutine(Animate());
FlingPrefab[] array = flingPrefabs;
for (int i = 0; i < array.Length; i++)
{
array[i].SetupPool(transform);
}
}
private void PlaySound(AudioClip clip)
{
if(source && clip)
{
source.PlayOneShot(clip);
}
}
private IEnumerator Animate()
{
for(; ; )
{
yield return new WaitForSeconds(Random.Range(waitMin, waitMax));
PlaySound(moveSound);
if (animator)
{
tk2dSpriteAnimationClip clip = animator.GetClipByName(sweatAnimation);
animator.Play(clip);
yield return new WaitForSeconds(clip.frames.Length / clip.fps);
animator.Play(idleAnimation);
}
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (!activated)
{
bool flag = false;
if(collision.tag == "Nail Attack")
{
flag = true;
float value = PlayMakerFSM.FindFsmOnGameObject(collision.gameObject, "damages_enemy").FsmVariables.FindFsmFloat("direction").Value;
float z = 0f;
Vector2 v = new Vector2(1.5f, 1.5f);
if (value < 45f)
{
z = Random.Range(340, 380);
}
else if (value < 135f)
{
z = Random.Range(340, 380);
}
else if (value < 225f)
{
v.x *= -1f;
z = Random.Range(70, 110);
}
else if (value < 360f)
{
z = Random.Range(250, 290);
}
GameObject[] array = slashEffects;
for (int i = 0; i < array.Length; i++)
{
GameObject gameObject = Instantiate(array[i], transform.position + effectOrigin, Quaternion.identity);
gameObject.transform.eulerAngles = new Vector3(0f, 0f, z);
gameObject.transform.localScale = v;
}
}
if(collision.tag == "Hero Spell")
{
flag = true;
GameObject[] array = slashEffects;
for (int i = 0; i < array.Length; i++)
{
GameObject gameObject2 = Instantiate(array[i], transform.position + effectOrigin, Quaternion.identity);
Vector3 position = gameObject2.transform.position;
position.z = 0.0031f;
gameObject2.transform.position = position;
}
}
if (flag)
{
activated = true;
GameObject[] array = enableChildren;
for (int i = 0; i < array.Length; i++)
{
array[i].SetActive(true);
}
if (cap)
{
cap.gameObject.SetActive(true);
Vector3 a = transform.position - collision.transform.position;
a.Normalize();
cap.AddForce(capHitForce * a, ForceMode2D.Impulse);
}
//foreach (FlingPrefab fling in flingPrefabs)
//{
// FlingObjects(fling);
//}
PlaySound(deathSound);
SetBroken();
//TODO:
GameCameras gameCameras = FindObjectOfType<GameCameras>();
if (gameCameras)
{
gameCameras.cameraShakeFSM.SendEvent("EnemyKillShake");
}
}
}
}
private void SetBroken()
{
StopCoroutine(animRoutine);
GetComponent<MeshRenderer>().enabled = false;
GameObject[] array = disableChildren;
for (int i = 0; i < array.Length; i++)
{
array[i].SetActive(false);
}
Collider2D[] array2 = disableColliders;
for (int i = 0; i < array2.Length; i++)
{
array2[i].enabled = false;
}
}
private void FlingObjects(FlingPrefab fling)
{
if (fling.prefab)
{
int num = Random.Range(fling.minAmount, fling.maxAmount + 1);
for (int i = 1; i <= num; i++)
{
GameObject gameObject = fling.Spawn();
gameObject.transform.position += new Vector3(fling.originVariation.x * Random.Range(-1f, 1f), fling.originVariation.y * Random.Range(-1f, 1f));
float num2 = Random.Range(fling.minSpeed, fling.maxSpeed);
float num3 = Random.Range(fling.minAngle, fling.maxAngle);
float x = num2 * Mathf.Cos(num3 * 0.017453292f);
float y = num2 * Mathf.Sin(num3 * 0.017453292f);
Vector2 velocity;
velocity.x = x;
velocity.y = y;
Rigidbody2D component = gameObject.GetComponent<Rigidbody2D>();
if (component)
{
component.velocity = velocity;
}
}
}
}
public void SetScuttlerAmount(int amount)
{
foreach (FlingPrefab flingPrefab in flingPrefabs)
{
if(flingPrefab.prefab.name == "Health Scuttler")
{
FlingPrefab flingPrefab2 = flingPrefab;
flingPrefab.maxAmount = amount;
flingPrefab.minAmount = amount;
flingPrefab.SetupPool(transform);
return;
}
}
}
[System.Serializable]
public class FlingPrefab
{
public GameObject prefab;
public List<GameObject> pool = new List<GameObject>();
public int minAmount;
public int maxAmount;
public Vector2 originVariation = new Vector2(0.5f, 0.5f);
public int minSpeed;
public int maxSpeed;
public float minAngle;
public float maxAngle;
public void SetupPool(Transform parent)
{
if (prefab)
{
pool.Capacity = maxAmount;
for (int i = 0; i < pool.Count; i++)
{
GameObject gameObject = Instantiate(prefab, parent);
gameObject.transform.localPosition = Vector3.zero;
gameObject.SetActive(false);
pool.Add(gameObject);
}
}
}
public GameObject Spawn()
{
foreach (GameObject gameObject in pool)
{
if (!gameObject.activeSelf)
{
gameObject.SetActive(true);
return gameObject;
}
}
return null;
}
}
}
回到UNTIY编辑器中,我们先把参数添加上去:
由于我还没做完整的生命水系统,所以你打破它也顶多是个空架子也不会生成生命水果粒和生命水果冻,那就暂且做到这里吧 ,以后我们做到生命水系统我会完善它的。
7.制作宝箱的基本行为
最后我们来做一个宝箱的基本行为,宝箱分为三种状态:Idle空闲,Opening正在打开,Opened已经打开同样我们需要一个tk2dSPrite和tk2dSpriteAnimation:
我们来给Chest添加上tk2dSprite和tk2danimation,它同样需要一个碰撞箱子
同样我们需要两个部分来欺骗玩家,当玩家打开箱子后,自身的meshrenderer会被关闭,打开Opened子对象上的top和Bot两个部分,使得玩家宝箱一直都是在打开状态的:
一些粒子系统如下:
Geo的对象池,我还没做所以为空:
开启后的效果:
这个就是复仇之魂的UI相关了,我们目前用不到先略过
这个同理:
我们来给Chest创建playmakerFSM名字就叫Chest Control
状态如下:
对于Burst Effects的状态机,我们让它播放完动画后设置为非激活状态即可:
总结
我们来到HeroController.cs设置好参数看看:
玩家不仅掉血了还找到了hazard复活位置,在指定地点复活了
对于可攻击玩家的钟乳石行为:
对于水滴行为:主打一个周而复始
对于可破坏的草,没有死亡动画都是直接销毁的:
对于可破坏的门:
对于崩塌岩层:在mask的遮罩下你宛如走在正常的路面
对于生命水的基本行为:
只能做到让它直接消失了,后面会补充的
对于宝箱:
这个白白的东西我们暂且捡不到,以后我们再来制作吧。
OK终于这期讲完了,下一期暂时没什么思路,先祝你们国庆节快乐吧。