unity 打靶游戏

设计规则

  • 地形:使用地形组件,上面有草、树;
  •  天空盒:使用天空盒,天空可随玩家是否到达射击点来切换天空盒;
  •  固定靶:有一个以上固定的靶标;
  •  运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
  •  射击位:地图上共两个射击位,仅在射击位附近可以拉弓射击,每个位置有5 次机会;
  •  驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
  •  游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
  •  碰撞与计分:在射击位,射中靶标的相应分数,规则自定;

游戏规则

动作按键

w s a d 为方向键

鼠标右键为拉弓,左键为射击

鼠标上下左右移动控制视角变化

按住shift 移动加速

游戏指引

通过地上的箭头指示下一个射击点的位置,引导玩家前往射击点。

记分规则

固定靶外靶1分,靶心2分;移动靶外靶3分,靶心4分。

部分资源和代码参考

天空盒资源使用Unity资源商店的Fantasy Skybox FREE,树资源使用Unity资源商店的SimpleNaturePack。

角色使用Unity资源商店的RyuGiKen的免费十字弩资源。

角色控制代码设计参考视频:

【Unity3D】 教你如何做一个第一人称视角的角色移动_哔哩哔哩_bilibili

游戏指引中的设计参考博客:

【Unity3D日常开发】Unity3D中实现箭头指向目标点的效果_unity引导路径箭头shader_恬静的小魔龙的博客-CSDN博客

靶子设计

外靶和靶心都用Mesh Collider实现碰撞体。

预制体scene

动画设计

蓄力射击动画

如下,除了Shoot后自动返回Empty,其他都用触发器控制。给十字弓使用。

half为Brend Tree,包括Empty和Fill,blend为0.5,如下:

移动动画

缓慢的来回移动。第一射击点,MoveTarget使用。

旋转动画

旋转往复,停止旋转和旋转间用触发器触发跳转。第二射击点,五个RotateTarget使用。

代码实现

角色部分

角色控制

