游戏要求:
地形:使用地形组件,上面有草、树;
天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
固定靶:有一个以上固定的靶标;
运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
素材
弩弓使用素材Classical Crossbow 资源
弩弓动画机
其中empty-pull为Blend Tree,参数power(0~1)控制蓄力拉弓程度。
其他三个参数为bool变量,start为true时为弓箭上弦,预备蓄力;
shoot_ready为true表示蓄力完毕,可以准备射击;
shoot为true时射击。
弩弓的组件:
characterController组件用来控制弩弓的基本移动和处理一些简单的碰撞
shijiaoyidong脚本控制弩弓行走和视角旋转
shoot脚本处理射箭的相关逻辑
弓箭(arrow)的组件:
Rigidbody刚体组件为弓箭添加重力等物理特性
Arrow_target脚本处理弓箭与靶子的碰撞
MeshCollider为碰撞体,碰撞检测的必要条件
靶子(target):
靶子预制件的制作比较简单,把两个圆柱体放到一起就好了。
Mesh Collider组件是与弓箭检测碰撞的必要条件
得分要点及代码说明:
地形:
直接使用地形编辑器编辑,无代码,效果可参考游戏视频。
值得一提的是树木的碰撞问题,要想实现人物与树木的碰撞检测,除了人物要添加characterController组件外,树木在作为素材添加到地形Terrain前还得添加碰撞器Collider
天空盒:
本代码的设计是按“k”可以切换天空盒,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class changeSkyBox : MonoBehaviour
{
public Material[] skyBoxes;
private int currentSkyboxIndex = 0;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
// 检测是否按下K键
if (Input.GetKeyDown(KeyCode.K))
{
// 切换到下一个天空盒
currentSkyboxIndex = (currentSkyboxIndex + 1) % skyBoxes.Length;
// 设置RenderSettings中的skybox属性
RenderSettings.skybox = skyBoxes[currentSkyboxIndex];
}
}
}
固定靶与移动靶:
主要是移动靶的固定点来回移动逻辑,通过代码实现,如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class target_move : MonoBehaviour
{
public float minX = 12f; // 最小X坐标
public float maxX = 20f; // 最大X坐标
public float moveSpeed = 2f; // 移动速度
void Update()
{
// 计算X坐标
float newX = Mathf.PingPong(Time.time * moveSpeed, maxX - minX) + minX;
// 更新靶的位置
transform.position = new Vector3(newX, transform.position.y, transform.position.z);
}
}
射击位:
射击位是两个圆柱体做成的圆台,逻辑是将两个圆台放置在地面上,当人物走到圆台上面时,检测到与圆台的碰撞,将shoot_pos置为true,即可开始射箭,当检测不到与射击位的碰撞时,shoot_pos为false,无法射击。
每次进入射击位时有五次射击机会,当射击机会为0时无法射击。
离开射击位时得分置0,射击机会置0
代码如下:
void OnControllerColliderHit(ControllerColliderHit hit)
{
// 检测是否与圆台碰撞
if (hit.gameObject.CompareTag("shoot_pos"))
{
Debug.Log("进入射击位");
shoot_pos = true;
}
else
{
arrow_target.score = 0;
shoot_pos = false;
arrowCount = 5;
UpdateArrowCountText();
for(int i = 0; i < arrowCount; i++)
{
if (arrows[i] != null)
{
Destroy(arrows[i]);
}
}
}
}
弩弓动画:
前面已经介绍了一部分,在此补上操作逻辑:
按住鼠标右键,代表射击力度的参数power逐渐增加,同时蓄力拉弓动画播放;
松开右键,power不变,同时shoot_ready变为true;
当shoot_ready变为true后,按下鼠标左键可以射击
射击代码如下:
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class shoot : MonoBehaviour
{
public Transform crossbow;
private Transform arrow;
private Animator shoot_animator;
public Transform arrowbegin; // 箭射出的位置
public Transform arrowPrefab; // 箭的预制体
public float maxPower = 1.0f;
public float powerIncreaseSpeed = 0.1f;
private bool shoot_pos = false;
private float currentPower = 0.0f;
public TextMeshProUGUI arrowCountText; // 引用显示剩余射箭次数的 UI Text 元素
private int arrowCount = 5; // 初始射箭次数
private GameObject[] arrows = new GameObject[5];
/* private bool isCharging = false;*/
private void Start()
{
shoot_animator = GetComponent<Animator>();
arrow = crossbow.Find("箭");
arrow.gameObject.SetActive(false);
}
void Update()
{
if (currentPower == 0.0f && Input.GetMouseButtonDown(1))
{
shoot_animator.SetBool("start", true);
}
// 检测鼠标右键按住
if (shoot_pos && Input.GetMouseButton(1) && arrowCount > 0)
{
arrow.gameObject.SetActive(true);
ChargePower();
}
// 检测鼠标右键松开
else if (shoot_pos && currentPower > 0 && Input.GetMouseButtonUp(1) && arrowCount > 0)
{
ReleasePower();
}
// 检测鼠标左键按下
if (shoot_pos && Input.GetMouseButtonDown(0) && currentPower > 0)
{
ShootArrow();
currentPower = 0.0f;
}
if (shoot_pos && Input.GetMouseButtonUp(0))
{
shoot_animator.SetBool("shoot", false);
arrow.gameObject.SetActive(false);
}
}
void OnControllerColliderHit(ControllerColliderHit hit)
{
// 检测是否与圆台碰撞
if (hit.gameObject.CompareTag("shoot_pos"))
{
Debug.Log("进入射击位");
shoot_pos = true;
}
else
{
arrow_target.score = 0;
shoot_pos = false;
arrowCount = 5;
UpdateArrowCountText();
for(int i = 0; i < arrowCount; i++)
{
if (arrows[i] != null)
{
Destroy(arrows[i]);
}
}
}
}
void ChargePower()
{
// 逐渐增加power值,最大不超过1
currentPower = Mathf.Min(currentPower + powerIncreaseSpeed * Time.deltaTime, maxPower);
shoot_animator.SetFloat("power", currentPower);
}
void ReleasePower()
{
// 设置shoot_ready为true
shoot_animator.SetBool("shoot_ready", true);
shoot_animator.SetBool("start", false);
}
void ShootArrow()
{
// 设置shoot为true,并重置power和shoot_ready
shoot_animator.SetBool("shoot_ready", false);
shoot_animator.SetBool("shoot", true);
// 实例化箭对象
Transform newArrow = Instantiate(arrowPrefab, arrowbegin.position, arrowbegin.rotation);
arrows[5 - arrowCount] = newArrow.gameObject;
// 计算射出的方向
Vector3 shootDirection = arrowbegin.forward;
// 获取弓的力度(power)作为箭的速度
float arrowSpeed = currentPower * 20;
// 为箭添加力
newArrow.GetComponent<Rigidbody>().velocity = shootDirection * arrowSpeed;
shoot_animator.SetFloat("power", 0.0f);
arrowCount--;
UpdateArrowCountText();
}
void UpdateArrowCountText()
{
arrowCountText.text = "Remain shoot counts: " + arrowCount.ToString();
}
}
游走:
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class shijiaoyidong : MonoBehaviour
{
public Transform bow; // 弓箭的Transform组件
public Transform cameraPivot; // 摄像机的Transform组件,作为摄像机的父对象
public float rotationSpeed = 2.0f;
public float moveSpeed = 5.0f;
private float gravity = 9.8f; // 重力加速度
private float verticalSpeed = 0.0f;
private CharacterController characterController;
void Start()
{
characterController = GetComponent<CharacterController>();
}
void Update()
{
ApplyGravity();
// 应用垂直速度
Vector3 verticalMovement = Vector3.up * verticalSpeed * Time.deltaTime;
characterController.Move(verticalMovement);
// 弓箭的上下左右旋转
RotateBow();
// 地面上行走
MoveOnGround();
}
void RotateBow()
{
// 获取鼠标水平和垂直移动的增量
float mouseX = Input.GetAxis("Mouse X");
float mouseY = Input.GetAxis("Mouse Y");
// 弓箭绕垂直轴旋转(左右移动)
bow.Rotate(Vector3.up, mouseX * rotationSpeed, Space.World);
// 弓箭绕水平轴旋转(上下移动),限制旋转角度在一定范围内
float newRotationX = bow.rotation.eulerAngles.x - mouseY * rotationSpeed;
newRotationX = Mathf.Clamp(newRotationX, 0, 90);
bow.rotation = Quaternion.Euler(newRotationX, bow.rotation.eulerAngles.y, 0);
}
void MoveOnGround()
{
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");
Vector3 moveDirection = new Vector3(horizontalInput, 0, verticalInput).normalized;
moveDirection = Camera.main.transform.TransformDirection(moveDirection);
moveDirection.y = 0;
characterController.Move(moveDirection * moveSpeed * Time.deltaTime);
}
void ApplyGravity()
{
// 如果角色不在地面上,应用重力
if (!characterController.isGrounded)
{
verticalSpeed -= gravity * Time.deltaTime;
}
else
{
// 如果角色在地面上,重置垂直速度
verticalSpeed = -0.5f; // 一个小的负值,防止角色陷入地面
}
}
}
碰撞与得分:
分数Score定义成静态全局变量,每次检测到弓箭与靶子的碰撞时增加分数。
射中外圈白色部分+1分,射中内圈红色部分+3分
代码如下:
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEditor.VersionControl;
using UnityEngine;
using UnityEngine.UI;
public class arrow_target : MonoBehaviour
{
private bool isEmbedded = false; // 标记箭是否嵌入靶子
public static int score = 0;
void OnCollisionEnter(Collision collision)
{
Debug.Log(collision.gameObject.name);
// 检测箭与靶子的碰撞
if (collision.gameObject.CompareTag("target") && !isEmbedded)
{
// 将箭嵌入靶子
EmbedArrow(collision.transform);
score += 1;
}
else if (collision.gameObject.CompareTag("target2") && !isEmbedded)
{
// 将箭嵌入靶子
EmbedArrow(collision.transform);
score += 3;
}
}
void EmbedArrow(Transform target)
{
// 将箭的父对象设置为靶子,使箭随着靶子的移动
transform.parent = target;
// 将箭的碰撞器设置为触发器,使其不再参与物理碰撞
GetComponent<Collider>().isTrigger = true;
// 将箭的刚体组件禁用,使其不再受到物理影响
GetComponent<Rigidbody>().isKinematic = true;
// 标记箭已经嵌入靶子
isEmbedded = true;
Debug.Log("箭嵌入靶子!");
}
}
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class Score : MonoBehaviour
{
public TextMeshProUGUI scoreText;
int score;
void Update()
{
score = arrow_target.score;
UpdateScore();
}
void UpdateScore()
{
scoreText.text = "Score:" + score;
}
}
游戏视频:
射箭
代码地址:gitee地址