Unity教学 项目4 3D求生枪手

视频教程:

https://www.bilibili.com/video/BV16F7zzqEJF?spm_id_from=333.788.videopod.sections&vd_source=25b783f5f945c4507229e9dec657b5bb

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秒后销毁敌人对象   
    }
}
相关推荐
做cv的小昊4 小时前
计算机图形学:【Games101】学习笔记04——着色(光照与基本着色模型,着色频率、图形管线、纹理映射)
笔记·学习·3d·图形渲染·光照贴图·计算机图形学
CreasyChan14 小时前
C# 反射详解
开发语言·前端·windows·unity·c#·游戏开发
IMPYLH16 小时前
Lua 的 Coroutine(协程)模块
开发语言·笔记·后端·中间件·游戏引擎·lua
二狗哈18 小时前
Cesium快速入门18:Entity材质设置
3d·webgl·材质·cesium·地图可视化
二狗哈1 天前
Cesium快速入门19:Entity折线材质
3d·webgl·材质·cesium·地图可视化
kk哥88991 天前
从建模到渲染:C4D 2025 全流程 3D 创作提升最新版本下载安装步骤
3d
zl_vslam1 天前
SLAM中的非线性优-3D图优化之相对位姿g2o::EdgeSE3Expmap(十)
人工智能·算法·计算机视觉·3d
二狗哈1 天前
Cesium快速入门17:与entity和primitive交互
开发语言·前端·javascript·3d·webgl·cesium·地图可视化
世洋Blog1 天前
SiYangUnityEventSystem,一个Unity中的事件系统
观察者模式·unity·c#·游戏引擎·事件系统