角色提前添加CharacterController组件,该脚本挂载到角色上,手动设置移动速度等属性进行调试。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Schema;
using UnityEngine;

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

    private CharacterController cc;

    public float movespeed;

    public float jumpspeed;


    private float h_move, v_move;

    private Vector3 dir;

    public float gravity;
    private Vector3 velocity;

    public Transform groundCheck;

    public float checkRadius;

    public LayerMask groundLayer;
    public LayerMask shootLayer;

    private bool isGround;

    private Animator ani;

    GameObject arrow;
    // 导入箭预制件
    public GameObject arrowPrefab;
    // 箭发射点
    public Transform arrowBegin;
    // 射箭用力大小
    private float force;
    // 判断动画切换
    string curStateName,lastStateName;
    // 射击允许
    private bool shootAdmitted;
    // 天空
    public Material sky1;
    public Material sky2;

    FirstSceneController controller;

    GameObject playerCamera;

    void Start()
    {
        cc = GetComponent<CharacterController>();
        ani = GetComponent<Animator>();
        arrow = transform.Find("箭").gameObject;
        playerCamera = transform.Find("第一人称视角").gameObject;
        lastStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
        shootAdmitted =false;
        controller = SSDirector.GetInstance().currentSceneController as FirstSceneController;
    }

    // Update is called once per frame
    void Update()
    {
        // 实现角色相机和主游戏相机切换
        if (controller.GetState() == (int)GameState.Ready || controller.GetState() == (int)GameState.GameOver)
        {
            playerCamera.SetActive(false);
            return;
        }
        else
        {
            playerCamera.SetActive(true);
        }
        // 角色落地
        if (isGround && velocity.y<0)
        {
            velocity.y = -2f;
        }
        // 到达射击点
        shootAdmitted = Physics.CheckSphere(groundCheck.position, checkRadius, shootLayer);
        // 申请射击许可
        if(shootAdmitted)controller.setGameState((int)GameState.Shooting);
        else controller.setGameState((int)GameState.Play);

        // 到达或离开射击点后,天空盒转换
        if (controller.checkShoot())
        {
            transform.Find("第一人称视角").gameObject.GetComponent<Skybox>().material = sky2;
        }
        else
        {
            transform.Find("第一人称视角").gameObject.GetComponent<Skybox>().material = sky1;
        }
        // 如果按住shift键,加速
        if(Input.GetKey("left shift"))
        {
            movespeed = 15;
        }
        else
        {
            movespeed = 5;
        }
        // 当前动画如果是Blend Tree(half),取half第二个动画名Fill
        if (ani.GetCurrentAnimatorClipInfoCount(0) > 1)
        {
            curStateName = ani.GetCurrentAnimatorClipInfo(0)[1].clip.name;
        }
        else
        {
            curStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
        }
        // 蓄力,自带的箭可见;射出,自带的箭不可见
        if (curStateName == "Fill")
        {
            arrow.SetActive(true);
        }else if (curStateName == "Shoot")
        {
            arrow.SetActive(false) ;
        }

        // 右键触发蓄力,力量加10
        if (Input.GetButtonDown("Fire2")&& controller.checkShoot())
        {
            ani.SetTrigger("fill");
            force += 10;
        }
        // 限制力量最大为20
        force = Mathf.Clamp(force, 0f, 20f);
        // 左键触发射击,射击成功将生成一支箭并射出
        if (Input.GetButtonDown("Fire1") && controller.checkShoot())
        {
            if(lastStateName == "Fill" || lastStateName == "Hold")
            {
                GameObject new_arrow = Instantiate<GameObject>(arrowPrefab, arrowBegin.position, transform.localRotation);

                new_arrow.GetComponent<Rigidbody>().AddForce(transform.forward * force);

                controller.Shooted();
                force = 0;
            }
            ani.SetTrigger("shoot");

        }

        // lastStateName更新最新动画状态
        if(curStateName != lastStateName)
        {
            lastStateName = curStateName;
        }
        // 前后左右移动
        h_move = Input.GetAxis("Horizontal")*movespeed;
        v_move = Input.GetAxis("Vertical")*movespeed;
        dir = transform.forward * v_move + transform.right * h_move;

        cc.Move(dir*Time.deltaTime);

        isGround = Physics.CheckSphere(groundCheck.position, checkRadius,groundLayer);
        // 下落
        velocity.y -=  gravity*Time.deltaTime;
        cc.Move(velocity*Time.deltaTime);

    }

    // 是否到达射击点
    public bool arrived()
    {
        if (shootAdmitted)
            return true;
        return false;
    }

    // 所有射击点的箭都用完
    public bool ArrowNull()
    {
        return controller.ArrowNull();
    }
}

视野控制

挂载在角色的相机子对象“第一人称视角”上,提前设计好该对象的位置,手动设置鼠标灵敏度等属性。

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

public class cameraController : MonoBehaviour
{
    // Start is called before the first frame update
    public Transform player;

    private float mouseX, mouseY;

    public float mouseSensitivity;

    public float xRotation;
    public float yRotation;

    private void Update()
    {
        mouseX = Input.GetAxis("Mouse X")*mouseSensitivity*Time.deltaTime;
        mouseY = Input.GetAxis("Mouse Y")*mouseSensitivity*Time.deltaTime;

        xRotation += mouseX;
        yRotation -= mouseY;

        yRotation = Mathf.Clamp(yRotation, -50f, 50f);

        player.localRotation= Quaternion.Euler(yRotation, xRotation, 0);
    }
}

箭与靶子的碰撞检测

挂载在箭预制件上,检测箭是否与靶子碰撞,并控制靶子的动画变化。

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

