目录
一、游戏介绍
1、游戏要求
- 基础分:有博客;
- 1-3分钟视频:视频呈现游戏主要游玩过程;
- 地形:使用地形组件,上面有草、树;
- 天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶:有一个以上固定的靶标;
- 运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
- 驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
2、游戏规则
每个射击位有5次射箭机会,只有在射击位上方才能射箭。当箭射在靶上时,靶心得6分,之后每向外一环得分减少一份,靶子有固定靶和运动靶。射箭时还会根据鼠标点击的时间长短来决定蓄力的大小,从而控制箭射出去的力量。
二、游戏实现
为了实现游戏要求,步骤如下:
1、地形
在GameObject-3D Object-Terrain上创建地形,然后在Asset Store上下载合适的树资源包,在Unity中导入资源包之后将树和草预制对象放在创建的Terrain上,即可形成想要的地形。
2、天空盒
为了实现随时间自动切换天空盒,在Asset Store上导入资源包后,在摄像机上添加组件SkyBox Materials,并将想要进行切换的天空盒添加在组件中,然后在使用脚本控制,可以实现没过5秒切换一次天空盒背景,代码如下:
public class changeskybox : MonoBehaviour
{
public Material[] skyboxMaterials;
// Start is called before the first frame update
void Start()
{
StartCoroutine(ChangeSkybox());
}
IEnumerator ChangeSkybox()
{
while (true)
{
// 随机选择一个天空盒子材质
int index = Random.Range(0, skyboxMaterials.Length);
RenderSettings.skybox = skyboxMaterials[index];
// 等待一段时间后再切换天空盒子
yield return new WaitForSeconds(5);
}
}
}
3、固定靶和运动靶
可以先创建并生成一个靶子的对象,使用不同大小的Cylinder分别作为靶子的不同环,并在上面添加Mesh Collider作为碰撞器,同时添加脚本来为每一环来设置分数
public class TargetData : MonoBehaviour
{
// 挂载到Target上的每一环,用来返回该环对应的分数。
public int GetScore()
{
string name = this.gameObject.name;
int score = 7 - (name[0] - '0');
return score;
}
}
将创建的靶子作为预制,然后生成更多靶子。对于运动靶,需要先添加一个Animator,然后在Animation中设置靶子的运动轨迹就可以完成运动靶的设计,同时为了处理箭射中靶子之后的事件,还需要在每个靶子上添加一个触发的脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TriggerController : MonoBehaviour
{
int score = 0;
public int scores = 0;
void Start()
{
scoreController = Singleton<ScoreController>.Instance;
}
// 当有箭射中某一环后触发
void OnCollisionEnter(Collision collider)
{
//得到箭
GameObject arrow = collider.gameObject;
Rigidbody rb = arrow.GetComponent<Rigidbody>();
if (arrow == null)
{
return;
}
if (collider.gameObject.tag == "Arrow")
{
arrow.GetComponent<Rigidbody>().isKinematic = true;
// 分数控制器
score = this.gameObject.gameObject.GetComponent<TargetData>().GetScore();
scores += score;
Debug.Log(this.gameObject.name);
}
}
}
4、射击位
同样使用Cylinder来完成射击位的设计,然后需要设置每个射击位的射击次数,添加的脚本代码如下:
public class SpotController : MonoBehaviour
{
public int shots;
// Start is called before the first frame update
void Start()
{
shots = 5;
}
}
5、弩弓管理
在弩和箭上,需要实现的功能如下:
1、对弩弓是否到达射击位进行判断,并且完成射击次数的管理
2、支持蓄力半拉弓,鼠标点击时蓄力,在松开鼠标时箭释放,同时蓄力大小用Slider显示出来
3、防止弩弓被碰撞时飞走
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CrossbowController : MonoBehaviour
{
public float mouseSensitivity = 100.0f;
//public Transform playerBody;
float force;
const float maxForce = 0.5f; // 最大力量
const float chargeRate = 0.1f; // 每0.3秒蓄力的量
Animator animator;
float mouseDownTime;
bool isCharging;
public Slider Powerslider;
public int speed;
public bool ready_to_shoot;
private bool atSpot = false;
public SpotController[] spots;
private SpotController inspot;
string message;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
animator = GetComponent<Animator>();
ready_to_shoot = false;
inspot = null;
message = "";
}
void Update()
{
foreach(SpotController s in spots)
{
// 获取两个物体在 X 和 Z 轴上的位置
Vector2 position1 = new Vector2(s.gameObject.transform.position.x, s.gameObject.transform.position.z);
Vector2 position2 = new Vector2(this.gameObject.transform.position.x, this.gameObject.transform.position.z);
// 计算在 X 和 Z 轴上的距离
float distance = Vector2.Distance(position1, position2);
if (distance<=0.5)//位于射击位上方
{
atSpot = true;
inspot = s;
if (s.shots > 0)
{
ready_to_shoot = true;
}
else
{
ready_to_shoot = false;
}
break;
}
else
{
atSpot = false;
//shots = 0;
inspot = null;
ready_to_shoot = false;
}
}
if (ready_to_shoot)
{
//按照鼠标按下的时间蓄力,每0.3秒蓄0.1的力(最多0.5)加到animator的power属性上,并用相应的力射箭
if (Input.GetMouseButtonDown(0)) // 0表示鼠标左键
{
mouseDownTime = Time.time; // 记录鼠标按下的时间
isCharging = true; // 开始蓄力
Powerslider.gameObject.SetActive(true);
animator.SetTrigger("pull");
}
if (isCharging)
{
float holdTime = Time.time - mouseDownTime; // 计算鼠标按下的时间
force = Mathf.Min(holdTime / 0.3f * chargeRate, maxForce); // 计算蓄力的量,最大为0.5
Powerslider.value = force / maxForce; // 更新力量条的值
animator.SetFloat("power", force + 0.5f);
}
if (Input.GetMouseButtonUp(0) && isCharging)
{
isCharging = false; // 停止蓄力
animator.SetTrigger("hold");
float holdTime = Time.time - mouseDownTime; // 计算鼠标按下的时间
force = Mathf.Min(holdTime / 0.3f * chargeRate, maxForce); // 计算蓄力的量,最大为0.5
animator.SetFloat("hold_power", force + 0.5f); // 将蓄力的量加到animator的power属性上
StartCoroutine(DelayedFireCoroutine(force));
Powerslider.value = 0;
animator.SetTrigger("shoot");
Debug.Log("setrigger");
}
}
else
{
return;
}
}
IEnumerator DelayedFireCoroutine(float f)
{
Debug.Log("Ready to fire!!");
yield return new WaitForSeconds(0f);
fire(f);
inspot.shots--;
}
public void fire(float f)
{
// Your existing fire code
GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
ArrowController aw = arrow.AddComponent<ArrowController>();
// 使用Find方法通过子对象的名字获取子对象
Transform childTransform1 = transform.Find("弦");
aw.transform.position = childTransform1.position;
aw.transform.rotation = Quaternion.LookRotation(this.transform.forward);
Rigidbody arrow_db = arrow.GetComponent<Rigidbody>();
arrow.tag = "Arrow";
arrow_db.AddForce(100 * f * this.transform.forward);
}
void OnCollisionEnter(Collision collider)
{
this.gameObject.GetComponent<Rigidbody>().isKinematic = true;
}
void OnCollisionExit(Collision collision)
{
this.gameObject.GetComponent<Rigidbody>().isKinematic = false;
}
void OnGUI()
{
GUIStyle style = new GUIStyle(GUI.skin.label);
style.fontSize = 20; // Set the font size to 30
if (atSpot)
{
// Draw the message with the new GUIStyle
message = "您已到达射击位,剩余射击次数:" + inspot.shots;
}
else
{
message = "";
}
GUI.Label(new Rect(300, 350, 500, 100), message, style);
}
}
6、游走
为了在鼠标移动的时候弩弓能跟着旋转,需要代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Rotate : MonoBehaviour
private float rotationSpeed = 5f;
public float maxRotationX = 60f; // 上下旋转的最大角度
private float currentRotationX = 0f; // 当前上下旋转角度
void Update()
{
FollowMouse();
}
void FollowMouse()
{
// 获取鼠标在水平和垂直方向的移动距离
float mouseX = Input.GetAxis("Mouse X");
float mouseY = Input.GetAxis("Mouse Y");
// 根据鼠标移动距离计算旋转角度
float rotationX = mouseY * rotationSpeed;
float rotationY = mouseX * rotationSpeed;
// 左右旋转围绕 Y 轴
transform.Rotate(Vector3.up, rotationY, Space.World);
// 计算上下旋转角度并限制范围
currentRotationX -= rotationX;
currentRotationX = Mathf.Clamp(currentRotationX, -maxRotationX, maxRotationX);
// 应用上下旋转围绕局部 X 轴
Quaternion targetRotation = Quaternion.Euler(currentRotationX, transform.localEulerAngles.y, transform.localEulerAngles.z);
transform.localRotation = targetRotation;
}
}
为了能够使用键盘实现弩弓位置的改变,从而使弩弓能够到达射击位,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move : MonoBehaviour
{
//public float speed = 1f; // 允许设置速度
public float moveSpeed = 3.0f;
public float rotateSpeed = 90f;
void Update()
{
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
transform.localPosition = this.transform.position + this.transform.forward * z * Time.fixedDeltaTime+ this.transform.right * x * Time.fixedDeltaTime;
}
}
7、碰撞与计分
为了最后统计总得分,需要将所有靶子上的TriggerController中的得分加在一起,并在页面中显示出来,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
int totalScore;
public TriggerController[] controller;
// Start is called before the first frame update
void Start()
{
totalScore = 0;
}
// Update is called once per frame
void OnGUI()
{
int tScore = 0;
foreach (var con in controller)
tScore += con.scores;
// Create a new GUIStyle
GUIStyle style = new GUIStyle(GUI.skin.label);
style.fontSize = 20; // Set the font size to 30
// Draw the score label with the new GUIStyle
GUI.Label(new Rect(10, 10, 200, 60), "Score: " + tScore, style);
totalScore = tScore;
}
}