介绍
角色动作方面包括行走、奔跑、跳跃、武器切换、弹夹更换、武器展示、弹壳抛出效果、射击效果、全自动与半自动射击效果、瞄准效果、后坐力效果、弹痕效果等多种动作。
非玩家角色(NPC)具备多个动画状态,包括固定路径巡逻、行走、奔跑、寻路攻击等多种行为。
太空地图中拥有滑动门、激光枪、钥匙、传送门、电脑设备等多种功能性组件。
玩家可手持手枪或步枪,与NPC交火,取得钥匙,然后前往传送门以完成任务。在此过程中,游戏中丰富的特效将会呈现,包括玩家受伤特效、射击效果、钥匙获得特效、场景加载特效以及场景结束特效。
鼠标移动控制视角
- 声明了鼠标灵敏度、玩家位置和摄像机旋转角度的变量。
- 在游戏开始时,锁定光标在窗口中心并隐藏。
- 在每帧更新中,根据鼠标移动量和灵敏度计算摄像机的旋转。
- 限制摄像机旋转在垂直方向上的角度,避免过度旋转。
- 将计算出的旋转应用于摄像机和玩家位置,实现视角控制。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 摄像机的旋转
/// 玩家左右旋转控制视线左右移动
/// 摄像机上下旋转控制视线上下移动
/// </summary>
public class MouseLook : MonoBehaviour
{
public float mouseSensitivity=100f;//鼠标灵敏度
public Transform playerBody;//玩家的位置
private 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()
{
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);//限制轴值的累计(这里就能发现上90度和下90度角正好相对于了90的轴值)
transform.localRotation = Quaternion.Euler(xRotation, 0f,0f);
playerBody.Rotate(Vector3.up * mouseX);
}
}
行走、奔跑、跳跃、下蹲
这脚本控制玩家的基本动作和声音效果:
-
变量和属性:
- 定义了变量和属性,如移动速度、跳跃力度、重力、地面检测等。
-
移动和跳跃:
- 使用
Moveing()
方法实现玩家移动,包括获取移动方向、用CharacterController.Move()
移动、模拟跳跃和在空中减小高度。
- 使用
-
下蹲:
- 通过
Crouch()
方法修改CharacterController
的高度实现下蹲。
- 通过
-
声音效果:
- 使用
PlayFootStepSound()
方法播放移动音效,根据行走和奔跑状态播放不同音效。
- 使用
-
其他功能:
- 检测是否在斜坡上,施加额外力量;检测是否在地面上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 控制玩家移动
/// </summary>
public class PlayerMovement : MonoBehaviour
{
private CharacterController characterController;
public float walkSpeed = 10f;// 移动速度
public float runSpeed=15f;//奔跑速度
public float jumpForce = 3f;//跳跃力度
private Vector3 velocity;//设置玩家Y轴的一个冲量变化
private Vector3 moveDirction; //设置移动方向
public float gravity = -9f; //设置重力
public Transform groundCheck;//地面检测物体
private float groundDistance = 0.4f;//与地面的距离
public LayerMask groundMask;
private bool isJump;//判断是否在跳跃
private bool isGround;//判断是否在地面上
public bool isWalk;//判断是否在行走
public bool isRun;//判断是否在奔跑
private bool isCrouch;//判断是否蹲下
[SerializeField] private float slopeForce=6.0f; //走斜坡施加的力(是一个乘量)
[SerializeField] private float slopeForceRayLength=2.0f; //斜坡射线长度(自定义量)
[Header("键位设置")]
[SerializeField] [Tooltip("跳跃按键")] private string jumpInputName = "Jump";
[SerializeField] [Tooltip("奔跑按键")] private KeyCode runInputName;
[SerializeField] [Tooltip("下蹲按键")] private KeyCode crouchInputName;
private AudioSource audioSource;
public AudioClip walkingSound;
public AudioClip runingSound;
void Start()
{
characterController = GetComponent<CharacterController>();
audioSource = GetComponent<AudioSource>();
audioSource.clip = walkingSound;
audioSource.loop = true;
}
// Update is called once per frame
void Update()
{
CheckGround();
Moveing();
Crouch();
}
/// <summary>
/// 判断是否在斜坡上
/// </summary>
/// <returns></returns>
public bool OnSlope()
{
if (isJump)
return false;
RaycastHit hit;
//向下打出射线(检查是否在斜坡上)
if (Physics.Raycast(transform.position, Vector3.down, out hit, characterController.height / 2 * slopeForceRayLength))
{
//如果触碰到的点的法线,不是在(0,1,0)这个方向上的,那么就人物处在斜坡上
if (hit.normal != Vector3.up)
{
return true;
}
}
return false;
}
/// <summary>
/// 地面检测
/// </summary>
public void CheckGround() {
//在 groundCheck 位置上做一个球体检测判断是否处在地面上
isGround = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);
//如果处在地面上,重力设置成一个固定值
if (isGround && velocity.y < 0)
{
velocity.y = -2f;
}
}
/// <summary>
/// 跳跃
/// </summary>
public void Jump() {
isJump = Input.GetButtonDown(jumpInputName);
//施加跳跃的力
if (isJump && isGround)
{
velocity.y = Mathf.Sqrt(jumpForce * -2f * gravity);
//velocity.y = 20f;
}
}
/// <summary>
/// 下蹲
/// </summary>
public void Crouch() {
isCrouch = Input.GetKey(crouchInputName);
if (isCrouch)
{
characterController.height = 1f;
}
else
{
characterController.height =1.8f;
}
}
/// <summary>
/// 移动
/// </summary>
public void Moveing()
{
float speed;
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
isRun = Input.GetKey(runInputName);
isWalk = (Mathf.Abs(h) > 0 || Mathf.Abs(v) > 0) ? true : false;
speed = isRun ? runSpeed : walkSpeed; //设置行走或奔跑的速度
moveDirction = (transform.right * h + transform.forward * v).normalized;//设置玩家移动方向(将移动速度进行规范化,防止斜向走速度变大)
characterController.Move(moveDirction * speed * Time.deltaTime);//移动
velocity.y += gravity * Time.deltaTime;//不在地面上(空中,累加重力值)
characterController.Move(velocity * Time.deltaTime); //施加重力
Jump();
//如果处在斜面上移动
if (OnSlope())
{
//向下增加力量
characterController.Move(Vector3.down * characterController.height / 2 * slopeForce * Time.deltaTime);
}
PlayFootStepSound();
}
///播放移动的音效
public void PlayFootStepSound() {
if (isGround && moveDirction.sqrMagnitude > 0.9f)
{
audioSource.clip = isRun ? runingSound : walkingSound;//设置行走或者奔跑的音效
if (!audioSource.isPlaying)
{
audioSource.Play();
}
}
else
{
if (audioSource.isPlaying)
{
audioSource.Pause();
}
}
}
}
射击、后坐力、射速、瞄准、弹痕、枪火、抛壳
武器射击:控制射击位置、射程、子弹数量、射速、射击精度等参数,并播放相关音效、动画和特效。
武器装弹:控制子弹数量、弹匣容量、备弹数量、自动装弹、手动装弹等功能,并播放相关音效、动画和UI界面的更新。
武器切换:控制游戏中玩家切换主武器、副武器、全自动和半自动射击模式的功能,并更新UI界面的状态。
武器精度:根据射击模式(全自动或半自动)控制射击精度,实现游戏中的真实射击体验。
界面更新:控制UI界面的更新,包括弹匣数量、备弹数量、射击模式、准星等界面元素的实时更新。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 武器射击
/// </summary>
public class WeaponController : MonoBehaviour
{
public PlayerMovement PM;
public Transform shooterPoint; // 射击的位置(枪口)
public int range = 100; // 武器射程
public int bulletsMag = 31; // 一个弹匣数量
public int bulletLeft = 300; // 备弹
public int currentBullets; // 当前子弹数
public ParticleSystem muzzleFlash; // 枪口火焰特效
public GameObject hitParticles; // 子弹击中粒子特效
public GameObject bulletHole; // 弹孔
public Light muzzleFlashLight; // 枪口火焰灯光
public float fireRate = 0.1f; // 射速
public float bulletdamage = 10.0f; // 单发子弹伤害
private float fireTimer; // 计时器
private float SpreadFactor; // 射击的一点偏移量
[Header("键位设置")]
[SerializeField] [Tooltip("填装子弹按键")] private KeyCode reloadInputName;
[SerializeField] [Tooltip("查看武器按键")] private KeyCode inspectInputName;
[SerializeField] [Tooltip("主武器按键")] private KeyCode AutoRifleKey;
[SerializeField] [Tooltip("副武器按键")] private KeyCode HandGunKey;
[SerializeField] [Tooltip("自动半自动切换按键")] private KeyCode GunShootModelInputName;
private Animator anim;
/*音效参数*/
private AudioSource audioSource;
public AudioClip AK47ShoundClip; /*枪声音效片段*/
public AudioClip reloadAmmoLeftClip; // 换子弹1音效片段
public AudioClip reloadOutOFAmmoClip; // 换子弹2音效片段(拉枪栓)
private bool isReloading; // 判断是否在装弹
private bool isAiming; // 判断是否在瞄准
public Transform casingSpawnPoint; // 子弹壳抛出的位置
public Transform casingPrefab; // 子弹壳预制体
private Camera mainCamera;
public ChooseGunController CGC; // 声明切换武器类的实例
/*使用枚举区分全自动和半自动模式*/
public enum ShootMode { AutoRifle, SemiGun };
public ShootMode shootingMode;
private bool GunShootInput; // 根据全自动和半自动 射击的键位输入发生改变
private int modeNum = 1; // 模式切换的一个中间参数(1:全自动模式,2:半自动模式)
private string shootModelName;
/*UI的设置*/
public Image crossHairUI;
public Text ammoTextUI;
public Text ShootModelTextUI;
public Transform mainCameraTransform;
public float recoilAmount = 2.0f; // 后坐力的强度
public float recoilRecoverySpeed = 5.0f; // 后坐力恢复速度
private Vector3 originalCameraPosition; // 原始相机位置,用于恢复后坐力
private Quaternion originalCameraRotation;
private void Start()
{
// 获取标签为"maincamera"的相机的Transform组件
GameObject mainCameraObject = GameObject.FindGameObjectWithTag("MainCamera");
if (mainCameraObject != null)
{
mainCameraTransform = mainCameraObject.transform;
originalCameraPosition = mainCameraTransform.localPosition; // 记录原始相机位置
originalCameraRotation = mainCameraTransform.localRotation;
}
audioSource = GetComponent<AudioSource>();
anim = GetComponent<Animator>();
currentBullets = bulletsMag;
mainCamera = Camera.main;
shootingMode = ShootMode.AutoRifle; // AK47步枪默认是全自动模式
shootModelName = "AK47";
UpdateAmmoUI();
}
private void Update()
{
if (health.isDead)
{
anim.SetBool("Run", false);
anim.SetBool("Walk", false);
anim.SetBool("Aim", false);
return;
}
// 切换模式(全自动和半自动)
if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 1)
{
modeNum = 1;
shootModelName = "全自动";
shootingMode = ShootMode.AutoRifle;
ShootModelTextUI.text = shootModelName;
}
else if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 0)
{
modeNum = 0;
shootModelName = "半自动";
shootingMode = ShootMode.SemiGun;
ShootModelTextUI.text = shootModelName;
}
/*控制射击模式的转换 后面就要用代码去动态控制了*/
switch (shootingMode)
{
case ShootMode.AutoRifle:
GunShootInput = Input.GetMouseButton(0);
fireRate = 0.1f;
break;
case ShootMode.SemiGun:
GunShootInput = Input.GetMouseButtonDown(0);
fireRate = 0.2f;
break;
}
if (GunShootInput && currentBullets > 0)
{
GunFire();
}
else
{
muzzleFlashLight.enabled = false;
}
// 计时器加时间
if (fireTimer < fireRate)
{
fireTimer += Time.deltaTime;
}
anim.SetBool("Run", PM.isRun); // 播放跑步动画
anim.SetBool("Walk", PM.isWalk);
// 获取动画状态机第一层动画的状态
AnimatorStateInfo info = anim.GetCurrentAnimatorStateInfo(0);
// 两种换子弹的东湖
if (info.IsName("reload_ammo_left") || info.IsName("reload_out_of_ammo"))
{
isReloading = true;
}
else
{
isReloading = false;
}
if (Input.GetKeyDown(reloadInputName) && currentBullets < bulletsMag && bulletLeft > 0)
{
Reload();
}
SpreadFactor = (isAiming) ? 0f : 0.01f;
DoingAim();
if (Input.GetKeyDown(inspectInputName))
{
anim.SetTrigger("Inspect");
}
// 切换主武器(自动步枪)
if (Input.GetKeyDown(AutoRifleKey))
{
CGC.ChangeWeapon(0);
}
// 切换副武器(手枪)
if (Input.GetKeyDown(HandGunKey))
{
CGC.ChangeWeapon(1);
}
if (mainCameraTransform != null)
{
mainCameraTransform.localPosition = Vector3.Lerp(mainCameraTransform.localPosition, originalCameraPosition, Time.deltaTime * recoilRecoverySpeed);
}
}
// 更新UI
public void UpdateAmmoUI()
{
ammoTextUI.text = currentBullets + " / " + bulletLeft;
ShootModelTextUI.text = shootModelName;
}
/// <summary>
/// 瞄准的逻辑
/// </summary>
public void DoingAim()
{
if (Input.GetMouseButton(1) && !isReloading && !PM.isRun)
{
isAiming = true;
anim.SetBool("Aim", isAiming);
crossHairUI.gameObject.SetActive(false);
mainCamera.fieldOfView = 25; // 瞄准的时候摄像机视野变小
}
else
{
isAiming = false;
anim.SetBool("Aim", isAiming);
crossHairUI.gameObject.SetActive(true);
mainCamera.fieldOfView = 60; // 瞄准的时候摄像机视野恢复
}
}
/// <summary>
/// 射击逻辑
/// </summary>
public void GunFire()
{
// 控制射速,当前弹夹打光了,正在装子弹,正在奔跑 就不可以发射了
if (fireTimer < fireRate || currentBullets <= 0 || isReloading || PM.isRun) return;
RaycastHit hit;
Vector3 shootDirection = shooterPoint.forward;
// 改成这个,shootDirection shooterPoint这个游戏物体进行小的偏移(TransformDirection 将local坐标转换为世界坐标)
shootDirection = shootDirection + shooterPoint.TransformDirection(new Vector3(Random.Range(-SpreadFactor, SpreadFactor), Random.Range(-SpreadFactor, SpreadFactor)));
if (Physics.Raycast(shooterPoint.position, shootDirection, out hit, range))
{
Debug.Log(hit.transform.name + "打到了");
if (hit.transform.CompareTag("zombie"))
{
zombiehealth zombieHealth = hit.transform.GetComponent<zombiehealth>();
if (zombieHealth != null)
{
zombieHealth.TakeDamage(10f);
}
GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
Destroy(hitParticleEffect, 1f);
}
else
{
GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
Destroy(hitParticleEffect, 1f);
GameObject bulletHoleEffect = Instantiate(bulletHole, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
Destroy(bulletHoleEffect, 0.8f);
}
if (mainCameraTransform != null)
{
Vector3 recoilVector = Vector3.up * 0.3f;
mainCameraTransform.localPosition += recoilVector;
}
}
if (!isAiming)
{
anim.CrossFadeInFixedTime("fire", 0.1f); // 播放普通开火动画(使用动画的淡出淡入效果)
}
else
{
// 瞄准状态下,播放瞄准的开火动画
anim.Play("aim_fire", 0, 0f);
}
muzzleFlash.Play(); // 播放火光特效
muzzleFlashLight.enabled = true;
PlayerShootSound(); // 播放射击音效
// 实例抛弹壳
Instantiate(casingPrefab, casingSpawnPoint.transform.position, casingSpawnPoint.transform.rotation);
currentBullets--;
UpdateAmmoUI();
fireTimer = 0f; // 重置计时器
}
public void PlayerShootSound()
{
audioSource.clip = AK47ShoundClip;
audioSource.Play();
}
/// <summary>
/// 填装弹药逻辑
/// </summary>
public void Reload()
{
if (bulletLeft <= 0) return;
DoReloadAnimation();
// 计算需要填充的子弹
int bulletToLoad = bulletsMag - currentBullets;
// 计算备弹扣除的子弹
int bulletToReduce = (bulletLeft >= bulletToLoad) ? bulletToLoad : bulletLeft;
bulletLeft -= bulletToReduce; // 备弹减少
currentBullets += bulletToReduce; // 当前子弹增加
UpdateAmmoUI();
}
// 播放装弹动画
public void DoReloadAnimation()
{
if (currentBullets > 0)
{
anim.Play("reload_ammo_left", 0, 0);
audioSource.clip = reloadAmmoLeftClip;
audioSource.Play();
}
if (currentBullets == 0)
{
anim.Play("reload_out_of_ammo", 0, 0);
audioSource.clip = reloadOutOFAmmoClip;
audioSource.Play();
}
}
}
手臂摇摆
这个脚本通过监听鼠标输入,在武器手臂模型的位置上添加摇摆效果,增加了武器的真实感和动态性。实现了武器摇摆效果,具体包括:
-
摇摆参数:定义了摇摆的幅度(amout)、平滑值(smoothAmout)和最大幅度(maxAmout)。
-
初始位置:记录武器初始位置。
-
Start
函数:在开始时获取武器的初始位置。 -
Update
函数:每帧更新,根据鼠标输入控制武器的手臂模型位置。 -
摇摆计算:根据鼠标的移动输入,计算出摇摆的偏移量。
-
限制范围:限制摇摆的偏移量在一定范围内。
-
位置更新:根据摇摆的偏移量和初始位置,平滑地更新武器的位置,实现摇摆效果。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 武器摇摆
/// </summary>
public class WeaponSway : MonoBehaviour
{
/*摇摆的参数*/
public float amout; //摇摆幅度
public float smoothAmout;//一个平滑值
public float maxAmout;//最大摇摆幅度
private Vector3 originPostion; //初始位置
// Start is called before the first frame update
void Start()
{
//自身位置(相对于父级物体变换得位置)
originPostion = transform.localPosition;
}
// Update is called once per frame
void Update()
{
//设置武器手臂模型位置得值,
float movementX = -Input.GetAxis("Mouse X") * amout;
float movementY = -Input.GetAxis("Mouse Y") * amout;
//限制
movementX = Mathf.Clamp(movementX, -maxAmout, maxAmout);
movementY = Mathf.Clamp(movementY, -maxAmout, maxAmout);
Vector3 finallyPostion = new Vector3(movementX, movementY, 0);
//手柄位置变换
transform.localPosition = Vector3.Lerp(transform.localPosition, finallyPostion + originPostion, Time.deltaTime * smoothAmout);
}
}
手枪
这脚本实现了一个武器射击系统,包括:
- 武器属性:定义射程、弹匣容量等武器属性。
- 射击特效:创建火焰、粒子和弹孔特效。
- 射击模式:区分全自动和半自动射击,设置键位。
- 音效管理:播放射击和换弹音效。
- 瞄准与UI:按鼠标右键瞄准,显示准心和弹药数量。
- 射击逻辑:根据模式、键位和弹药数量触发射击。
- 装弹逻辑:播放动画、音效并填装弹药。
- 后坐力效果:模拟枪口抖动。
- 武器切换:按键切换主副武器。
- 更新UI:显示子弹数和射击模式。
- 瞄准逻辑:控制瞄准状态下视野和动画。
这些功能共同构建了玩家可以与武器进行交互的复杂系统。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class HandGunController : MonoBehaviour
{
// 引用其他脚本和游戏对象
public PlayerMovement PM;
public Transform shooterPoint; // 武器射击的位置(枪口)
// 武器参数
public int range = 100; // 武器射程
public int bulletsMag = 31; // 一个弹匣数量
public int currentBullets; // 当前子弹数
public int bulletLeft = 300; // 备弹
// 枪口特效和音效
public ParticleSystem muzzleFlash; // 枪口火焰特效
public GameObject hitParticles; // 子弹击中粒子特效
public GameObject bulletHole; // 弹孔
public Light muzzleFlashLight; // 枪口火焰灯光
public AudioClip HandGunSound; // 枪声音效片段
public AudioClip reloadAmmoLeftClip; // 换子弹1音效片段
public AudioClip reloadOutOFAmmoClip; // 换子弹2音效片段(拉枪栓)
// 武器射速和键位设置
public float fireRate = 0.1f; // 射速
private float fireTimer; // 计时器
private float SpreadFactor; // 射击的一点偏移量
[SerializeField] private KeyCode reloadInputName; // 填装子弹按键
[SerializeField] private KeyCode inspectInputName; // 查看武器按键
[SerializeField] private KeyCode AutoRifleKey; // 主武器按键
[SerializeField] private KeyCode HandGunKey; // 副武器按键
[SerializeField] private KeyCode GunShootModelInputName; // 自动半自动切换按键
// 控制器和状态
private Animator anim; // 动画控制器
private AudioSource audioSource; // 音效控制器
private bool isReloading; // 是否在装弹
private bool isAiming; // 是否在瞄准
// 子弹壳抛出
public Transform casingSpawnPoint; // 子弹壳抛出的位置
public Transform casingPrefab; // 子弹壳预制体
private Camera mainCamera; // 主摄像机
public ChooseGunController CGC; // 切换武器类的实例
// 射击模式和UI
public enum ShootMode { AutoRifle, SemiGun }; // 射击模式枚举
public ShootMode shootingMode; // 当前射击模式
private bool GunShootInput; // 根据射击模式改变的射击键位输入
private int modeNum = 0; // 模式切换的参数(1:全自动模式,2:半自动模式)
private string shootModelName; // 射击模式名称
// UI元素
public Image crossHairUI; // 十字准心
public Text ammoTextUI; // 弹药文本
public Text ShootModelTextUI; // 射击模式文本
private void Start()
{
audioSource = GetComponent<AudioSource>();
anim = GetComponent<Animator>();
currentBullets = bulletsMag;
mainCamera = Camera.main;
shootingMode = ShootMode.SemiGun; // G18手枪默认是半自动模式
shootModelName = "clock";
UpdateAmmoUI();
}
private void Update()
{
// 切换射击模式(全自动和半自动)
if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 1)
{
modeNum = 1;
shootModelName = "全自动";
shootingMode = ShootMode.AutoRifle;
ShootModelTextUI.text = shootModelName;
}
else if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 0)
{
modeNum = 0;
shootModelName = "半自动";
shootingMode = ShootMode.SemiGun;
ShootModelTextUI.text = shootModelName;
}
// 根据射击模式确定射击输入和射速
switch (shootingMode)
{
case ShootMode.AutoRifle:
GunShootInput = Input.GetMouseButton(0);
fireRate = 0.08f;
break;
case ShootMode.SemiGun:
GunShootInput = Input.GetMouseButtonDown(0);
fireRate = 0.2f;
break;
}
// 射击逻辑
if (GunShootInput && currentBullets > 0)
{
GunFire();
}
else
{
muzzleFlashLight.enabled = false;
}
// 计时器更新
if (fireTimer < fireRate)
{
fireTimer += Time.deltaTime;
}
anim.SetBool("Run", PM.isRun);
anim.SetBool("Walk", PM.isWalk);
AnimatorStateInfo info = anim.GetCurrentAnimatorStateInfo(0);
// 判断是否在换子弹动画中
if (info.IsName("reload_ammo_left") || info.IsName("reload_out_of_ammo"))
{
isReloading = true;
}
else
{
isReloading = false;
}
// 换子弹逻辑
if (Input.GetKeyDown(reloadInputName) && currentBullets < bulletsMag && bulletLeft > 0)
{
Reload();
}
SpreadFactor = (isAiming) ? 0f : 0.01f;
DoingAim();
// 查看武器动画
if (Input.GetKeyDown(inspectInputName))
{
anim.SetTrigger("Inspect");
}
// 切换武器
if (Input.GetKeyDown(AutoRifleKey))
{
CGC.ChangeWeapon(0); // 切换到主武器(自动步枪)
}
if (Input.GetKeyDown(HandGunKey))
{
CGC.ChangeWeapon(1); // 切换到副武器(手枪)
}
}
// 更新UI
public void UpdateAmmoUI()
{
ammoTextUI.text = currentBullets + " / " + bulletLeft;
ShootModelTextUI.text = shootModelName;
}
// 瞄准逻辑
public void DoingAim()
{
if (Input.GetMouseButton(1) && !isReloading && !PM.isRun)
{
isAiming = true;
anim.SetBool("Aim", isAiming);
crossHairUI.gameObject.SetActive(false);
mainCamera.fieldOfView = 40;
}
else
{
isAiming = false;
anim.SetBool("Aim", isAiming);
crossHairUI.gameObject.SetActive(true);
mainCamera.fieldOfView = 60;
}
}
// 射击逻辑
public void GunFire()
{
if (fireTimer < fireRate || currentBullets <= 0 || isReloading || PM.isRun) return;
RaycastHit hit;
Vector3 shootDirection = shooterPoint.forward;
shootDirection = shootDirection + shooterPoint.TransformDirection(new Vector3(Random.Range(-SpreadFactor, SpreadFactor), Random.Range(-SpreadFactor, SpreadFactor)));
if (Physics.Raycast(shooterPoint.position, shootDirection, out hit, range))
{
Debug.Log(hit.transform.name + "打到了");
if (hit.transform.CompareTag("zombie"))
{
zombiehealth zombieHealth = hit.transform.GetComponent<zombiehealth>();
if (zombieHealth != null)
{
zombieHealth.TakeDamage(20f);
}
GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
Destroy(hitParticleEffect, 1f);
}
else
{
GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
Destroy(hitParticleEffect, 1f);
GameObject bulletHoleEffect = Instantiate(bulletHole, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
Destroy(bulletHoleEffect, 0.8f);
}
}
if (!isAiming)
{
anim.CrossFadeInFixedTime("fire", 0.1f);
}
else
{
anim.Play("aim_fire", 0, 0f);
}
muzzleFlash.Play();
muzzleFlashLight.enabled = true;
PlayerShootSound();
Instantiate(casingPrefab, casingSpawnPoint.transform.position, casingSpawnPoint.transform.rotation);
currentBullets--;
UpdateAmmoUI();
fireTimer = 0f;
}
// 播放射击音效
public void PlayerShootSound()
{
audioSource.clip = HandGunSound;
audioSource.Play();
}
// 填装子弹逻辑
public void Reload()
{
if (bulletLeft <= 0) return;
DoReloadAnimation();
int bulletToLoad = bulletsMag - currentBullets;
int bulletToReduce = (bulletLeft >= bulletToLoad) ? bulletToLoad : bulletLeft;
bulletLeft -= bulletToReduce;
currentBullets += bulletToReduce;
UpdateAmmoUI();
}
// 播放填装子弹动画
public void DoReloadAnimation()
{
if (currentBullets > 0)
{
anim.Play("reload_ammo_left", 0, 0);
audioSource.clip = reloadAmmoLeftClip;
audioSource.Play();
}
if (currentBullets == 0)
{
anim.Play("reload_out_of_ammo", 0, 0);
audioSource.clip = reloadOutOFAmmoClip;
audioSource.Play();
}
}
}
切枪效果
这脚本实现了武器切换功能,主要包括:
-
武器集合:使用列表(List)存储多个武器游戏对象。
-
初始索引:在开始时初始化索引。
-
武器切换方法:根据传入的武器索引,控制显示对应的武器并隐藏其他武器。
-
循环遍历:遍历武器列表,根据索引设置武器的显示状态。
-
子武器处理:根据切换情况,更新对应子武器的弹药UI显示。
这个脚本通过列表管理多个武器,并根据切换情况动态显示和隐藏武器,实现了角色切换武器的功能。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 武器切换
/// </summary>
public class ChooseGunController : MonoBehaviour
{
//武器设置为集合,存放多个武器
public List<GameObject> Weapons=new List<GameObject>();
private int index;
// Start is called before the first frame update
void Start()
{
index = 0;
}
//切换武器方法(全部武器挂载人物手上,默认隐藏,,谁需要就显示显示出来)
public void ChangeWeapon(int WeaponIndex)
{
for (int i = 0; i < Weapons.Count; i++)
{
if (WeaponIndex == i)
{
Weapons[i].gameObject.SetActive(true);
if (WeaponIndex == 0)
{
Weapons[i].gameObject.GetComponentInChildren<WeaponController>().UpdateAmmoUI();
// print(Weapons[i].gameObject.name);
}
else if (WeaponIndex == 1)
{
Weapons[i].gameObject.GetComponentInChildren<HandGunController>().UpdateAmmoUI();
//print(Weapons[i].gameObject.name);
}
}
else
{
Weapons[i].gameObject.SetActive(false);
}
}
}
}
动画状态机
行走、奔跑、跳跃、开枪、换弹、切枪、瞄准
玩家血量
这脚本管理玩家的生命和死亡:
- 有声音效果:受伤和死亡音效。
- 跟踪生命值和死亡状态。
- 控制死亡效果:播放音效、变暗场景、延迟后重载。
- 当玩家受伤时,减少生命值和出血量。
- 逐渐降低场景的颜色饱和度。
- 如果玩家死亡,重新加载当前场景。
using UnityEngine;
using UnityStandardAssets.ImageEffects;
using UnityEngine.SceneManagement;
public class Health : MonoBehaviour
{
public AudioClip hitSound; // 受伤音效
public AudioClip deathSound; // 死亡音效
public float maxHealth = 100f; // 最大生命值
public float currentHealth = 100f; // 当前生命值
private float timer = 0; // 计时器
public static bool isDead = false; // 是否死亡
private ColorCorrectionCurves colorCurves; // 颜色校正曲线
private MouseLook mouth; // 鼠标观察
private void Start()
{
BleedBehavior.BloodAmount = 0; // 出血行为的血量
colorCurves = Camera.main.GetComponent<ColorCorrectionCurves>(); // 获取颜色校正组件
mouth = Camera.main.GetComponent<MouseLook>(); // 获取鼠标观察组件
}
void Update()
{
if (isDead)
{
LevelReset(); // 重置关卡
}
}
// 受伤方法
public void TakeDamage(float damage)
{
if (isDead)
{
return; // 如果已经死亡,不处理伤害
}
currentHealth -= damage; // 减少生命值
if (currentHealth <= 0f)
{
Die(); // 死亡
}
else
{
currentHealth -= damage;
AudioSource.PlayClipAtPoint(hitSound, transform.position); // 播放受伤音效
}
BleedBehavior.BloodAmount += Mathf.Clamp01(damage / currentHealth); // 增加出血量
BleedBehavior.BloodAmount = 0.52f; // 固定出血量(这一行可能需要调整或删除)
}
// 死亡方法
private void Die()
{
isDead = true; // 设置死亡状态
AudioSource.PlayClipAtPoint(deathSound, transform.position); // 播放死亡音效
//this.GetComponent<PlayerMovement>().enabled = false;
mouth.enabled = false;
colorCurves.enabled = true;
Invoke("ReloadCurrentScene", 6f); // 延迟重载当前场景
}
// 重置关卡方法
public void LevelReset()
{
//timer += Time.deltaTime;
colorCurves.saturation -= (Time.deltaTime / 1.5f); // 减少饱和度
//colorCurves.saturation = Mathf.Max(0, colorCurves.saturation);
}
// 重新加载当前场景方法
private void ReloadCurrentScene()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
isDead = false;
}
}
新地图
创建太空站地图。
场景颜色渐变
using UnityEngine;
using UnityEngine.UI;
public class FadeOut : MonoBehaviour
{
public float speed = 1.0f;
private Image image;
private float alpha = 1.0f;
public AudioClip enter;
private void Start()
{
image = GetComponent<Image>();
GetComponent<AudioSource>().PlayOneShot(enter);
}
private void Update()
{
alpha = Mathf.Lerp(alpha, 0.0f, speed * Time.deltaTime);
image.color = new Color(image.color.r, image.color.g, image.color.b, alpha);
}
}
激光墙
玩家通过激光墙会受到伤害。
给激光墙添加光源、空间音效。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Laser : MonoBehaviour
{
public int damage = 30; // 伤害值
public float damageDelay = 1f; // 伤害延迟
private float lastDamageTime = 0f; // 上次伤害时间
private GameObject player; // 玩家对象
void Start()
{
player = GameObject.FindGameObjectWithTag("Player"); // 获取玩家对象
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject == player && Time.time > lastDamageTime + damageDelay)
{
player.GetComponent<Health>().TakeDamage(damage); // 对玩家造成伤害
lastDamageTime = Time.time; // 更新上次伤害时间
}
}
}
获取钥匙
玩家获取钥匙才能打开后续的门。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class card : MonoBehaviour
{
public AudioClip keyGrab;
private GameObject player;
void Start()
{
player = GameObject.FindGameObjectWithTag("Player");
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject == player)
{
AudioSource.PlayClipAtPoint(keyGrab, transform.position);
Destroy(this.gameObject);
}
}
}
滑动门
玩家触碰到门,门会打开。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class door : MonoBehaviour
{
private GameObject player;
public AudioClip enter;
public AudioClip exit;
public float speed = 1f;
private bool isOpen = false;
private bool isMoving = false;
private Vector3 targetPosition;
private void Start()
{
player = GameObject.FindGameObjectWithTag("Player");
//targetPosition = transform.position;
}
private void Update()
{
if (isMoving)
{
// 修复:更新门的位置而不是碰撞体的位置
this.gameObject.transform.position = Vector3.Lerp(transform.position, targetPosition, speed * Time.deltaTime);
if (Vector3.Distance(transform.position, targetPosition) < 0.01f)
{
isMoving = false;
}
}
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject == player && !isOpen)
{
isOpen = true;
targetPosition = new Vector3(transform.position.x, 5.3f, transform.position.z);
AudioSource.PlayClipAtPoint(enter, transform.position);
isMoving = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject == player && isOpen)
{
isOpen = false;
targetPosition = new Vector3(transform.position.x, 1f, transform.position.z);
AudioSource.PlayClipAtPoint(exit, transform.position);
isMoving = true;
}
}
}
NPC属性
-
Start()
: 在脚本启动时进行初始化,获取组件引用、设置初始参数、获取玩家对象,禁用导航代理,获取动画控制器。 -
Update()
: 在每帧更新中处理椭圆运动、朝向、接近原始位置的计算,以及判断玩家是否在视野范围内,根据情况设置移动方式和动画。 -
OnTriggerEnter(Collider other)
: 当碰撞体进入触发器范围时,检查碰撞对象标签是否为玩家,如果是,设置攻击动画、播放音效,并对玩家造成伤害。 -
OnTriggerExit(Collider other)
: 当碰撞体离开触发器范围时,检查碰撞对象标签是否为玩家,如果是,恢复奔跑动画。
脚本实现了僵尸的椭圆运动、追踪玩家、攻击玩家等基本逻辑。
using UnityEngine;
using UnityEngine.AI;
public class zombie : MonoBehaviour
{
public float distance = 5f; // 移动距离
public float speed = 1f; // 移动速度
public float eccentricity = 0.8f; // 离心率
public float rotationSpeed = 5f; // 旋转速度
public float scale = 1f; // 移动倍率
public float maxDistance = 5f; // 最大距离,当玩家距离小于该值时,设置isInSight为true
public int damage = 30;
private Vector3 startPosition;
private float elapsedTime = 0f;
private GameObject player;
private bool isInSight ;
public AudioClip Sound;
public AudioClip bit;
private Animator zombieanimator;
private NavMeshAgent agent;
public bool isEllipseMovementEnabled ; // 是否允许椭圆运动
public bool canmove ; // 是否允许椭圆运动
public zombiehealth zomhealth;
private void Start()
{
zomhealth=this.GetComponent<zombiehealth>();
isInSight = false;
isEllipseMovementEnabled = true;
canmove = false;
startPosition = transform.position;
player = GameObject.FindGameObjectWithTag("Player");
agent = GetComponent<NavMeshAgent>();
agent.enabled = false;
zombieanimator = GetComponent<Animator>();
}
private void Update()
{
if (isEllipseMovementEnabled&&!zomhealth.isDead)
{
elapsedTime += Time.deltaTime * speed;
// 根据参数计算椭圆轨迹上的位置
float x = distance * Mathf.Cos(elapsedTime * scale);
float z = distance * eccentricity * Mathf.Sin(elapsedTime * scale);
Vector3 targetPosition = startPosition + new Vector3(x, 0f, z);
// 平滑移动
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * speed);
// 计算椭圆曲线上的切线向量
float dx = -distance * scale * Mathf.Sin(elapsedTime * scale);
float dz = distance * eccentricity * scale * Mathf.Cos(elapsedTime * scale);
Vector3 tangent = new Vector3(dx, 0f, dz).normalized;
// 计算平滑的旋转
Quaternion targetRotation = Quaternion.LookRotation(tangent, Vector3.up);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, Time.deltaTime * rotationSpeed);
// 检查是否接近原始位置
float distanceToStart = Vector3.Distance(transform.position, startPosition);
if (distanceToStart < 0.1f)
{
// 返回原始位置,重置时间
elapsedTime = 0f;
}
}
if (!isInSight)
{
float playerDistance = Vector3.Distance(transform.position, player.transform.position);
if (playerDistance < maxDistance&&!zomhealth.isDead)
{
isEllipseMovementEnabled = false; // 停止椭圆运动
agent.enabled = true;
canmove=true;
isInSight = true;
AudioSource.PlayClipAtPoint(Sound, transform.position);
zombieanimator.SetBool("run",true);
}
}
if(canmove){
agent.SetDestination(player.gameObject.transform.position);}
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
// zombieanimator.SetBool("run",false);
zombieanimator.SetBool("attack",true);
AudioSource.PlayClipAtPoint(bit, transform.position);
player.GetComponent<health>().TakeDamage(damage);
}
}
void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "Player")
{
zombieanimator.SetBool("run",true);
zombieanimator.SetBool("attack",false);
//zombieanimator.SetBool("attack",true);
}
}
}
攻击逻辑
玩家开枪能对怪兽造成伤害,怪兽接触到玩家也能造成伤害,这脚本管理了僵尸的生命和死亡。
-
生命管理:
- 跟踪最大生命值和当前生命值。
- 接收伤害并减少生命值,如果已死亡则不再处理。
-
死亡效果:
- 播放死亡声音,禁用移动和导航代理。
- 设置死亡动画,1.5秒后销毁僵尸对象。
using UnityEngine;
using UnityEngine.AI;
public class zombiehealth : MonoBehaviour
{
public AudioClip deathSound; // 死亡时播放的声音
public float maxHealth = 100f; // 僵尸的最大生命值
public float currentHealth; // 当前生命值
public bool isDead; // 僵尸是否已死亡
private NavMeshAgent agent; // 导航网格代理组件的引用
private Animator zombieanimator; // 动画控制器组件的引用
public zombie zom; // 僵尸脚本的引用
private void Start()
{
zom = this.GetComponent<zombie>(); // 获取僵尸脚本组件
isDead = false; // 初始化死亡状态为否
currentHealth = maxHealth; // 设置初始生命值
agent = GetComponent<NavMeshAgent>(); // 获取导航网格代理组件
zombieanimator = GetComponent<Animator>(); // 获取动画控制器组件
}
public void TakeDamage(float damage)
{
if (isDead)
{
return; // 如果已死亡,退出函数
}
currentHealth -= damage; // 减少生命值
if (currentHealth <= 0f)
{
Die(); // 如果生命值小于等于0,触发死亡
}
else
{
currentHealth -= damage; // 多余的减少生命值操作
}
}
private void Die()
{
GetComponent<AudioSource>().PlayOneShot(deathSound); // 播放死亡声音
isDead = true; // 设置死亡状态为真
zom.canmove = false; // 禁用僵尸移动
agent.enabled = false; // 禁用导航网格代理
zombieanimator.SetBool("dead", true); // 在动画控制器中设置"dead"参数为真
Destroy(this.gameObject, 1.5f); // 1.5秒后销毁僵尸对象
}
}
终点传送门
这脚本实现了玩家离开游戏场景的效果:
-
声音与特效:
- 定义退出声音(ex)和退出音乐(exitmusic)。
- 获取玩家对象和主摄像机的色彩校正曲线特效(ColorCorrectionCurves)组件。
-
重置场景:
- 在离开触发区域后,增加色彩饱和度,逐渐变暗场景。
- 播放退出声音和音乐,延迟后重新加载当前场景。
-
场景重载:
- 重新加载当前游戏场景。
这个脚本通过渐变色彩和播放音效,为玩家离开游戏场景创造了一个独特的效果。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityStandardAssets.ImageEffects;
public class exit : MonoBehaviour
{
public AudioClip ex;
private GameObject player;
public float delayTime=5f;
private ColorCorrectionCurves colorCurves;
private bool isend=false;
public AudioClip exitmusic;
// Start is called before the first frame update
void Start()
{
player = GameObject.FindGameObjectWithTag("Player");
colorCurves = Camera.main.GetComponent<ColorCorrectionCurves>();
}
void Update()
{
if (isend)
{
LevelReset();
}
}
public void LevelReset()
{
//timer += Time.deltaTime;
colorCurves.saturation += (Time.deltaTime);
colorCurves.saturation = Mathf.Max(0, colorCurves.saturation);
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject == player)
{
isend=true;
colorCurves.enabled=true;
AudioSource.PlayClipAtPoint(ex, transform.position);
Invoke("playexitmusic", 1.5f);
Invoke("ReloadCurrentScene", delayTime);
}
}
private void ReloadCurrentScene()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
private void playexitmusic()
{
AudioSource.PlayClipAtPoint(exitmusic, transform.position);
}
}