视频教程:
1. 项目初始化
创建项目"ServivalShooter"
导入包"Survival Shooter.unitypackage"
导入环境、灯光预设,创建背景音乐物体,隐藏 Unity 自带灯光

2. 玩家导入、移动
导入玩家


给"Player"添加脚本"PlayerMovement"

csharp
using UnityEngine;
/// <summary>
/// 控制玩家角色的移动
/// </summary>
public class PlayerMovement : MonoBehaviour
{
/// <summary>
/// 玩家移动速度
/// </summary>
public float speed = 6f;
/// <summary>
/// 玩家刚体组件的引用
/// </summary>
private Rigidbody rb;
/// <summary>
/// 在对象激活时获取刚体组件
/// </summary>
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
/// <summary>
/// 在固定时间间隔内处理物理移动
/// </summary>
private void FixedUpdate()
{
// 获取水平和垂直方向的输入
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
// 计算移动向量
Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
// 移动玩家到新的位置
rb.MovePosition(transform.position + movement * speed * Time.fixedDeltaTime);
}
}
3. 摄像机跟随

CameraFollow.cs
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
public float smoothing = 5f;
private GameObject player;
private Vector3 offset;
private void Awake()
{
player = GameObject.FindGameObjectWithTag("Player");
}
// Start is called before the first frame update
void Start()
{
offset = transform.position - player.transform.position;
}
// Update is called once per frame
void Update()
{
}
private void FixedUpdate()
{
transform.position = Vector3.Lerp(transform.position, offset + player.transform.position, smoothing * Time.deltaTime);
}
}
4. 玩家跟随鼠标朝向
设置摄像机

设置地面



PlayerMovement.cs
csharp
using UnityEngine;
/// <summary>
/// 控制玩家角色的移动和旋转
/// </summary>
public class PlayerMovement : MonoBehaviour
{
/// <summary>
/// 玩家移动速度(单位:米/秒)
/// </summary>
public float speed = 6f;
/// <summary>
/// 玩家刚体组件的引用,用于物理移动
/// </summary>
private Rigidbody rb;
/// <summary>
/// 在脚本激活时初始化,获取刚体组件
/// </summary>
private void Awake()
{
// 获取附加到同一游戏对象的Rigidbody组件
rb = GetComponent<Rigidbody>();
}
/// <summary>
/// 固定时间间隔调用的物理更新方法(默认每秒50次)
/// </summary>
private void FixedUpdate()
{
// 获取水平和垂直方向的输入(WASD或方向键)
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// 处理移动和旋转
Move(h, v); // 根据输入移动玩家
Turning(); // 使玩家朝向鼠标位置
}
/// <summary>
/// 根据输入移动玩家
/// </summary>
/// <param name="h">水平输入(-1到1)</param>
/// <param name="v">垂直输入(-1到1)</param>
void Move(float h, float v)
{
// 创建移动向量(忽略Y轴,因为是在平面上移动)
Vector3 movement = new Vector3(h, 0.0f, v);
// 使用刚体移动玩家位置,确保物理效果正确
// Time.fixedDeltaTime确保移动速度与帧率无关
rb.MovePosition(transform.position + movement * speed * Time.fixedDeltaTime);
}
/// <summary>
/// 使玩家朝向鼠标所在位置
/// </summary>
void Turning()
{
// 从主摄像机创建一条射线,方向指向鼠标屏幕位置
Ray cameraRay = Camera.main.ScreenPointToRay(Input.mousePosition);
// 获取地面层的图层,用于射线检测
int floorLayer = LayerMask.GetMask("Ground");
RaycastHit floorHit;
// 发射射线检测地面碰撞
bool isTouchFloor = Physics.Raycast(cameraRay, out floorHit, 100, floorLayer);
if (isTouchFloor)
{
// 计算从玩家位置到鼠标点击位置的向量
Vector3 playerToMouse = floorHit.point - transform.position;
// 确保玩家只在水平面上旋转(Y轴设为0)
playerToMouse.y = 0;
// 创建目标旋转(使玩家朝向鼠标位置)
Quaternion targetRotation = Quaternion.LookRotation(playerToMouse);
// 应用旋转到刚体
rb.MoveRotation(targetRotation);
}
}
}
5. 静置、移动动画
增加动画控制器

设置静置动画、移动动画切换

增加参数 isWalking

静置切换为移动

移动切换为静置

编辑代码 PlayerMovement.cs
使用控制器改变 isWalking 参数值进而控制动画切换



