游戏简介
地图上共有3个射箭关卡,每个关卡对应一个射击位,站在射击位即可开启关卡。每个关卡需要在特定时间内射击一定数量的靶子。每个关卡有三次机会,但若自知成功无望也可离开射击位以重置关卡(不消耗机会)。弓箭数量不限,但必须前一支箭落地或是击中目标才能发射下一支箭,因此对待高处目标需谨慎。同时,地图上也有三个隐藏的靶子,全部找到可改变结束的恭喜语句。

游戏操作
WASD:人物移动
Shift:长按跑步
鼠标移动:视角旋转
左键:长按为弓弩蓄力,松开即发射
Q:切换天空盒

演示视频
【3d游戏编程作业5——射箭小游戏】 https://www.bilibili.com/video/BV13M411Z7Mo/?share_source=cop...

实现思路
1、弓弩移动:
使用标准资源包中的FPS预设,将弓弩设为其子物体实现
2、蓄力动画:
使用混合动画树,将零蓄动画和满蓄动画、零蓄动画和发射动画分别进行混合,并用同一个变量drawstrength设置其混合比例,在代码中修改drawstrength的值,即可实现不同蓄力程度的动画。再设置Holding变量,为真时可从Empty向Fill转移,为假时可从Fill向Shoot转移。代码中还会根据长按时间修改动画速度,使得不同的蓄力时间有着不同的动画速度。
e148aac28d8ed137b02015122a84a92.png
Fill的混合树:
3b5355aee535c644eea35369fa80e26.png
Fire的混合树:
5643c3f58da98eaf88775157802e413.png
3、箭运动:
为实体箭赋予刚体组件。在发射动画结束时触发动画时间,隐藏动画箭的同时将实体箭移动到动画箭的位置,并根据drawstrength设置其初速度,即可实现箭的发射与运动
4、箭的碰撞:
为箭和被碰物体设置碰撞盒,在箭进入碰撞事件时,判断碰到物体的Tag,实现不同的操作。
5、天空盒切换:
修改Renderer.skybox
6、射击位实现:
射击位设置碰撞盒并开启is Trigger,触发OnTriggerIn和OnTriggerExit时判断对象tag是否为Player,并进行相应操作。

具体代码
1、SceneController:控制场景物体的启/禁用,并处理游戏通关逻辑

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using Unity.VisualScripting;
using UnityEngine;

public class SceneController : MonoBehaviour
{
    private bool[] placeState;
    private bool[] winflag;
    private bool[] hiddenflag;
    private int skystate;
    private Camera cam;
    private Material[] skybox;
    private GameObject player;
    private GameObject arrow;
    private Rigidbody arrig;
    private TrailRenderer arrtrail;

    // Start is called before the first frame update
    void Start()
    {
        placeState = new bool[3]{true,true,true};
        winflag = new bool[3]{false,false,false};
        hiddenflag = new bool[3]{false,false,false};
        skystate = 1;
        skybox = new Material[3]{Resources.Load("sunrise") as Material,
                                    Resources.Load("sunset") as Material,
                                    Resources.Load("night") as Material};
        RenderSettings.skybox = skybox[skystate];
        cam = Camera.main;
        player = Instantiate(Resources.Load<GameObject>("prefabs/player"),transform.position,Quaternion.identity);
        arrow = Instantiate(Resources.Load<GameObject>("prefabs/Arrow"),Vector3.zero,Quaternion.identity);
        arrig = arrow.GetComponent<Rigidbody>();
        arrow.SetActive(false);
        arrtrail = arrow.transform.gameObject.GetComponent<TrailRenderer>();
        
    }

    // Update is called once per frame
    void Update()
    {
        //切换天空盒
        if(Input.GetKeyDown(KeyCode.Q)){
            skystate = (skystate + 1) % 3;
            RenderSettings.skybox = skybox[skystate];
        }
    }

    public void Reset(){
        //重置胜负变量
        for(int i = 0;i<3;i++){
            placeState[i] = true;
            winflag[i] = false;
            hiddenflag[i] = false;
        }
        //重置天空盒
        skystate = 1;
        RenderSettings.skybox = skybox[skystate];
        //重启弓弩,玩家归位,箭矢隐藏
        player.transform.Find("FirstPersonCharacter").Find("Crossbow").gameObject.SetActive(true);
        arrow.SetActive(false);
        //重置关卡数据
        // GameObject root = GameObject.Find("Root");
        // foreach (Transform child in root.transform)
        // {
        //         child.gameObject.SetActive(true);
        //         child.GetComponent<LevelSet>().Reset();
        //         child.gameObject.SetActive(false);
        // }
        //重置触发站台状态
        GameObject.Find("startplace0").GetComponent<standplaceaction>().Reset();
        GameObject.Find("startplace1").GetComponent<standplaceaction>().Reset();
        GameObject.Find("startplace2").GetComponent<standplaceaction>().Reset();
        //重置gui关卡计数
        Singleton<UserGUI>.Instance.guifinishlevel = 0;
    }