public class coll : MonoBehaviour
{


    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag.CompareTo("target_1") == 0)
        {
            /*插在固定靶上*/
            this.gameObject.GetComponent<Rigidbody>().isKinematic = true;
            singleton<ScoreRecorder>.Instance.record(1);// 1分
        }
        else if (collision.gameObject.tag.CompareTo("target_2") == 0)
        {
            /*插在固定靶心上*/
            this.gameObject.GetComponent<Rigidbody>().isKinematic = true;
            singleton<ScoreRecorder>.Instance.record(2);// 2分
        }
        else if (collision.gameObject.tag.CompareTo("target_move_1") == 0)
        {
            /*插在移动靶*/
            this.gameObject.GetComponent<Rigidbody>().isKinematic = true;// 运动学
            this.gameObject.transform.parent = collision.gameObject.transform;
            //collision.gameObject.GetComponent<Rigidbody>().AddForce(this.gameObject.GetComponent<Rigidbody>().velocity);
            singleton<ScoreRecorder>.Instance.record(3);// 3分
            // 旋转靶 停止,再次射中就重新旋转
            if (collision.transform.parent.gameObject.name[0].CompareTo('R') == 0)
            {
                Animator ani = collision.transform.parent.gameObject.GetComponent<Animator>(); 
                string curStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
                Debug.Log(curStateName);
                if (curStateName=="Rotate")
                    ani.SetTrigger("end");
                else
                    ani.SetTrigger("start");
            }

        }
        else if (collision.gameObject.tag.CompareTo("target_move_2") == 0)
        {
            /*插在移动靶靶心*/
            this.gameObject.GetComponent<Rigidbody>().isKinematic = true;// 运动学
            this.gameObject.transform.parent = collision.gameObject.transform;
            singleton<ScoreRecorder>.Instance.record(4);// 4分
            // 旋转靶 停止,再次射中就重新旋转
            if (collision.transform.parent.gameObject.name[0].CompareTo('R') == 0)
            {
                Animator ani = collision.transform.parent.gameObject.GetComponent<Animator>();// 靶心
                string curStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
                Debug.Log(curStateName);
                if (curStateName == "Rotate")
                    ani.SetTrigger("end");
                else
                    ani.SetTrigger("start");
            }
        }
        else
        {
            Destroy(gameObject);
        }
    }


    // Start is called before the first frame update
    void Start()
    {
        Color color = (Random.Range(1, 4) == 1) ? Color.green : (Random.Range(1, 4) == 2) ? Color.red : Color.blue;

        GameObject.Find("MoveTrack").GetComponent<TrailRenderer>().startColor = color;
    }

    // Update is called once per frame
    void Update()
    {
        /*需要改成回收*/
        if(gameObject.transform.position.y <-10)
            Destroy(gameObject);
    }
}

路径显示

挂载在地形对象Map上,使用箭头图形材质球,points列表首位为玩家,第二则是射击点对象。只有角色没到射击点并且没有用完所有射击点的箭时,画箭头路径,指向当前射击点。

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

/// <summary>
/// 导航箭头
/// </summary>
public class NavPathArrow : MonoBehaviour
{
    public MeshRenderer meshRenderer;//箭头3D对象Quad
    public List<Transform> points = new List<Transform>();//路径点
    private List<MeshRenderer> lines = new List<MeshRenderer>();//显示的路径

    public float xscale = 1f;//缩放比例
    public float yscale = 1f;

    bool draw = true;

    void Start()
    {
        //箭头宽度缩放值
        xscale = meshRenderer.transform.localScale.x;
        //箭头长度缩放值
        yscale = meshRenderer.transform.localScale.y;
    }

    //画路径
    public void DrawPath()
    {
        if (points == null || points.Count <= 1)
            return;
        for (int i = 0; i < points.Count - 1; i++)
        {
            DrawLine(points[i].position, points[i + 1].position, i);
        }
    }

    //画路径 参数为路径点数组
    public void DrawPath(Vector3[] points)
    {
        if (points == null || points.Length <= 1)
            return;
        for (int i = 0; i < points.Length - 1; i++)
        {
            DrawLine(points[i], points[i + 1], i);
        }
    }

    //隐藏路径
    public void HidePath()
    {
        for (int i = 0; i < lines.Count; i++)
            lines[i].gameObject.SetActive(false);
    }

    //画路径
    private void DrawLine(Vector3 start, Vector3 end, int index)
    {
        //Debug.Log(transform.gameObject.name);
        MeshRenderer mr;
        if (index >= lines.Count)
        {
            mr = Instantiate(meshRenderer);
            lines.Add(mr);
        }
        else
        {
            mr = lines[index];
        }

        var tran = mr.transform;
        var length = Vector3.Distance(start, end);
        tran.localScale = new Vector3(xscale, length, 1);
        start.y = end.y;
        tran.position = (start + end) / 2;
        //指向end
        tran.LookAt(end);
        //旋转偏移
        tran.Rotate(90, 0, 0);
        mr.material.mainTextureScale = new Vector2(1, length * yscale);
        mr.gameObject.SetActive(true);
    }

