• 周三. 4 月 22nd, 2026

物嫩软件资讯网

软件资讯来物嫩

Unity——第一人称射击游戏

admin@wunen

3 月 27, 2025

前言

在该游戏中,玩家将操控一个携带弓弩的角色,在有限的时间内尽量获得更多的积分,积分可以通过射中靶子获得,靶子有静止靶和移动靶,不同靶子得分不同。玩家必须要进入指定的射击区内才可以进行射击,射击区的射击数有限。


项目地址


视频地址

一、制作角色player

使用一个Capsule胶囊体来作为角色的身体,并把弓和一个摄像机作为胶囊体的子对象,调整摄像机和弓的位置使得画面合理。

二、角色控制

1.MouseLook视角控制

该脚本挂在在摄像机与弓上,使得视角能够随鼠标的运动而转动。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MouseLook : MonoBehaviour
{
    // Start is called before the first frame update

    public float mouseSensitivity = 100f;  //鼠标灵敏度

    public Transform playerBody;

    float xRotation = 0f;

    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);// limit the angle

        // rotate the camera within Y axis
        transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
        // rotate the player within X axis
        playerBody.Rotate(Vector3.up * mouseX);
    }
}

2.PlayerMovement角色移动

该脚本实现了通过WASD来操作角色移动

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting.APIUpdating;

public class PlayerMovement : MonoBehaviour
{   
    public CharacterController controller;
    public float speed = 12f;
    private float gravity = 9.8f;
    // Start is called before the first frame update

    Vector3 move;
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {   
        
        if(controller.isGrounded){

        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        //Vector3 move = new Vector3(x, 0f, z);// × global movement, we dont want
        move = transform.right * x + transform.forward * z;// move along the local coordinates right and forward

        }
        move.y = move.y - gravity*Time.deltaTime;
        controller.Move(move * speed * Time.deltaTime);
    }
    
}

三、Terrain地形制作

创建一个Terrain地形,使用下载的资源将地形染绿,然后用黄色的地形刷子刷出黄色的道路,再往上面加一些花草、树木、房子、山进行装饰,便能做出一块简单的地形。

四、实现天空盒切换SkyboxSwitcher

用一个数组skyboxMaterials存储多个天空盒材质,并在按下Q时进行切换,把该脚本挂载在摄像机上,并在Inspector上配置想要的天空盒材质。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkyboxSwitcher : MonoBehaviour
{
    public Material[] skyboxMaterials; // 存储不同天空盒的材质
 
    private int currentSkyboxIndex = 0; // 当前天空盒的索引
 
 
 
    void Start()
 
    {
 
        RenderSettings.skybox = skyboxMaterials[currentSkyboxIndex]; // 初始设置天空盒
 
    }
 
 
 
    void Update()
 
    {
 
        // 检测按下 'P' 键
 
        if (Input.GetKeyDown(KeyCode.P))
 
        {
 
            // 切换到下一个天空盒
 
            SwitchSkybox();
 
        }
 
    }
 
 
 
    void SwitchSkybox()
 
    {
 
        // 增加索引,确保循环切换
 
        currentSkyboxIndex = (currentSkyboxIndex + 1) % skyboxMaterials.Length;
 
 
 
        // 设置新的天空盒材质
 
        RenderSettings.skybox = skyboxMaterials[currentSkyboxIndex];
 
    }

}

五、实现拉弓与射箭ShootController