    //在动画箭处生成实体箭
    public void ArrowSpawn(Vector3 pos, Quaternion rot,float drawstrength,Vector3 forw){
        if(arrow.activeSelf == false){
            arrow.SetActive(true);
            arrtrail.Clear();
            arrow.transform.position = pos;
            arrow.transform.rotation = rot;
            arrig.velocity = 50 * drawstrength * forw;
            arrtrail.enabled = true;
        }
    }

    //启动关卡
    public void ShootingStart(int number){
        if(placeState[number]){
            GameObject root = GameObject.Find("Root");
            GameObject level = root.transform.Find(number.ToString()).gameObject;
            level.SetActive(true);
            foreach (Transform child in level.transform)
            {
                child.gameObject.SetActive(true);
            }
            Singleton<UserGUI>.Instance.state = 1;
        }
    }

    //暂时关闭关卡
    public void ShootingEnd(int number){
        GameObject root = GameObject.Find("Root");
        GameObject level = root.transform.Find(number.ToString()).gameObject;
        level.GetComponent<LevelSet>().InitialTime();
        level.SetActive(false);
        Singleton<UserGUI>.Instance.state = 0;
    }
    //禁用关卡
    public void Shutdown(int number){
        GameObject root = GameObject.Find("Root");
        root.transform.Find(number.ToString()).gameObject.SetActive(false);
        //让射击位失活,站上去不再启动关卡
        GameObject.Find("startplace"+number.ToString()).GetComponent<standplaceaction>().ShutdownLevel();
        Singleton<UserGUI>.Instance.state = 0;
        placeState[number] = false;
        CheckIfEnd();
    }
    //射中隐藏靶子
    public void HitHiddenTarget(string name){
        Debug.Log(name + " hitted!");
        hiddenflag[(int)char.GetNumericValue(name[12])] = true;
    }
    //成功完成一关
    public void FinishLevel(int number){
        Singleton<UserGUI>.Instance.AddFinishLevel();
        Singleton<UserGUI>.Instance.state = 0;
        winflag[number] = true;
        Shutdown(number);
    }
    //检查是否结束游戏
    public void CheckIfEnd(){
        bool ifend = true;
        bool ifwin = true;
        bool iffoundhidden = true;
        foreach (bool state in placeState){
            if(state == true){
                ifend = false;
                break;
            }
        }
        if(ifend == true){
            foreach (bool flag in winflag){
                if(flag == false){
                    ifwin = false;
                    break;
                }
            }
        }
        //所有关卡被关闭,且所有关卡被成功完成
        if(ifend && ifwin){
            //判断是否所有隐藏靶子被找到
            foreach (bool flag in hiddenflag){
                if(flag == false){
                    iffoundhidden = false;
                    break;
                }
            }
            player.transform.Find("FirstPersonCharacter").Find("Crossbow").gameObject.SetActive(false);
            Singleton<UserGUI>.Instance.SetWinSentence(iffoundhidden);
            Singleton<UserGUI>.Instance.state = 2;
        }
        //所有关卡被关闭,但有关卡未被成功完成
        else if (ifend){
            player.transform.Find("FirstPersonCharacter").Find("Crossbow").gameObject.SetActive(false);
            Singleton<UserGUI>.Instance.state = 3;
        }
    }
}

2、shootaction:附着于十字弩对象,主要负责修改弩动画

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


public class shootaction : MonoBehaviour
{
    private Animator ani;
    public float fulltime;
    private float holdtime;
    private Transform crossbowtrans;
    private Transform arrowtrans;
 
    // Start is called before the first frame update
    void Start()
    {
        ani = GetComponent<Animator>();
        holdtime = 0;
        ani.SetFloat("drawstrength", 0);
        fulltime = 1;
        crossbowtrans = transform;
        //获取动画箭transform
        arrowtrans = crossbowtrans.Find("箭");
    }
 
    private void FixedUpdate()
    {
        //检测是否按下左键
        if(Input.GetButton("Fire1"))
        {
            //由于动画状态切换有延迟,为保证按下即可开始蓄力,不判断是否处于Fill状态,而是判断是否不在Fire状态
            if(!ani.GetCurrentAnimatorStateInfo(0).IsName("Fire")){
                arrowtrans.gameObject.SetActive(true);
                ani.SetBool("Holding",true);
            }
            //修改drawstrength
            if(holdtime < fulltime && !ani.GetCurrentAnimatorStateInfo(0).IsName("Fire"))
                holdtime += Time.deltaTime;
            ani.SetFloat("drawstrength", holdtime / fulltime);
            if(holdtime != 0){
                //修改动画时间,使得短按长按的动画时间不同,可以在放开鼠标的同时就发射。
                ani.speed = fulltime / holdtime;
            }
            else{
                ani.speed = 1;
            }
        }
        else
        {
            ani.SetBool("Holding", false);
            holdtime = 0;
        }
    }
    // Update is called once per frame

