目录
一、加载资源包
本项目中使用了Cross Bow
、VegetationSpawner
、Military Target
和Fantacy Skybox F REE
资源包,均可在Asset Store免费下载。其中Cross Bow
提供了弓和箭的预制,VegetationSpawner
提供了树的预制,Military Target
提供了靶子的预制,Fantacy Skybox F REE
提供了天空盒和地形的预制。
二、定义人物
首先创建一个胶囊体作为我们的人物,将摄像头和弓的预制模型挂载在人物上,从而在第一人称视角能看到手持弓箭的场景。注意要调节摄像头的高度,使得视角在一个合适的位置。
三、人物的控制
首先为人物添加一个Character Controller
脚本MouseLook
用于控制视角
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MouseLook : MonoBehaviour
{
// Start is called before the first frame update
public float mouseSensitivity = 300f;
public Transform playerBody;
float xRotation = 0f;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
}
// Update is called once per frame
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, -90f, 90f);// limit the angle
// rotate the camera within Y axis
transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
// rotate the player within X axis
playerBody.Rotate(Vector3.up * mouseX);
}
}
脚本PlayMovement
用以控制人物移动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting.APIUpdating;
public class PlayerMovement : MonoBehaviour
{
public CharacterController controller;
public float speed = 12f;
private float gravity = 9.8f;
// Start is called before the first frame update
Vector3 move;
void Start()
{
}
// Update is called once per frame
void Update()
{
if(controller.isGrounded){
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
//Vector3 move = new Vector3(x, 0f, z);// × global movement, we dont want
move = transform.right * x + transform.forward * z;// move along the local coordinates right and forward
}
move.y = move.y - gravity*Time.deltaTime;
controller.Move(move * speed * Time.deltaTime);
}
}
四、搭建地形
首先创建一个Terrain
对象,在上面添加加载树的预制和草的预制,从而形成人物活动的主要区域
再将人物放入Terrain
中,此时启动游戏就能以第一视角在地形中走动
五、设置箭的属性
设置箭飞出去的速度、销毁时间。当箭与靶子发生碰撞时,箭会成为靶子的子对象,并且在一定时间后自动销毁。当箭没有射中靶子时,会在一个更短的时间内销毁。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowFeature : MonoBehaviour
{
public Vector3 startPos;
public Vector3 startDir;
public Transform target;//collider transform
public float speed;
public float destoryTime;
Rigidbody rb;
// Start is called before the first frame update
void Start()
{
rb = GetComponent<Rigidbody>();
destoryTime = 10f;
}
// Update is called once per frame
void Update()
{
destoryTime -= Time.deltaTime;
if (destoryTime < 0)
{
Destroy(this.transform.gameObject);
}
}
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "target")
{
if (!rb.isKinematic)
{
rb.isKinematic = true;
target = collision.gameObject.transform;
transform.SetParent(target);
}
destoryTime = 5f;
}
}
}
六、设置靶子的属性
靶子在被箭射中后,会将总体的分数加一,同时在这个靶子的得分也会加一
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TargetController : MonoBehaviour
{
public int basepoint;//初始分数
public float aniSpeed = 1f;//动画执行速度
public int scores;//单个靶点的分数
// Use this for initialization
void Start()
{
this.tag = "target";
//初始分数
scores= 0;
}
// Update is called once per frame
void Update()
{
}
//打到靶子
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "Arrow")
{
scores += 1;
//增加游戏总分数
Singleton<UserGUI>.Instance.AddScore(1);
}
}
}
七、设置弓的控制器
弓按住左键能进行蓄力,蓄力过程播放动画。在蓄力完成后按下右键进行发射一支箭,箭的初始速度根据蓄力的程度来确定。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ShootController : MonoBehaviour
{
float force;// 蓄力力量
const float maxForce = 1f; // 最大力量
const float chargeRate = 0.1f; // 每0.3秒蓄力的量
Animator animator;//弓动画控制
float mouseDownTime;// 记录鼠标蓄力时间
bool isCharging=false;//是否正在蓄力
bool isFired=true; //是否已经将蓄力的箭发射
public Slider Powerslider;//蓄力条
public SpotController currentSpotController;
public bool readyToShoot = false;//是否可以开始射击,通过检测是否进入射击位置来设置
public int shootNum = 0;// 剩余设计次数
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
animator.SetFloat("power", 1f);
}
// Update is called once per frame
void Update()
{
if (!readyToShoot)
{
Powerslider.gameObject.SetActive(false);
return;
}
//按照鼠标按下的时间蓄力,每0.3秒蓄0.1的力(最多0.5)加到animator的power属性上,并用相应的力射箭
if (Input.GetMouseButtonDown(0) && isFired) // 0表示鼠标左键
{
isFired = false;
mouseDownTime = Time.time; // 记录鼠标按下的时间
isCharging = true; // 开始蓄力
Powerslider.gameObject.SetActive(true);//显示蓄力条
animator.SetTrigger("start");
}
//根据蓄力程度更新弓的动画
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);
}
//鼠标左键弹起,此时进入hold动画
if(Input.GetMouseButtonUp(0))
{
animator.SetTrigger("hold");
isCharging = false;
}
//按下鼠标右键,将弓箭发射出去
if (Input.GetMouseButtonDown(1) && readyToShoot)
{
isFired = true;
//isCharging = false; // 停止蓄力
animator.SetTrigger("shoot");
animator.SetFloat("power", force); // 将蓄力的量加到animator的power属性上
StartCoroutine(DelayedFireCoroutine(force));//延迟0.5s后射击
Powerslider.value = 0;//清零蓄力条
animator.SetFloat("power", 0f);
//update shootNum
shootNum--;
currentSpotController.shootNum--;
Singleton<UserGUI>.Instance.SetShootNum(shootNum);
if (shootNum == 0)
{
readyToShoot = false;
}
}
}
//协程:开火
IEnumerator DelayedFireCoroutine(float f)
{
yield return new WaitForSeconds(0.2f);//等待0.2s后
fire(f);
}
//射击,创建箭的对象并且给其赋予属性
public void fire(float f)
{
GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
ArrowFeature arrowFeature = arrow.AddComponent<ArrowFeature>();
// 使用Find方法通过子对象的名字获取arrow子对象
Transform originArrowTransform = transform.Find("mark");
arrow.transform.position = originArrowTransform.position;
arrow.transform.rotation = transform.rotation;
Rigidbody arrow_db = arrow.GetComponent<Rigidbody>();
arrowFeature.startPos = arrowFeature.transform.position;
arrow.tag = "Arrow";
arrow_db.velocity = transform.forward * 100 * f;
}
}
八、射击位置
一个射击位置对应着几个靶子
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpotController : MonoBehaviour
{
public int shootNum;
//该射击点位对应的靶
public TargetController[] targetControllers;
// Start is called before the first frame update
void Start()
{
this.tag = "spot";
shootNum = 5;
}
// Update is called once per frame
void Update()
{
}
}
九、人与射击位置的检测
人在进入射击位置之后才能拉弓、射箭。并且读取射击位置对应的靶子的分数信息,显示在左上角。
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public ShootController CrossBow;// 弓对象
private bool atSpot = false;// 是否到达射击点
private SpotController spot;// 射击位controller
private TargetController[] targetControllers;//target controller
void Start()
{
CrossBow = GetComponentInChildren<ShootController>();
}
void Update()
{
//如果进入了射击位置
if (atSpot)
{
//屏幕左上角出现对应提示
Singleton<UserGUI>.Instance.SetIsAtSpot(true);
Singleton<UserGUI>.Instance.SetShootNum(spot.shootNum);
if (targetControllers != null)
{
//获取这个射击位置对应靶子的分数信息
int sumSpotScore = 0;
foreach (TargetController targetController in targetControllers)
{
sumSpotScore += targetController.scores;
}
Singleton<UserGUI>.Instance.SetSpotScore(sumSpotScore);
}
}
else
{
Singleton<UserGUI>.Instance.SetIsAtSpot(false);
Singleton<UserGUI>.Instance.SetShootNum(0);
}
}
private void OnCollisionEnter(Collision collision)
{
// 与射击点位撞击
if (collision.gameObject.tag == "spot")
{
spot = collision.gameObject.GetComponentInChildren<SpotController>();
atSpot = true;
if (spot.shootNum > 0)
{
CrossBow.GetComponentInChildren<ShootController>().readyToShoot = true;
CrossBow.GetComponentInChildren<ShootController>().shootNum = spot.shootNum;
CrossBow.GetComponentInChildren<ShootController>().currentSpotController = spot;
}
targetControllers = spot.targetControllers;
if (targetControllers != null)
{
int sumSpotScore = 0;
foreach (TargetController targetController in targetControllers)
{
sumSpotScore += targetController.scores;
}
Singleton<UserGUI>.Instance.SetSpotScore(sumSpotScore);
}
}
}
private void OnCollisionExit(Collision collision)
{
if (collision.gameObject.tag == "spot")
{
Debug.Log("collideExit with spot");
CrossBow.GetComponentInChildren<ShootController>().readyToShoot = false;
atSpot = false;
}
}
}
十、天空盒切换
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkyboxSwitcher : MonoBehaviour
{
public Material[] skyboxMaterials; // 存储不同天空盒的材质
private int currentSkyboxIndex = 0; // 当前天空盒的索引
void Start()
{
RenderSettings.skybox = skyboxMaterials[currentSkyboxIndex]; // 初始设置天空盒
}
void Update()
{
// 检测按下 'P' 键
if (Input.GetKeyDown(KeyCode.P))
{
// 切换到下一个天空盒
SwitchSkybox();
}
}
void SwitchSkybox()
{
// 增加索引,确保循环切换
currentSkyboxIndex = (currentSkyboxIndex + 1) % skyboxMaterials.Length;
// 设置新的天空盒材质
RenderSettings.skybox = skyboxMaterials[currentSkyboxIndex];
}
}
十一、提示的GUI
GUI包含了显示当前的分数、显示进入射击位置、显示当前射击位置的得分、显示准星。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.EventSystems.EventTrigger;
public enum GameStatus
{
Playing,
GameOver,
}
struct Status
{
public int score;
public string tip;
public float tipShowTime;
public bool atSpot;
public int shootNum;
public GameStatus gameStatus;
public int spotScore;
}
public class UserGUI : MonoBehaviour
{
private ISceneController currentSceneController;
private GUIStyle playInfoStyle;
private Status status;
public int crosshairSize = 20;//准星线条长度
public float zoomSpeed = 5f;//滚轮切换视野大小的速度
public float minFOV = 20f;//最小视野宽
public float maxFOV = 60f;//最大视野宽
// Start is called before the first frame update
void Start()
{
Init();
currentSceneController = Director.GetInstance().CurrentSceneController;
// set style
playInfoStyle = new GUIStyle();
playInfoStyle.normal.textColor = Color.black;
playInfoStyle.fontSize= 25;
// init status
status.shootNum = 0;
status.score = 0;
status.tip = string.Empty;
status.atSpot = false;
status.gameStatus = GameStatus.Playing;
status.spotScore = 0;
status.tipShowTime = 0;
}
private void Init()
{
}
// Update is called once per frame
void Update()
{
status.tipShowTime -= Time.deltaTime;
}
private void OnGUI()
{
// show user page
ShowPage();
}
public void SetGameState(GameStatus gameStatus)
{
status.gameStatus = gameStatus;
}
/*
set property of status
*/
public void SetScore(int score)
{
status.score = score;
}
public void SetSpotScore(int score)
{
status.spotScore = score;
}
public void AddScore(int score)
{
status.score+=score;
}
public void SetShootNum(int shootNum)
{
status.shootNum = shootNum;
}
public void SetIsAtSpot(bool isAtSpot)
{
status.atSpot = isAtSpot;
}
private void ShowPage()
{
switch (status.gameStatus)
{
case GameStatus.Playing:
ShowPlayingPage();
break;
case GameStatus.GameOver:
ShowGameoverPage();
break;
}
}
private void ShowGameoverPage()
{
GUI.Label(new Rect(Screen.width / 2 - 40, 60, 60, 100), "游戏结束!", playInfoStyle);
}
private void ShowPlayingPage()
{
GUI.Label(new Rect(10, 10, 60, 100), "正在游戏",playInfoStyle);
if (status.atSpot)
{
// Draw the message with the new GUIStyle
GUI.Label(new Rect(10, 50, 500, 100), "您已到达射击位,剩余射击次数:" + status.shootNum, playInfoStyle);
GUI.Label(new Rect(10, 90, 500, 100), "您在该靶点的射击分数:" + status.spotScore, playInfoStyle);
}
GUI.Label(new Rect(10, 130, 500, 100), "游戏总分数:" + status.score, playInfoStyle);
// show 准星
float screenWidth = Screen.width;
float screenHeight = Screen.height;
float centerX = screenWidth / 2f;
float centerY = screenHeight / 2f;
// 设置准星颜色
GUI.color = Color.red;
// 绘制准星
GUI.DrawTexture(new Rect(centerX - crosshairSize / 2f + 15f, centerY + 5f , crosshairSize, 2f), Texture2D.whiteTexture);
GUI.DrawTexture(new Rect(centerX - 1f + 15f, centerY - crosshairSize / 2f + 5f, 2f, crosshairSize), Texture2D.whiteTexture);
// 恢复GUI颜色设置
GUI.color = Color.white;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController
{
void LoadResources();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Director : System.Object
{
static Director _instance;
// 关联场记实例
public ISceneController CurrentSceneController { get; set; }
// 获取当前的场记
public static Director GetInstance()
{
if (_instance == null)
{
_instance = new Director();
}
return _instance;
}
public static void ReloadCurrentScene()
{
int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
SceneManager.LoadScene(currentSceneIndex);
}
}
十二、蓄力条
蓄力条使用Slider
组件,挂载在摄像机上