项目描述:
游戏描述:游戏中你将扮演玩家逃脱敌人的追捕,在城市中收集钥匙打开逃离的大门。
项目所用的资源与使用的工具:
主要使用unity2022,blender等工具。游戏大部分模型是来自unity资源商店,而里面一些人物骨骼动画则是自己用blender做的关键帧动画。(个人只能做一些比较简单的动画,所以不算太美观,如下图,可以看到添加的关键帧也不多)
游戏部分界面:
主界面及游戏内一些ui:
游戏实机演示:
游戏演示
部分脚本:
玩家控制脚本:主要是用CharacterController 组件来实现人物的移动跳跃等。为避免人物控制脚本代码太多太杂,角色的动画控制用了另一个AnimatorController脚本来控制,提高代码可读性,可维护性。
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
private AnimatorController animatorController;
private CharacterStats characterStats;
[Tooltip("角色控制器")] public CharacterController characterController;
private float horizontal;
private float vertical;
//[Header("移动")]
//[Tooltip("角色行走的速度")] public float walkSpeed = 6f;
[Tooltip("当前速度")] private float speed;
[Tooltip("角色移动的方向")] private Vector3 moveDirection;
private int maxHP;
private int currentHP;
private float jumpHeight;
private bool isJump;
private bool isGround;
public float sphereRadius = 2.0f; //检测地面半径
public Transform groundCheck; //检测地面位置
private float _verticalVelocity;
private float Gravity = -9.81f;
public bool isAttacked; //角色是否被攻击
public bool escape; //角色逃脱一次敌人攻击
private static int pressFNum=0;
public Text text;
public void Awake()
{
isAttacked = false;
escape = false; //刚开始没被攻击不视作逃脱
}
void Start()
{
text.enabled = false;
Debug.Log(gameObject.name);
//speed = walkSpeed;
characterStats = GetComponent<CharacterStats>();
animatorController = GetComponent<AnimatorController>();
speed = characterStats.Speed;
jumpHeight = characterStats.JumpHeight;
maxHP = characterStats.MaxHealth;
characterStats.CurrentHealth = maxHP;
currentHP= maxHP;
}
void Update()
{
if (isAttacked == false)
{
text.enabled = false;
escape = true;
animatorController.HadAttack(false);
pressFNum = 0;
PlayerMove();
isGround = IsGrounded();
SetJump();
}
else
{
escape = false;
animatorController.HadAttack(true);
HPChange();
}
Debug.Log("IsAttack: " + isAttacked);
}
void PlayerMove()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
moveDirection = transform.right * horizontal + transform.forward * vertical; // 计算移动方向
//将该向量从局部坐标系转换为世界坐标系,得到最终的移动方向,效果和上面的一样
// moveDirection = transform.TransformDirection(new Vector3(h, 0, v));
moveDirection = moveDirection.normalized; // 归一化移动方向,避免斜向移动速度过快
if (speed <= 10.0f && !Mathf.Approximately(horizontal, 0.0f) || !Mathf.Approximately(vertical, 0.0f))
{
animatorController.Walk(speed);
}
else animatorController.Standby(0f);
//animatorController.Walk(3.0f);
characterController.Move(moveDirection * Time.deltaTime * speed);
//characterController.Move(moveDirection * speed * Time.deltaTime + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
}
void SetJump()
{
bool jump = Input.GetKeyDown(KeyCode.Space);
if (isGround)
{
isJump = false;
//在角色着地时垂直速度无限下降
if (_verticalVelocity < 0.0f) _verticalVelocity = -2f;
if (jump)
{
Debug.Log("空格按下");
// H * -2 * G 的平方根 = 达到期望高度所需的速度
_verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * Gravity);
//animatorController.Jump(!isJump);
}
}
else
{
isJump = true;
//animatorController.Jump(false);
}
//垂直速度随时间减少
_verticalVelocity += 2 * Gravity * Time.deltaTime;
characterController.Move(new Vector3(0f,_verticalVelocity,0f)*Time.deltaTime);
//characterController.Move(moveDirection * speed * Time.deltaTime + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
}
bool IsGrounded()
{
Collider[] colliders = Physics.OverlapSphere(groundCheck.position, sphereRadius);
foreach (Collider collider in colliders)
{
Debug.Log("name:" + collider.gameObject.name);
if (collider.gameObject != gameObject && !IsChildOf(collider.transform, transform))//忽略角色和其子物体的碰撞体
return true;
}
return false;
}
//在场景视图显示检测,方便调试
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
//地面检测可视化
Gizmos.DrawWireSphere(groundCheck.position, sphereRadius);
}
//判断child 是否是parent子集
bool IsChildOf(Transform child, Transform parent)
{
while (child != null)
{
if (child == parent) return true;
child = child.parent;
}
return false;
}
public void HPChange()
{
text.enabled = true;
float startTime;
startTime = Time.time;
UIHealthyBar.instance.SetValue(currentHP/(float)maxHP);
if (Input.GetKeyDown(KeyCode.F))
{
pressFNum += 1;
Debug.Log("PressF: " + pressFNum);
}
if(pressFNum>=1)
{//若玩家挣脱敌人后自己受到一次伤害
currentHP -= 2; characterStats.CurrentHealth = currentHP;
isAttacked = false;
}
if (currentHP <=0)
{
SceneManager.LoadScene("GameOver");
}
}
}
而对于敌人的控制则不是让脚本继承MonoBehaviour,而是继承自StateMachineBehaviour,这样做有个好处,就是只需要把这些脚本挂载在animator controller上就行,当过度到该动画器的特定动画时,就会自动调用挂载在这些动画状态上的脚本。由于大部分敌人可能具有相同的逻辑,这样一来就不用频繁给敌人物体挂载脚本,而是在动画器上的各个动画挂载上对应脚本,然后就可以将该animator controller添加到敌人物体的animator上就行,便可以实现敌人的一些逻辑。当想要修改或者增添逻辑时直接找到该动画器就可以找到这些脚本功能,不必在去层级窗口找一个个对应的敌人物体,提高了代码的可维护性,避免了给gameobject频繁添加脚本时带来的错误。
敌人巡逻脚本:(用navigation的自动寻路导航实现敌人会在玩家附近一定距离内巡逻,当敌人距离玩家一定距离时,则设置过渡到追击动画,此时就会自动启用追击玩家的脚本)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class EnemyPatrolState : StateMachineBehaviour
{
float timer;
public float patrolingTime = 10f;
Transform player;
NavMeshAgent agent;
NavMeshAgent pg;
private Vector3 currentPosintion;
public float detectionArea = 18f;
public float patrolSpeed = 8f;
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
player = GameObject.FindGameObjectWithTag("Player").transform;
agent = animator.GetComponent<NavMeshAgent>();
pg = player.GetComponent<NavMeshAgent>();
agent.speed = patrolSpeed;
timer = 0;
currentPosintion = agent.destination;
Vector3 nextPosition = new Vector3(Random.Range(0f, 55.0f), 0, Random.Range(0f, 55.0f)) + pg.destination;
Debug.Log("nextposition: " + agent.name + ": " + nextPosition);
agent.SetDestination(nextPosition);
}
public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
timer += Time.deltaTime;
if (agent.remainingDistance<0.5f)
{
Vector3 nextPosition = new Vector3(Random.Range(0f, 55.0f), 0, Random.Range(0f, 55.0f)) +pg.destination;
//Debug.Log("nextposition: " + agent.name +": " + nextPosition);
agent.SetDestination(nextPosition);
timer = 0f;
}
float distanceFromPlayer = Vector3.Distance(player.position,animator.transform.position);
if(distanceFromPlayer < detectionArea)
{
animator.SetBool("isChasing", true);
}
}
}
项目所用的脚本: