事情的起因:
首先我之前在b站的时候突然发现有个大佬说复刻了空洞骑士,点进去一看发现很多场景都福源道非常详细,当时我除了觉得大佬很强的同时也想自己试一下,而且当时对玩家血条设计等都很模糊,就想着问up主,结果因为制作的时间过了很久了,大佬也有些答不上来,于是我就先下来,然后一直跟着其它视频继续学,这几天闲着就试着通过大佬的代码能不能逐步做一个空洞骑士的mod出来,所幸前面的步骤都比较顺利,通过大佬的代码还是能慢慢做出来
(Steam截图镇个楼)
学习目标:
大佬的视频以及Github源码:
【Unity3D】空洞骑士の复刻_哔哩哔哩_bilibili
项目开源:https://github.com/dreamCirno/Hollow-Knight-Imitation
学习内容:
初始工作就先创建一个2D项目,然后本项目需要准备的插件有点多,把没必要的插件删除后就这些了,ProCamera2D,Input system,Post Poccessing,PlayerMaker(这个我没买)
打开开源项目,先别一次性把Assets的项目全部导入,不然肯定一堆报错的,我们先把角色的精灵图导入,然后再拖入几个地板,然后场景就暂时这样了。
接着我们要为玩家创建动作了。
创建Input Actions命名为InputControl,然后这些都是老操作了。
然后就生成一个C#脚本名字就叫InputControl,然后创建一个名字叫InputManger的空对象以及一个同名脚本、
我们暂时只用到GamePlayer的动作表所以就先这样写了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InputManager : MonoBehaviour
{
private static InputControl inputControl;
public static InputControl InputControl
{
get
{
if(inputControl == null)
{
inputControl = new InputControl();
}
return inputControl;
}
}
private void OnEnable()
{
InputControl.GamePlayer.Movement.Enable();
InputControl.GamePlayer.Jump.Enable();
InputControl.GamePlayer.Attack.Enable();
}
private void OnDisable()
{
InputControl.GamePlayer.Movement.Disable();
InputControl.GamePlayer.Jump.Disable();
InputControl.GamePlayer.Attack.Disable();
}
}
玩家类脚本:
我们为我们的Player创建一个名字叫CharacterController2D的脚本。
然后为我们的Player对象添加上组件
2D物理材质如下
首先我们先实现玩家的移动和转向
对于移动我们采用InputSystem对于行为动作的订阅事件和退订事件,用vectorInput读入键盘的输入,
对于转向则根据任务面部朝向,当向右移动的时候transform.localScale为(-1,1,1),向左则为(1,1,1);
using Com.LuisPedroFonseca.ProCamera2D;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class CharacterController2D : MonoBehaviour
{
#region Propertries
readonly Vector3 flippedScale = new Vector3(-1, 1, 1);
private Rigidbody2D controllerRigibody;
[Header("依赖脚本")] Animator animator;
[Header("移动参数")]
[SerializeField] float maxSpeed = 0.0f;
[SerializeField] float maxGravityVelocity = 10.0f;
[SerializeField] float jumpForce = 0.0f;
[SerializeField] float groundedGravityScale = 0.0f;
[SerializeField] float jumpGravityScale = 0.0f;
[SerializeField] float fallGravityScale = 0.0f;
private Vector2 vectorInput;
private int jumpCount;
private bool JumpInput;
private float counter;
private bool enableGravity;
private bool canMove;
private bool isOnGround;
private bool isFacingLeft;
private bool isJumping;
private bool isFalling;
private int animatorFirstLandingBool;
private int animatorGroundedBool;
private int animatorMovementSpeed;
private int animatorVelocitySpeed;
private int animatorJumpTrigger;
private int animatorDoubleJumpTrigger;
[Header("其它参数")]
[SerializeField] private bool firstLanding;
#endregion
#region CallBackFunctions
private void Awake()
{
controllerRigibody = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
}
private void OnEnable()
{
InputManager.InputControl.GamePlayer.Movement.performed += ctx => vectorInput = ctx.ReadValue<Vector2>();
InputManager.InputControl.GamePlayer.Jump.started += Jump_Started;
InputManager.InputControl.GamePlayer.Jump.performed += Jump_Performed;
InputManager.InputControl.GamePlayer.Jump.canceled += Jump_Canceled;
}
private void OnDisable()
{
InputManager.InputControl.GamePlayer.Movement.performed -= ctx => vectorInput = ctx.ReadValue<Vector2>();
InputManager.InputControl.GamePlayer.Jump.started -= Jump_Started;
InputManager.InputControl.GamePlayer.Jump.performed -= Jump_Performed;
InputManager.InputControl.GamePlayer.Jump.canceled -= Jump_Canceled;
}
private void Start()
{
animatorFirstLandingBool = Animator.StringToHash("FirstLanding");
animatorGroundedBool = Animator.StringToHash("Grounded");
animatorVelocitySpeed = Animator.StringToHash("Velocity");
animatorMovementSpeed = Animator.StringToHash("Movement");
animatorJumpTrigger = Animator.StringToHash("Jump");
animatorDoubleJumpTrigger = Animator.StringToHash("DoubleJump");
animator.SetBool(animatorFirstLandingBool, firstLanding);
enableGravity = true;
canMove = true;
}
private void FixedUpdate()
{
UpdateVelocity();
UpdateDirection();
}
#endregion
#region Movement
private void UpdateVelocity()
{
Vector2 velocity = controllerRigibody.velocity;
if (vectorInput.x != 0)
{
velocity.y = Mathf.Clamp(velocity.y, -maxGravityVelocity / 2, maxGravityVelocity / 2);
}
else
{
velocity.y = Mathf.Clamp(velocity.y, -maxGravityVelocity, maxGravityVelocity);
}
animator.SetFloat(animatorVelocitySpeed, controllerRigibody.velocity.y);
if (canMove)
{
controllerRigibody.velocity = new Vector2(vectorInput.x * maxSpeed, velocity.y);
animator.SetInteger(animatorMovementSpeed, (int)vectorInput.x);
}
}
private void UpdateDirection()
{
//控制玩家的旋转
if (controllerRigibody.velocity.x > 1f && isFacingLeft)
{
isFacingLeft = false;
transform.localScale = flippedScale;
}
else if (controllerRigibody.velocity.x < -1f && !isFacingLeft)
{
isFacingLeft = true;
transform.localScale = Vector3.one;
}
}
private void UpdateGrounding(Collision2D collision,bool exitState)
{
if (exitState)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian") || collision.gameObject.layer == LayerMask.NameToLayer("Soft Terrian"))
{
isOnGround = false;
}
}
else
{
//判断为落地状态
if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian")
|| collision.gameObject.layer == LayerMask.NameToLayer("Soft Terrian")
&& collision.contacts[0].normal == Vector2.up
&& !isOnGround)
{
isOnGround = true;
isJumping = false;
isFalling = false;
}
//判断为头顶碰到物体状态
else if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian") || collision.gameObject.layer == LayerMask.NameToLayer("Soft Terrian")
&& collision.contacts[0].normal == Vector2.down && isJumping)
{
}
}
animator.SetBool(animatorGroundedBool, isOnGround);
}
public void StopHorizontalMovement()
{
Vector2 velocity = controllerRigibody.velocity;
velocity.x = 0;
controllerRigibody.velocity = velocity;
animator.SetInteger(animatorMovementSpeed, 0);
}
public void SetIsOnGrounded(bool state)
{
isOnGround = state;
animator.SetBool(animatorGroundedBool, isOnGround);
}
#endregion
#region Combat
private void Jump_Canceled(InputAction.CallbackContext context)
{
}
private void Jump_Performed(InputAction.CallbackContext context)
{
}
private void Jump_Started(InputAction.CallbackContext context)
{
}
private void OnCollisionEnter2D(Collision2D collision)
{
UpdateGrounding(collision, false);
}
private void OnCollisionStay2D(Collision2D collision)
{
UpdateGrounding(collision, false);
}
private void OnCollisionExit2D(Collision2D collision)
{
UpdateGrounding(collision, true);
}
#endregion
#region Others
public void FirstLanding()
{
}
#endregion
}
接着我们制作动画,制作好Idle,walk,Run的动画
由于我们还没为动画判断条件Grounded作代码判断条件,所以就先创建一个空对象用于地面检测
再给他一个脚本
using UnityEngine;
public class GroundDetector : MonoBehaviour
{
private CharacterController2D character;
private void Awake()
{
character = FindObjectOfType<CharacterController2D>();
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian"))
{
character.SetIsOnGrounded(true);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian"))
{
character.SetIsOnGrounded(false);
}
}
}
移动的脚本做完了我们还需要做跳跃,跳跃分为一段跳和二段跳,首先打开CharacterController2D,我们将通过跳跃计数器决定播放一段跳或是二段跳的动画,并通过判断条件决定什么时候重置动画
using Com.LuisPedroFonseca.ProCamera2D;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class CharacterController2D : MonoBehaviour
{
#region Propertries
readonly Vector3 flippedScale = new Vector3(-1, 1, 1);
private Rigidbody2D controllerRigibody;
[Header("依赖脚本")] Animator animator;
[Header("移动参数")]
[SerializeField] float maxSpeed = 0.0f;
[SerializeField] float maxGravityVelocity = 10.0f;
[SerializeField] float jumpForce = 0.0f;
[SerializeField] float groundedGravityScale = 0.0f;
[SerializeField] float jumpGravityScale = 0.0f;
[SerializeField] float fallGravityScale = 0.0f;
private Vector2 vectorInput;
private int jumpCount;
private bool JumpInput;
private float counter;
private bool enableGravity;
private bool canMove;
private bool isOnGround;
private bool isFacingLeft;
private bool isJumping;
private bool isFalling;
private int animatorFirstLandingBool;
private int animatorGroundedBool;
private int animatorMovementSpeed;
private int animatorVelocitySpeed;
private int animatorJumpTrigger;
private int animatorDoubleJumpTrigger;
[Header("其它参数")]
[SerializeField] private bool firstLanding;
#endregion
#region CallBackFunctions
private void Awake()
{
controllerRigibody = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
}
private void OnEnable()
{
InputManager.InputControl.GamePlayer.Movement.performed += ctx => vectorInput = ctx.ReadValue<Vector2>();
InputManager.InputControl.GamePlayer.Jump.started += Jump_Started;
InputManager.InputControl.GamePlayer.Jump.performed += Jump_Performed;
InputManager.InputControl.GamePlayer.Jump.canceled += Jump_Canceled;
}
private void OnDisable()
{
InputManager.InputControl.GamePlayer.Movement.performed -= ctx => vectorInput = ctx.ReadValue<Vector2>();
InputManager.InputControl.GamePlayer.Jump.started -= Jump_Started;
InputManager.InputControl.GamePlayer.Jump.performed -= Jump_Performed;
InputManager.InputControl.GamePlayer.Jump.canceled -= Jump_Canceled;
}
private void Start()
{
animatorFirstLandingBool = Animator.StringToHash("FirstLanding");
animatorGroundedBool = Animator.StringToHash("Grounded");
animatorVelocitySpeed = Animator.StringToHash("Velocity");
animatorMovementSpeed = Animator.StringToHash("Movement");
animatorJumpTrigger = Animator.StringToHash("Jump");
animatorDoubleJumpTrigger = Animator.StringToHash("DoubleJump");
animator.SetBool(animatorFirstLandingBool, firstLanding);
enableGravity = true;
canMove = true;
}
private void FixedUpdate()
{
UpdateVelocity();
UpdateJump();
UpdateDirection();
UpdateGravityScale();
}
#endregion
#region Movement
private void UpdateVelocity()
{
Vector2 velocity = controllerRigibody.velocity;
if (vectorInput.x != 0)
{
velocity.y = Mathf.Clamp(velocity.y, -maxGravityVelocity / 2, maxGravityVelocity / 2);
}
else
{
velocity.y = Mathf.Clamp(velocity.y, -maxGravityVelocity, maxGravityVelocity);
}
animator.SetFloat(animatorVelocitySpeed, controllerRigibody.velocity.y);
if (canMove)
{
controllerRigibody.velocity = new Vector2(vectorInput.x * maxSpeed, velocity.y);
animator.SetInteger(animatorMovementSpeed, (int)vectorInput.x);
}
}
private void UpdateDirection()
{
//控制玩家的旋转
if (controllerRigibody.velocity.x > 1f && isFacingLeft)
{
isFacingLeft = false;
transform.localScale = flippedScale;
}
else if (controllerRigibody.velocity.x < -1f && !isFacingLeft)
{
isFacingLeft = true;
transform.localScale = Vector3.one;
}
}
private void UpdateJump()
{
if(isJumping && controllerRigibody.velocity.y < 0)
{
isFalling = true;
}
if (JumpInput)
{
controllerRigibody.AddForce(new Vector2(0,jumpForce), ForceMode2D.Impulse);
isJumping = true;
}
if(isOnGround && !isJumping && jumpCount != 0) //如果已经落地了,则重置跳跃计数器
{
jumpCount = 0;
counter = Time.time - counter;
}
}
private void UpdateGravityScale()
{
var gravityScale = groundedGravityScale;
if (!isOnGround)
{
gravityScale = controllerRigibody.velocity.y > 0.0f ? jumpGravityScale : fallGravityScale;
}
if (!enableGravity)
{
gravityScale = 0;
}
controllerRigibody.gravityScale = gravityScale;
}
private void JumpCancel()
{
JumpInput = false;
isJumping = false;
if(jumpCount == 1)
{
animator.ResetTrigger(animatorJumpTrigger);
}
else if(jumpCount == 2)
{
animator.ResetTrigger(animatorDoubleJumpTrigger);
}
}
private void UpdateGrounding(Collision2D collision,bool exitState)
{
if (exitState)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian") || collision.gameObject.layer == LayerMask.NameToLayer("Soft Terrian"))
{
isOnGround = false;
}
}
else
{
//判断为落地状态
if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian")
|| collision.gameObject.layer == LayerMask.NameToLayer("Soft Terrian")
&& collision.contacts[0].normal == Vector2.up
&& !isOnGround)
{
isOnGround = true;
isJumping = false;
isFalling = false;
//effect
}
//判断为头顶碰到物体状态
else if (collision.gameObject.layer == LayerMask.NameToLayer("Terrian") || collision.gameObject.layer == LayerMask.NameToLayer("Soft Terrian")
&& collision.contacts[0].normal == Vector2.down && isJumping)
{
JumpCancel();
}
}
animator.SetBool(animatorGroundedBool, isOnGround);
}
public void StopHorizontalMovement()
{
Vector2 velocity = controllerRigibody.velocity;
velocity.x = 0;
controllerRigibody.velocity = velocity;
animator.SetInteger(animatorMovementSpeed, 0);
}
public void SetIsOnGrounded(bool state)
{
isOnGround = state;
animator.SetBool(animatorGroundedBool, isOnGround);
}
#endregion
#region Combat
private void Jump_Canceled(InputAction.CallbackContext context)
{
JumpCancel();
}
private void Jump_Performed(InputAction.CallbackContext context)
{
JumpCancel();
}
private void Jump_Started(InputAction.CallbackContext context)
{
counter = Time.time;
if(jumpCount <= 1)
{
++jumpCount;
if(jumpCount == 1)
{
//Anim+Audio
animator.SetTrigger(animatorJumpTrigger);
}
else if(jumpCount == 2)
{
//Anim+Audio+Effect
animator.SetTrigger(animatorDoubleJumpTrigger);
}
}
else
{
return;
}
JumpInput = true;
}
private void OnCollisionEnter2D(Collision2D collision)
{
UpdateGrounding(collision, false);
}
private void OnCollisionStay2D(Collision2D collision)
{
UpdateGrounding(collision, false);
}
private void OnCollisionExit2D(Collision2D collision)
{
UpdateGrounding(collision, true);
}
#endregion
#region Others
public void FirstLanding()
{
}
#endregion
}
对于动画我们则要创建一个新的动画状态机名字就叫Jump StateMachine
为我们的Jump,Fall,Soft Land,Double Jump添加好动画
接着就是动画连线了。凡是到Jump和DoubleJump都只用Triiger来作为动画转化条件
回到Base状态机中,Walk,Run,Idle的动画到Jump状态机的动画暂时只有Jump和Fall,而且动画条件也都是一模一样的
除此之外我们还要为动画添加行为脚本,
由此我们先对部分创建好行为脚本。
这些里面大多都是添加音乐和粒子效果所以先不用管,但FallingBehavior则要进行修改
using UnityEngine;
public class FallingBehavior : StateMachineBehaviour
{
float lastPositionY;
float fallDistance;
CharacterController2D character;
private void Awake()
{
//audio
character = FindObjectOfType<CharacterController2D>();
}
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
fallDistance = 0;
animator.SetFloat("FallDistance", fallDistance);
//auido
}
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if(lastPositionY > character.transform.position.y)
{
fallDistance += lastPositionY - character.transform.position.y;
}
lastPositionY = character.transform.position.y;
animator.SetFloat("FallDistance", fallDistance);
}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
//audio
}
public void ResetAllParams()
{
lastPositionY = character.transform.position.y;
fallDistance = 0;
}
}
学习产出:
参数先随便设计,设计好后效果如图。