csharp
using UnityEngine;
/// <summary>
/// 控制玩家角色的移动
/// </summary>
public class PlayerMovement : MonoBehaviour
{
/// <summary>
/// 玩家移动速度
/// </summary>
public float speed = 6f;
/// <summary>
/// 玩家刚体组件的引用
/// </summary>
private Rigidbody rb;
/// <summary>
/// 玩家动画组件的引用
/// </summary>
private Animator anim;
/// <summary>
/// 在对象激活时获取刚体和动画组件
/// </summary>
private void Awake()
{
rb = GetComponent<Rigidbody>();
anim = GetComponent<Animator>();
}
/// <summary>
/// 在固定时间间隔内处理物理移动、旋转和动画
/// </summary>
private void FixedUpdate()
{
// 获取水平和垂直方向的输入
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// 移动角色
Move(h, v);
// 角色朝向鼠标指向位置
Turing();
// 播放行走动画
Animating(h, v);
}
/// <summary>
/// 根据输入移动角色
/// </summary>
/// <param name="h">水平输入</param>
/// <param name="v">垂直输入</param>
void Move(float h, float v)
{
// 计算移动向量
Vector3 movement = new Vector3(h, 0.0f, v);
// 移动玩家到新的位置
rb.MovePosition(transform.position + movement * speed * Time.fixedDeltaTime);
}
/// <summary>
/// 让角色朝向鼠标指向的地面位置
/// </summary>
void Turing()
{
// 创建一条从主摄像机到鼠标位置的射线
Ray cameraRay = Camera.main.ScreenPointToRay(Input.mousePosition);
// 获取地面层的LayerMask
int floorLayer = LayerMask.GetMask("Ground");
RaycastHit floorHit;
// 射线检测是否击中地面
bool isTouchFloor = Physics.Raycast(cameraRay, out floorHit, 100, floorLayer);
if (isTouchFloor)
{
// 计算角色到鼠标点击点的方向向量
Vector3 v3 = floorHit.point - transform.position;
v3.y = 0; // 保持水平方向
// 计算朝向该方向的旋转
Quaternion q = Quaternion.LookRotation(v3);
// 旋转角色
rb.MoveRotation(q);
}
}
/// <summary>
/// 根据输入设置动画参数,控制行走动画
/// </summary>
/// <param name="h">水平输入</param>
/// <param name="v">垂直输入</param>
void Animating(float h, float v)
{
// 判断是否有输入
bool isWalking = h != 0 || v != 0;
// 设置动画参数
anim.SetBool("isWalking", isWalking);
}
}
6. 发射效果
声音

灯光


粒子效果

新建脚本挂载

MyPlayerShooting.cs
csharp
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 控制玩家射击行为的脚本
/// </summary>
public class MyPlayerShooting : MonoBehaviour
{
/// <summary>
/// 枪械音效组件
/// </summary>
private AudioSource gunAudio;
/// <summary>
/// 距离上次射击经过的时间
/// </summary>
private float shootTime = 0f;
/// <summary>
/// 射击间隔(秒)
/// </summary>
private float shootDelay = 0.15f;
/// <summary>
/// 枪口闪光持续时间比例
/// </summary>
private float lightRate = 0.2f;
/// <summary>
/// 枪口闪光灯组件
/// </summary>
private Light shootLight;
/// <summary>
/// 射击线渲染组件
/// </summary>
private LineRenderer shootLine;
/// <summary>
/// 射击粒子特效组件
/// </summary>
private ParticleSystem shootParticleSystem;
/// <summary>
/// 获取所需组件的引用
/// </summary>
private void Awake()
{
gunAudio = GetComponent<AudioSource>();
shootLight = GetComponent<Light>();
shootLine = GetComponent<LineRenderer>();
shootParticleSystem = GetComponent<ParticleSystem>();
}
// Start 在游戏开始前调用(此处未使用)
void Start()
{
}
/// <summary>
/// 每帧更新,处理射击逻辑和特效显示
/// </summary>
void Update()
{
// 累加时间
shootTime += Time.deltaTime;
// 检测射击输入且满足射击间隔
if (Input.GetButton("Fire1") && shootTime >= shootDelay)
{
Shooting();
}
// 控制枪口闪光和射线显示时间
if (shootTime >= shootDelay * lightRate)
{
shootLight.enabled = false;
shootLine.enabled = false;
}
}
/// <summary>
/// 执行射击操作,包括音效、特效和射线显示
/// </summary>
private void Shooting()
{
// 重置射击计时
shootTime = 0;
// 播放射击音效
gunAudio.Play();
// 显示枪口闪光
shootLight.enabled = true;
// 设置射线起点为枪口位置
shootLine.SetPosition(0, transform.position);
// 设置射线终点为前方100单位
shootLine.SetPosition(1, transform.position + transform.forward * 100);
// 显示射线
shootLine.enabled = true;
// 播放粒子特效
shootParticleSystem.Play();
}
}
7. 添加敌人
添加敌人模型,并添加相应组件

