3D游戏建模与设计大作业:基于Unity平台编写游戏:打靶游戏(Target Games)

1.项目要求

  • 基础分(2分):有博客;
  •  1-3分钟视频(2分):视频呈现游戏主要游玩过程;
  •  地形(2分):使用地形组件,上面有草、树;
  •  天空盒(2分):使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
  •  固定靶(2分):有一个以上固定的靶标;
  •  运动靶(2分):有一个以上运动靶标,运动轨迹,速度使用动画控制;
  •  射击位(2分):地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
  •  驽弓动画(2分):支持蓄力半拉弓,然后 hold,择机 shoot;
  •  游走(2分):玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
  •  碰撞与计分(2分):在射击位,射中靶标的相应分数,规则自定;

2.游戏简介

  打靶游戏为第一人称射击游戏,玩家通过长按鼠标左键实现拉弓,松开坐标鼠标左键则弓箭飞出。玩家通过鼠标的移动实现瞄准,通过按键盘的WSAD或上下左右四个方向键实现在地图内的移动。此外,为了营造有趣的射击环境,玩家可以按C键实现正午和夕阳模式下的天空盒的切换。游戏中有5个射击位,玩家只有在射击位上才能进行拉弓射击。游戏中有2个固定靶和3个移动靶,靶子分为红色靶心和白色靶白,击中不同颜色区域有不同的得分,移动靶在树木左右移动,难度较固定靶更大。

3.游戏规则

1)击中移动靶靶心得3分,靶白得2分。

2)击中固定靶靶心得2分,靶白得1分。

3)游戏中有5个射击位,只有在射击位上可以进行射击,每个射击位有5次射击机会。

4)当地图上所有射击位的所有射击次数被用光时,即用完25次射击次数,游戏结束。

4.游戏编写

  本游戏基于Unity引擎编写,使用的系统为Windows10,使用的脚本文件语言为C#,首先需要确保保证电脑中安装Unity引擎,我的版本是2023.3.8f1c1,还需下载VSCode或VStudio用于编写代码,还需要Unity的Plastic SCM用于管理代码,具体安装方法这里不过多介绍。

接着我们打开Unity,找到项目,选择新项目。

选择3D,项目命名为Target Games,保存地址我选择的是D盘。(启动版本管理可选可不选)。

本项目中运用到的代码、文件非常多,为了保证界面的简洁性,建议在Assests项目栏中如下图建

立几个文件夹方便管理。

 其中只有Prefabs、Scenes、Scripts、Materials和Terrain文件夹是我们要自己处理的,其它的只要需要从Assets Store中导入。

建立方法如下:在Assets栏中空白区用鼠标右键点击,选择Create中的Folder生成我们需要的游戏文件。

然后去Assets Store中导入资源,在Window中选择Asset Store。

然后点击Search online,程序会自动跳转到浏览器。

首先导入天空盒,选择添加至我的资源。

 然后同理导入弓弩和树。

 然后在Window中选择Package Manager,Unity会弹出Package Manager窗口。

 Package Manager窗口中选择My Assets。我们以导入Classical Crobow为例,选择Import。

全选后选择Import。

这样就成功导入资源,对于树木以及天空盒也同理。 

接着我们可以布置地形,布置地形、树木和草地的具体方式可以在这些链接中的教程进行。

【Unity3D】地形Terrain - 知乎 (zhihu.com)

如何在地形上绘制草丛和树木 - 技术问答 - Unity官方开发者社区

在Unity中 改变地形(Terrain),并加上水面、树、草地、材质(地板上色)_unity创建地形山草树水房子-CSDN博客

 我的地形图如下所示,其中白色长条为射击位。

为了实现玩家不会碰到靶子这一要求,可以在靶子四个方向周围设置透明的长方体Cube,但记住Cube的高度不要太高,以免射出的弓箭碰到Cube。

现在我们观察下靶子,游戏中靶子如图所示,有三片区域,红色靶心、白色区域和黑色区域,击中不同区域有不同的得分。

