赞
踩
打靶游戏为第一人称射击游戏,玩家通过长按鼠标左键实现拉弓,松开坐标鼠标左键则弓箭飞出。玩家通过鼠标的移动实现瞄准,通过按键盘的WSAD或上下左右四个方向键实现在地图内的移动。此外,为了营造有趣的射击环境,玩家可以按C键实现正午和夕阳模式下的天空盒的切换。游戏中有5个射击位,玩家只有在射击位上才能进行拉弓射击。游戏中有2个固定靶和3个移动靶,靶子分为红色靶心和白色靶白,击中不同颜色区域有不同的得分,移动靶在树木左右移动,难度较固定靶更大。
1)击中移动靶靶心得3分,靶白得2分。
2)击中固定靶靶心得2分,靶白得1分。
3)游戏中有5个射击位,只有在射击位上可以进行射击,每个射击位有5次射击机会。
4)当地图上所有射击位的所有射击次数被用光时,即用完25次射击次数,游戏结束。
本游戏基于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;
- }
- }
- }
到此,我们的游戏就编写完成了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。