游戏要求
- 地形:使用地形组件,上面有草、树;
- 天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶:有一个以上固定的靶标;
- 运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
- 弩弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
项目下载与展示
下载地址:
游戏展示:
具体实现
人物控制
人物控制脚本
-
首先,脚本中声明了一些变量和引用:
controller
:人物控制器,用于控制玩家角色的移动。moveSpeed
:人物移动速度。gravity
:模拟重力,用于在玩家角色下落时施加向下的速度。mouseSpeed
:鼠标控制角色旋转的速度。xRot
:角色绕X轴的旋转角度。velocity
:角色的当前速度向量。playerFire
:对应的弩箭发射脚本的引用。
-
在
Start()
方法中,获取角色的CharacterController
组件,并将其赋值给controller
变量。同时,锁定鼠标在屏幕中心并隐藏鼠标指针。 -
在
Update()
方法中,调用Move()
方法和Rotation()
方法来处理角色的移动和旋转。 -
Move()
方法用于处理角色的移动,具体操作如下:- 获取水平轴(左右键盘方向键)和垂直轴(上下键盘方向键)的输入。
- 根据输入的值计算移动方向向量。
- 使用人物控制器的
Move()
方法来移动角色,根据移动速度和时间间隔计算位移。 - 根据模拟重力的值,更新角色的垂直速度。
- 使用人物控制器的
Move()
方法再次移动角色,根据垂直速度和时间间隔计算位移。
-
Rotation()
方法用于处理角色的旋转,具体操作如下:- 获取鼠标在水平和垂直方向上的输入。
- 根据鼠标输入和鼠标控制速度计算角色绕Y轴和X轴的旋转量。
- 限制角色绕X轴的旋转角度在特定范围内。
- 更新角色的旋转值,使其绕Y轴和X轴旋转。
射击区域实现
-
OnTriggerEnter(Collider other)
方法是当玩家角色进入触发器时调用的方法,具体操作如下:- 判断进入触发器的游戏对象的标签是否为"FirePoint"。
- 如果是,则将
playerFire
脚本中的fire
变量设置为true
,表示可以发射箭。
-
OnTriggerExit(Collider other)
方法是当玩家角色退出触发器时调用的方法,具体操作如下:- 判断退出触发器的游戏对象的标签是否为"FirePoint"。
- 如果是,则将
playerFire
脚本中的fire
变量设置为false
,表示不可发射箭。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
//人物控制器
private CharacterController controller;
//人物移动速度
public float moveSpeed = 2f;
//模拟重力
public float gravity = -10f;
public float mouseSpeed = 40f;
float xRot = 0f;
Vector3 velocity;
public PlayerFire playerFire;
private void Start()
{
controller = GetComponent<CharacterController>();
Cursor.lockState = CursorLockMode.Locked; // 锁定鼠标在屏幕中心
Cursor.visible = false; // 隐藏鼠标指针
}
void Update()
{
Move();
Rotation();
}
//移动
public void Move()
{
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
//方向
Vector3 dir = transform.right * x + transform.forward * z;
controller.Move(dir * moveSpeed * Time.deltaTime);
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
//旋转
public void Rotation()
{
//鼠标控制
float mouseX = Input.GetAxis("Mouse X") * mouseSpeed * Time.deltaTime;
float mouseY = Input.GetAxis("Mouse Y") * mouseSpeed * Time.deltaTime;
xRot -= mouseY;
//人物移动
xRot = Mathf.Clamp(xRot, -20f, 10f);
transform.rotation = Quaternion.Euler(xRot, transform.rotation.eulerAngles.y + mouseX, 0f);
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "FirePoint")
{
playerFire.fire = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "FirePoint")
{
playerFire.fire = false;
}
}
}
标靶控制
标靶控制脚本
-
首先,定义了一个枚举类型
MoveDirection
,用于表示移动的方向,包括前后、左右和上下三个方向。 -
在脚本中声明了一些变量和引用:
isMovingTarget
:是否为移动靶,用于控制靶子是否需要移动。distance
:反复移动的距离。speed
:移动速度。moveDirection
:移动方向,根据枚举类型MoveDirection
来确定。startPos
:初始位置,用于记录靶子的初始位置。isReverseDirection
:是否反向移动,用于控制靶子的移动方向。
-
在
Start()
方法中,将靶子的初始位置赋值给startPos
变量。 -
在
Update()
方法中,如果isMovingTarget
为true
,则执行靶子的移动逻辑。- 根据移动方向确定移动向量
dir
。 - 如果需要反向移动,则将移动向量取反。
- 计算物体下一帧的位置
nextPos
,根据移动速度和时间间隔计算位移。 - 判断物体是否超出移动范围,如果超出则改变移动方向。
- 更新物体的位置。
- 根据移动方向确定移动向量
-
在
OnCollisionEnter(Collision collision)
方法中,当箭与靶子发生碰撞时调用的方法,具体操作如下:- 判断碰撞的游戏对象的标签是否为"Arrow"。
- 将碰撞的刚体设为运动学,使其不受物理引擎的影响。
- 获取碰撞点的位置
collisionPoint
,通过GetContact(0)
获取第一个碰撞点的位置。 - 将箭的父物体设置为靶子,使箭跟随靶子移动。
- 计算碰撞点和靶子中心点之间的距离。
- 根据距离的范围进行逻辑判断,根据是否为移动靶子和距离的不同,设置不同的文本内容即所得分数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum MoveDirection
{
Forward,
Left,
Up
}
public class TargetController : MonoBehaviour
{
public bool isMovingTarget;//是否为移动靶
public float distance = 5f; // 反复移动的距离
public float speed = 2f; // 速度
public MoveDirection moveDirection; // 移动方向
private Vector3 startPos; // 初始位置
private bool isReverseDirection; // 是否反向移动
private void Start()
{
startPos = transform.position; // 记录初始位置
}
private void Update()
{
if (isMovingTarget)
{
// 根据移动方向确定移动向量
Vector3 dir = Vector3.zero;
if (moveDirection == MoveDirection.Forward)
{
//前后
dir = transform.forward;
}
else if (moveDirection == MoveDirection.Left)
{
//左右
dir = -transform.right;
}
else if (moveDirection == MoveDirection.Up)
{
//上下
dir = transform.up;
}
// 如果需要反向移动,则将移动向量取反
if (isReverseDirection)
{
dir = -dir;
}
// 计算物体下一帧的位置
Vector3 nextPos = transform.position + dir * speed * Time.deltaTime;
// 判断物体是否超出移动范围,如果超出则改变移动方向
if (Vector3.Distance(startPos, nextPos) > distance)
{
isReverseDirection = !isReverseDirection; // 反向移动
}
// 更新物体的位置
transform.position = nextPos;
}
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "Arrow")
{
collision.rigidbody.isKinematic = true;
// 获取第一个碰撞点的位置
Vector3 collisionPoint = collision.GetContact(0).point;
//设置父物体为靶子
collision.transform.parent = transform;
// 计算碰撞点和物体中心点之间的距离
float distance = Vector3.Distance(collisionPoint, transform.position);
// 在这里进行与中心点距离逻辑判断 小于0.25f为靶心
if (distance <= 0.25f)
{
if (isMovingTarget)
{
SetText.Instance.SetStr("移动靶靶心区域", 10);
}
else
{
SetText.Instance.SetStr("固定靶靶心区域", 5);
}
}
else if(distance > 0.25f && distance < 0.4f)
{
if (isMovingTarget)
{
SetText.Instance.SetStr("移动靶中间区域", 6);
}
else
{
SetText.Instance.SetStr("固定靶中间区域", 3);
}
}
else
{
if (isMovingTarget)
{
SetText.Instance.SetStr("移动靶边缘区域", 3);
}
else
{
SetText.Instance.SetStr("固定靶边缘区域", 1);
}
}
}
}
}
弩弓发射与蓄力
弩箭发射脚本
-
首先,脚本中声明了一些变量和引用:
animator
:用于获取角色的动画组件。arrowObj
:箭的预制体,即用于实例化箭的游戏对象。arrowSpeed
:箭的移动速度。holdTime
:蓄力时间,用于记录玩家按住鼠标右键的时间。fire
:控制是否能发射箭的布尔变量。
-
在
Start()
方法中,获取角色的Animator
组件,并将其赋值给animator
变量。 -
在
Update()
方法中,如果fire
为真,则进行以下操作:- 如果玩家按下鼠标右键(
Input.GetMouseButtonDown(1)
),则触发角色的蓄力动画(通过设置动画控制器中的触发器)。 - 如果玩家一直按住鼠标右键(
Input.GetMouseButton(1)
),则累加蓄力时间,并将蓄力时间传递给动画控制器中的浮点数参数。 - 如果玩家松开鼠标右键(
Input.GetMouseButtonUp(1)
),则销毁场景中的所有箭对象、触发角色的发射动画,并调用Fire()
方法发射箭,并重置蓄力时间为0。
- 如果玩家按下鼠标右键(
-
Fire(float holdForce)
方法用于发射箭,具体操作如下:- 实例化箭对象,位置为当前对象的子对象的位置,旋转为当前对象的子对象的旋转。
- 获取箭对象的刚体组件。
- 根据蓄力时间和箭的速度,设置箭的初始速度(方向为当前对象的前方向)。
-
DestroyArrows()
方法用于删除场景中所有的箭对象,具体操作如下:- 通过标签查找所有具有"Arrow"标签的游戏对象。
- 循环遍历找到的箭对象数组,并销毁每个箭对象。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 弩箭发射脚本
/// </summary>
public class PlayerFire : MonoBehaviour
{
private Animator animator;
//箭的预制体
public GameObject arrowObj;
//箭的移动速度
public float arrowSpeed = 25;
//蓄力时间
float holdTime;
//是否能发射
public bool fire;
void Start()
{
animator = GetComponent<Animator>();
}
void Update()
{
if (fire)
{
//鼠标右键发射
if (Input.GetMouseButtonDown(1))
{
animator.SetTrigger("hold");
}
else if (Input.GetMouseButton(1))
{
holdTime += Time.deltaTime;
animator.SetFloat("holdTime", holdTime);
}
else if (Input.GetMouseButtonUp(1))
{
DestroyArrows();
animator.SetTrigger("shoot");
Fire(holdTime);
//清空上次蓄力时间
holdTime = 0;
}
}
}
/// <summary>
/// 发射
/// </summary>
/// <param name="holdForce"></param>
public void Fire(float holdForce)
{
//实例化箭
GameObject arrow = Instantiate(arrowObj, transform.GetChild(0).position, transform.GetChild(0).rotation);
//获得刚体
Rigidbody arrowRigidbody = arrow.GetComponent<Rigidbody>();
//根据蓄力大小发射弩箭
arrowRigidbody.velocity = transform.forward * holdForce * arrowSpeed;
}
/// <summary>
/// 删除所有箭
/// </summary>
public void DestroyArrows()
{
GameObject[] arrows = GameObject.FindGameObjectsWithTag("Arrow");
for (int i = 0; i < arrows.Length; i++)
{
Destroy(arrows[i]);
}
}
}
动画控制
、
天空盒切换
-
在脚本中声明了一些变量和引用:
skyboxMaterials
:天空盒材质的数组,用于存储多个天空盒材质。currentIndex
:当前天空盒的索引,用于记录当前正在使用的天空盒材质。timer
:计时器,用于计算时间。countDownTime
:倒计时时间,表示在多长时间后切换到下一个天空盒材质。
-
在
Start()
方法中,将初始的天空盒材质设置为数组中的第一个材质。 -
在
Update()
方法中,每帧更新计时器的值,累加时间。 -
如果计时器的值超过等于倒计时时间,表示时间已经达到了切换天空盒的条件,执行以下操作:
- 将当前天空盒索引加1,切换到下一个天空盒材质。
- 如果当前天空盒索引超过等于天空盒材质数组的长度,即索引越界,将当前天空盒索引重置为0,重新从第一个天空盒材质开始循环。
- 将新的天空盒材质设置为渲染设置中的天空盒材质。
- 将计时器重置为0,重新开始计时。
using UnityEngine;
public class SkyBox : MonoBehaviour
{
public Material[] skyboxMaterials; // 天空盒材质数组
private int currentIndex = 0; // 当前天空盒索引
private float timer = 0f; // 计时器
public float countDownTime = 10f; //倒计时时间
private void Start()
{
RenderSettings.skybox = skyboxMaterials[currentIndex]; // 设置初始天空盒材质
}
private void Update()
{
timer += Time.deltaTime; // 计时器累加
if (timer >= countDownTime) // 当计时器达到倒计时时间
{
currentIndex++; // 切换到下一个天空盒材质
if (currentIndex >= skyboxMaterials.Length)//索引越界判断
{
currentIndex = 0;
}
RenderSettings.skybox = skyboxMaterials[currentIndex]; // 设置新的天空盒材质
timer = 0f; // 重置计时器
}
}
}
场景管理
-
脚本中声明了两个公共方法:
GotoScene(int i)
:用于加载场景。接收一个整数参数i
作为要加载的场景的索引。QuitGame()
:用于退出游戏。
-
在
GotoScene(int i)
方法中,调用SceneManager.LoadScene(i)
来加载指定索引的场景。SceneManager.LoadScene()
是Unity提供的静态方法,用于加载场景。通过传入场景的索引,可以加载对应的场景。 -
在
QuitGame()
方法中,调用Application.Quit()
来退出游戏。Application.Quit()
是Unity提供的静态方法,用于退出应用程序。
using System.Collections;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using UnityEngine;
public class SceneMgr : MonoBehaviour
{
//加载场景
public void GotoScene(int i)
{
SceneManager.LoadScene(i);
}
//退出游戏
public void QuitGame()
{
Application.Quit();
}
}
时间控制与文本显示
时间控制脚本
-
在脚本中声明了一些变量和引用:
countdownTime
:倒计时的总时间。countdownText
:倒计时时间的UI文本组件。currentTime
:当前剩余时间。over
:游戏结束面板的游戏对象。scoreText
:得分的UI文本组件。
-
在
Start()
方法中,将时间缩放设置为1,即正常时间流逝的状态。初始化当前剩余时间为倒计时时间。 -
在
Update()
方法中,每帧更新当前剩余时间。- 使用
Time.deltaTime
来减去流逝的时间,更新剩余时间。 - 使用
Mathf.Max()
函数将剩余时间限制在大于等于0的范围内。 - 如果剩余时间小于等于0,调用
GameOver()
方法,游戏结束。 - 更新倒计时UI文本的显示。
- 使用
-
FormatTime(float time)
方法用于将时间格式化为分:秒的形式。- 将剩余时间转换为整数的分钟数和秒数。
- 使用字符串插值将分钟数和秒数格式化为"00:00"的形式。
-
GameOver()
方法用于处理游戏结束的逻辑。- 激活游戏结束面板。
- 将得分显示在UI文本组件中,通过访问
SetText.Instance.score
来获取得分值。 - 将鼠标锁定模式设置为None,显示鼠标光标。
- 将时间缩放设置为0,即暂停游戏。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class TimeController : MonoBehaviour
{
//设置倒计时时间
public float countdownTime = 1000f;
//倒计时ui文本
public Text countdownText;
//当前时间
private float currentTime;
public GameObject over;
public Text scoreText;
void Start()
{
Time.timeScale = 1;
//初始化倒计时时间
currentTime = countdownTime;
}
void Update()
{
currentTime -= Time.deltaTime;
// 使用Mathf.Max函数将currentTime限制在0以上
currentTime = Mathf.Max(currentTime, 0);
if (currentTime <= 0)
{
GameOver();
}
countdownText.text = FormatTime(currentTime);
}
// 更改时间显示模式
private string FormatTime(float time)
{
int minutes = (int)(time / 60);
int seconds = (int)(time % 60);
return $"{minutes:00}:{seconds:00}";
}
//游戏结束
public void GameOver()
{
over.SetActive(true);
scoreText.text = SetText.Instance.score.ToString();
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
Time.timeScale = 0;
}
}
文本设置脚本
-
在脚本中声明了一些变量和引用:
score
:分数的整数值。scoreText
:分数的UI文本组件。Instance
:用于实现单例模式的静态引用。tipsText
:游戏提示文本的UI文本组件。
-
在
Start()
方法中,将Instance
设置为当前脚本实例,用于实现单例模式。获取游戏提示文本的UI文本组件。 -
SetStr(string str, int sc)
方法用于设置游戏提示文本和更新分数。- 接收字符串参数
str
和整数参数sc
,表示射中的目标和得分。 - 将得分加上射中目标的得分。
- 使用字符串插值将射中目标和得分格式化为提示文本,并显示在游戏提示文本的UI文本组件中。
- 将分数转换为字符串,并更新分数的UI文本组件显示。
- 使用
Invoke()
方法在1.5秒后调用EmptyStr()
方法,清空游戏提示文本。
- 接收字符串参数
-
EmptyStr()
方法用于清空游戏提示文本。- 将游戏提示文本设置为空字符串,清空提示文本的显示。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SetText : MonoBehaviour
{
public int score = 0;
public Text scoreText;
public static SetText Instance;
Text tipsText;
private void Start()
{
Instance = this;
tipsText = GetComponent<Text>();
}
public void SetStr(string str , int sc)
{
score += sc;
tipsText.text = "射中" + str + "+" + sc + "分";
scoreText.text = score.ToString();
Invoke("EmptyStr", 1.5f);
}
public void EmptyStr()
{
tipsText.text = string.Empty;
}
}