对于移动靶,我们需要自己制作动画,我们制作了一个名称为text的动画,具体制作过程可以·参考这个教程:Unity动画系统详解1:在Unity中如何制作动画? - 知乎 (zhihu.com) 

注意该动画只有能移动一次,至于如何来回重复移动我们需要使用脚本TargetMove来实现。

此外我们还需要制作了名称为Text的Animator。

 制作完动画后需要放入Animator,连线方式为右键动画,选择Make Transition然后选择你需要链接到的状态。

另外我们还设置了射击位,玩家在射击位中才能进行射击,如图所示:

同样为了防止玩家走出地图,我们用四个透明的大Cube来实现空气墙效果。

对于拉弓,老师课程上演示说使用Blend Tree实现,但课后我觉得十分难使用,于是制作了另外一个动画Energy Storage,这个动画事实上只是为了保持拉弓的状态,并不用设计动画的画面帧。

然后将动画按照如图所示放入Animator中,该Animator有3个trigger,一个为pull,一个为shooting,一个是hold,还有两个float,一个是Blend,一个holdTime,具体如下图所示。

其中,new hold状态为一个blend tree,由自带的动画Empty和Hold混合而成。

此外,我们还需要设置状态机的转换条件,其中Empty状态跳转到new hold状态的条件为Holding==false。

 new hold状态跳转到shoot状态的条件为shooting==false。

Shoot状态跳转到Empty状态无需额外的条件。

 我们还使用了UGUI来显示游戏得分、击中位置、游戏结束。游戏标题等信息,还在canvas中添加了Restart按钮重启游戏。

对于天空盒的布置,直接拉入Scene中即可。

我们刚刚编写的游戏界面场景Main,此外我们还要创建开始界面场景Start。

创建过程如图所示:

 在Start中创建画布canvas,如图所示:

 画布的canvas布置可以按照参考这个教程:【精选】【Unity3D-UGUI系列】(一)Canvas 画布组件详解_unity为什么显示画布内容了-CSDN博客

然后我的Start场景中布置如下:

 我们的场景布置完成,现在开始编写脚本代码,在Scripts文件夹中有这些文件夹。

我们先来实现视角移动,首先将弓弩拉入场景并命名为player,然后将主相机Main Camera移动到Player作为Player的子类。

 然后我们用脚本CameraMove来实现用鼠标进行视角360度移动,具体代码如下:

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

//鼠标控制视角
public class CameraMove : MonoBehaviour
{
    //鼠标x轴灵敏度
    public float mouseXSensitivity = 80f;
    //人物
    private Transform player;
    //旋转角度
    float xRotation = 0f;

    private void Start()
    {
        player = transform.parent.transform;
    }


    // Update is called once per frame
    void Update()
    {
        float mouseX = Input.GetAxis("Mouse X") * mouseXSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseXSensitivity * Time.deltaTime;
        xRotation -= mouseY;
        //y轴最大旋转角度为正负90;
        xRotation = Mathf.Clamp(xRotation, -45f, 10f);
        transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
        player.Rotate(Vector3.up * mouseX);
    }
}

记得把CameraMove脚本挂到Main Camera上。

然后我们还需要实现玩家的移动,玩家可以通过WSAD或者方向键进行地图内的四个方向运动,我们用脚本PlayerMove实现,具体代码如下:

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

public class PlayerMove : MonoBehaviour
{
    //人物控制器
    private CharacterController controller;
    //人物移动速度
    public float speed = 2f;
    public float gravity = -15f;
    Vector3 velocity;

    private void Start()
    {
        controller = GetComponent<CharacterController>();
    }


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


    public void Move()
    {
        //键盘输入
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        Vector3 move = transform.right * x + transform.forward * z;

        controller.Move(move * speed * Time.deltaTime);

        velocity.y += gravity * Time.deltaTime;

        controller.Move(velocity * Time.deltaTime);
    }
}

记得把PlayerMove脚本挂到Player上。

然后我们通过TargetMove脚本实现移动靶的来回移动,由于我们做的Text动画只有一个方向的单次移动,我们需要用代码实现靶子在水平方向上的多次来回移动,具体代码如下:

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

