Unity从零开始制作空洞骑士①制作人物的移动跳跃转向以及初始的动画制作
Posted dangoxiba
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity从零开始制作空洞骑士①制作人物的移动跳跃转向以及初始的动画制作相关的知识,希望对你有一定的参考价值。
事情的起因:
首先我之前在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;
学习产出:
参数先随便设计,设计好后效果如图。
unity虚拟摇杆
参考技术A 在手机游戏中,虚拟摇杆游戏中的虚拟遥感很常见,先根据自己已经会虚拟摇杆来制作,虚拟摇杆醉主要的核心是C#部分
2D虚拟摇杆
先弄个自定义的触发事件,然后给每一个需要触发的事件按钮添加,EventTrigget,需要注意的是这个事件要继承unity的接口,
制作unity的摇杆的话,需要继承unity的的UI接口 IBeginDragHandler, IDragHandler, IEndDragHandler,IPointerClickHandler
首先先定义四个委托
public Action<GameObject, PointerEventData> onBeginDrag;
public Action<GameObject, PointerEventData> onDrag;
public Action<GameObject, PointerEventData> onEndDrag;
public Action<GameObject, PointerEventData> onClick;
其次是重写unity继承的四个接口,把每个委托的回调放进去这些接口函数里面,一但满足条件,委托会执行函数回调方法
public void OnBeginDrag(PointerEventData eventData)
if (onBeginDrag != null)
onBeginDrag(gameObject, eventData);
public void OnDrag(PointerEventData eventData)
if (onDrag != null)
onDrag(gameObject, eventData);
public void OnEndDrag(PointerEventData eventData)
if (onEndDrag != null)
onEndDrag(gameObject, eventData);
public void OnPointerClick(PointerEventData eventData)
if (onClick != null)
onClick(gameObject, eventData);
有了这些委托方法之后,如何使用,这个调用方法,会在初始化注册事件,
public static EventTrigger GetEventCallBack(GameObject obj)
Event trigger = obj.GetComponent<EventTrigger>();
if (trigger == null)
trigger = obj.AddComponent<EventTrigger>();
return trigger;
创建以一个控制虚拟摇杆StickJoyUI脚本,注册委托事件
TriggerEvent.Get(bgObj).onBeginDrag = OnDragBegin;
TriggerEvent.Get(bgObj).onDrag = OnDrag;
TriggerEvent.Get(bgObj).onEndDrag = OnDragEnd;
TriggerEvent.Get(transform.Find("changeAttack").gameObject).onClick = OnChangeAttack;
TriggerEvent.Get(transform.Find("AttackBtn").gameObject).onClick = OnAttackBtn;
再来看看虚拟摇杆部分OnDragBegin OnDrag OnDragEnd 这个三个方法控制虚拟摇杆,
private void OnDragBegin(GameObject obj, PointerEventData evetData)
private void OnDrag(GameObject obj, PointerEventData evetData)
Vector2 point;
//ScreenPointToLocalPointInRectangle("需要转换的对象的父级的RectTrasform","鼠标的位置","当前的摄像机","转换后的ui的相对坐标")
bg为要拖拽的对象的父级对象,里面还有一个小圆PointTf,鼠标的位置取拖拽事件的的position,鼠标的位置取拖拽事件的的摄像机,转
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(bgTf, evetData.position, evetData.pressEventCamera, out point))
//拖动的方向
Vector2 v = (point - Vector2.zero).normalized;
x = v.x;
y = v.y;
if (Vector2.Distance(point, Vector2.zero) <= R)
pointTf.anchoredPosition = point;
else //超出移动的半径
//位置 = 初始位置 + 方向 * 距离
pointTf.anchoredPosition = Vector2.zero + v * R;
private void OnDragEnd(GameObject obj, PointerEventData evetData)
pointTf.anchoredPosition = Vector2.zero;
x = 0;
y = 0;
Ps:2D角色还需要注意人物朝向问题
只需要把里面的的X值和Y值赋予给他实时更新,然后在写一个Flip方法判断朝向问题,本质是修改一下缩放的X改成相反的值就好了
//朝向
void Flip()
if (x > 0)
transform.localScale = new Vector3(1, 1, 1);
else if (x < 0)
transform.localScale = new Vector3(-1, 1, 1);
3D模式:把把X值和Y赋予个移动函数的x值和z值即可
接下用JS实现虚拟摇杆(思路跟上面差不多,但是数据的处理稍有差别而已)
首先引入玩家模块
var Player = require("player");
属性定义
cc.Class(
extends: cc.Component,
properties:
stickNode:
default:null,
type:cc.Node
,
fillNode:
default:null,
type:cc.Node
,
skillBtn:
default:null,
type:cc.Node
,
player:
default:null,
type:Player
,
R:50,
stick_x:0,
stick_y:0
,
在初始化函数Onlade中注册事件
onLoad ()
this.stickNode = cc.find("Stick",this.node);
this.fillNode = cc.find("Stick/fill",this.node);
this.skillBtn = cc.find("skillBtn",this.node);
//注册开始拖拽,拖拽事件,拖拽结束事件,和点击事件,这个更上面的四个接口类似
this.stickNode.on(cc.Node.EventType.TOUCH_START,this.onTouchStart,this);
this.stickNode.on(cc.Node.EventType.TOUCH_MOVE,this.onTouchMove,this);
this.stickNode.on(cc.Node.EventType.TOUCH_END,this.onTouchEnd,this);
this.stickNode.on(cc.Node.EventType.TOUCH_CANCEL,this.onTouchCancel,this);
this.skillBtn.on("click",this.onClickSkillBtn,this);
,
onTouchStart(event)
,
onTouchMove(event)
var pos = event.getLocation();//v2类型 坐标系
//当前这个鼠标的世界坐标转换成当前节点的相对坐标
pos = this.stickNode.convertToNodeSpaceAR(pos);
if(pos.magSqr()<=this.R*this.R)
this.fillNode.setPosition(pos);
else
this.fillNode.x = pos.normalizeSelf().x*this.R;
this.fillNode.y = pos.normalizeSelf().y*this.R;
this.stick_x = pos.normalizeSelf().x;
this.stick_y = pos.normalizeSelf().y;
,
onTouchEnd(event)
this.fillNode.x = 0;
this.fillNode.y = 0;
this.stick_x = 0;
this.stick_y = 0;
,
onTouchCancel(event)
this.fillNode.x = 0;
this.fillNode.y = 0;
this.stick_x = 0;
this.stick_y = 0;
,
onClickSkillBtn()
//执行玩家脚本使用技能的方法
this.player.playSkill();
PS:计算上面的pos的方向, 上面为啥半径平方而不是把开方呢??,开方消耗性能,给半径平方两者比较也能比较,相对与前者开方,后者半径平方更加节省性能,采用迂回思路,感觉这种思想有很多地方都用到,就比如前面的浮点数处理,既然有误差,那么我可以放大倍数,然后要使用时候,我再缩小倍数。
以上是关于Unity从零开始制作空洞骑士①制作人物的移动跳跃转向以及初始的动画制作的主要内容,如果未能解决你的问题,请参考以下文章