按下左键时会进行蓄力,蓄力时会给下一次发射的箭提供一个力,按下右键可以把箭发射出去。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ShootController : MonoBehaviour
{
    float force;// 蓄力力量
    const float maxForce = 1f;  // 最大力量
    const float chargeRate = 0.1f; // 每0.3秒蓄力的量
    Animator animator;//弓动画控制
    float mouseDownTime;// 记录鼠标蓄力时间
    bool isCharging=false;//是否正在蓄力
    public AudioSource audio;
    bool isFired=true; //是否已经将蓄力的箭发射
    public Slider Powerslider;//蓄力条
    public SpotController currentSpotController;

    public bool readyToShoot = false;//是否可以开始射击
    public int shootNum;// 剩余射击次数
    // Start is called before the first frame update
    void Start()
    {
        shootNum = 0; 
        Singleton<UserGUI>.Instance.SetShootNum(shootNum);
        animator = GetComponent<Animator>();
        animator.SetFloat("power", 1f);
    }

    // Update is called once per frame
    void Update()
    {
        // if(shootNum>0){
        //     readyToShoot = true;
        // }
        if (!readyToShoot)
        {
            Powerslider.gameObject.SetActive(false);
            return;
        }else{
            Powerslider.gameObject.SetActive(true);
        }
        //按照鼠标按下的时间蓄力,每0.3秒蓄0.1的力(最多0.5)加到animator的power属性上,并用相应的力射箭
        if (Input.GetMouseButtonDown(0) && isFired) // 0表示鼠标左键
        {   
            isFired = false;
            mouseDownTime = Time.time;  // 记录鼠标按下的时间
            isCharging = true;  // 开始蓄力
            Powerslider.gameObject.SetActive(true);//显示蓄力条
            animator.SetTrigger("start"); // 启动射箭动画
        }

        //根据蓄力程度更新弓的动画
        if (isCharging)
        {
            float holdTime = Time.time - mouseDownTime; // 计算鼠标按下的时间
            force = Mathf.Min(holdTime / 0.3f * chargeRate, maxForce); // 计算蓄力的量,最大为0.5
            Powerslider.value = force / maxForce; // 更新力量条的值
            animator.SetFloat("power", force);
        }

        //鼠标左键弹起,此时进入hold动画
        if(Input.GetMouseButtonUp(0))
        {
            animator.SetTrigger("hold");
            isCharging = false;
        }

        //按下鼠标右键,将弓箭发射出去
        if (Input.GetMouseButtonDown(1) && readyToShoot)
        {   
            audio = GetComponent<AudioSource>();
            audio.Play();
            isFired = true;
            //isCharging = false;  // 停止蓄力
            animator.SetTrigger("shoot");
            animator.SetFloat("power", force);  // 将蓄力的量加到animator的power属性上
            StartCoroutine(DelayedFireCoroutine(force));//延迟0.5s后射击
            Powerslider.value = 0;//清零蓄力条
            animator.SetFloat("power", 0f);

            //update shootNum
            shootNum--;
            currentSpotController.shootNum--;
            Singleton<UserGUI>.Instance.SetShootNum(shootNum);
            if (shootNum == 0)
            {
                readyToShoot = false;
            }
        }
    }

    public void AddShootNum(int sn){
        shootNum += sn;
        Singleton<UserGUI>.Instance.SetShootNum(shootNum);
    } 
    //协程:开火
    IEnumerator DelayedFireCoroutine(float f)
    {
        yield return new WaitForSeconds(0.2f);//等待0.2s后
        fire(f);    
    }

    //射击,创建箭的对象并且给其赋予属性
    public void fire(float f)
    {
        GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
        ArrowFeature arrowFeature = arrow.AddComponent<ArrowFeature>();
        // 使用Find方法通过子对象的名字获取arrow子对象
        Transform originArrowTransform = transform.Find("箭");
        
        arrow.transform.position = originArrowTransform.position;
        arrow.transform.rotation = transform.rotation;

        Rigidbody arrow_db = arrow.GetComponent<Rigidbody>();

        arrowFeature.startPos = arrowFeature.transform.position;
        arrow.tag = "Arrow";
        arrow_db.velocity = transform.forward * 100 * f;
    }

}

在屏幕上方放置了一个蓄力条Slider,用于显示蓄力程度。

为了让拉弓和射箭的动画能够正常配合,还需要给弓加上一个Animator组件。

六、利用双摄像机实现瞄准

在角色player上添加一个摄像机,放在弓弩的前方,通过按下Q键进行摄像机切换,这样就有了瞄准的效果