public class TargetMove : MonoBehaviour
{
    public float speed = 5f; // 物体移动的速度
    public float distance = 10f; // 物体移动的距离

    private Vector3 startPosition;
    private float direction = 1f;

    void Start()
    {
        startPosition = transform.position;
    }

    void Update()
    {
        // 计算物体下一帧的位置
        Vector3 nextPosition = transform.position + new Vector3(speed * direction * Time.deltaTime, 0f, 0f);

        // 判断物体是否超出移动范围,如果超出则改变移动方向
        if (Vector3.Distance(startPosition, nextPosition) > distance)
        {
            direction *= -1f;
        }

        // 更新物体的位置
        transform.position = nextPosition;
    }
}

记得把TargetMove脚本挂到移动靶上。

现在我们来实现天空盒的切换,在该游戏中我们实现了按C键实现正午与夕阳的天空盒切换,我们是通过布尔变量isSkybox1Active来实现,默认isSkybox1Active=true,当玩家按下c键时isSkybox1Active=false,再按一次isSkybox1Active=true,我们用脚本SkyboxSwitcher来实现这一功能,具体代码如下:

using UnityEngine;

public class SkyboxSwitcher : MonoBehaviour
{
    public Material skybox1; // 第一个天空盒材质
    public Material skybox2; // 第二个天空盒材质

    private bool isSkybox1Active = true; // 当前激活的天空盒

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.C))
        {
            SwitchSkybox();
        }
    }

    private void SwitchSkybox()
    {
        isSkybox1Active = !isSkybox1Active;

        if (isSkybox1Active)
        {
            RenderSettings.skybox = skybox1;
        }
        else
        {
            RenderSettings.skybox = skybox2;
        }
    }
}

我们在本次的界面UI设计都是UGUI,在游戏界面时,当弓箭击中靶子时会显示“在XX号射击位上射中XX号靶心/靶白,加XX分”,为了能保证在击中后才显示该文本,我们需要额外编写函数设置激活态,我们用TipsText脚本实现提示隐藏,具体代码如下:

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

public class TipsText : MonoBehaviour
{
    public void Close()
    {
        gameObject.SetActive(false);
    }
}

在游戏界面,我们除了要显示“在XX号射击位上射中XX号靶心/靶白,加XX分”,还需要实时显示玩家已经得了几分,我们额外用函数void SetScore(int score)来实现,脚本Tips的具体代码如下所示:

using UnityEngine.UI;

public class Tips : MonoBehaviour
{
    public static Tips Instance;
    public GameObject tips;
    public Text tipsText;

    private int score;
    public Text scoreText;

    private void Awake()
    {
        Instance = this;
    }

    public void SetText(string str)
    {
        tipsText.text = str;
        tips.SetActive(true);
    }

    public void SetScore(int score)
    {
        this.score += score;
        scoreText.text = "当前分数:" + this.score;
    }
}

记得要把脚本挂到控件上。 

现在我们需要实现场景切换,选择File->Build Settings。

将我们创建的场景拖入到Scenes in Build中。 

然后创建脚本Load Scene实现场景切换,具体代码如下:

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

public class LoadScene : MonoBehaviour
{
    public void Load(int level)
    {
        SceneManager.LoadScene(level);
    }
}

记得要把脚本挂到控件上。 

我们现在编写击中靶子得分的脚本Target,变量isSportTarget用于判断是否为运动靶,当isSportTarget==true时为运动靶,弓箭击中运动靶的靶心得3分,击中靶白得2分;当isSportTarget==false时为固定靶,弓箭击中固定靶的靶心得2分,击中靶白得1分;其中Circle是靶白,Bullseye是靶心。具体代码如下:

using UnityEngine;

public class Target : MonoBehaviour
{
    public int score = 1; // 分数
    //是否为运动靶
    public bool isSportsTarget;
    private Transform point;
    public int indexTarget;

