本项目实现了基于Unity的射箭游戏,概况如下:
- 地形:使用地形组件,上面有草、树;
- 天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶:有一个以上固定的靶标;
- 运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
- 驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
代码仓库:射箭游戏:github仓库
代码文件:
Arrow.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Arrow : MonoBehaviour
{
public Vector3 starting_point;
Transform target;
Vector3 delta;
Rigidbody rb;
// Start is called before the first frame update
void Start()
{
rb = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update()
{
if(rb.isKinematic == true)
{
this.transform.position = target.position + delta;
}
}
void OnCollisionEnter(Collision collision)
{ if (rb.isKinematic == false)
{
rb.isKinematic = true;
target = collision.gameObject.transform;
delta = transform.position - target.position;
}
}
}
这段代码定义了一个名为 Arrow
的 Unity 组件,该组件控制一个游戏对象(预期为箭)的行为。
以下是这段代码的详细解释:
-
starting_point
用于存储箭的初始位置,但在此代码中未被使用。 -
target
用于存储箭碰撞的目标游戏对象的Transform
组件。 -
delta
用于存储箭与目标游戏对象之间的初始距离。 -
rb
用于存储箭的Rigidbody
组件,这是 Unity 中用于物理运算的组件。
在 Start
方法中,箭的 Rigidbody
组件被初始化。
在 Update
方法中,如果箭的 Rigidbody
组件被设置为 kinematic (即,不受物理引擎的影响,但可以通过代码控制其移动),那么箭的位置会被更新为目标游戏对象的位置加上初始距离。
在 OnCollisionEnter
方法中,当箭与其他游戏对象发生碰撞时,如果箭的 Rigidbody
组件未被设置为 kinematic,那么箭的 Rigidbody
组件会被设置为 kinematic,箭的目标会被设置为碰撞的游戏对象,初始距离会被设置为箭与目标游戏对象之间的距离。
CrossBow.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CrossBow : MonoBehaviour
{
public float mouseSensitivity = 100.0f;
public Transform playerBody;
private float xRotation = 0.0f;
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;
public int shots;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
animator = GetComponent<Animator>();
ready_to_shoot = false;
shots = -1;
}
void Update()
{
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, 0f, 180f);
transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
playerBody.Rotate(Vector3.up * mouseX);
//控制水平移动
float H = Input.GetAxis("Horizontal");
float V = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(H, 0.0f, V);
movement = Camera.main.transform.rotation * movement * speed;
Rigidbody rb = playerBody.GetComponent<Rigidbody>();
//Debug.Log(movement);
rb.velocity = new Vector3(movement.x, rb.velocity.y, movement.z);
if(Input.GetKeyDown(KeyCode.Space))
{
rb.velocity = new Vector3(rb.velocity.x, rb.velocity.y + 3, rb.velocity.z);
}
if(!ready_to_shoot)
{
return;
}
//按照鼠标按下的时间蓄力,每0.3秒蓄0.1的力(最多0.5)加到animator的power属性上,并用相应的力射箭
if (Input.GetMouseButtonDown(0)) // 0表示鼠标左键
{
mouseDownTime = Time.time; // 记录鼠标按下的时间
isCharging = true; // 开始蓄力
Powerslider.gameObject.SetActive(true);
}
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("fire");
Debug.Log("setrigger");
float holdTime = Time.time - mouseDownTime; // 计算鼠标按下的时间
force = Mathf.Min(holdTime / 0.3f * chargeRate, maxForce); // 计算蓄力的量,最大为0.5
animator.SetFloat("Power", force + 0.5f); // 将蓄力的量加到animator的power属性上
StartCoroutine(DelayedFireCoroutine(force));
Powerslider.value = 0;
}
}
IEnumerator DelayedFireCoroutine(float f)
{
Debug.Log("Ready to fire!!");
yield return new WaitForSeconds(0.5f);
fire(f);
shots--;
}
public void fire(float f)
{
// Your existing fire code
GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
Arrow aw = arrow.AddComponent<Arrow>();
// 使用Find方法通过子对象的名字获取子对象
Transform childTransform1 = transform.Find("mark");
aw.transform.position = childTransform1.position;
aw.transform.rotation = Quaternion.LookRotation(this.transform.up);
Rigidbody arrow_db = arrow.GetComponent<Rigidbody>();
aw.starting_point = aw.transform.position;
arrow.tag = "Arrow";
Debug.Log("starting_poing:" + aw.starting_point);
arrow_db.velocity = 100 * f * this.transform.up;
}
}
这段代码定义了一个名为 CrossBow
的 Unity 组件,用于控制玩家角色(预计为拿着弩的角色)的行为。具体包括角色移动、视角旋转、跳跃、弩的蓄力射击等。
以下是这段代码的详细解释:
-
Start
方法中,游戏开始时,鼠标被锁定在屏幕中央,初始化弩的Animator
组件,设置弩未准备好射击,弩的射击次数为-1。 -
Update
方法中,首先根据鼠标移动设置角色视角旋转,然后根据输入控制角色水平移动和跳跃,接着如果弩未准备好射击则返回。如果弩准备好射击,当鼠标左键按下时开始计算蓄力时间和力量,当鼠标左键松开时结束蓄力并射出箭。 -
DelayedFireCoroutine
方法是一个协程,它会在鼠标左键松开后半秒进行射击。 -
fire
方法中,实例化一个箭的游戏对象,设置箭的位置、方向和速度,并射出箭。
Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public GameObject CrossBow;
private bool atSpot = false;
private Spot spot;
void Start()
{
}
void Update()
{ if (atSpot && CrossBow.GetComponent<CrossBow>().shots >= 0)
{
spot.shots = CrossBow.GetComponent<CrossBow>().shots;
}
if(spot.shots == 0)
{
CrossBow.GetComponent<CrossBow>().ready_to_shoot = false;
}
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "Spot")
{
spot = collision.gameObject.GetComponent<Spot>();
atSpot = true;
if (spot.shots > 0)
{
CrossBow.GetComponent<CrossBow>().ready_to_shoot = true;
CrossBow.GetComponent<CrossBow>().shots = spot.shots;
}
spot = collision.gameObject.GetComponent<Spot>();
}
else if(collision.gameObject.name == "Terrain")
{
CrossBow.GetComponent<CrossBow>().ready_to_shoot = false;
atSpot = false;
}
}
private void OnCollisionExit(Collision collision)
{
if (collision.gameObject.name == "Spot")
{
CrossBow.GetComponent<CrossBow>().ready_to_shoot= false;
atSpot = false;
}
}
void OnGUI()
{
if (atSpot)
{
// Create a new GUIStyle
GUIStyle style = new GUIStyle(GUI.skin.label);
style.fontSize = 30; // Set the font size to 30
// Draw the message with the new GUIStyle
GUI.Label(new Rect(10, 50, 500, 100), "您已到达射击位,剩余射击次数:"+spot.shots, style);
}
}
}
以下是这段代码的详细解释:
-
Update
方法中,如果角色在射击位并且弩还有剩余的射击次数,那么更新射击位的剩余射击次数。如果射击位的剩余射击次数为0,那么设置弩未准备好射击。 -
OnCollisionEnter
方法中,当角色碰撞到一个带有 "Spot" 标签的对象时,获取该对象的Spot
组件,设置角色在射击位,如果射击位有剩余的射击次数,那么设置弩准备好射击并更新弩的剩余射击次数。当角色碰撞到一个名为 "Terrain" 的对象时,设置弩未准备好射击,设置角色不在射击位。 -
OnCollisionExit
方法中,当角色离开一个名为 "Spot" 的对象时,设置弩未准备好射击,设置角色不在射击位。 -
OnGUI
方法中,如果角色在射击位,那么显示一个消息,告诉玩家他们已经到达射击位,和剩余的射击次数。
总的来说,这段代码实现了一个游戏角色与射击位的交互,包括角色到达射击位时弩可以射击,角色离开射击位或射击位的剩余射击次数为0时弩不能射击,以及在界面上显示角色是否在射击位和剩余的射击次数。
Spot.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spot : MonoBehaviour
{
public int shots;
// Start is called before the first frame update
void Start()
{
shots = 5;
}
// Update is called once per frame
void Update()
{
}
}
这段代码定义了射击位的属性,即允许的射击次数为5
Target.cs
using UnityEngine;
using System.Collections;
public class Target : MonoBehaviour
{ public int basepoint;
public float pace;
public bool is_moving;
int sign;
Vector3 ori;
public int scores;
// Use this for initialization
void Start()
{
if(is_moving == false)
{
pace = 0;
}
else
{
sign = 1;
ori = this.transform.position;
}
}
// Update is called once per frame
void Update()
{
if(is_moving == true)
{
this.transform.position += new Vector3(sign * 0.01f, 0, 0);
if(transform.position.x - ori.x > 5 || transform.position.x - ori.x < -5 )
{
sign *= -1;
}
}
}
// OnCollisionEnter is called when this collider/rigidbody has begun touching another rigidbody/collider
private void OnCollisionEnter(Collision collision)
{
// Get the first contact point
ContactPoint contact = collision.contacts[0];
// Get the starting_point property from the arrow's script
// Replace "ArrowScript" with the name of the script that contains the starting_point property
if (collision.gameObject.tag == "Arrow")
{
Vector3 starting_point = collision.gameObject.GetComponent<Arrow>().starting_point;
// Calculate the distance
float distance = Vector3.Distance(contact.point, starting_point);
Debug.Log("hitting point:" + contact.point);
// Print the distance
Debug.Log("Distance: " + distance);
Transform center = transform.Find("center");
int factor = 0;
if(Vector3.Distance(contact.point,center.position)<0.09)
{
factor = 5;
}
else if(Vector3.Distance(contact.point, center.position) < 0.37)
{
factor = 3;
}
else
{
factor = 1;
}
if(distance > 5)
{
scores += basepoint + factor * (int)distance;
}
}
}
}
这段代码定义了靶子的行为,靶子可以在一定范围内来回移动,在被射中时,可以根据弓箭飞行的距离以及命中点离靶心的位置进行算分,具体来说,命中点越接近靶心,分数翻的倍数越高,而基数是箭飞行的距离(5米以内不得分)。另外,击中移动的靶子可以额外增加10分。
main.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public GameObject t1;
public GameObject t2;
private int totalScore;
private int previousScore;
private Color scoreColor = Color.white;
void Start()
{
totalScore = 0;
previousScore = 0;
}
IEnumerator ChangeColorBack()
{
// Wait for 1 second
yield return new WaitForSeconds(1);
// Change the color back to white
scoreColor = Color.white;
}
void Update()
{
int t1Score = t1.GetComponent<Target>().scores;
int t2Score = t2.GetComponent<Target>().scores;
previousScore = totalScore;
totalScore = t1Score + t2Score;
if (totalScore != previousScore)
{
// Change the score color to red when the score changes
scoreColor = Color.red;
// Call a coroutine to change the color back to white after 1 second
StartCoroutine(ChangeColorBack());
}
}
void OnGUI()
{
// Set the GUI color to scoreColor
GUI.color = scoreColor;
// Create a new GUIStyle
GUIStyle style = new GUIStyle(GUI.skin.label);
style.fontSize = 30; // Set the font size to 30
// Draw the score label with the new GUIStyle
GUI.Label(new Rect(10, 10, 200, 60), "Score: " + totalScore, style);
}
}
该代码实现了计算两个靶子的分数之和,并显示在屏幕上,在得分时,分数会短暂变成红色。