ChangeCamera

该脚本挂载在一个空物体上,用于实现摄像机的切换,初始时瞄准摄像机camera_1处于不激活状态,当按下瞄准按钮Q后,两个摄像机状态互换。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChangeCamera : MonoBehaviour
{
    public GameObject camera_0;//设置成public可以使unity中出现如下图所示的
    public GameObject camera_1;
    bool isActive_0;
    bool isActive_1;
    // Start is called before the first frame update
    void Start()
    {
        isActive_0 = true;
        isActive_1 = false;
        camera_1.SetActive(isActive_1);
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.Q)){
            change();
        }
    }

    public void change(){
        isActive_0 = isActive_0 ? false : true;
        isActive_1 = isActive_1 ? false : true;
        camera_0.SetActive(isActive_0);
        camera_1.SetActive(isActive_1);
    }
}

七、箭的属性ArrowFeatrue

如果与靶子发生了碰撞,箭会停留在靶子上5s

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ArrowFeature : MonoBehaviour
{
    public Vector3 startPos;
    public Vector3 startDir;
    public Transform target;//collider transform
    public float speed;
    public float destoryTime;

    Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        destoryTime = 10f;
    }

    // Update is called once per frame
    void Update()
    {
        destoryTime -= Time.deltaTime;
        if (destoryTime < 0)
        {
            Destroy(this.transform.gameObject);
        }
    }
    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "target")
        {
            if (!rb.isKinematic)
            {
                rb.isKinematic = true;
                target = collision.gameObject.transform;
                transform.SetParent(target);
            }
            destoryTime = 5f;

        }

    }

}

八、靶子控制器TargetController

有一个bool类型的变量move来决定该靶子是否为移动靶,如果为移动靶,通过计算下一帧的位置来实现移动。

通过判断是否与箭单位发生碰撞,来实现加分。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TargetController : MonoBehaviour
{
    public int basepoint;//初始分数
    public bool move; // 是否移动靶
    //public float aniSpeed = 1f;//动画执行速度
    public int scores;//单个靶点的分数
    //靶子的移动速度
    public float speed = 5f; 
    //靶子的移动距离
    public float distance = 10f; 
 
    //靶子的起始位置
    private Vector3 startPosition;
    //靶子的移动方向
    private float direction = 1f;

    // Use this for initialization
    void Start()
    {   
        //记录起始位置
        startPosition = transform.position;
        this.tag = "target";
        //move = false;
        //初始分数
        scores= 0;
    }

    private void move_target(){
        //计算下一帧的位置
        Vector3 nextPosition = transform.position + new Vector3(speed * direction * Time.deltaTime, 0f, 0f);
 
        // 判断是否超出移动范围,超出则改变移动方向
        if (Vector3.Distance(startPosition, nextPosition) > distance)
        {
            direction *= -1f;
        }
 
        // 更新位置
        transform.position = nextPosition;
    }
    // Update is called once per frame
    void Update()
    {
        if(move){
            move_target();
        }
    }

    //打到靶子
    private void OnCollisionEnter(Collision collision)
    {

        if (collision.gameObject.tag == "Arrow")
        {
            
            //scores += 1;
            //增加游戏总分数
            if(move){
                Singleton<UserGUI>.Instance.AddScore(3);
            }
            else{
                Singleton<UserGUI>.Instance.AddScore(1);
            }
            
            
        }
    }

}

九、射击区实现

首先创建一个Cylinder对象,调整高度为0.05,这样看起来就如同平面一样,把这个对象作为射击区域,添加Rigidbody和Box Collider组件,来获取与角色的碰撞信息。

SpotController