    private void Start()
    {
        point = transform.parent;
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Bullet"))
        {
            // 检测到子弹碰撞
            CalculateScore();
            collision.transform.GetComponent<Rigidbody>().isKinematic = true;
            collision.transform.position = new Vector3(collision.contacts[0].point.x, collision.contacts[0].point.y, collision.contacts[0].point.z - Random.Range(-0.3f, -0.5f));
            collision.gameObject.transform.parent = point;
        }
    }

    private void CalculateScore()
    {
        if (gameObject.tag == "Bullseye")
        {
            // 碰撞到红色靶心
            if (isSportsTarget)
            {
                // 在这里处理得分逻辑
                Tips.Instance.SetScore(3);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶心,加3分");

            }
            else
            {
                Debug.Log("得到二分!");
                // 在这里处理得分逻辑,例如增加两分
                Tips.Instance.SetScore(2);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶心,加2分");
            }
        }
        else if (gameObject.tag == "Circle")
        {
            // 碰撞到白色圆圈
            if (isSportsTarget)
            {
                Debug.Log("得二分!");
                // 在这里处理得分逻辑
                Tips.Instance.SetScore(2);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶白,加2分");
            }
            else
            {
                Debug.Log("得到一分!");
                // 在这里处理得分逻辑,例如增加两分
                Tips.Instance.SetScore(1);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶白,加1分");
            }
        }
    }
}

记得要把Targer脚本挂到每个靶子上,且记得靶白的Tag选择为Circle。

靶心的Tag选择为Bullseye。

 此外,记得移动靶中Target脚本需要勾选变量isSportTarget,而固定靶不要勾选isSportTarget。

除了考虑弓箭与靶子的碰撞,我们还需要考虑弓箭与树木和地形的碰撞。

对于弓箭与树木的碰撞,我们使用脚本Tree实现碰撞判断,具体代码如下:

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

public class Tree : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Bullet"))
        {
            // 检测到子弹碰撞
            collision.transform.GetComponent<Rigidbody>().isKinematic = true;
            collision.transform.position = new Vector3(collision.contacts[0].point.x, collision.contacts[0].point.y, collision.contacts[0].point.z - Random.Range(-0.1f, -0.2f));
        }
    }
}

记得要把Tree脚本挂在树上。 

对于弓箭与地形的碰撞,我们使用脚本Terrain实现碰撞判断,具体代码如下:

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

public class Terrain : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Bullet"))
        {
            // 检测到子弹碰撞
            collision.transform.GetComponent<Rigidbody>().isKinematic = true;
            collision.transform.position = new Vector3(collision.contacts[0].point.x, collision.contacts[0].point.y, collision.contacts[0].point.z - Random.Range(-0.5f, -0.8f));
        }
    }
}

记得地形组件上需要挂载Tree和Terrain脚本。

现在我们编写判断玩家是否在射击位上的脚本ShootingArea,我们用isPlayer和isArrow判断玩家是否在射击位上,OnTriggerStay(Collider other)函数实现玩家在射击位上能做的操作,OnTriggerExit(Collider other)函数规定玩家在不在射击位上能不能做的操作,具体代码如下:

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

public class ShootingArea : MonoBehaviour
{
    //弓箭次数
    public int arrowCount = 5;
    //是否可以射箭
    public bool isArrow;
    private bool isPlayer;

    private void OnTriggerStay(Collider other)
    {
        if (isPlayer) return;
        if (other.gameObject.tag == "Player")
        {
            isPlayer = true;
            isArrow = true;
            other.gameObject.transform.GetComponent<Bow>().shootingArea = this;
        }
    }


    private void OnTriggerExit(Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            isPlayer = false;
            if (other.gameObject.transform.GetComponent<Bow>().shootingArea != null)
            {
                arrowCount = other.gameObject.transform.GetComponent<Bow>().shootingArea.arrowCount;
            }
            isArrow = false;
            other.gameObject.transform.GetComponent<Bow>().shootingArea = null;
        }
    }
}

记得要把脚本ShootingArea挂到射击位控件上。

 现在我们编写控制弓箭的脚本Bow,该脚本有以下几个函数,下面简单介绍每个函数。