    //Fire动画结束时被调用,提供实体箭所需的位置、朝向、速度等信息,并隐藏动画箭
    public void Shoot()
    {
        Vector3 pos = arrowtrans.position;
        Quaternion rot = crossbowtrans.rotation;
        Vector3 forw = crossbowtrans.forward;
        Singleton<SceneController>.Instance.ArrowSpawn(pos,rot,ani.GetFloat("drawstrength"),forw);
        arrowtrans.gameObject.SetActive(false);
    }
}

3、arrowaction:附着于实体箭,主要负责箭的碰撞检测

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SocialPlatforms.Impl;

public class arrowaction : MonoBehaviour
{
    //箭的拖尾
    private TrailRenderer trail;
    // Start is called before the first frame update
    private SceneController sc;
    void Awake(){
        trail = transform.gameObject.GetComponent<TrailRenderer>();
        sc = Singleton<SceneController>.Instance;
    }

    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    private void OnCollisionEnter(Collision collision)
    {
        // 检查碰撞对象是否是地面
        if (collision.gameObject.CompareTag("Ground"))
        {
            // 在碰撞到地面时禁用箭
            DisableArrow();
        }
        //检测碰撞对象是否为普通靶子
        if (collision.gameObject.CompareTag("Target"))
        {
            Debug.Log("Hit Target!");
            //将对应关卡的剩余靶子数-1
            if(collision.gameObject.transform.parent != null){
                collision.gameObject.transform.parent.parent.GetComponent<LevelSet>().lasttargetnum -= 1;
            }
            //让被射中靶子消失
            collision.gameObject.transform.parent.gameObject.SetActive(false);
            DisableArrow();
        }
        //检测碰撞对象是否为隐藏靶子
        if (collision.gameObject.CompareTag("HiddenTarget"))
        {
            if(sc == null)
                sc = Singleton<SceneController>.Instance;
            Debug.Log(collision.gameObject.name);
            sc.HitHiddenTarget(collision.gameObject.name);
            collision.gameObject.SetActive(false);
            DisableArrow();
        }
    }
    private void DisableArrow()
    {
        //禁用拖尾
        trail.enabled = false;
        
        gameObject.SetActive(false);
    }
}

4、LevelSet:附着于关卡对象(孩子有多个普通靶子的空对象,每关对应一个),负责控制本关的参数。

using System.Collections;
using System.Collections.Generic;
using UnityEditor.SearchService;
using UnityEngine;

public class LevelSet : MonoBehaviour
{
    //设定总靶数和剩余靶数
    [SerializeField]
    public int targetnum;
    public int lasttargetnum;
    //机会数
    [SerializeField]
    private int chancenum;
    private int lastchance;
    //时间
    [SerializeField]
    public float time;
    private float lasttime;
    //触发器,用于触发gui时间的更新
    private float trigger;
    //关卡编号
    [SerializeField]
    private int number;
    private SceneController sc;
    private UserGUI gui;
    // Start is called before the first frame update
    void Awake(){
        lastchance = chancenum;
    }
    //每次启用都被调用
    void OnEnable()
    {
        Debug.Log("Start!");
        lasttime = time;
        lasttargetnum = targetnum;
        sc = Singleton<SceneController>.Instance;
        trigger = 0;
        gui = Singleton<UserGUI>.Instance;
        gui.UpdateTime(lasttime);
        gui.UpdateChance(lastchance);
    }

    // Update is called once per frame
    void Update()
    {
            lasttime -= Time.deltaTime;
            trigger += Time.deltaTime;
            if(trigger >= 1){
                gui.UpdateTime(lasttime);
                trigger = 0;
            //所有靶子被击倒,成功完成该关
            }
            if(lasttargetnum <= 0){
                //在禁用关卡前就重置关卡,若在按Reset按钮的重新启用后再调用该函数,会因update函数先于该Reset函数执行而出错
                Reset();
                sc.FinishLevel(number);
            }
            //关卡超时
            if(lasttime <= 0){
                lastchance--;
                //立即重置时间,避免重复减少机会数
                InitialTime();
                gui.UpdateChance(lastchance);
                //机会数为0,禁用关卡
                if(lastchance <= 0){
                    if (sc == null)
                        sc = Singleton<SceneController>.Instance;
                    //与上一个Reset原因相同
                    Reset();
                    sc.Shutdown(number);
                    return;
                }
                if (sc == null)
                    sc = Singleton<SceneController>.Instance;
                //暂时关闭关卡
                sc.ShootingEnd(number);
            }

    }
    public void InitialTime(){
        lasttime = time;
    }

