效果预览
踩死人物时会有烟雾上升
搭建场景
首先下载资源https://pan.baidu.com/s/1QhfsEbu7RD22rQhZAx6ysA
然后导入到unity中,搭建好场景(导入env和天空盒),添加人物,把人物放到空物体下,给人物添加角色控制器,设置好人物的胶囊碰撞体积
角色控制器带有部分的刚体功能,并且自带胶囊碰撞器
书写控制移动的脚本
接下来创建脚本,由于需要玩家和敌人,我们可以让玩家和敌人共同继承自一个character基类。
character.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Character : MonoBehaviour
{
protected CharacterController cc;
protected Animator animator;
public float runSpeed;
public float jumpPower=10;
Vector3 pendingVelocity;//包含了速度和方向
// Start is called before the first frame update
void Awake()
{
cc = GetComponent<CharacterController>();
animator = GetComponentInChildren<Animator>();
}
// Update is called once per frame
void Update()
{
//移动
pendingVelocity.z = 0f;
cc.Move(pendingVelocity * Time.deltaTime);
//模拟重力
pendingVelocity.y += cc.isGrounded ? 0f : Physics.gravity.y * 10f * Time.deltaTime;
}
public void Jump()
{
if (cc.isGrounded)
{
pendingVelocity.y = jumpPower;
}
}
public void Move(float inputX)
{
pendingVelocity.x = inputX * runSpeed;
}
public void Rotate(Vector3 lookDir,float turnSpeed)
{
}
public void Death()
{
}
public void TakeDamage(Character inflicter,int damage)//承受伤害
//inflicter是加害者,受害者是自己
{
}
public void AttackCheck()//攻击检测
{
}
public void GrabCheck()
{
}
}
playercharacter.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCharacter : Character
{
// Update is called once per frame
}
playercontroller.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
PlayerCharacter character;
// Start is called before the first frame update
void Start()
{
character = GetComponent<PlayerCharacter>();
}
// Update is called once per frame
void Update()
{
var h = Input.GetAxis("Horizontal");
character.Move(h);
if (h != 0)
{
var dir = Vector3.right * h;
character.Rotate(dir, 10);
}
if (Input.GetKeyDown(KeyCode.Space))
{
character.Jump();
}
if (Input.GetKeyDown(KeyCode.G))
{
character.GrabCheck();
}
}
}
以上脚本写完后 就可以实现移动和跳跃的效果了
这里需要注意一点:charactercontroller自带Move函数,参数是vec3向量,效果是提供速度,但并不会提供重力,因此想要实现重力的效果需要自己模拟:
private float gravity=10f;
//模拟重力
pendingVelocity.y -= cc.isGrounded ? 0f : gravity * 10f * Time.deltaTime;
动画效果
跳跃的动画效果:
当人物不处于地面时则播放跳跃的动画,当人物处于地面时,结束跳跃的动画进入地面的动画,因此在move和jump之间添加bool变量:grounded
move由blend tree组合而成:
需要注意 speed参数是自己添加的参数,范围为0到20,这里的20是设置好的最大速度:
动画状态间的切换:
然后还需要在update函数中更新speed和grounded的值即可:
void Update()
{
//移动
pendingVelocity.z = 0f;
cc.Move(pendingVelocity * Time.deltaTime);
animator.SetFloat("Speed", cc.velocity.magnitude);
animator.SetBool("Grounded", cc.isGrounded);
//更新重力
pendingVelocity.y -= cc.isGrounded ? 0f : gravity * 10f * Time.deltaTime;
}
跑步:
跳跃:
流畅的旋转
旋转需要在两个角度间进行球面插值
public void Rotate(Vector3 lookDir,float turnSpeed)
{
rotateComplete = false;
var targetPos = transform.position + lookDir;
var characterPos = transform.position;
//去除y轴影响
characterPos.y = 0;
targetPos.y = 0;
//角色面朝目标的向量
Vector3 faceToDir = targetPos - characterPos;
//角色面朝目标方向的四元数
Quaternion faceToQuat = Quaternion.LookRotation(faceToDir);
//球面插值
Quaternion slerp = Quaternion.Slerp(transform.rotation, faceToQuat, turnSpeed * Time.deltaTime);
if (slerp == faceToQuat)
{
rotateComplete = true;
}
transform.rotation = slerp;
}
相机跟随
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCamera : MonoBehaviour
{
public Transform target;
public float distance = 8.0f;
public float height=-1.0f;
// Start is called before the first frame update
// Update is called once per frame
void LateUpdate()
{
if (!target) return;
transform.position = target.position;
transform.position -= Vector3.forward * distance;
transform.position = new Vector3(transform.position.x, height, transform.position.z);
}
}
直接将这个脚本添加给相机,然后给相机添加上目标即可
踩踏攻击
public int damage = 100;
public int health = 100;
public GameObject deathFX;
void Update()
{
……
AttackCheck();
}
public void TakeDamage(Character inflicter,int damage)//承受伤害
//inflicter是加害者,受害者是自己
{
inflicter.Jump();
health -= damage;
if (health <= 0)
{
Death();
}
}
public void AttackCheck()//攻击检测
{
var dist = cc.height / 2;
RaycastHit hit;
if(Physics.Raycast(transform.position,Vector3.down,out hit, dist + 0.05f))
{
if (hit.transform.GetComponent<Character>() && hit.transform != transform)
{
hit.transform.GetComponent<Character>().TakeDamage(this, damage);
}
}
}
死亡时的粒子特效
创建一个材质(在project窗口中),设置属性
然后再创建一个粒子系统(在和人物那些所在的窗口中)
设置好一系列的属性:
在人物死亡时,产生一个粒子:
public void Death()
{
//fx:特效
var fx = Instantiate(deathFX, transform.position, Quaternion.LookRotation(new Vector3(0,1,0)));
//Destroy(fx, 2);
Destroy(gameObject);
}
抓取物体脚本
由于抓取只在player中,因此在playercharacter中实现grab函数:
PlayerCharacter.cs:
public Transform grabSocket;
public Transform grabObject;
public void GrabCheck()
{
if (grabObject != null && rotateComplete)
{
grabObject.transform.SetParent(null);
grabObject.GetComponent<Rigidbody>().isKinematic = false;
grabObject = null;
}
else
{
var dist = cc.radius + 1f;
RaycastHit hit;
if(Physics.Raycast(transform.position,transform.forward,out hit, dist))
{
if (hit.collider.CompareTag("GrabBox"))
{
grabObject = hit.transform;
grabObject.SetParent(grabSocket);
grabObject.localPosition = Vector3.zero;
grabObject.localRotation = Quaternion.identity;
grabObject.GetComponent<Rigidbody>().isKinematic = true;
}
}
}
}
抓取物体动画
抓取物体的动画需要单独在手上实现,因此我们可以新开一个动画层来实现
然后我们需要将基类中的update函数类型设置为protected型,然后在子类playerCharacter中修改动画控制器中的bool值,以及别忘了修改另外一个动画层的layer值:
private void Start()
{
animator.SetLayerWeight(armsAnimatorLayer, 1f);//除了基层以外,其他的层次初始权重默认是0,这里需要手动设置。
}
private void Update()
{
animator.SetBool("Grab", grabObject ? true : false);
base.Update();
}
查看动画的遮罩
另外,我们还可以点击这里
在这里可以查看动画,以及每个动画的遮罩信息:
完整代码
Character.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Character : MonoBehaviour
{
protected CharacterController cc;
protected Animator animator;
public float runSpeed=20;
public float jumpPower=20;
public int damage = 100;
public int health = 100;
public GameObject deathFX;
private float gravity = 10f;
Vector3 pendingVelocity;//包含了速度和方向
protected bool rotateComplete = true;
// Start is called before the first frame update
void Awake()
{
cc = GetComponent<CharacterController>();
animator = GetComponentInChildren<Animator>();
}
// Update is called once per frame
protected void Update()
{
//移动
pendingVelocity.z = 0f;
cc.Move(pendingVelocity * Time.deltaTime);
animator.SetFloat("Speed", cc.velocity.magnitude);
animator.SetBool("Grounded", cc.isGrounded);
//更新重力
pendingVelocity.y -= cc.isGrounded ? 0f : gravity * 10f * Time.deltaTime;
AttackCheck();
}
public void Jump()
{
if (cc.isGrounded)
{
pendingVelocity.y = jumpPower;
}
}
public void Move(float inputX)
{
pendingVelocity.x = inputX * runSpeed;
}
public void Rotate(Vector3 lookDir,float turnSpeed)
{
rotateComplete = false;
var targetPos = transform.position + lookDir;
var characterPos = transform.position;
//去除y轴影响
characterPos.y = 0;
targetPos.y = 0;
//角色面朝目标的向量
Vector3 faceToDir = targetPos - characterPos;
//角色面朝目标方向的四元数
Quaternion faceToQuat = Quaternion.LookRotation(faceToDir);
//球面插值
Quaternion slerp = Quaternion.Slerp(transform.rotation, faceToQuat, turnSpeed * Time.deltaTime);
if (slerp == faceToQuat)
{
rotateComplete = true;
}
transform.rotation = slerp;
}
public void Death()
{
//fx:特效
var fx = Instantiate(deathFX, transform.position, Quaternion.LookRotation(new Vector3(0,1,0)));
//Destroy(fx, 2);
Destroy(gameObject);
}
public void TakeDamage(Character inflicter,int damage)//承受伤害
//inflicter是加害者,受害者是自己
{
inflicter.Jump();
health -= damage;
if (health <= 0)
{
Death();
}
}
public void AttackCheck()//攻击检测
{
var dist = cc.height / 2;
RaycastHit hit;
if(Physics.Raycast(transform.position,Vector3.down,out hit, dist + 0.05f))
{
if (hit.transform.GetComponent<Character>() && hit.transform != transform)
{
hit.transform.GetComponent<Character>().TakeDamage(this, damage);
}
}
}
public void GrabCheck()
{
}
}
PlayerController.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
PlayerCharacter character;
// Start is called before the first frame update
void Start()
{
character = GetComponent<PlayerCharacter>();
}
// Update is called once per frame
void Update()
{
var h = Input.GetAxis("Horizontal");
character.Move(h);
if (h != 0)
{
var dir = Vector3.right * h;
character.Rotate(dir, 10);
}
if (Input.GetKeyDown(KeyCode.Space))
{
character.Jump();
}
if (Input.GetKeyDown(KeyCode.G))
{
character.GrabCheck();
}
}
}
PlayerCharacter:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCharacter : Character
{
public Transform grabSocket;
public Transform grabObject;
public int armsAnimatorLayer = 1;
private void Start()
{
animator.SetLayerWeight(armsAnimatorLayer, 1f);//除了基层以外,其他的层次初始权重默认是0,这里需要手动设置。
}
private void Update()
{
animator.SetBool("Grab", grabObject ? true : false);
base.Update();
}
// Update is called once per frame
public void GrabCheck()
{
if (grabObject != null && rotateComplete)
{
grabObject.transform.SetParent(null);
grabObject.GetComponent<Rigidbody>().isKinematic = false;
grabObject = null;
}
else
{
var dist = cc.radius + 1f;
RaycastHit hit;
if(Physics.Raycast(new Vector3(transform.position.x, transform.position.y+0.5f, transform.position.z), transform.forward,out hit, dist))
{
if (hit.collider.CompareTag("GrabBox"))
{
grabObject = hit.transform;
grabObject.SetParent(grabSocket);
grabObject.localPosition = Vector3.zero;
grabObject.localRotation = Quaternion.identity;
grabObject.GetComponent<Rigidbody>().isKinematic = true;
}
}
}
}
}
PlayerCamera:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCamera : MonoBehaviour
{
public Transform target;
public float distance = 8.0f;
public float height=-1.0f;
// Start is called before the first frame update
// Update is called once per frame
void LateUpdate()
{
if (!target) return;
transform.position = target.position;
transform.position -= Vector3.forward * distance;
transform.position = new Vector3(transform.position.x, height, transform.position.z);
}
}
AIController:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AIController : MonoBehaviour
{
Character character;
float lastCheckStateTime = 0;
float simulateInputX;
bool simulateJump;
// Start is called before the first frame update
void Start()
{
character = GetComponent<Character>();
}
// Update is called once per frame
void Update()
{
if (Time.time > lastCheckStateTime + 2)
{
lastCheckStateTime = Time.time;
//模拟敌人的输入
simulateInputX = Random.Range(-1f, 1f);
simulateJump = Random.Range(0, 2) == 1 ? true : false;
}
MoveControl(simulateInputX);
JumpControl(simulateJump);
}
void MoveControl(float inputX)
{
character.Move(inputX);
if (inputX != 0)
{
var dir = Vector3.right * inputX;
character.Rotate(dir, 10);
}
}
void JumpControl(bool jump)
{
if (jump)
{
character.Jump();
simulateJump = false;
}
}
}