UpdateBowStretch():根据拉弓的距离设置弓的拉伸效果。

ShootArrow():计算蓄力时间和箭矢初速度,并实例化箭矢。

FindBullet():销毁上一次射出的箭,以免出现新箭射到射出的老箭上面。

FindShootingArea():查找判断还有发射的靶场。

LockCursor(bool a):隐藏鼠标锁鼠标

using UnityEngine;
using UnityEngine.UI;

public class Bow : MonoBehaviour
{
    public GameObject arrowPrefab;  // 箭的预制体
    public Transform arrowSpawnPoint;  // 箭的生成点
    public float maxPullDistance = 3f; // 最大拉弓距离
    public float maxPullForce = 100f; // 最大拉弓力量
    public float minPullTime = 1f; // 最小蓄力时间
    public float maxPullTime = 5f; // 最大蓄力时间
    public float arrowFlightSpeed = 10f; // 箭的飞行速度

    private float pullStartTime; // 开始蓄力的时间
    private float pullDistance; // 箭飞行距离
    //播放动画
    private Animator anim;

    public ShootingArea shootingArea;
    public Text arrowCountTxt;
    public GameObject arrowCount;

    public GameObject over;

    void Start()
    {
        Time.timeScale = 1;
        anim = GetComponent<Animator>();
        LockCursor(true);

    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape) && Cursor.visible) { LockCursor(false); }

        if (Input.GetMouseButtonDown(0) && Cursor.visible == false) { LockCursor(true); }

        if (shootingArea == null) 
        {
            arrowCount.SetActive(false);
            return;
        }
        else
        {
            arrowCount.SetActive(true);
            arrowCountTxt.text = "剩余箭:" + shootingArea.arrowCount;
        }
        

        if (shootingArea.isArrow && shootingArea.arrowCount > 0)
        {
            if (Input.GetMouseButtonDown(0))
            {
                pullStartTime = 0;
                anim.SetTrigger("hold");
                //清除所有弓箭
                FindBullet();
            }
            else if (Input.GetMouseButton(0))
            {
                //计算蓄力时间
                pullStartTime += Time.deltaTime;
                //设置蓄力动画
                anim.SetFloat("holdTime", pullStartTime);
            }//鼠标抬起阶段
            else if (Input.GetMouseButtonUp(0))
            {
                pullDistance = pullStartTime;
                pullStartTime = 0;
                anim.SetTrigger("shoot");
                ShootArrow();
                Invoke("FindShootingArea", 1.5f);
            }
        }
    }

    private void ShootArrow()
    {
        // 实例化箭矢
        GameObject arrow = Instantiate(arrowPrefab, arrowSpawnPoint.position, arrowSpawnPoint.rotation);
        Rigidbody arrowRigidbody = arrow.GetComponent<Rigidbody>();
        arrowRigidbody.velocity = transform.forward * pullDistance * 30f;
        shootingArea.arrowCount -= 1;
        arrowCountTxt.text = "剩余箭:" + shootingArea.arrowCount;
    }

    public void FindBullet()
    {
        var bullets = GameObject.FindGameObjectsWithTag("Bullet");
        for (int i = 0; i < bullets.Length; i++)
        {
            Destroy(bullets[i]);
        }
    }

    public void FindShootingArea()
    {
        
        var ShootingAreas = GameObject.FindGameObjectsWithTag("ShootingArea");
        var temp = 0;
        for (int i = 0; i < ShootingAreas.Length; i++)
        {
            if (ShootingAreas[i].transform.GetComponent<ShootingArea>().arrowCount > 0)
            {
                temp++;
            }
        }
        if (temp <= 0)
        {
            LockCursor(false);
            over.SetActive(true);
            Time.timeScale = 0;
        }
    }

    public void LockCursor(bool a)
    {
        if (a)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
        else
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }
    }
}

到此,我们的游戏就编写完成了。

5.游戏演示

3D游戏编程与设计大作业:打靶游戏(Target Games)_哔哩哔哩bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值