前言
在该游戏中,玩家将操控一个携带弓弩的角色,在有限的时间内尽量获得更多的积分,积分可以通过射中靶子获得,靶子有静止靶和移动靶,不同靶子得分不同。玩家必须要进入指定的射击区内才可以进行射击,射击区的射击数有限。
一、制作角色player
使用一个Capsule胶囊体来作为角色的身体,并把弓和一个摄像机作为胶囊体的子对象,调整摄像机和弓的位置使得画面合理。
二、角色控制
1.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 = 100f; //鼠标灵敏度
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);
}
}
2.PlayerMovement角色移动
该脚本实现了通过WASD来操作角色移动
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地形,使用下载的资源将地形染绿,然后用黄色的地形刷子刷出黄色的道路,再往上面加一些花草、树木、房子、山进行装饰,便能做出一块简单的地形。
四、实现天空盒切换SkyboxSwitcher
用一个数组skyboxMaterials存储多个天空盒材质,并在按下Q时进行切换,把该脚本挂载在摄像机上,并在Inspector上配置想要的天空盒材质。
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];
}
}
五、实现拉弓与射箭ShootController
按下左键时会进行蓄力,蓄力时会给下一次发射的箭提供一个力,按下右键可以把箭发射出去。
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;//是否正在蓄力
public AudioSource audio;
bool isFired=true; //是否已经将蓄力的箭发射
public Slider Powerslider;//蓄力条
public SpotController currentSpotController;
public bool readyToShoot = false;//是否可以开始射击
public int shootNum;// 剩余射击次数
// Start is called before the first frame update
void Start()
{
shootNum = 0;
Singleton<UserGUI>.Instance.SetShootNum(shootNum);
animator = GetComponent<Animator>();
animator.SetFloat("power", 1f);
}
// Update is called once per frame
void Update()
{
// if(shootNum>0){
// readyToShoot = true;
// }
if (!readyToShoot)
{
Powerslider.gameObject.SetActive(false);
return;
}else{
Powerslider.gameObject.SetActive(true);
}
//按照鼠标按下的时间蓄力,每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)
{
audio = GetComponent<AudioSource>();
audio.Play();
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;
}
}
}
public void AddShootNum(int sn){
shootNum += sn;
Singleton<UserGUI>.Instance.SetShootNum(shootNum);
}
//协程:开火
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("箭");
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;
}
}
在屏幕上方放置了一个蓄力条Slider,用于显示蓄力程度。
为了让拉弓和射箭的动画能够正常配合,还需要给弓加上一个Animator组件。
六、利用双摄像机实现瞄准
在角色player上添加一个摄像机,放在弓弩的前方,通过按下Q键进行摄像机切换,这样就有了瞄准的效果
ChangeCamera
该脚本挂载在一个空物体上,用于实现摄像机的切换,初始时瞄准摄像机camera_1处于不激活状态,当按下瞄准按钮Q后,两个摄像机状态互换。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ChangeCamera : MonoBehaviour
{
public GameObject camera_0;//设置成public可以使unity中出现如下图所示的
public GameObject camera_1;
bool isActive_0;
bool isActive_1;
// Start is called before the first frame update
void Start()
{
isActive_0 = true;
isActive_1 = false;
camera_1.SetActive(isActive_1);
}
// Update is called once per frame
void Update()
{
if(Input.GetKeyDown(KeyCode.Q)){
change();
}
}
public void change(){
isActive_0 = isActive_0 ? false : true;
isActive_1 = isActive_1 ? false : true;
camera_0.SetActive(isActive_0);
camera_1.SetActive(isActive_1);
}
}
七、箭的属性ArrowFeatrue
如果与靶子发生了碰撞,箭会停留在靶子上5s
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;
}
}
}
八、靶子控制器TargetController
有一个bool类型的变量move来决定该靶子是否为移动靶,如果为移动靶,通过计算下一帧的位置来实现移动。
通过判断是否与箭单位发生碰撞,来实现加分。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TargetController : MonoBehaviour
{
public int basepoint;//初始分数
public bool move; // 是否移动靶
//public float aniSpeed = 1f;//动画执行速度
public int scores;//单个靶点的分数
//靶子的移动速度
public float speed = 5f;
//靶子的移动距离
public float distance = 10f;
//靶子的起始位置
private Vector3 startPosition;
//靶子的移动方向
private float direction = 1f;
// Use this for initialization
void Start()
{
//记录起始位置
startPosition = transform.position;
this.tag = "target";
//move = false;
//初始分数
scores= 0;
}
private void move_target(){
//计算下一帧的位置
Vector3 nextPosition = transform.position + new Vector3(speed * direction * Time.deltaTime, 0f, 0f);
// 判断是否超出移动范围,超出则改变移动方向
if (Vector3.Distance(startPosition, nextPosition) > distance)
{
direction *= -1f;
}
// 更新位置
transform.position = nextPosition;
}
// Update is called once per frame
void Update()
{
if(move){
move_target();
}
}
//打到靶子
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "Arrow")
{
//scores += 1;
//增加游戏总分数
if(move){
Singleton<UserGUI>.Instance.AddScore(3);
}
else{
Singleton<UserGUI>.Instance.AddScore(1);
}
}
}
}
九、射击区实现
首先创建一个Cylinder对象,调整高度为0.05,这样看起来就如同平面一样,把这个对象作为射击区域,添加Rigidbody和Box Collider组件,来获取与角色的碰撞信息。
SpotController
这个类主要用于记录当前射击区内还有多少次射击机会。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpotController : MonoBehaviour
{
public int shootNum;
// Start is called before the first frame update
void Start()
{
//this.tag = "spot";
shootNum = 5;
}
// Update is called once per frame
void Update()
{
}
}
PlayerController
这个类挂载在角色身上,主要用于处理碰撞事件,如果与射击区发生了碰撞,即进入了射击区,便会进入射击状态,如果离开了射击区,即结束了碰撞,则会退出射击状态。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public ShootController CrossBow;// 弓对象
private bool atSpot = false;// 是否到达射击点
private SpotController spot;// 射击位controller
private TargetController[] targetControllers;//target controller
// Start is called before the first frame update
void Start()
{
CrossBow = GetComponentInChildren<ShootController>();
}
// Update is called once per frame
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){
// Debug.Log("1");
if(collision.gameObject.tag == "spot"){
spot = collision.gameObject.GetComponentInChildren<SpotController>();
atSpot = true;
//Debug.Log("sss");
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;
}
}
}
十、UserGUI用户界面
主要包含这些部分:提示词、倒计时、进入射击区时的提示与射击次数提示,游戏结束提示。
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 GameStatus gameStatus;
public int shootNum;
public int spotScore;
}
public class UserGUI : MonoBehaviour
{
private float timerDuration = 300f; // 5分钟,单位为秒
private float timer; // 计时器
private GUIStyle playInfoStyle;
private Status status;
public int crosshairSize = 20;//准星线条长度
// Start is called before the first frame update
void Start()
{
//Time.timeScale = 0f;
timer = 0f;
// set style
playInfoStyle = new GUIStyle();
playInfoStyle.normal.textColor = Color.blue;
playInfoStyle.fontSize= 25;
status.gameStatus = GameStatus.Playing;
// init status
//status.shootNum = 5;
status.score = 0;
status.tip = string.Empty;
status.spotScore = 0;
status.atSpot = false;
}
// Update is called once per frame
void Update()
{
// 更新计时器
timer += Time.deltaTime;
// 检查是否已经到了设定的时间
if (timer >= timerDuration)
{
// 执行计时结束后的逻辑
status.gameStatus = GameStatus.GameOver;
}
}
public void AddScore(int score){
status.score += score;
}
public void SetShootNum(int shootnum){
status.shootNum = shootnum;
}
private void showPage(){
GUI.Label(new Rect(10, 60, 500, 100), "P——切换天空盒" ,playInfoStyle);
GUI.Label(new Rect(10, 90, 500, 100), "Q——进入瞄准状态", playInfoStyle );
GUI.Label(new Rect(10, 180, 500, 100), "剩余时间" + Mathf.Round(timerDuration-timer).ToString() + "s", playInfoStyle);
GUI.Label(new Rect(10, 150, 500, 100), "游戏总分数:" + status.score, playInfoStyle);
//GUI.Label(new Rect(10, 120, 500, 100), "剩余箭数:" + status.shootNum, 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;
if (status.atSpot)
{
// Draw the message with the new GUIStyle
GUI.Label(new Rect(10, 120, 500, 100), "您已到达射击位,剩余射击次数:" + status.shootNum, playInfoStyle);
//GUI.Label(new Rect(10, 90, 500, 100), "您在该靶点的射击分数:" + status.spotScore, playInfoStyle);
}
// if (GUI.Button(new Rect(10, 200, 100, 50), "补充子弹"))
// {
// Singleton<ShootController>.Instance.SetShootNum(5);
// }
}
private void showEndPage(){
playInfoStyle.fontSize = 65;
GUI.Label(new Rect(
(Screen.width - 500) / 2 - 100, // X坐标:屏幕宽度的一半减去标签宽度的一半
(Screen.height - 100) / 2 , // Y坐标:屏幕高度的一半减去标签高度的一半
500, // 宽度
100), "游戏结束,总得分为:"+status.score , playInfoStyle);
}
private void OnGUI(){
if(status.gameStatus == GameStatus.Playing){
showPage();
}
else{
showEndPage();
}
}
public void SetIsAtSpot(bool isspot){
status.atSpot = isspot;
}
}
十一、背景音乐与射击音效
背景音乐
创建一个空物体,添加Audio Source组件,往里面添加背景音乐文件,勾选play on awake,这样它就会在游戏开始后播放
射击音效
在弓物体上添加Audio Source组件,添加射击音效文件,然后在shootController里添加代码,实现每次发射弓箭时播放射击音效。
//按下鼠标右键,将弓箭发射出去
if (Input.GetMouseButtonDown(1) && readyToShoot)
{
audio = GetComponent<AudioSource>();
audio.Play();
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;
}
}
