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资源商店给我们提供了角色控制器的资源,可以进行使用