目录
游戏要求
本项目游戏是3D游戏设计的一个期中大作业,其中具体的实验要求如下所示:
- 地形:使用地形组件,上面有草、树;
- 天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶:有一个以上固定的靶标;
- 运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
- 驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
项目地址与视频展示
本游戏项目的代码仓库地址为:github的仓库地址
本游戏项目的演示视频地址为:本打靶(射箭)游戏演示视频
玩家动作表(游戏规则表)
动作 | 条件 | 结果 |
WASD | 玩家(弩弓)在设计好的地形上 | 玩家(弩弓)前后左右移动 |
按下鼠标右键 | 无 | 天空盒发生变化 |
玩家(弩弓) 移动 | 玩家(弩弓)与树木、靶子发生碰撞 | 玩家(弩弓)停止移动 |
按下鼠标左键 | 玩家(弩弓)在射击位置上 | 弩弓开始蓄力 |
长按鼠标左键 | 无 | 弩弓的拉力不断增加直至所设定的上限 |
松开鼠标左键 | 无 | 弩弓根据长按的时间所计算的拉力来发射弩箭 |
鼠标移动 | 无 | 玩家(第一人称)的视觉范围发生变化 |
弩箭射中靶子 | 弩箭的碰撞检测体与靶子的碰撞检测体发生碰撞 | 总得分根据我们为每一个靶子所设定的分数进行增加 |
图形设计
弩弓与弩箭设计
在本游戏项目中,我们采用的弩弓与弩箭预制体为导入的资源包里面的预制体和model,采用的是资源商店里面下载的Classical Crossbow。如图所示:
靶子设计
通过不同大小、不同形状的基础物体来构成。最外面的绿色区域为Cube,白色区域为Cylinder,而最里面的靶心(红色)区域也是为Cylinder,通过修改其半径来显示不同的Cylinder,同时还需要为靶子不同的部分构建不同的碰撞检测体。最后将这一个构成的集合物体建成预制体,以供生成后面的靶子。
天空盒设计
在本次游戏项目中,我们还需要实现天空盒的切换,而我在这里采用的方法就是通过按动鼠标右键来切换天空盒。我们首先需要构建天空盒,我们可以直接从资源商店中导入相应的资源,我们导入的是资源是8K Skybox Pack Free,在这一个资源中,我们可以看到有很多个天空盒供我们自己选择,我们随便选择两种来实现天空盒的切换即可。我们采用的是天空盒3和天空盒10。
地形设计
我们首先通过鼠标左键添加3D对象中的Terrain,然后根据Inspector界面中的不同选择来对地形进行修改和描绘,可以增高或降低地形,可以为地形增加不同的纹理。我们还可以为地形种树,我们选择不同的树木,然后点击Terrain即可实现种树的功能。一样的,我们还可以选择种草,我们选择不一样的小草,然后点击Terrain即可实现种草的功能。我们可以通过不同的选项来实现对地形进行不同的修改功能。
动画控制器设计
在本游戏项目中,主要的动画控制为弩箭的蓄力、发射等,我们导入的资源中,它已经自带了一些动作包以及一个动画控制器,我们可以直接采用才动画控制器,然后在这个的基础上进行修改即可。
原理及代码设计
弩弓与弩箭的实现Bow
1. Start(): 游戏开始时的初始化操作。设置时间缩放为正常速度,获取弓的动画控制器,锁定鼠标光标。
2. Update(): 每帧执行的更新操作:
检测是否按下了 Escape 键来切换光标锁定状态。
检测是否按下了鼠标左键来切换光标锁定状态。
检查射击区域是否存在,如果不存在则隐藏箭的数量文本并返回。
如果射击区域存在,显示箭的数量文本,并更新箭的数量显示。
检查是否可以射击箭,并且当前还有剩余的箭。
如果按下鼠标左键,重置拉动开始时间,触发弓的拉动动画,并调用 FindBullet() 函数清除场 景中的箭。
如果按住鼠标左键,增加拉动箭头的时间,并将该时间值设置为弓的拉动动画的参数。
如果松开鼠标左键,将拉动距离设为拉动开始时间,重置拉动开始时间,触发弓的射击动 画,并调用 ShootArrow() 函数发射箭,并在1.5秒后调用 FindShootingArea() 函数查找射击 区域。
3. ShootArrow(): 发射箭的函数。实例化箭游戏对象,获取箭的刚体组件,根据拉动距离设置箭的速度,减少射击区域的箭数量,并更新箭的数量显示。
4. FindBullet(): 清除场景中的箭的函数。通过标签找到所有的箭游戏对象,然后销毁它们。
5. FindShootingArea(): 查找射击区域的函数。通过标签找到所有的射击区域游戏对象,检查它们是否还有剩余的箭。如果没有任何射击区域剩余箭了,解锁光标,显示游戏结束的 UI,并将时间缩放设为0,即暂停游戏。
6. LockCursor(bool a): 锁定/解锁光标的函数。根据传入的布尔值来切换光标的锁定状态和可见性。如果传入 true,则锁定光标并隐藏;如果传入 false,则解锁光标并显示。
using UnityEngine;
using UnityEngine.UI;
public class Bow : MonoBehaviour
{
//导入箭的预制体
public GameObject arrowPrefab;
//箭的Transform组件
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;
//箭的数量text
public Text arrowCountTxt;
//箭的数量UI
public GameObject arrowCount;
//游戏介绍的UI
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;
}
}
}
玩家移动控制器PlayerMove
1. Start(): 游戏开始时的初始化操作。获取角色的 CharacterController 组件。
2. Update(): 每帧执行的更新操作。调用 Move() 函数来处理角色的移动逻辑。
3. Move(): 处理角色移动的函数:
通过输入获取水平方向(X轴)和垂直方向(Z轴)的按键输入值。
根据输入值计算出移动方向,并将其乘以移动速度和时间增量,得到最终的移动向量。
使用 CharacterController 的 Move() 方法来移动角色,传入计算得到的移动向量。
根据重力值和时间增量,更新垂直方向上的速度(velocity.y)。
再次使用 CharacterController 的 Move() 方法来应用垂直方向上的速度变化,使角色受到重 力影响下落或上升。
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);
}
}
射击区域控制ShootingArea
1. arrowCount: 用于记录射击区域内剩余的箭的数量,初始值为10。
2. isArrow: 用于表示射击区域内是否有可用的箭。
3. isPlayer: 用于记录玩家是否在射击区域内。
4. OnTriggerStay(Collider other): 当有物体停留在射击区域内时触发的函数:
检查如果玩家已经在射击区域内,直接返回,不执行后续操作。
检查与射击区域碰撞的物体是否具有 "Player" 标签。
如果是玩家物体发生碰撞:
更新相关的变量,将 isPlayer 和 isArrow 设置为 true。
获取玩家物体上的 Bow 脚本,并将射击区域设置为当前的脚本。
5. OnTriggerExit(Collider other): 当有物体离开射击区域时触发的函数:
检查离开触发器的物体是否具有 "Player" 标签。
如果是玩家物体离开:
更新相关变量,将 isPlayer 设置为 false。
检查玩家物体上的 Bow 脚本的 shootingArea 是否为 null。
如果不为 null,将射击区域内的箭矢数量赋值给玩家物体上的 Bow 脚本的射击区域的箭矢数 量。
将 isArrow 设置为 false。
将玩家物体上的 Bow 脚本的射击区域设置为 null。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShootingArea : MonoBehaviour
{
//将该区域的可用箭数初始化为10
public int arrowCount = 10;
//设置一个变量记录该区域是否有箭
public bool isArrow;
//记录玩家是否在区域内
private bool isPlayer;
private void OnTriggerStay(Collider other)
{
//如果玩家已经在区域内
if (isPlayer) return;
//如果该区域的触发器与标签为player的玩家发生碰撞
if (other.gameObject.tag == "Player")
{
//更新相关的变量
isPlayer = true;
isArrow = true;
//获取玩家物体上的脚本,并且将射击区域设置为当前的脚本
other.gameObject.transform.GetComponent<Bow>().shootingArea = this;
}
}
private void OnTriggerExit(Collider other)
{
//如果触发器与标签为player的玩家离开碰撞
if (other.gameObject.tag == "Player")
{
//更新相关变量
isPlayer = false;
if (other.gameObject.transform.GetComponent<Bow>().shootingArea != null)
{
//将射击区域内的箭矢数量赋值给玩家物体上的Bow脚本的射击区域的箭矢数量
arrowCount = other.gameObject.transform.GetComponent<Bow>().shootingArea.arrowCount;
}
isArrow = false;
//将玩家物体上的Bow脚本的射击区域设置为null
other.gameObject.transform.GetComponent<Bow>().shootingArea = null;
}
}
}
靶子各个区域不同得分的控制Target
1. score: 靶子的得分,初始值为1。
2. isSportsTarget: 表示靶子是否为运动靶子。
3. point: 靶子的位置点的Transform组件。
4. indexTarget: 靶子的索引。
5. Start(): 在脚本启动时进行初始化操作。获取靶子的父对象作为位置点。
6. OnCollisionEnter(Collision collision): 当靶子弩箭弹发生碰撞时触发的函数:
检查碰撞的物体是否具有 "Bullet" 标签。
如果是弩箭发生碰撞:
调用 CalculateScore() 函数计算得分。
将碰撞的弩箭的刚体设为运动学(isKinematic),停止其物理模拟。
将碰撞点的位置稍微偏移并将弩箭放置在靶子的位置点下。
将弩箭的父对象设置为位置点。
7. CalculateScore(): 计算得分的函数:
检查靶子的标签。
如果靶子的标签为 "Bullseye",表示击中了靶心:
检查靶子是否为运动靶子。
如果是运动靶子,将得分加10,并显示相应的提示信息。
如果不是运动靶子,将得分加8,并显示相应的提示信息。
如果靶子的标签为 "Circle",表示击中了白色区域:
检查靶子是否为运动靶子。
如果是运动靶子,将得分加5,并显示相应的提示信息。
如果不是运动靶子,将得分加3,并显示相应的提示信息。
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()
{
//如果靶子的标签为Bullseye,即为靶心
if (gameObject.tag == "Bullseye")
{
// 如果为运动靶子
if (isSportsTarget)
{
// 将得分加3
Tips.Instance.SetScore(10);
Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加10分");
}
else
{
//如果不是运动靶子,将得分加2
Tips.Instance.SetScore(8);
Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加3分");
}
}
// 如果靶子的标签为Circle,即为白色区域
else if (gameObject.tag == "Circle")
{
//如果为运动靶子
if (isSportsTarget)
{
// 将得分加2
Tips.Instance.SetScore(5);
Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加5分");
}
else
{
// 如果不是运动靶子,就将得分加1
Tips.Instance.SetScore(3);
Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加3分");
}
}
}
}
控制靶子的移动TargetMove
1. speed: 靶子的移动速度。
2. distance: 靶子的移动距离。
3. startPosition: 靶子的起始位置。
4. direction: 靶子的移动方向,初始值为1。
5. Start(): 在脚本启动时进行初始化操作。记录靶子的起始位置。
6. Update(): 在每一帧更新时执行的函数:
计算下一帧的位置,根据当前位置、移动速度、移动方向和时间间隔来计算。
判断下一帧的位置与起始位置之间的距离是否超过设定的移动距离。
如果超过移动距离,改变移动方向(乘以-1),以实现来回移动。
更新靶子的位置为计算得到的下一帧位置。
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;
}
}
提示和得分管理Tips
1. Instance: Tips类的静态实例,用于在其他脚本中访问Tips类的实例。
2. tips: 提示框的游戏对象。
3. tipsText: 提示文本的Text组件。
4. score: 当前的得分。
5. scoreText: 显示得分的Text组件。
6. Awake(): 在脚本被加载时执行的函数。将Tips类的实例设置为当前实例。
7. SetText(string str): 设置提示文本的函数:
接收一个字符串参数,用于设置提示文本的内容。
将提示文本设置为接收的字符串。
激活提示框的游戏对象,使其可见。
8. SetScore(int score): 设置得分的函数:
接收一个整数参数,表示要增加的得分。
将接收的得分加到当前的得分上。
将得分显示在得分文本中,格式为 "分数: 得分值"。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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;
}
}
天空盒切换SkyboxSwitcher
1. skybox1: 第一个天空盒的材质。
2. skybox2: 第二个天空盒的材质。
3. isSkybox1Active: 当前激活的天空盒是否为天空盒1的标志,初始值为true。
4. Update(): 在每一帧更新时执行的函数:
检测是否按下鼠标右键(按钮编号为1)。
如果按下右键,调用SwitchSkybox()函数切换天空盒。
5. SwitchSkybox(): 切换天空盒的函数:
切换天空盒的状态,将isSkybox1Active的值取反。
如果isSkybox1Active为true,将渲染设置中的天空盒材质设置为第一个天空盒材质。
如果isSkybox1Active为false,将渲染设置中的天空盒材质设置为第二个天空盒材质。
using UnityEngine;
public class SkyboxSwitcher : MonoBehaviour
{
//第一个天空盒
public Material skybox1;
//第二个天空盒
public Material skybox2;
//当前激活的天空盒是否为天空盒1
private bool isSkybox1Active = true;
private void Update()
{
//如果按下C键,切换天空盒
if (Input.GetMouseButtonDown(1))
{
SwitchSkybox();
}
}
private void SwitchSkybox()
{
//切换天空盒的状态
isSkybox1Active = !isSkybox1Active;
if (isSkybox1Active)
{
//设置渲染设置中的天空盒材质为第一个天空盒材质
RenderSettings.skybox = skybox1;
}
else
{
//设置渲染设置中的天空盒材质为第二个天空盒材质
RenderSettings.skybox = skybox2;
}
}
}
主相机(主视觉)移动控制器CameraMove
1. mouseXSensitivity: 鼠标在X轴上的灵敏度,用于控制视角旋转的速度。
2. player: 用于存储玩家对象的Transform组件。
3. xRotation: 控制视角绕X轴旋转的角度。
4. Start(): 在脚本启动时执行的函数:
获取父级对象的Transform组件并赋值给player变量。
5. Update(): 在每一帧更新时执行的函数:
获取鼠标在X轴和Y轴上的移动距离(输入值)并乘以鼠标灵敏度和时间间隔。
将Y轴移动距离(mouseY)累加到xRotation上,用于控制视角绕X轴旋转。
限制xRotation的值在-45到10之间,以限制视角的上下旋转幅度。
将当前的xRotation应用于摄像机的本地旋转,实现视角的上下旋转。
将X轴移动距离(mouseX)乘以玩家对象的向上方向(Vector3.up)应用于玩家对象的旋 转,实现视角的左右旋转。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//鼠标控制视角
public class CameraMove : MonoBehaviour
{
//鼠标x轴灵敏度
public float mouseXSensitivity = 25f;
//人物
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);
}
}