    void Update()
    {
        if (draw)
        {
            HidePath();
            DrawPath();
        }
        else
        {
            HidePath();
        }
        // 没到目的地且所有射击点的箭还没射完就画线
        if (!points[0].GetComponent<playerController>().arrived() && !points[0].GetComponent<playerController>().ArrowNull())
        {
            draw = true;
        }
        else
        {
            draw = false;
        }
    }
}

场景控制

FirstController

实现场景控制,包括计分器、场景加载,并且实现用户界面接口方法,挂载在一直存在的一个相机对象上。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UIElements;

enum GameState
{
    Ready = 0, Play = 1, Shooting = 2, GameOver = 3
};

enum scene_children
{
    player = 0,target = 1, shootpoint = 2, environment = 3
}

public class FirstSceneController : MonoBehaviour, ISceneController,IUserAction
{
    ScoreRecorder scoreRecorder;
    int arrowNum = 0;

    int game_state = 0;
    private GameObject shootPoint;
    private GameObject scene;

    private List<Vector3> shootPointPositions = new List<Vector3>();
    private int shootPointNo;
    private void Awake()
    {
        SSDirector director = SSDirector.GetInstance();
        director.currentSceneController = this;
        scoreRecorder = gameObject.AddComponent<ScoreRecorder>() as ScoreRecorder;

        shootPointPositions.Add(new Vector3(0.1f, 0.1f, 9.1f));
        shootPointPositions.Add(new Vector3(5.4f, 0.1f, -22.5f));
        shootPointNo = 0;
    }

    public void GameStart()
    {
        LoadResources();
        if(game_state == 0)
        {
            game_state = (int)GameState.Play;
            shootPointNo = 0;
            if (shootPointNo < shootPointPositions.Count)
                shootPoint.transform.position = shootPointPositions[shootPointNo];
        }
    }

    public void LoadResources()
    {
        /*加载地形*/
        scene = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/scene"), Vector3.zero, Quaternion.identity);
        shootPoint = scene.transform.GetChild((int)scene_children.shootpoint).gameObject;
        this.gameObject.GetComponent<Camera>().targetDisplay = 1;
    }

    public void Restart()
    {
        /*开始状态*/
        setGameState(0);
        /*得分恢复*/
        scoreRecorder.set_zero();
    }

    public void End()
    {
        setGameState(3);
        this.gameObject.GetComponent<Camera>().targetDisplay = 0;

    }


    public int GetState()
    {
        /*返回当前游戏状态*/

        return game_state;
    }

    public int GetScore()
    {
        /*获取计分器的分数*/
        return scoreRecorder.get_score();
    }

    public int GetArrowNum()
    {
        /*获取箭数*/
        if(game_state == (int)GameState.Shooting)
            return arrowNum;
        return 0;
    }


    public bool checkShoot()
    {
        if (game_state != (int)GameState.Shooting || arrowNum == 0)
            return false;
        else
            return true;
    }


    public void setArrowNum(int num)
    {
        if(!checkShoot())
            arrowNum = num;
    }

    public void Shooted()
    {
        arrowNum -= 1;
    }

    public void setGameState(int i)
    {
        if (i == 0 && game_state == (int)GameState.GameOver)
        {
            game_state = (int)GameState.Ready;
        }
        else if(i == 2&&game_state == (int)GameState.Play && shootPointNo < shootPointPositions.Count)
        {// 如果游戏开始 且 进入新射击点
            game_state = (int)GameState.Shooting;
            setArrowNum(5);
        }
        // 开始射击
        else if(i == 1&&game_state == (int)GameState.Shooting)
        {
            game_state = (int)GameState.Play;
        }
        //游戏结束
        else if (i == 3)
        {
            game_state = (int)GameState.GameOver;
            scene.SetActive(false);
        }

    }
    // 所有射击点的箭都用完
    public bool ArrowNull()
    {
        return shootPointNo >= shootPointPositions.Count;
    }

    private void Update()
    {
        // 射击完后
        if (game_state == (int)GameState.Shooting && arrowNum == 0 )
        {
            game_state = (int)GameState.Play;
            // 移动射击点
            shootPointNo += 1;
            if (shootPointNo < shootPointPositions.Count)
                shootPoint.transform.position = shootPointPositions[shootPointNo];

            
        }
        //Debug.Log(game_state);

    }
}

SSDirector

导演类,帮助获取场景控制器等对象单例。

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

