Unity大作业-第一人称射箭游戏

视频地址:BILIBILI
代码地址:Gitee

一、加载资源包

本项目中使用了Cross BowVegetationSpawnerMilitary TargetFantacy Skybox F REE资源包,均可在Asset Store免费下载。其中Cross Bow提供了弓和箭的预制,VegetationSpawner提供了树的预制,Military Target提供了靶子的预制,Fantacy Skybox F REE提供了天空盒和地形的预制。

二、定义人物

首先创建一个胶囊体作为我们的人物,将摄像头和弓的预制模型挂载在人物上,从而在第一人称视角能看到手持弓箭的场景。注意要调节摄像头的高度,使得视角在一个合适的位置。
在这里插入图片描述
在这里插入图片描述

三、人物的控制

首先为人物添加一个Character Controller
在这里插入图片描述

脚本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 = 300f;

    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);
    }
}

脚本PlayMovement用以控制人物移动

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中,此时启动游戏就能以第一视角在地形中走动
在这里插入图片描述

五、设置箭的属性

设置箭飞出去的速度、销毁时间。当箭与靶子发生碰撞时,箭会成为靶子的子对象,并且在一定时间后自动销毁。当箭没有射中靶子时,会在一个更短的时间内销毁。

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;

        }

    }

}

六、设置靶子的属性

靶子在被箭射中后,会将总体的分数加一,同时在这个靶子的得分也会加一

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

public class TargetController : MonoBehaviour
{
    public int basepoint;//初始分数

    public float aniSpeed = 1f;//动画执行速度
    public int scores;//单个靶点的分数
    // Use this for initialization
    void Start()
    {   
        this.tag = "target";
       
        //初始分数
        scores= 0;
    }

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

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

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

}

七、设置弓的控制器

弓按住左键能进行蓄力,蓄力过程播放动画。在蓄力完成后按下右键进行发射一支箭,箭的初始速度根据蓄力的程度来确定。

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;//是否正在蓄力

    bool isFired=true; //是否已经将蓄力的箭发射
    public Slider Powerslider;//蓄力条
    public SpotController currentSpotController;

    public bool readyToShoot = false;//是否可以开始射击,通过检测是否进入射击位置来设置
    public int shootNum = 0;// 剩余设计次数

    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>();
        animator.SetFloat("power", 1f);
    }

    // Update is called once per frame
    void Update()
    {
        if (!readyToShoot)
        {
            Powerslider.gameObject.SetActive(false);
            return;
        }

        //按照鼠标按下的时间蓄力,每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)
        {   
            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;
            }
        }
    }

    //协程:开火
    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("mark");
        
        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;
    }
}

八、射击位置

一个射击位置对应着几个靶子

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

public class SpotController : MonoBehaviour
{
    public int shootNum;
    //该射击点位对应的靶
    public TargetController[] targetControllers;

    // Start is called before the first frame update
    void Start()
    {   
        this.tag = "spot";
        shootNum = 5;
    }

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

九、人与射击位置的检测

人在进入射击位置之后才能拉弓、射箭。并且读取射击位置对应的靶子的分数信息,显示在左上角。

using UnityEngine;

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

    void Start()
    {
        CrossBow = GetComponentInChildren<ShootController>();

    }

    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)
    {
        // 与射击点位撞击

        if (collision.gameObject.tag == "spot")
        {
            
            spot = collision.gameObject.GetComponentInChildren<SpotController>();
            atSpot = true;

            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;
        }
    }

}


十、天空盒切换

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];
 
    }

}

十一、提示的GUI

GUI包含了显示当前的分数、显示进入射击位置、显示当前射击位置的得分、显示准星。

在这里插入图片描述

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 int shootNum;
    public GameStatus gameStatus;
    public int spotScore;
}

public class UserGUI : MonoBehaviour
{
    private ISceneController currentSceneController;
    private GUIStyle playInfoStyle;
    private Status status;

    public int crosshairSize = 20;//准星线条长度
    public float zoomSpeed = 5f;//滚轮切换视野大小的速度
    public float minFOV = 20f;//最小视野宽
    public float maxFOV = 60f;//最大视野宽

    // Start is called before the first frame update
    void Start()
    {
        Init();
        currentSceneController = Director.GetInstance().CurrentSceneController;

        // set style
        playInfoStyle = new GUIStyle();
        playInfoStyle.normal.textColor = Color.black;
        playInfoStyle.fontSize= 25;

        // init status
        status.shootNum = 0;
        status.score = 0;
        status.tip = string.Empty;
        status.atSpot = false;
        status.gameStatus = GameStatus.Playing;
        status.spotScore = 0;
        status.tipShowTime = 0;
    }

    private void Init()
    {
      
    }

    // Update is called once per frame
    void Update()
    {
        status.tipShowTime -= Time.deltaTime;
    }

    private void OnGUI()
    {
        // show user page
        ShowPage();
    }

    public void SetGameState(GameStatus gameStatus)
    {
        status.gameStatus = gameStatus;
    }

    /*
     set property of status
     */

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

    public void SetSpotScore(int score)
    {
        status.spotScore = score;
    }

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

    public void SetShootNum(int shootNum)
    {
        status.shootNum = shootNum;
    }
    
    public void SetIsAtSpot(bool isAtSpot)
    {
        status.atSpot = isAtSpot;
    }

    private void ShowPage()
    {
        switch (status.gameStatus)
        {
            case GameStatus.Playing:
                ShowPlayingPage();
                break;
            case GameStatus.GameOver:
                ShowGameoverPage();
                break;
        }
    }

    private void ShowGameoverPage()
    {
        GUI.Label(new Rect(Screen.width / 2 - 40, 60, 60, 100), "游戏结束!", playInfoStyle);
        
    }

    private void ShowPlayingPage()
    {
        
        GUI.Label(new Rect(10, 10, 60, 100), "正在游戏",playInfoStyle);

        if (status.atSpot)
        {
            // Draw the message with the new GUIStyle
            GUI.Label(new Rect(10, 50, 500, 100), "您已到达射击位,剩余射击次数:" + status.shootNum, playInfoStyle);
            GUI.Label(new Rect(10, 90, 500, 100), "您在该靶点的射击分数:" + status.spotScore, playInfoStyle);
        }
        GUI.Label(new Rect(10, 130, 500, 100), "游戏总分数:" + status.score, 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;

    }

}



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

public interface ISceneController
{
    void LoadResources();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;


public class Director : System.Object
{
    static Director _instance;
    // 关联场记实例
    public ISceneController CurrentSceneController { get; set; }
    // 获取当前的场记
    public static Director GetInstance()
    {
        if (_instance == null)
        {
            _instance = new Director();
        }
        return _instance;
    }

    public static void ReloadCurrentScene()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(currentSceneIndex);
    }
}

十二、蓄力条

蓄力条使用Slider组件,挂载在摄像机上
在这里插入图片描述

十三、最终效果

在这里插入图片描述

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>