设置组件



安装"AI Navigation"包

设置并烘焙可导航区域

编写脚本 MyEnemyMovement.cs,实现敌人跟随玩家

csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MyEnemyMovement : MonoBehaviour
{
private Transform player;
private NavMeshAgent nav;
private void Awake()
{
player = GameObject.FindGameObjectWithTag("Player").transform;
nav = GetComponent<NavMeshAgent>();
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
nav.SetDestination(player.position);
}
}
添加并设置移动动画

8. 攻击敌人
8.1. 线条绘制
射线命中敌人,射线止于敌人处,未命中敌人则射出 100 个单位长度
设置敌人图层

编写设计代码 MyPlayerShooting.cs


csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyPlayerShooting : MonoBehaviour
{
private float shootDelay = 0.15f;//发射延迟
private float shootTime = 0f;//发射时间
private float shootRate = 0.2f;//发射关闭效果延迟比例
private Light shootLight;//发射灯光
private LineRenderer shootLine;//发射激光射线
private ParticleSystem shootParticle;//发射粒子效果
private AudioSource shootAduio;//枪声音
//开枪发射射线相关的变量
private Ray shootRay;
private RaycastHit shootHit;
private int shootMask;
private void Awake()
{
shootLight = GetComponent<Light>();
shootLine = GetComponent<LineRenderer>();
shootParticle = GetComponent<ParticleSystem>();
shootAduio = GetComponent<AudioSource>();
shootMask = LayerMask.GetMask("Shootable");
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
//上次发射后经过时间
shootTime = shootTime + Time.deltaTime;
//如果点击鼠标左键且满足发射延迟则进行发射
if (Input.GetButton("Fire1") && shootTime >= shootDelay)
{
Shooting();
}
//经过0.03秒则关闭发射效果
//if(shootTime >= shootDelay * shootRate)
if(shootTime >= 0.03)
{
//关闭发射灯光效果
shootLight.enabled = false;
//关闭发射射线效果
shootLine.enabled = false;
}
}
void Shooting()
{
shootTime = 0f;
//开启发射灯光效果
shootLight.enabled = true;
//设置发射射线位置
shootLine.SetPosition(0, transform.position);
shootLine.SetPosition(1, transform.position + 100 * transform.forward);
//开启发射射线效果
shootLine.enabled = true;
//播放发射粒子效果
shootParticle.Play();
//播放发射声音
shootAduio.Play();
//发射射线,检测是否击中敌人
shootRay.origin = transform.position;
shootRay.direction = transform.forward;
//如果击中敌人,则获取敌人脚本并调用其受伤方法
if (Physics.Raycast(shootRay, out shootHit, 100, shootMask))
{
shootLine.SetPosition(1, shootHit.point);
}
else
{
//如果没有击中敌人,则设置射线终点为100米远
shootLine.SetPosition(1, transform.position + 100 * transform.forward);
}
}
}
8.2. 声音、特效
敌人物体添加组件血量脚本、被攻击粒子特效、被攻击声音