public class SSDirector : System.Object
{
    private static SSDirector instance;
    public ISceneController currentSceneController { get; set; }
    public static SSDirector GetInstance()
    {
        if (instance == null)
        {
            instance = new SSDirector();
        }
        return instance;
    }
}

单例模板类

由于直接获取唯一的计分器等。

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

public class singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    protected static T instance;
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));
                if (instance == null)
                {
                    Debug.LogError("An instance of " + typeof(T)
                        + " is needed in the scene, but there is none.");
                }
            }
            return instance;
        }
    }
}

ScoreRecorder

计分器,负责分数管理

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

public class ScoreRecorder : MonoBehaviour
{
    // Start is called before the first frame update
    int score;

    void Start()
    {
        score = 0;
    }

    public void set_zero()
    {
        score = 0;
    }

    public void record(int sc) { score += sc; }
    public int get_score() { return score; }
}

UserGUI玩家界面

挂载在一直存在的相机对象上,实现各个游戏状态的用户界面显示,额外显示加分提示作为得分反馈。

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

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    private bool game_start;
    private GUIStyle style = new GUIStyle();
    private GUIStyle style2 = new GUIStyle();
    private GUIStyle style3 = new GUIStyle();
    private GUIStyle style4 = new GUIStyle();// 规则字体
    FirstSceneController controller;

    public int time;// 加分提示显示
    int time_count;// 加分显示计时
    int pre_score;// 加分前得分
    string rules;// 规则
    // Start is called before the first frame update
    void Start()
    {
        action = SSDirector.GetInstance().currentSceneController as IUserAction;
        game_start = false;
        style.normal.textColor = Color.red;
        style.fontSize = 16;
        style2.normal.textColor = new Color(1, 1, 1);
        style2.fontSize = 25;
        style3.normal.textColor = new Color(0, 1, 0);
        style3.fontSize = 17;
        style4.normal.textColor = Color.black;
        style4.fontSize = 17;
        pre_score = 0;
        rules = "w s a d 为方向键\n鼠标右键为拉弓\n鼠标左键为射击\n按住shift 移动加速\n固定靶外靶1分,靶心2分\n移动靶外靶3分,靶心4分";

        controller = SSDirector.GetInstance().currentSceneController as FirstSceneController;
    }

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

    private void OnGUI()
    {
        // 提示计时
        if (controller.GetScore() > pre_score && time_count < 0)
        {
            time_count = 0;
        }
        if(time_count >= 0)
        {
            time_count++;
        }
        if (time_count > time)
        {
            pre_score = controller.GetScore();
            time_count = -1;
        }
        if (controller.GetState() == (int)GameState.GameOver)
        {
            game_start=false;
            pre_score = 0;
            GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 3, 200, 100), "游戏结束\n你的得分是" + action.GetScore().ToString(), style2);
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 , 100, 50), "重新开始"))
            {
                controller.Restart();
            }
        }
        else if (game_start)
        {
            GUI.Label(new Rect(10, 5, 200, 50), "分数:", style);
            GUI.Label(new Rect(55, 5, 200, 50), pre_score.ToString(), style);
            GUI.Label(new Rect(85, 5, 200, 50),((action.GetScore()-pre_score>0)? "+"+(action.GetScore() - pre_score).ToString():""), style3);
            GUI.Label(new Rect(Screen.width-100, 30, 200, 50), "箭支数: ", style);
            GUI.Label(new Rect(Screen.width-50, 30, 200, 50), action.GetArrowNum().ToString(),style);
            GUI.Label(new Rect(10, Screen.height-150, 200, 200), rules, style4);

            if (GUI.Button(new Rect(Screen.width-100, 0, 100, 30), "结束游戏"))
            {
                action.End();
                return;
            }
        }
        else
        {
            GUI.Label(new Rect(Screen.width / 2 - 80, Screen.height / 3, 100, 100), "Arrow Shooting", style2);
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 , 100, 50), "游戏开始"))
            {
                game_start = true;
                controller.GameStart();
            }
        }

    }
}

接口类

场景控制接口和用户动作接口

using System;

public interface ISceneController
{
    void LoadResources();
}

public interface IUserAction
{
    void End();
    void Restart();
    int GetState();
    int GetScore();
    int GetArrowNum();
}

游戏演示

大作业 - 打靶游戏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值