    public void Reset(){
        lastchance = chancenum;
        //被主动disable的子对象对象无法跟随父对象一起enable,因此需手动重启
        foreach (Transform child in transform)
        {
            child.gameObject.SetActive(true);
        }
    }
}

5、standplaceaction:附着于射击位,负责检测玩家的进入和退出,并据此启动/暂时关闭对应关卡

using System.Collections;
using System.Collections.Generic;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

public class standplaceaction : MonoBehaviour
{
    private SceneController sc;

    //射击位编号,对应关卡位编号
    [SerializeField]
    private int number;

    //射击位被激活和未激活的材质,分别为白色和红色
    private Material active;
    private Material unactive;

    //修改材质用
    private Renderer ren;

    //用于使射击位赋活/失活
    private bool isactive;
    // Start is called before the first frame update
    void Start()
    {
        sc = Singleton<SceneController>.Instance;
        active = Instantiate(Resources.Load<Material>("Material/active"));
        unactive = Instantiate(Resources.Load<Material>("Material/unactive"));
        ren = GetComponent<Renderer>();
        ren.material = unactive;
        isactive = true;
    }

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

    //检测Player进入射击位
    private void OnTriggerEnter(Collider other)
    {
        if(isactive){
            if (other.CompareTag("Player"))
            {
                ren.material = active;
                if(sc == null)
                    sc = Singleton<SceneController>.Instance;
                sc.ShootingStart(number);
            }
        }
    }

    //检测Player离开射击位
    private void OnTriggerExit(Collider other){
        if(isactive){
            if (other.CompareTag("Player"))
            {
                ren.material = unactive;
                if(sc == null)
                    sc = Singleton<SceneController>.Instance;
                sc.ShootingEnd(number);
            }
        }
    }

    //使射击位失活
    public void ShutdownLevel(){
        isactive = false;
        ren.material = unactive;
    }

    //使射击位赋活
    public void Reset(){
        isactive = true;
    }
}

6、UserGUI:负责关卡信息与游戏结束信息显示

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

public class UserGUI : MonoBehaviour
{
    public int state;//0为普通状态显示,1为关卡中显示,2为胜利显示,3为失败显示
    private int width = Screen.width / 5;
    private int height = Screen.height / 15;
    private GUIStyle titleStyle;
    private int guiChance;
    private int guiTime;
    public int guifinishlevel;
    private string winsentence;

    // Start is called before the first frame update
    void Start()
    {
        state = 0;
        titleStyle = new GUIStyle();
        titleStyle.normal.textColor = Color.black;
        titleStyle.normal.background = null;
        titleStyle.fontSize = 50;
        titleStyle.alignment = TextAnchor.MiddleCenter;
        guiChance = 0;
        guiTime = 20;
        guifinishlevel = 0;
        winsentence = "But there might be something else...";
    }


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

    void OnGUI(){
        if(state == 0){
            GUI.Label(new Rect(0,Screen.height - height,width,height),"Finish Level: "+ guifinishlevel.ToString() + "/3");
        }
        if (state == 1){
            GUI.Label(new Rect(Screen.width / 2,0,width,height),"Left Time: "+ guiTime.ToString());
            GUI.Label(new Rect(Screen.width / 2 - width,0,width,height),"Left Chance: "+guiChance.ToString());
        }
        if(state == 2){
            GUI.Label(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 0.1f, width, height), "You Win!", titleStyle);
            GUI.Label(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 0.1f + height, width, 2 * height), winsentence);
            bool button = GUI.Button(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 3 / 7, width, height), "Reset");
            if(button){
                state = 0;
                Singleton<SceneController>.Instance.Reset();
            }
        }
        if(state == 3){
            GUI.Label(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 0.1f, width, height), "You Lose!", titleStyle);
            bool button = GUI.Button(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 3 / 7, width, height), "Reset");
            if(button){
                Singleton<SceneController>.Instance.Reset();
            }
        }

    }

    //更新关卡剩余时间
    public void UpdateTime(float lasttime){
        guiTime = (int)lasttime;
    }

    //更新关卡剩余机会显示
    public void UpdateChance(int lastchance){
        guiChance = lastchance;
    }
    //增加完成关卡数
    public void AddFinishLevel(){
        guifinishlevel++;
    }

    //设置隐藏靶相关语句
    public void SetWinSentence(bool iffoundhidden){
        if(iffoundhidden)
            winsentence = "And you found all the hidden target!";
        else
            winsentence = "But there might be something else...";
    }
}

礼貌的羽毛球
1 声望0 粉丝