编写脚本 MyEnemyHealth.cs
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyEnemyHealth : MonoBehaviour
{
public int hp = 100;
private ParticleSystem enemyParticleSystem;
private AudioSource audioSource;
private void Awake()
{
enemyParticleSystem = GetComponent<ParticleSystem>();
audioSource = GetComponent<AudioSource>();
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void TakeDamage(int damage)
{
hp -= damage;// 减少生命值
audioSource.Play();// 播放受伤音效
enemyParticleSystem.transform.position = transform.position;// 设置粒子效果位置
enemyParticleSystem.Play();// 播放粒子效果
}
}
修改脚本 MyPlayerShooting.cs

csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyPlayerShooting : MonoBehaviour
{
private float shootDelay = 0.15f;//发射延迟
private float shootTime = 0f;//发射时间
private float shootRate = 0.2f;//发射关闭效果延迟比例
private Light shootLight;//发射灯光
private LineRenderer shootLine;//发射激光射线
private ParticleSystem shootParticle;//发射粒子效果
private AudioSource shootAduio;//枪声音
//开枪发射射线相关的变量
private Ray shootRay;
private RaycastHit shootHit;
private int shootMask;
private void Awake()
{
shootLight = GetComponent<Light>();
shootLine = GetComponent<LineRenderer>();
shootParticle = GetComponent<ParticleSystem>();
shootAduio = GetComponent<AudioSource>();
shootMask = LayerMask.GetMask("Shootable");
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
//上次发射后经过时间
shootTime = shootTime + Time.deltaTime;
//如果点击鼠标左键且满足发射延迟则进行发射
if (Input.GetButton("Fire1") && shootTime >= shootDelay)
{
Shooting();
}
//经过0.03秒则关闭发射效果
//if(shootTime >= shootDelay * shootRate)
if(shootTime >= 0.03)
{
//关闭发射灯光效果
shootLight.enabled = false;
//关闭发射射线效果
shootLine.enabled = false;
}
}
void Shooting()
{
shootTime = 0f;
//开启发射灯光效果
shootLight.enabled = true;
//设置发射射线位置
shootLine.SetPosition(0, transform.position);
shootLine.SetPosition(1, transform.position + 100 * transform.forward);
//开启发射射线效果
shootLine.enabled = true;
//播放发射粒子效果
shootParticle.Play();
//播放发射声音
shootAduio.Play();
//发射射线,检测是否击中敌人
shootRay.origin = transform.position;
shootRay.direction = transform.forward;
//如果击中敌人,则获取敌人脚本并调用其受伤方法
if (Physics.Raycast(shootRay, out shootHit, 100, shootMask))
{
shootLine.SetPosition(1, shootHit.point);
MyEnemyHealth enemyHealth = shootHit.collider.GetComponent<MyEnemyHealth>();
enemyHealth.TakeDamage(10); //假设每次射击造成10点伤害
}
else
{
//如果没有击中敌人,则设置射线终点为100米远
shootLine.SetPosition(1, transform.position + 100 * transform.forward);
}
}
}
8.3. 受到伤害
收到玩家攻击,血量减少,血量为 0 时播放死亡动画,死亡后不能动

MyEnemyHealth.cs

csharp
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyEnemyHealth : MonoBehaviour
{
public int hp = 100;
public AudioClip deathClip;
private ParticleSystem enemyParticleSystem;
private AudioSource audioSource;
private Animator animator;
private CapsuleCollider capsuleCollider;
private void Awake()
{
enemyParticleSystem = GetComponent<ParticleSystem>();
audioSource = GetComponent<AudioSource>();
animator = GetComponent<Animator>();
capsuleCollider = GetComponent<CapsuleCollider>();
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void TakeDamage(int damage)
{
hp -= damage;// 减少生命值
if (hp <= 0) Death();
audioSource.Play();// 播放受伤音效
enemyParticleSystem.transform.position = transform.position;// 设置粒子效果位置
enemyParticleSystem.Play();// 播放粒子效果
}
private void Death()
{
hp = 0; // 确保生命值不小于0
audioSource.clip = deathClip;
audioSource.Play();
animator.SetTrigger("death");
capsuleCollider.enabled = false;// 禁用碰撞体
}
public void StartSinking()
{
}
}
MyEnemyMovement.cs

csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MyEnemyMovement : MonoBehaviour
{
private Transform player;
private NavMeshAgent nav;
private MyEnemyHealth enemyHealth;
private void Awake()
{
player = GameObject.FindGameObjectWithTag("Player").transform;
nav = GetComponent<NavMeshAgent>();
enemyHealth = GetComponent<MyEnemyHealth>();
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(enemyHealth.hp <= 0) return; // 如果敌人已死亡,则不进行移动
nav.SetDestination(player.position);
}
}
8.4. 死亡
MyEnemyHealth.cs


csharp
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MyEnemyHealth : MonoBehaviour
{
public int hp = 100;
public AudioClip deathClip;
private ParticleSystem enemyParticleSystem;
private AudioSource audioSource;
private Animator animator;
private CapsuleCollider capsuleCollider;
private bool isSinking = false;
private void Awake()
{
enemyParticleSystem = GetComponent<ParticleSystem>();
audioSource = GetComponent<AudioSource>();
animator = GetComponent<Animator>();
capsuleCollider = GetComponent<CapsuleCollider>();
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (isSinking)
{
transform.Translate(Vector3.down * Time.deltaTime * 2f);// 向下移动,模拟沉没效果
}
}
public void TakeDamage(int damage)
{
hp -= damage;// 减少生命值
if (hp <= 0) Death();
audioSource.Play();// 播放受伤音效
enemyParticleSystem.transform.position = transform.position;// 设置粒子效果位置
enemyParticleSystem.Play();// 播放粒子效果
}
private void Death()
{
hp = 0; // 确保生命值不小于0
audioSource.clip = deathClip;// 设置死亡音效
audioSource.Play();// 播放死亡音效
animator.SetTrigger("death");// 设置死亡动画触发器
capsuleCollider.enabled = false;// 禁用胶囊碰撞器
GetComponent<Rigidbody>().isKinematic = true; // 设置刚体为静态
GetComponent<NavMeshAgent>().enabled = false; // 禁用导航网格代理
StartSinking();
}
public void StartSinking()
{
isSinking = true;
Destroy(gameObject, 2f); // 2秒后销毁敌人对象
}
}