目录
一、游戏介绍
(1)游戏内容设计需求
(2)游戏规则制定(玩家动作表)
二、游戏制作过程
(1)导入需要的游戏资源包
(2)地形
(3)天空盒
(4)固定靶和运动靶
(5)射击位(区域)
(6)玩家(弩弓)
1. 实现玩家的游走和视角变化控制
2. 实现弩弓蓄力动画
3. 弩弓的动画控制与发射
(7)碰撞与计分
(8)GUI
三、 游戏展示
一、游戏介绍
(1)游戏内容设计需求
基于Unity平台制作一款第一人称射箭游戏,游戏中玩家需要移动到固定的射击区域,将弓箭射击到靶子即可获得分数。
- 地形:使用地形组件,上面有草、树;
- 天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶:有一个以上固定的靶标;
- 运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
- 驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
(2)游戏规则制定(玩家动作表)
动作 | 条件 | 结果 |
按下WASD键 | 玩家(弩弓)进入设计好的地形 | 玩家(弩弓)进行前后左右的移动 |
无 | 时间每变化5秒 | 天空盒进行切换 |
玩家(弩弓)发生移动 | 玩家(弩弓)与树木、靶子发生碰撞 | 玩家(弩弓)停止移动 |
长按空格键 | 玩家(弩弓)在射击区域 | 弩弓开始蓄力,根据长按时间不断增加弩弓蓄力的强度直至最大 |
按下鼠标左键 | 玩家(弩弓)在射击区域且在该区域中的射击次数大于0 | 弩弓根据长按的时间所计算的强度来发射弓箭,玩家(弩弓)在该射击区域的射击次数减1 |
按下鼠标右键并进行方向拖拽移动 | 无 | 玩家(第一人称)/弓箭的视角/发射角度发生变化 |
弓箭射中靶子 | 弓箭的碰撞检测体与靶子的碰撞检测体发生碰撞 | 提示射中靶子,若射中固定靶则分数加10,移动靶则分数加20 |
弓箭射中地面 | 弓箭的碰撞检测体与地形的碰撞检测体发生碰撞 | 提示射中地面,不加分 |
二、游戏制作过程
(1)导入需要的游戏资源包
在Unity Asset Store - The Best Assets for Game Making中添加以上四个资源包,然后在Package Manager中下载资源包并将其import到项目资源中。
(2)地形
使用Terrain
组件绘制地形:
1. 使用资源包Fantasy Skybox FREE
中的地表与草地纹理绘制地形;
2. 使用资源包Dream Forest Tree中的树预制件和草预制件绘制树和草。
3. 为了让玩家不能穿过树木,需要为树预制件添加BoxCollider碰撞体组件:
绘制好的地形如下:
(3)天空盒
1. 创建几个Material文件
,将Shader
属性设置为Skybox-cubemap
,并导入资源包Fantasy Skybox FREE
中的天空盒图片。
2.编写脚本文件Skyboxswitcher,实现每5秒自动更换天空盒。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Skyboxswitcher : MonoBehaviour
{
// 存储轮换的天空盒
public Material[] skys;
// 当前天空盒数组索引
private int index = 0;
// 轮换时间
private int changeTime = 5;
void Start()
{
InvokeRepeating("changeBox", 0, changeTime);
}
void changeBox()
{
RenderSettings.skybox = skys[index];
index++;
index %= skys.Length;
}
}
3. 将脚本文件挂载到新建的空game object "Skyboxcontroller"中,并将构建的几个天空Material文件添加到Skyboxcontroller的inspector中的skys数组中,即可实现天空盒的切换。
(4)固定靶和运动靶
1. 添加资源包Military target中的靶子预制体到场景中,游戏设置3个固定靶和3个移动方向不同的运动靶。
2. 设置两种靶子的tag以便后面区分:
3. 添加碰撞体组件
4. 实现运动靶的不同方向移动
游戏中添加了两个脚本实现了靶子前后,左右,上下三种方向的运动。以左右运动脚本为例,将Moved.cs挂载到靶子对象上就可以实现其左右方向的运动:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move2 : MonoBehaviour
{
public float moveSpeed = 5f; // 靶子的平移速度
public float moveDistance = 5f; // 靶子平移的距离
private Vector3 initialPosition; // 初始位置
private bool movingRight = true; // 靶子是否向右移动
void Start()
{
initialPosition = transform.position;
}
void Update()
{
MoveTarget();
}
void MoveTarget()
{
// 计算下一帧的位置
Vector3 nextPosition = transform.position + Vector3.right * (movingRight ? 1 : -1) * moveSpeed * Time.deltaTime;
// 判断是否达到平移的距离
float distanceToInitial = Vector3.Distance(nextPosition, initialPosition);
if (distanceToInitial >= moveDistance)
{
// 如果达到距离,改变方向
movingRight = !movingRight;
}
// 更新位置
transform.position = nextPosition;
}
}
(5)射击位(区域)
游戏中设置了两个射击区域area1和area2,在下面的archercontroller.cs中实现了:
1. 对玩家射箭区域的限制,只有到这两个区域里面才可进行射击;
2. 对每个区域射击次数的限制,每个区域分别只有十次机会。
(6)玩家(弩弓)
1. 实现玩家的游走和视角变化控制
创建一个Capsule的3D对象,将Main Camera移动到Player中作为子对象,再将Classical Crossbow资源包中的弩弓预制体Crossbow拖入Main Camera作为子对象。
编写脚本cameracontrolller挂载到Player对象上,即可实现玩家的游走和视角变化控制:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cameracontroller : MonoBehaviour
{
// 在场景中游览的相机(不要给相机加碰撞器!)
public Transform tourCamera;
#region 相机移动参数
public float moveSpeed = 10.0f;
public float rotateSpeed = 150.0f;
public float shiftRate = 2.0f;// 按住Shift加速
public float minDistance = 0.5f;// 相机离不可穿过的表面的最小距离(小于等于0时可穿透任何表面)
#endregion
#region 运动速度和其每个方向的速度分量
private Vector3 direction = Vector3.zero;
private Vector3 speedForward;
private Vector3 speedBack;
private Vector3 speedLeft;
private Vector3 speedRight;
private Vector3 speedUp;
private Vector3 speedDown;
#endregion
void Start()
{
if (tourCamera == null) tourCamera = gameObject.transform;
}
void Update()
{
GetDirection();
// 检测是否离不可穿透表面过近
RaycastHit hit;
while (Physics.Raycast(tourCamera.position, direction, out hit, minDistance))
{
// 消去垂直于不可穿透表面的运动速度分量
float angel = Vector3.Angle(direction, hit.normal);
float magnitude = Vector3.Magnitude(direction) * Mathf.Cos(Mathf.Deg2Rad * (180 - angel));
direction += hit.normal * magnitude;
}
if (tourCamera.localPosition.y > 3.3f)
{
tourCamera.localPosition = new Vector3(tourCamera.localPosition.x, 3.3f, tourCamera.localPosition.z);
}
if (tourCamera.localPosition.y < 2.8f)
{
tourCamera.localPosition = new Vector3(tourCamera.localPosition.x, 2.8f, tourCamera.localPosition.z);
}
tourCamera.Translate(direction * moveSpeed * Time.deltaTime, Space.World);
}
private void GetDirection()
{
#region 加速移动
if (Input.GetKeyDown(KeyCode.LeftShift)) moveSpeed *= shiftRate;
if (Input.GetKeyUp(KeyCode.LeftShift)) moveSpeed /= shiftRate;
#endregion
#region 键盘移动
// 复位
speedForward = Vector3.zero;
speedBack = Vector3.zero;
speedLeft = Vector3.zero;
speedRight = Vector3.zero;
speedUp = Vector3.zero;
speedDown = Vector3.zero;
// 获取按键输入
if (Input.GetKey(KeyCode.W)) speedForward = tourCamera.forward;
if (Input.GetKey(KeyCode.S)) speedBack = -tourCamera.forward;
if (Input.GetKey(KeyCode.A)) speedLeft = -tourCamera.right;
if (Input.GetKey(KeyCode.D)) speedRight = tourCamera.right;
if (Input.GetKey(KeyCode.E)) speedUp = Vector3.up;
if (Input.GetKey(KeyCode.Q)) speedDown = Vector3.down;
direction = speedForward + speedBack + speedLeft + speedRight + speedUp + speedDown;
#endregion
#region 鼠标旋转
if (Input.GetMouseButton(1))
{
// 转相机朝向
tourCamera.RotateAround(tourCamera.position, Vector3.up, Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime);
tourCamera.RotateAround(tourCamera.position, tourCamera.right, -Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime);
// 转运动速度方向
direction = V3RotateAround(direction, Vector3.up, Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime);
direction = V3RotateAround(direction, tourCamera.right, -Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime);
}
#endregion
}
/// <summary>
/// 计算一个Vector3绕旋转中心旋转指定角度后所得到的向量。
/// </summary>
/// <param name="source">旋转前的源Vector3</param>
/// <param name="axis">旋转轴</param>
/// <param name="angle">旋转角度</param>
/// <returns>旋转后得到的新Vector3</returns>
public Vector3 V3RotateAround(Vector3 source, Vector3 axis, float angle)
{
Quaternion q = Quaternion.AngleAxis(angle, axis);// 旋转系数
return q * source;// 返回目标点
}
}
2. 实现弩弓蓄力动画
Animator动画控制器的转移顺序如下,在Empty->half的转移过程添加bool条件“isPulling”,在half->shoot的转移过程添加bool条件“Fire” 。
其中half为混合树,Empty是没蓄力状态的动作,Fill是蓄力拉满的动作,
3. 弩弓的动画控制与发射
archercontroller.cs代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class archercontroller : MonoBehaviour
{
Animator animator;
bool isPulling = false;
float pullDuration = 0f;
public float maxPullDuration = 2f; // 最长拉弓时间
public float arrowSpeed = 1f; // 箭矢速度
float pullStrength;
public Transform firePoint;
public Camera maincam;
public int area1Shots = 10;
public int area2Shots = 10;
void Start()
{
// 获取弓上的Animator组件
animator = GetComponent<Animator>();
}
void Update()
{
if (Isinarea())
{
// 当按下空格键时触发状态切换
if (Input.GetKeyDown(KeyCode.Space))
{
pullDuration = 0;
animator.SetBool("Fire", false);
animator.SetFloat("power", 0);
isPulling = true;
animator.SetBool("isPulling", true);
//animator.SetBool("Holding", false);
}
// 持续按下空格键时记录按下时间,决定拉弓的强度
if (isPulling)
{
pullDuration += Time.deltaTime;
// 将按下的时间映射到0到1的范围,作为拉弓强度的参数
pullStrength = Mathf.Clamp01(pullDuration / maxPullDuration);
animator.SetFloat("power", pullStrength);
}
if (Input.GetKeyUp(KeyCode.Space))
{
isPulling = false;
//animator.SetBool("Holding", true);
animator.SetBool("isPulling", false);
}
// 当点击鼠标左键时触发射击
if (Input.GetMouseButtonDown(0))
{
Vector3 currentPosition = transform.position;
Vector3 area1 = new Vector3(269.1f, 0f, 279f);
Vector3 area2 = new Vector3(219.3f, 0f, 274.7f);
float distance1 = Vector3.Distance(currentPosition, area1); // 计算目标点和玩家位置之间的距离
float distance2 = Vector3.Distance(currentPosition, area2);
float radius = 10f; // 圆的半径
if (distance1 <= radius&&area1Shots>0)
{
area1Shots--;
animator.SetBool("Fire", true);
animator.SetFloat("power", 0);
// animator.SetBool("Holding", false);
Fire(pullStrength);
}
else if (distance2 <= radius && area2Shots > 0)
{
area2Shots--;
animator.SetBool("Fire", true);
animator.SetFloat("power", 0);
// animator.SetBool("Holding", false);
Fire(pullStrength);
}
}
}
}
public void Fire(float holdForce)
{
GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
arrow.AddComponent<ArrowController>();
ArrowController arrowController = arrow.GetComponent<ArrowController>();
arrowController.cam = maincam;
arrow.transform.position = firePoint.transform.position;
arrow.transform.rotation = Quaternion.LookRotation(this.transform.forward);
Rigidbody rd = arrow.GetComponent<Rigidbody>();
if (rd != null)
{
rd.AddForce(this.transform.forward * 60 * holdForce);
}
}
bool Isinarea()
{
Vector3 currentPosition = transform.position;
Vector3 area1 = new Vector3(269.1f, 0f, 279f);
Vector3 area2 = new Vector3(219.3f, 0f, 274.7f);
float distance1 = Vector3.Distance(currentPosition, area1); // 计算目标点和玩家位置之间的距离
float distance2 = Vector3.Distance(currentPosition, area2);
float radius = 10f; // 圆的半径
if (distance1 <= radius || distance2 <= radius)
{
return true;
}
else
{
return false;
}
}
}
将该脚本挂载在Crossbow上,将Main Camera添加到脚本,并添加一个弓箭发射点的位置对象pos到Fire Point上:
(7)碰撞与计分
1. 给弓箭添加Capsule collider碰撞体和刚体属性:
2. 碰撞处理与计分
检测碰撞,如果碰到静态靶,分数+10,如果碰到动态靶,分数+20,同时生成UI提示射中,销毁箭。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowController : MonoBehaviour
{
// Start is called before the first frame update
private Rigidbody rb;
public float Score = 0;
public Camera cam;
private UserGui ui;
public float hitDisplayTime = 2f; // 提示显示时间
public archercontroller archerControllerScript;
void Start()
{
rb = GetComponent<Rigidbody>();
ui = cam.GetComponent<UserGui>();
archerControllerScript = GameObject.FindObjectOfType<archercontroller>();
}
// Update is called once per frame
void Update()
{
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("staticTarget"))
{
ShowHitMessage("Hit Static Target! 加十分!");
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
ui.Score += 10;
Destroy(gameObject);
}
else if (collision.gameObject.CompareTag("MovingTarget"))
{
ShowHitMessage("Hit Moving Target! 加二十分!");
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
ui.Score += 20;
Destroy(gameObject);
}
else if (collision.gameObject.CompareTag("Ground"))
{
ShowHitMessage("Hit the Ground!");
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
Destroy(gameObject);
}
}
void ShowHitMessage(string message)
{
StartCoroutine(DisplayHitMessage(message));
}
IEnumerator DisplayHitMessage(string message)
{
ui.SetHitMessage(message); // 假设 UserGui 类有 SetHitMessage 方法用于设置提示信息
yield return new WaitForSeconds(hitDisplayTime);
ui.ClearHitMessage(); // 假设 UserGui 类有 ClearHitMessage 方法用于清除提示信息
}
}
(8)GUI
UserGUI:显示各个提示信息(射击次数,是否射中和分数),该代码挂载到摄像机上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGui : MonoBehaviour
{
// Start is called before the first frame update
public float Score = 0;
public archercontroller archerControllerScript;
private string hitMessage = "";
void Start()
{
archerControllerScript = GameObject.FindObjectOfType<archercontroller>();
}
// Update is called once per frame
void Update()
{
}
private void OnGUI()
{
GUIStyle style = new GUIStyle();
style.fontSize = 14;
style.normal.textColor = Color.black;
// 定义游戏介绍文本内容
string introText = "移动位置请按WASD\n蓄力拉弓请按空格\n发射弓箭请按左键\n调整发射角度/视角请按右键";
// 在屏幕左上角绘制游戏介绍文本
GUI.Label(new Rect(10, 10, 300, 100), introText, style);
style.fontSize = 24;
style.normal.textColor = Color.red;
// 在屏幕右上角显示分数
GUI.Label(new Rect(Screen.width - 150, 20, 150, 30), "Score: " + Score, style);
// 显示射击次数
style.fontSize = 18;
GUI.Label(new Rect(Screen.width - 250, Screen.height - 20, 250, 30), "每个射击区域有十次射击机会 ",style);
GUI.Label(new Rect(Screen.width - 150, Screen.height - 50, 150, 30), "Area 1 Shots: " + archerControllerScript.area1Shots,style);
GUI.Label(new Rect(Screen.width - 150, Screen.height - 80, 150, 30), "Area 2 Shots: " + archerControllerScript.area2Shots,style);
GUI.Label(new Rect( 50, Screen.height - 50, 150, 30), hitMessage, style);
}
public void SetHitMessage(string message)
{
hitMessage = message;
}
public void ClearHitMessage()
{
hitMessage = "";
}
}
三、 游戏展示
视频地址:Unity3D 第一人称射箭游戏_哔哩哔哩_bilibili