这个类主要用于记录当前射击区内还有多少次射击机会。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpotController : MonoBehaviour
{
    public int shootNum;
    // Start is called before the first frame update
    void Start()
    {
        //this.tag = "spot";
        shootNum = 5;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

PlayerController

这个类挂载在角色身上,主要用于处理碰撞事件,如果与射击区发生了碰撞,即进入了射击区,便会进入射击状态,如果离开了射击区,即结束了碰撞,则会退出射击状态。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public ShootController CrossBow;// 弓对象
    private bool atSpot = false;// 是否到达射击点
    private SpotController spot;// 射击位controller
    private TargetController[] targetControllers;//target controller

    // Start is called before the first frame update
    void Start()
    {
        CrossBow = GetComponentInChildren<ShootController>();
    }

    // Update is called once per frame
    void Update()
    {
        //如果进入了射击位置
        if (atSpot)
        {   
            //屏幕左上角出现对应提示
            Singleton<UserGUI>.Instance.SetIsAtSpot(true);
            Singleton<UserGUI>.Instance.SetShootNum(spot.shootNum);
            // if (targetControllers != null)
            // {
            //     //获取这个射击位置对应靶子的分数信息
            //     int sumSpotScore = 0;
            //     foreach (TargetController targetController in targetControllers)
            //     {
            //         sumSpotScore += targetController.scores;
            //     }
            //     //Singleton<UserGUI>.Instance.SetSpotScore(sumSpotScore);
            // }
        }
        else
        {
            Singleton<UserGUI>.Instance.SetIsAtSpot(false);
            Singleton<UserGUI>.Instance.SetShootNum(0);
        }
    }

    private void OnCollisionEnter(Collision collision){
       // Debug.Log("1");
        if(collision.gameObject.tag == "spot"){
            spot = collision.gameObject.GetComponentInChildren<SpotController>();
            atSpot = true;
            //Debug.Log("sss");
            if (spot.shootNum > 0)
            {
                CrossBow.GetComponentInChildren<ShootController>().readyToShoot = true;
                CrossBow.GetComponentInChildren<ShootController>().shootNum = spot.shootNum;
                CrossBow.GetComponentInChildren<ShootController>().currentSpotController = spot;
            }

            
            // targetControllers = spot.targetControllers; 
            // if (targetControllers != null)
            // {
            //     int sumSpotScore = 0;
            //     foreach (TargetController targetController in targetControllers)
            //     {
            //         sumSpotScore += targetController.scores;
            //     }
            //     Singleton<UserGUI>.Instance.SetSpotScore(sumSpotScore);
            // }
        }
    }

        private void OnCollisionExit(Collision collision)
    {
        if (collision.gameObject.tag == "spot")
        {
            Debug.Log("collideExit with spot");
            CrossBow.GetComponentInChildren<ShootController>().readyToShoot = false;
            atSpot = false;
        }
    }

}

十、UserGUI用户界面

主要包含这些部分:提示词、倒计时、进入射击区时的提示与射击次数提示,游戏结束提示。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.EventSystems.EventTrigger;

public enum GameStatus
{
    Playing,
    GameOver,
}
struct Status
{
    public int score;
    public string tip;
    //public float tipShowTime;
    public bool atSpot;
    public GameStatus gameStatus;
    public int shootNum;
    public int spotScore;
}

public class UserGUI : MonoBehaviour
{
    private float timerDuration = 300f; // 5分钟,单位为秒
    private float timer;               // 计时器
    private GUIStyle playInfoStyle;
    private Status status; 
    public int crosshairSize = 20;//准星线条长度
    // Start is called before the first frame update
    void Start()
    {
        //Time.timeScale = 0f;
        timer = 0f;
        // set style
        playInfoStyle = new GUIStyle();
        playInfoStyle.normal.textColor = Color.blue;
        playInfoStyle.fontSize= 25;
        status.gameStatus = GameStatus.Playing;
        // init status
        //status.shootNum = 5;
        status.score = 0;
        status.tip = string.Empty;
        status.spotScore = 0;
        status.atSpot = false;
    }

    // Update is called once per frame
    void Update()
    {
        // 更新计时器
        timer += Time.deltaTime;
        // 检查是否已经到了设定的时间
        if (timer >= timerDuration)
        {
            // 执行计时结束后的逻辑
            status.gameStatus = GameStatus.GameOver;
        }
    }

    public void AddScore(int score){
        status.score += score;
    }

    public void SetShootNum(int shootnum){
        status.shootNum = shootnum;
    }

    private void showPage(){
        GUI.Label(new Rect(10, 60, 500, 100), "P——切换天空盒" ,playInfoStyle);
        GUI.Label(new Rect(10, 90, 500, 100), "Q——进入瞄准状态", playInfoStyle );
        GUI.Label(new Rect(10, 180, 500, 100), "剩余时间" + Mathf.Round(timerDuration-timer).ToString() + "s", playInfoStyle);
        GUI.Label(new Rect(10, 150, 500, 100), "游戏总分数:" + status.score, playInfoStyle);
        //GUI.Label(new Rect(10, 120, 500, 100), "剩余箭数:" + status.shootNum, playInfoStyle);
        // show 准星
        float screenWidth = Screen.width;
        float screenHeight = Screen.height;
        float centerX = screenWidth / 2f;
        float centerY = screenHeight / 2f;
        // 设置准星颜色
        GUI.color = Color.red;
        // 绘制准星
        GUI.DrawTexture(new Rect(centerX - crosshairSize / 2f + 15f, centerY + 5f , crosshairSize, 2f), Texture2D.whiteTexture);
        GUI.DrawTexture(new Rect(centerX - 1f + 15f, centerY - crosshairSize / 2f + 5f, 2f, crosshairSize), Texture2D.whiteTexture);
        // 恢复GUI颜色设置
        GUI.color = Color.white;

        if (status.atSpot)
        {
            // Draw the message with the new GUIStyle
            GUI.Label(new Rect(10, 120, 500, 100), "您已到达射击位,剩余射击次数:" + status.shootNum, playInfoStyle);
            //GUI.Label(new Rect(10, 90, 500, 100), "您在该靶点的射击分数:" + status.spotScore, playInfoStyle);
        }

        // if (GUI.Button(new Rect(10, 200, 100, 50), "补充子弹"))
        // {
        //     Singleton<ShootController>.Instance.SetShootNum(5);
        // }
    }

    private void showEndPage(){
        playInfoStyle.fontSize = 65;
        GUI.Label(new Rect(
                (Screen.width - 500) / 2 - 100,  // X坐标:屏幕宽度的一半减去标签宽度的一半
                (Screen.height - 100) / 2  , // Y坐标:屏幕高度的一半减去标签高度的一半
                500,                        // 宽度
                100), "游戏结束,总得分为:"+status.score , playInfoStyle);
    }
    private void OnGUI(){
        if(status.gameStatus == GameStatus.Playing){
            showPage();
        }
        else{
            showEndPage();
        }
    }
    public void SetIsAtSpot(bool isspot){
        status.atSpot = isspot;
    }
}

十一、背景音乐与射击音效

背景音乐

创建一个空物体,添加Audio Source组件,往里面添加背景音乐文件,勾选play on awake,这样它就会在游戏开始后播放

射击音效

在弓物体上添加Audio Source组件,添加射击音效文件,然后在shootController里添加代码,实现每次发射弓箭时播放射击音效。

//按下鼠标右键,将弓箭发射出去
        if (Input.GetMouseButtonDown(1) && readyToShoot)
        {   
            audio = GetComponent<AudioSource>();
            audio.Play();
            isFired = true;
            //isCharging = false;  // 停止蓄力
            animator.SetTrigger("shoot");
            animator.SetFloat("power", force);  // 将蓄力的量加到animator的power属性上
            StartCoroutine(DelayedFireCoroutine(force));//延迟0.5s后射击
            Powerslider.value = 0;//清零蓄力条
            animator.SetFloat("power", 0f);

            //update shootNum
            shootNum--;
            currentSpotController.shootNum--;
            Singleton<UserGUI>.Instance.SetShootNum(shootNum);
            if (shootNum == 0)
            {
                readyToShoot = false;
            }
        }

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注