一、游戏要求
- 基础分(2分):有博客;
- 1-3分钟视频(2分):视频呈现游戏主要游玩过程;
- 地形(2分):使用地形组件,上面有草、树;
- 天空盒(2分):使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶(2分):有一个以上固定的靶标;
- 运动靶(2分):有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位(2分):地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
- 驽弓动画(2分):支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走(2分):玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分(2分):在射击位,射中靶标的相应分数,规则自定。
二、游戏实现
1、导入必要的包
- Fantasy Skybox FREE 用于天空和地形
- 十字弩 Classical Crossbow 弩和弓箭
- Realistic Tree 9 [Rainbow Tree] 树
- Crosshairs | 25+ Free Crosshairs Pack 准星
2、创建预制件
箭 Arrow
使用导入包中的箭,子对象MoveTrack把Trail Renderer中的Autodestruct取消勾选,HitPos添加以下两个component
弩 Crossbow
使用导入包的Crossbow,修改动画使得符合任务要求
在Empty到Pull需要start被触发;Pull中的Parameter选为Power,但这里将Power固定为0.3,因为要实现hold的效果;Hold中的Parameter选为holdPower,holdPower可以在0.3到1之间变化,本游戏通过滚轮调整;Hold到Shoot需要shoot触发
每个状态设置如下(Pull和Hold是Blend Tree)
target
自行制作,效果如下
三个环其实是三个很薄的圆柱,分别对应1、2、3分,Cube是靶身,Cube1是下面的杆子
每个环要加上Mesh Collider,但不要勾选Convex和Is Trigger,颜色自己制作并分别挂载在每个环上;Cube添加Box Collider。此外,以上物体要挂载RingController脚本,并将Ring Score按分值设置(Cube分值为0)
slide target
制作方法基本和target一样,只是多一个动画,实现滑动的功能
x坐标在第0帧为-30,第30帧为-35,第60帧为-30,y一直为0,z一直为20。
stage target和rotate target
和target基本相同,但由于这两个prefab都是用于闯关模式,不用计分,只检测命中,所以所有子对象不挂载RingController,而且子对象不设置Collider。在主对象中设置如下
stage target和rotate target只有动画不一样
stage target只有被击中后倒下的动画,Nothing用于过渡,什么动作都不执行,设置这个状态是因为Entry到下一个状态不能设置条件,Nothing到fall down需要isShot触发
动画实现也很简单,第0帧x为0,第60帧x为90,其他都不变
rotate target只是比stage target多一个旋转的动作
rotate动画实现如下
x最低点为-1.5,最高点为1.5,y最高点为3。这里要注意红线,在每个Key点使用Auto,才会让旋转看起来更圆滑,不会卡顿。
3、场景其他物体设置
Main Camera
添加以下两个component是因为,本游戏没有用“人”做碰撞体,所以为了模拟人会和物体发生碰撞,就在主相机上设置碰撞体。
各种树
使用导入包的树,但是要自己添加Mesh Collider。
Terrain
自由DIY
Second Camera
用于箭射出去后跟踪箭的走向,只有下面两个属性需要调整(在Component--Camera调)
GameObject
空对象,用于挂载脚本
Shoot point 和 Stage start
两个红色薄圆柱,一定要记得把Capsule Collider删掉!!!(在这里debug了好久)
Canvas
无关紧要的东西,只是顺便学一下怎么在场景里能有字,效果就是这个字在场景中是立体的,怎么创建可以查看其他资料,在此不展开讲述。
三、代码实现
类图
代码
Adapter模式
动作基类SSAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject
{
public bool enable = true; //是否正在进行此动作
public bool destroy = false; //是否需要被销毁
public GameObject gameObject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //动作完成后的消息通知者
protected SSAction() { }
//子类可以使用下面这两个函数
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
public virtual void FixedUpdate()
{
throw new System.NotImplementedException();
}
}
动作管理类基类SSActionManager.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();
// Update is called once per frame
protected void Update()
{
foreach (SSAction ac in waitingAdd) actions[ac.GetInstanceID()] = ac;
waitingAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID()); // release action
}
else if (ac.enable)
{
ac.Update(); // update action
}
}
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
waitingDelete.Clear();
}
protected void FixedUpdate()
{
foreach (SSAction ac in waitingAdd) actions[ac.GetInstanceID()] = ac;
waitingAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID()); // release action
}
else if (ac.enable)
{
ac.Update(); // update action
}
}
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
}
箭的射击动作ArrowShootAction.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class ArrowShoot : SSAction
{
public float arrowSpeed = 40f; // 箭的速度
private float arrowForce; //拉弓力度
public static ArrowShoot GetSSAction(float force)
{
ArrowShoot shootarrow = CreateInstance<ArrowShoot>();
shootarrow.arrowForce = force;
return shootarrow;
}
// Start is called before the first frame update
public override void Start()
{
// 摆脱弓的控制
gameObject.transform.parent = null;
// 设成物理模式
gameObject.GetComponent<Rigidbody>().isKinematic = false;
// 将箭的速度设为向前
gameObject.GetComponent<Rigidbody>().velocity = transform.forward * arrowSpeed * arrowForce;
}
// Update is called once per frame
public override void Update()
{
gameObject.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
}
public override void FixedUpdate()
{
// 回调颤抖动作
if (gameObject.tag == "onTarget")
{
this.destroy = true;
this.callback.SSActionEvent(this, this.gameObject);
}
}
}
箭的射击动作管理类ArrowShootAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowShootManager : SSActionManager, ISSActionCallback
{
// 箭飞行的动作
private ArrowShoot arrowShoot;
public MainController mainController;
void Start()
{
// 设置MainController的arrow_manager
mainController = (MainController)SSDirector.getInstance().currentController;
mainController.arrow_manager = this;
}
//箭飞行
public void Shoot(GameObject arrow, float force)
{
// 实例化一个飞行动作。
arrowShoot = ArrowShoot.GetSSAction(force);
// 调用SSActionmanager的方法运行动作。
this.RunAction(arrow, arrowShoot, this);
}
public void SSActionEvent(SSAction source, GameObject arrow = null)
{
ArrowTremble tremble = ArrowTremble.GetSSAction();
RunAction(arrow, tremble, this);
}
}
箭的颤抖动作ArrowTremble.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowTremble : SSAction
{
float tremble_radius = 1.5f;// 颤抖的程度
float tremble_time = 1.0f; // 颤抖的时间
Vector3 arrow_pos; // 箭的原位置
public static ArrowTremble GetSSAction()
{
ArrowTremble tremble_action = CreateInstance<ArrowTremble>();
return tremble_action;
}
// Start is called before the first frame update
public override void Start()
{
// 得到箭中靶时的位置
arrow_pos = this.transform.position;
}
// 实现箭的颤抖动作
// Update is called once per frame
public override void Update()
{ // 更新时间,得到剩余的颤抖时间
tremble_time -= Time.deltaTime;
if (tremble_time > 0)
{
// 获取头部的位置
Vector3 head_pos = this.transform.Find("HitPos").position;
// 围绕箭头颤抖
this.transform.Rotate(head_pos, tremble_radius);
}
else
{
// 将箭返回一开始的位置
transform.position = arrow_pos;
// 开始销毁动作
this.destroy = true;
// 回调为空
this.callback.SSActionEvent(this);
}
}
public override void FixedUpdate()
{
}
}
导演类SSDirector.cs
public class SSDirector : System.Object
{
// singlton instance
private static SSDirector _instance;
public MainController currentController { get; set; }
// get instance anytime anywhare!
public static SSDirector getInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
单实例Singleton.cs
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T) +
" is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
计分器ScoreRecorder.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour
{
public int score;
// Start is called before the first frame update
void Start()
{
score = 0;
}
// Update is called once per frame
public void RecordScore(int ringscore)
{
//增加新的值
score += ringscore;
}
}
ArrowFactory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowFactory : MonoBehaviour
{
private List<GameObject> dirtyArrow; // 正在被使用的arrow
private List<GameObject> freeArrow; // 没有被使用的arrow
public GameObject arrow = null;
void Start()
{
dirtyArrow = new List<GameObject>();
freeArrow = new List<GameObject>();
}
public GameObject GetArrow()
{
int arrowCount = freeArrow.Count;
if (arrowCount == 0)
{
arrow = GameObject.Instantiate(
Resources.Load<GameObject>("Prefabs/Arrow"), Vector3.zero, Quaternion.identity);
}
else
{
arrow = freeArrow[freeArrow.Count - 1];
freeArrow.RemoveAt(freeArrow.Count - 1);
// 箭在靶子上需要把标签改回来
if (arrow.tag == "onTarget")
{
arrow.tag = "arrow";
}
arrow.gameObject.SetActive(true);
}
// 先让箭在镜头前面不动
arrow.GetComponent<Rigidbody>().isKinematic = true;
// 箭跟随视角移动
arrow.transform.parent = Camera.main.transform;
arrow.transform.localPosition = new Vector3(0, 0, 1);
Vector3 cameraDirection = Camera.main.transform.forward;
arrow.transform.forward = cameraDirection.normalized;
dirtyArrow.Add(arrow);
return arrow;
}
public void FreeArrow(GameObject arrow)
{
foreach (GameObject a in dirtyArrow)
{
if (arrow.GetInstanceID() == a.GetInstanceID())
{
a.SetActive(false);
a.GetComponent<Rigidbody>().isKinematic = true;
dirtyArrow.Remove(a);
freeArrow.Add(a);
break;
}
}
}
}
各种预制件的Controller
主相机脚本CameraFlow.cs
挂载在主相机
using UnityEngine;
using System.Collections;
public class CameraFlow : MonoBehaviour
{
public GameObject bow;
private int gameState;
public float moveSpeed;
public LayerMask groundLayer;
// 水平视角移动的敏感度
public float sensitivityHor;
// 垂直视角移动的敏感度
public float sensitivityVer;
// 视角向上移动的角度范围,该值越小范围越大
public float upVer = -85;
// 视角向下移动的角度范围,该值越大范围越大
public float downVer = 85;
// 垂直旋转角度
private float rotVer;
// 水平旋转角度
private float rotHor;
public float smoothness;
private void Start()
{
rotVer = 0;
rotHor = 0;
sensitivityHor = 50f;
sensitivityVer = 30f;
smoothness = 10f;
transform.localPosition = new Vector3(0, 3, -10);
moveSpeed = 10f;
}
void Update()
{
// 更新gameState
gameState = SSDirector.getInstance().currentController.GetGameState();
}
void FixedUpdate()
{
MoveCamera();
RotateCameraTowardsMouse();
}
// 实现第一人称视角的移动
private void MoveCamera()
{
if (gameState == 0)
return;
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(horizontalInput, 0f, verticalInput) * moveSpeed * Time.deltaTime;
// 保持与地面的距离不变
transform.Translate(new Vector3(movement.x, 0f, movement.z));
transform.position = new Vector3(transform.position.x, 3f, transform.position.z);
}
// 实现视角转动
private void RotateCameraTowardsMouse()
{
if (gameState == 0)
return;
// 获取鼠标上下的移动位置
float mouseVer = Input.GetAxis("Mouse Y");
// 获取鼠标左右的移动位置
float mouseHor = Input.GetAxis("Mouse X");
// 鼠标往上移动,视角其实是往下移,所以要想达到视角也往上移的话,就要减去它
rotVer -= mouseVer * sensitivityVer;
rotHor += mouseHor * sensitivityHor;
// 限定上下移动的视角范围,即垂直方向不能360度旋转
rotVer = Mathf.Clamp(rotVer, upVer, downVer);
Quaternion targetRotation = Quaternion.Euler(rotVer, rotHor, 0);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, smoothness * Time.deltaTime);
}
}
副相机脚本SecondCamera.cs
挂载在副相机
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class SecondCamera : MonoBehaviour
{
public float show_time = 3f; // 相机展示时间
public bool show = false;
private GameObject curArrow; // 当前跟随的箭
private Vector3 offset; // 偏移量
void Start()
{
show = false;
curArrow = null;
offset = new Vector3(0, 0, -2);
}
void Update()
{
if (show == true)
{
show_time -= Time.deltaTime;
// 将相机位置设在箭的正后方
transform.position = curArrow.transform.position + curArrow.transform.forward + offset;
// 展示时间超过3秒,停止展示
if (show_time < 0)
{
curArrow = null;
show = false;
this.gameObject.SetActive(false);
}
}
}
// 展示副相机
public void ShowCamera(GameObject arrow)
{
curArrow = arrow;
show = true;
show_time = 3f;
this.gameObject.SetActive(true);
}
}
弩脚本BowController.cs
挂载在弩
using System.Linq.Expressions;
using UnityEngine;
using UnityEngine.UIElements;
public class BowController : MonoBehaviour
{
public Camera cam;
void Start()
{
// 跟随主相机移动
transform.SetParent(cam.transform);
transform.localPosition = new Vector3(1, -0.5f, 1);
}
}
箭脚本ArrowController.cs
挂载在arrow
using UnityEngine;
public class ArrowController : MonoBehaviour
{
private Rigidbody rb;
private bool hit = false;
void Start()
{
rb = GetComponent<Rigidbody>();
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
hit = true;
// 将箭的速度设为0
rb.velocity = Vector3.zero;
// 碰撞到地面时将刚体设置为运动学模式,停止物理运动
GetComponent<Rigidbody>().isKinematic = true;
}
}
private void FixedUpdate()
{
if (transform.parent == null && !hit)
transform.rotation = Quaternion.FromToRotation(
Vector3.forward, rb.velocity.normalized);
}
private void Update()
{
if (transform.parent != null && this.CompareTag("arrow"))
hit = false;
}
}
环脚本RingController.cs
挂载在target和slide target的子对象:三个薄圆柱和Cube
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RingController : MonoBehaviour
{
public int RingScore = 0; // 当前环的分值
public MainController scene;
public ScoreRecorder sc_recorder;
// Start is called before the first frame update
void Start()
{
scene = SSDirector.getInstance().currentController;
sc_recorder = scene.recorder;
}
// Update is called once per frame
void Update()
{
}
void OnCollisionEnter(Collision collision)
{
GameObject arrow = collision.gameObject;
//有箭中靶
if (arrow.CompareTag("arrow"))
{
// 将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = Vector3.zero;
// 使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
// 计分
sc_recorder.RecordScore(RingScore);
//标记箭为中靶
arrow.tag = "onTarget";
}
}
}
闯关模式靶子脚本StageTargetController.cs
挂载在rotate target和stage target的主对象上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StageTargetController : MonoBehaviour
{
public MainController scene;
public ScoreRecorder sc_recorder;
public bool isShot;
// Start is called before the first frame update
void Start()
{
scene = SSDirector.getInstance().currentController;
sc_recorder = scene.recorder;
isShot = false;
}
// Update is called once per frame
void Update()
{
}
void OnCollisionEnter(Collision collision)
{
GameObject arrow = collision.gameObject;
//有箭中靶
if (arrow.CompareTag("arrow") && !isShot)
{
// 将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = Vector3.zero;
// 使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
// 计分
sc_recorder.RecordScore(1);
//标记箭为中靶
arrow.tag = "onTarget";
isShot = true;
gameObject.GetComponent<Animator>().SetTrigger("isShot");
}
}
}
两个接口Interface
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController
{
void LoadResources();
}
using UnityEngine;
public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
// 回调函数
void SSActionEvent(SSAction source, GameObject arrow = null);
}
玩家交互界面UserGUI.cs
需要挂载在GameObject
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
private MainController action;
GUIStyle tip_style = new GUIStyle();
GUIStyle title_style = new GUIStyle();
GUIStyle button_style = new GUIStyle();
GUIStyle state_style = new GUIStyle();
GUIStyle time_style = new GUIStyle();
public Texture texture;
private float time = 0f;
// Start is called before the first frame update
void Start()
{
action = this.transform.gameObject.GetComponent<MainController>();
tip_style.fontSize = 30;
tip_style.normal.textColor = Color.blue;
title_style.normal.textColor = Color.red;
title_style.fontSize = 40;
button_style.normal.textColor = Color.red;
button_style.fontSize = 20;
state_style.normal.textColor = Color.red;
state_style.fontSize = 30;
time_style.normal.textColor = Color.red;
time_style.fontSize = 20;
}
// Update is called once per frame
void Update()
{
if (action.GetGameState() == 1)
{
// 实现射击
// 点击鼠标右键拉弓
if (Input.GetButtonDown("Fire2"))
{
action.LoadArrow();
}
if (action.GetisLoad())
{
// 调整射箭力量
float scrollInput = Input.GetAxis("Mouse ScrollWheel") / 2; //向上滑+1,反之-1
action.SetForce(scrollInput);
// 左键射击
if (Input.GetButtonDown("Fire1"))
{
action.ShootArrow();
}
}
// 实现闯关模式计时
if (action.GetIsStageMode() == 1)
time += Time.deltaTime;
// 进入/重启闯关模式
if (Input.GetKeyDown(KeyCode.R))
{
time = 0;
action.Restart();
}
}
// 切换天空
if (Input.GetKeyDown(KeyCode.Tab))
{
action.SwitchMaterial();
}
}
//
private void OnGUI()
{
if (action.GetGameState() == 1)
{
// 显示准星
int width = texture.width / 20;
int height = texture.height / 20;
Rect rect = new Rect( (Screen.width - width) / 2,
(Screen.height - height) / 2, width, height);
GUI.DrawTexture(rect, texture);
//显示各种信息
GUI.Label(new Rect(20, 10, 150, 50), "Score: ", button_style);
GUI.Label(new Rect(100, 10, 100, 50), action.GetScore().ToString(), state_style);
GUI.Label(new Rect(20, 50, 150, 50), "ArrowNum: ", button_style);
GUI.Label(new Rect(120, 50, 100, 50), action.GetArrowNum().ToString(), button_style);
if(action.GetIsStageMode() != 0)
GUI.Label(new Rect(20, 80, 150, 50), "Time: " + time + " s", time_style);
if(action.GetIsStageMode() == 2)
GUI.Label(new Rect(Screen.width / 2 - 250, Screen.height / 2, 400, 100),
"闯关模式结束,请按R重新开始", tip_style);
}
//游戏未开始,检测开始按键
else
{
GUI.Label(new Rect(Screen.width / 2 - 80, Screen.height / 2 - 320, 100, 100), " Counter Strike:\nPrimitive Campaign", title_style);
//开始按键如果按下,游戏开始
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 + 100, 100, 50), "游戏开始"))
{
action.BeginGame();
}
GUI.Label(new Rect(Screen.width / 2 - 350, Screen.height / 2, 400, 100),
"使用方向键控制弓箭移动,鼠标右键拉弓,左键射击\n按r进入闯关模式,按tab切换天空", tip_style);
}
}
}
场景控制器MainController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainController : MonoBehaviour, ISceneController
{
// 箭的动作管理者
public ArrowShootManager arrow_manager;
// 箭的工厂
public ArrowFactory factory;
// 副相机
public Camera second_camera;
//主相机
public Camera main_camera;
// 记录分数
public ScoreRecorder recorder;
// 弓
public GameObject bow;
// 箭
public GameObject arrow;
// 箭是否load
private bool isLoad;
private int arrow_num = 0; //射出的箭数
// 箭的队列
private Queue<GameObject> arrows = new Queue<GameObject>();
// 0是游戏未开始,1是游戏进行
private int gameState = 0;
// 天空
public List<Material> skyboxMaterials;
// 当前天空下标
private int cnt;
// 练习模式靶子
private List<GameObject> prac_target;
// 是否进入闯关模式
private int isStageMode; // 0是未进入,1是正在进行,2是完成
// 闯关模式靶子
private List<GameObject> stage_target;
public void LoadResources()
{
SSDirector director = SSDirector.getInstance();
director.currentController = this;
bow = Instantiate(Resources.Load("Prefabs/Crossbow", typeof(GameObject))) as GameObject;
LoadPracticeModeTarget();
main_camera = Camera.main;
second_camera = GameObject.Find("Second Camera").GetComponent<Camera>();
second_camera.gameObject.SetActive(false);
factory = gameObject.AddComponent<ArrowFactory>();
recorder = gameObject.AddComponent<ScoreRecorder>();
arrow_manager = gameObject.AddComponent<ArrowShootManager>();
RenderSettings.skybox = skyboxMaterials[cnt];
}
// 加载练习模式靶子
public void LoadPracticeModeTarget()
{
prac_target = new List<GameObject>();
GameObject target1 = Instantiate(Resources.Load(
"Prefabs/target", typeof(GameObject)),
new Vector3(-25, 0, 40), Quaternion.identity) as GameObject;
GameObject target2 = Instantiate(Resources.Load(
"Prefabs/target", typeof(GameObject)),
new Vector3(-45, 0, 10), Quaternion.identity) as GameObject;
GameObject target3 = Instantiate(Resources.Load(
"Prefabs/target", typeof(GameObject)),
new Vector3(-35, 0, 3), Quaternion.identity) as GameObject;
GameObject target4 = Instantiate(Resources.Load(
"Prefabs/slide target", typeof(GameObject))) as GameObject;
prac_target.Add(target1);
prac_target.Add(target2);
prac_target.Add(target3);
prac_target.Add(target4);
}
// 加载闯关模式靶子
private void LoadStageTarget()
{
// 释放之前的
if (stage_target != null && stage_target.Count > 0)
{
foreach(GameObject obj in stage_target)
{
Destroy(obj); // 这里可以用TargetFactory优化,避免重复生成删除
}
}
stage_target = new List<GameObject>();
GameObject target1 = Instantiate(Resources.Load(
"Prefabs/stage target", typeof(GameObject)),
new Vector3(73, 0.3f, -6), Quaternion.Euler(0, 90, 0)) as GameObject;
GameObject target2 = Instantiate(Resources.Load(
"Prefabs/stage target", typeof(GameObject)),
new Vector3(75, 0.3f, 18), Quaternion.identity) as GameObject;
GameObject target3 = Instantiate(Resources.Load(
"Prefabs/stage target", typeof(GameObject)),
new Vector3(85, 0.3f, 70), Quaternion.identity) as GameObject;
GameObject target4 = Instantiate(Resources.Load(
"Prefabs/stage target", typeof(GameObject)),
new Vector3(115, 0.3f, 30), Quaternion.Euler(0, 135, 0)) as GameObject;
GameObject target5 = Instantiate(Resources.Load(
"Prefabs/stage target", typeof(GameObject)),
new Vector3(145, 0.3f, 80), Quaternion.identity) as GameObject;
GameObject target6 = Instantiate(Resources.Load(
"Prefabs/rotate target", typeof(GameObject)),
new Vector3(210, 0, 80), Quaternion.Euler(0, 90, 0)) as GameObject;
stage_target.Add(target1);
stage_target.Add(target2);
stage_target.Add(target3);
stage_target.Add(target4);
stage_target.Add(target5);
stage_target.Add(target6);
}
// 判断是否能射箭
private bool canShoot()
{
// 获取主相机的位置
Vector3 cameraPosition = Camera.main.transform.position;
// 圆心和半径
Vector3 circleCenter = new Vector3(-34f, 0f, -10f);
float radius = 18f;
// 计算相机位置与圆心的距离
float distance = Vector3.Distance(cameraPosition, circleCenter);
// 判断是否在圆内
if (distance <= radius || isStageMode == 1)
return true;
return false;
}
// Start is called before the first frame update
void Start()
{
LoadResources();
isLoad = false;
bow.GetComponent<BowController>().cam = main_camera;
cnt = 0;
isStageMode = 0;
}
// Update is called once per frame
void Update()
{
// 更新天空
RenderSettings.skybox = skyboxMaterials[cnt];
if (gameState == 1)
{
foreach (GameObject arrow in arrows)
{
// 如果超出画面,设成运动学让箭不动
if (arrow.transform.position.z > 150)
{
arrow.GetComponent<Rigidbody>().isKinematic = true;
}
}
// 场景只能有3支箭
if (arrows.Count > 3)
{
// 获取队列第一个
GameObject tempArrow = arrows.Dequeue();
factory.FreeArrow(tempArrow);
}
// 更新stageMode状态
setStageMode();
}
}
// 返回是否装箭
public bool GetisLoad()
{
return isLoad;
}
// 装箭
public void LoadArrow()
{
if (gameState == 1 && !isLoad && canShoot())
{
//从工厂得到箭
arrow = factory.GetArrow();
arrows.Enqueue(arrow);
isLoad = true;
// 隐藏arrow
arrow.GetComponent<Renderer>().enabled = false;
arrow.GetComponentInChildren<TrailRenderer>().enabled = false;
bow.GetComponent<Animator>().SetTrigger("start");
}
}
// 设定力量
public void SetForce(float scrollInput)
{
float force = bow.GetComponent<Animator>().GetFloat("holdPower") + scrollInput;
force = Mathf.Clamp(force, 0.3f, 1);
bow.GetComponent<Animator>().SetFloat("holdPower", force);
}
// 射箭
public void ShootArrow()
{
if (gameState == 1 && isLoad && canShoot())
{
// 通过管理类发射动作
// 显现arrow
arrow.GetComponent<Renderer>().enabled = true;
arrow.GetComponentInChildren<TrailRenderer>().enabled = true;
float force = bow.GetComponent<Animator>().GetFloat("holdPower");
arrow_manager.Shoot(arrow, force);
bow.GetComponent<Animator>().SetTrigger("shoot");
bow.GetComponent<Animator>().SetFloat("holdPower", 0.3f);
//开启副相机
second_camera.GetComponent<SecondCamera>().ShowCamera(arrow);
arrow_num += 1;
isLoad = false;
}
}
// 返回当前分数
public int GetScore()
{
return recorder.score;
}
// 得到射出的箭的数量
public int GetArrowNum()
{
return arrow_num;
}
// 开始闯关模式
public void Restart()
{
arrow_num = 0;
recorder.score = 0;
main_camera.transform.position = new Vector3(50.69f, 0f, -10f);
isStageMode = 1;
LoadStageTarget();
foreach (GameObject arrow in arrows)
{
factory.FreeArrow(arrow);
}
arrows.Clear();
}
// 开始游戏
public void BeginGame()
{
gameState = 1;
// 隐藏鼠标
Cursor.visible = false;
}
// 得到游戏的状态
public int GetGameState()
{
return gameState;
}
// 更换天空
public void SwitchMaterial()
{
cnt = (cnt + 1) % skyboxMaterials.Count;
}
// 获取当前闯关模式状态
public int GetIsStageMode()
{ return isStageMode; }
// 用于更新闯关模式状态
private void setStageMode()
{
if (isStageMode == 1)
{
if (stage_target.Count == 0)
return;
// 所有靶子都被击中,该轮闯关游戏结束
foreach (GameObject tar in stage_target)
{
if (tar.GetComponent<StageTargetController>().isShot == false)
{
Debug.Log(tar.GetComponent<StageTargetController>().isShot);
return;
}
}
isStageMode = 2;
}
}
}
四、项目亮点
本次项目最大亮点在于,实现了箭的抛物线运动,成功模拟了箭在实际中的运动,该功能主要是靠以下代码实现:
transform.rotation = Quaternion.FromToRotation(
Vector3.forward, rb.velocity.normalized);
这行代码使得箭在发射出去之后,顺着自己的运动方向发生旋转,从而模拟出箭的抛物线运动。如果不使用这行代码,而只使用箭本身的刚体属性,就会出现不是箭头着地,而是箭身着地,着地的箭头朝向取决于发射时的朝向,而不是向着地面或者靶子。
五、游戏展示
Counter Strike: Primitive Campaign