unity給我們提供了多種角色移動的思路 先簡單整理下前三種
一、直接修改組件位置
最基礎的方式,是改變物體位置的最直接的方式
適用於既沒有物理系統,也對移動沒有特殊要求的情況
public float speed;
void Update()
{
Move();
}
void Move()
{
float MoveX = Input.GetAxisRaw("Horizontal");
float MoveZ = Input.GetAxisRaw("Vertical");
Vector3 dir = new Vector3(MoveX, 0, MoveZ).normalized;
//1、Translate
transform.Translate(dir * Time.deltaTime * speed);
//2、修改positon
//transform.position += dir * Time.deltaTime * speed;
//3、TransformPoint
//transform.TransformPoint(dir * Time.deltaTime * speed);
}
二、物理方法
物理方式通過對物體施加力,改變物體運動狀態的方式讓物體移動
其中最核心的組件就是Rigibody
簡單介紹Rigibody
參數 | 含義 | 功能 |
Mass | 質量 | 物體的質量(任意單位)。建議一個物體的質量不要與其他物體 相差100倍 |
Drag | 阻力 | 當受力移動時物體受到的空氣阻力。0表示沒有空氣阻力,極 大時使物體立即停止運動 |
Angular Drag | 角阻力 | 當受扭力旋轉時物體受到的空氣阻力。0表示沒有空氣阻力, 極大時使物體立即停止旋轉 |
Use Gravity | 使用重力 | 該物體是否受重力影響,若激活,則物體受重力影響 |
Is Kinematic | 是否是運動學 | 游戲對象是否遵循運動學物理定律,若激活,該物體不再受物理引擎驅動,而只能通過變換來操作。適用於模擬運動的平台或者模擬由鉸鏈關節連接的剛體 |
Interpolate | 插值 | 物體運動插值模式。當發現剛體運動時抖動,可以嘗試下面的 選項:None(無),不應用插值;Interpolate(內插值),基於上一幀變換來平滑本幀變換;Extrapolate(外插值),基於下一幀變換來 平滑本幀變換 |
Collision Detection | 碰撞檢測 | 碰撞檢測模式。用於避免高速物體穿過其他物體卻未觸發碰 撞。碰撞模式包括Discrete (不連續)、Continuous (連續)、 Continuous Dynamic (動態連續〉3種。其中,Discrete模式用來 檢測與場景中其他碰撞器或其他物體的碰撞;Continuous模式 用來檢測與動態碰撞器(剛體)的碰撞;Continuous Dynamic模 式用來檢測與連續模式和連續動態模式的物體的碰撞,適用於 高速物體 |
Constraints | 約束 | 對剛體運動的約束。其中,Freeze Position(凍結位置)表示剛體 在世界中沿所選軸的移動將無效,Freeze Rotation(凍結旋轉)表示剛體在世界中沿所選的X、Y、Z軸的旋轉將無效 |
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class RigibodyMove : MonoBehaviour
{
Rigidbody rb;
public float speed;
void Start()
{
rb = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update()
{
Move();
}
void Move()
{
float MoveX = Input.GetAxisRaw("Horizontal");
float MoveZ = Input.GetAxisRaw("Vertical");
Vector3 dir = new Vector3(MoveX, 0, MoveZ).normalized;
//1、AddForce
//rb.AddForce(dir * speed * Time.deltaTime);
//2、修改velocity
//rb.velocity += dir * speed * Time.deltaTime;
//3、MovePosition
rb.MovePosition(transform.position + dir * speed * Time.deltaTime);
}
}
三、角色控制器
unity提供了一個Character Controller組件,可以自動幫助我們構建碰撞和重力效果
但其中重力 只有調用SimpleMove方法移動會自動使用重力,使用Move方法移動則沒有重力,需要自己添加
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
public float speed;
public float gravity;
CharacterController cc;
bool isGrounded = false;
float fallSpeed = 0; //掉落速度 會隨着滯空時間不斷增加
// Start is called before the first frame update
void Start()
{
cc = GetComponent<CharacterController>();
}
// Update is called once per frame
void Update()
{
Movement();
}
void Movement()
{
//cc提供了isGrounded接口用於檢測是否落地
isGrounded = cc.isGrounded;
//不在地面時,下墜速度不斷增加
if (!isGrounded)
{
fallSpeed -= gravity;
}
else
{
if (fallSpeed < 0)
{
fallSpeed = 0;
}
}
//平移
float MoveX = Input.GetAxisRaw("Horizontal");
float MoveZ = Input.GetAxisRaw("Vertical");
//Vector3 dir = new Vector3(MoveX, 0, MoveZ).normalized;
//平移加上掉落
Vector3 v = (transform.forward * MoveZ + transform.right * MoveX).normalized * speed + transform.up * fallSpeed;
//1、simpleMove 帶重力的移動
//cc.SimpleMove(dir * speed * Time.deltaTime);
//2、Move 不帶重力 需要自己設置重力
cc.Move(v * Time.deltaTime);
}
}
四、拓展
4.1
事實上,一個完整的角色控制器需要考慮的問題有很多。像重力、跳躍、阻力、空中移動、 處理斜坡、處理台階、沖刺、蹲伏等等
我們需要考慮制作的游戲類型來選擇合適的方案
對比常用的兩種控制Character和RIgidBody,以下列出了其自帶的功能
CharacterController
- 處理斜坡
- 處理台階
- 不會卡牆
RigidBody
- 自帶重力
- 提供阻力
- 可以和物理對象交互
下面提供Brackeys大佬提供的較為完整的角色控制器代碼
首先是一個移動腳本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerController_Brackeys : MonoBehaviour
{
CharacterController cc;
public float speed = 12f;
public float gravity = -9.81f;
public float jumpHeight = 3f; //跳躍高度
public Transform groundCheck;
public float groundDistance = 0.4f;
public LayerMask groundMask;
Vector3 velocity;
bool isGrounded; //檢測是否在地面
void Start()
{
cc = GetComponent<CharacterController>();
}
// Update is called once per frame
void Update()
{
MoveMent();
}
void MoveMent()
{
isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);//檢測是否在地面
if(isGrounded && velocity.y < 0)
{
//防止在真正掉落到地面前 停止掉落
velocity.y = -2;
}
float MoveX = Input.GetAxisRaw("Horizontal");
float MoveZ = Input.GetAxisRaw("Vertical");
Vector3 dir = (transform.right * MoveX + transform.forward * MoveZ).normalized;
//平移
cc.Move(dir * speed * Time.deltaTime);
if (Input.GetButtonDown("Jump") && isGrounded)
{
//物理公式 v=sqrt(v*-2*g)
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
velocity.y += gravity * Time.deltaTime;
//再乘一個Time.deltaTime是由物理決定的 1/2gt^2
cc.Move(velocity * Time.deltaTime);
}
}
然后是一個視角控制器腳本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MouseLook : MonoBehaviour
{
public float mouseSensitivity = 100f;
public Transform playerBody;
float xRotation = 0f;
// Start is called before the first frame update
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
}
// Update is called once per frame
void Update()
{
FreeLook();
}
void FreeLook()
{
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, -90f, 90f);
//攝像頭旋轉x軸
transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
//角色旋轉y軸
playerBody.Rotate(Vector3.up * mouseX);
}
}
參數設置如下:
4.2
再提供一個更加全面的角色控制器,考慮了滑坡、高處掉下受傷、下落判斷、奔跑、兔子跳等(沒有包含視角控制)
// From: https://wiki.unity3d.com/index.php/FPSWalkerEnhanced
// Modified:
// 1. Namespace to prevent conflicts.
// 2. Only checks Horizontal and Vertical inputs.
namespace PixelCrushers.SceneStreamer.Example
{
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class FPSWalkerEnhanced : MonoBehaviour
{
[Tooltip("How fast the player moves when walking (default move speed).")]
[SerializeField]
private float m_WalkSpeed = 6.0f;
[Tooltip("How fast the player moves when running.")]
[SerializeField]
private float m_RunSpeed = 11.0f;
[Tooltip("If true, diagonal speed (when strafing + moving forward or back) can't exceed normal move speed; otherwise it's about 1.4 times faster.")]
[SerializeField]
public bool m_LimitDiagonalSpeed = true;
[Tooltip("If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down.")]
[SerializeField]
private bool m_ToggleRun = false;
//[Tooltip("How high the player jumps when hitting the jump button.")]
//[SerializeField]
//private float m_JumpSpeed = 8.0f;
[Tooltip("How fast the player falls when not standing on anything.")]
[SerializeField]
private float m_Gravity = 20.0f;
[Tooltip("Units that player can fall before a falling function is run. To disable, type \"infinity\" in the inspector.")]
[SerializeField]
private float m_FallingThreshold = 10.0f;
[Tooltip("If the player ends up on a slope which is at least the Slope Limit as set on the character controller, then he will slide down.")]
[SerializeField]
private bool m_SlideWhenOverSlopeLimit = false;
[Tooltip("If checked and the player is on an object tagged \"Slide\", he will slide down it regardless of the slope limit.")]
[SerializeField]
private bool m_SlideOnTaggedObjects = false;
[Tooltip("How fast the player slides when on slopes as defined above.")]
[SerializeField]
private float m_SlideSpeed = 12.0f;
[Tooltip("If checked, then the player can change direction while in the air.")]
[SerializeField]
private bool m_AirControl = false;
[Tooltip("Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast.")]
[SerializeField]
private float m_AntiBumpFactor = .75f;
//[Tooltip("Player must be grounded for at least this many physics frames before being able to jump again; set to 0 to allow bunny hopping.")]
//[SerializeField]
//private int m_AntiBunnyHopFactor = 1;
private Vector3 m_MoveDirection = Vector3.zero;
private bool m_Grounded = false;
private CharacterController m_Controller;
private Transform m_Transform;
private float m_Speed;
private RaycastHit m_Hit;
private float m_FallStartLevel;
private bool m_Falling;
private float m_SlideLimit;
private float m_RayDistance;
private Vector3 m_ContactPoint;
private bool m_PlayerControl = false;
//private int m_JumpTimer;
private void Start()
{
// Saving component references to improve performance.
m_Transform = GetComponent<Transform>();
m_Controller = GetComponent<CharacterController>();
// Setting initial values.
m_Speed = m_WalkSpeed;
m_RayDistance = m_Controller.height * .5f + m_Controller.radius;
m_SlideLimit = m_Controller.slopeLimit - .1f;
//m_JumpTimer = m_AntiBunnyHopFactor;
}
//private void Update()
//{
// // If the run button is set to toggle, then switch between walk/run speed. (We use Update for this...
// // FixedUpdate is a poor place to use GetButtonDown, since it doesn't necessarily run every frame and can miss the event)
// if (m_ToggleRun && m_Grounded && Input.GetButtonDown("Run"))
// {
// m_Speed = (m_Speed == m_WalkSpeed ? m_RunSpeed : m_WalkSpeed);
// }
//}
private void FixedUpdate()
{
float inputX = Input.GetAxis("Horizontal");
float inputY = Input.GetAxis("Vertical");
// If both horizontal and vertical are used simultaneously, limit speed (if allowed), so the total doesn't exceed normal move speed
float inputModifyFactor = (inputX != 0.0f && inputY != 0.0f && m_LimitDiagonalSpeed) ? .7071f : 1.0f;
if (m_Grounded)
{
bool sliding = false;
// See if surface immediately below should be slid down. We use this normally rather than a ControllerColliderHit point,
// because that interferes with step climbing amongst other annoyances
if (Physics.Raycast(m_Transform.position, -Vector3.up, out m_Hit, m_RayDistance))
{
if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
{
sliding = true;
}
}
// However, just raycasting straight down from the center can fail when on steep slopes
// So if the above raycast didn't catch anything, raycast down from the stored ControllerColliderHit point instead
else
{
Physics.Raycast(m_ContactPoint + Vector3.up, -Vector3.up, out m_Hit);
if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
{
sliding = true;
}
}
// If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine
if (m_Falling)
{
m_Falling = false;
if (m_Transform.position.y < m_FallStartLevel - m_FallingThreshold)
{
OnFell(m_FallStartLevel - m_Transform.position.y);
}
}
// If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down
if (!m_ToggleRun)
{
m_Speed = Input.GetKey(KeyCode.LeftShift) ? m_RunSpeed : m_WalkSpeed;
}
// If sliding (and it's allowed), or if we're on an object tagged "Slide", get a vector pointing down the slope we're on
if ((sliding && m_SlideWhenOverSlopeLimit) || (m_SlideOnTaggedObjects && m_Hit.collider.tag == "Slide"))
{
Vector3 hitNormal = m_Hit.normal;
m_MoveDirection = new Vector3(hitNormal.x, -hitNormal.y, hitNormal.z);
Vector3.OrthoNormalize(ref hitNormal, ref m_MoveDirection);
m_MoveDirection *= m_SlideSpeed;
m_PlayerControl = false;
}
// Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines
else
{
m_MoveDirection = new Vector3(inputX * inputModifyFactor, -m_AntiBumpFactor, inputY * inputModifyFactor);
m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection) * m_Speed;
m_PlayerControl = true;
}
// Jump! But only if the jump button has been released and player has been grounded for a given number of frames
//if (!Input.GetButton("Jump"))
//{
// m_JumpTimer++;
//}
//else if (m_JumpTimer >= m_AntiBunnyHopFactor)
//{
// m_MoveDirection.y = m_JumpSpeed;
// m_JumpTimer = 0;
//}
}
else
{
// If we stepped over a cliff or something, set the height at which we started falling
if (!m_Falling)
{
m_Falling = true;
m_FallStartLevel = m_Transform.position.y;
}
// If air control is allowed, check movement but don't touch the y component
if (m_AirControl && m_PlayerControl)
{
m_MoveDirection.x = inputX * m_Speed * inputModifyFactor;
m_MoveDirection.z = inputY * m_Speed * inputModifyFactor;
m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection);
}
}
// Apply gravity
m_MoveDirection.y -= m_Gravity * Time.deltaTime;
// Move the controller, and set grounded true or false depending on whether we're standing on something
m_Grounded = (m_Controller.Move(m_MoveDirection * Time.deltaTime) & CollisionFlags.Below) != 0;
}
// Store point that we're in contact with for use in FixedUpdate if needed
private void OnControllerColliderHit(ControllerColliderHit hit)
{
m_ContactPoint = hit.point;
}
// This is the place to apply things like fall damage. You can give the player hitpoints and remove some
// of them based on the distance fallen, play sound effects, etc.
private void OnFell(float fallDistance)
{
print("Ouch! Fell " + fallDistance + " units!");
}
}
}
此外unity資源商店給我們提供了角色控制器的資源,可以進行使用