游戏简介
地图上共有3个射箭关卡,每个关卡对应一个射击位,站在射击位即可开启关卡。每个关卡需要在特定时间内射击一定数量的靶子。每个关卡有三次机会,但若自知成功无望也可离开射击位以重置关卡(不消耗机会)。弓箭数量不限,但必须前一支箭落地或是击中目标才能发射下一支箭,因此对待高处目标需谨慎。同时,地图上也有三个隐藏的靶子,全部找到可改变结束的恭喜语句。
游戏操作
WASD:人物移动
Shift:长按跑步
鼠标移动:视角旋转
左键:长按为弓弩蓄力,松开即发射
Q:切换天空盒
演示视频
【3d游戏编程作业5——射箭小游戏】 https://www.bilibili.com/video/BV13M411Z7Mo/?share_source=cop...
实现思路
1、弓弩移动:
使用标准资源包中的FPS预设,将弓弩设为其子物体实现
2、蓄力动画:
使用混合动画树,将零蓄动画和满蓄动画、零蓄动画和发射动画分别进行混合,并用同一个变量drawstrength设置其混合比例,在代码中修改drawstrength的值,即可实现不同蓄力程度的动画。再设置Holding变量,为真时可从Empty向Fill转移,为假时可从Fill向Shoot转移。代码中还会根据长按时间修改动画速度,使得不同的蓄力时间有着不同的动画速度。
Fill的混合树:
Fire的混合树:
3、箭运动:
为实体箭赋予刚体组件。在发射动画结束时触发动画时间,隐藏动画箭的同时将实体箭移动到动画箭的位置,并根据drawstrength设置其初速度,即可实现箭的发射与运动
4、箭的碰撞:
为箭和被碰物体设置碰撞盒,在箭进入碰撞事件时,判断碰到物体的Tag,实现不同的操作。
5、天空盒切换:
修改Renderer.skybox
6、射击位实现:
射击位设置碰撞盒并开启is Trigger,触发OnTriggerIn和OnTriggerExit时判断对象tag是否为Player,并进行相应操作。
具体代码
1、SceneController:控制场景物体的启/禁用,并处理游戏通关逻辑
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using Unity.VisualScripting;
using UnityEngine;
public class SceneController : MonoBehaviour
{
private bool[] placeState;
private bool[] winflag;
private bool[] hiddenflag;
private int skystate;
private Camera cam;
private Material[] skybox;
private GameObject player;
private GameObject arrow;
private Rigidbody arrig;
private TrailRenderer arrtrail;
// Start is called before the first frame update
void Start()
{
placeState = new bool[3]{true,true,true};
winflag = new bool[3]{false,false,false};
hiddenflag = new bool[3]{false,false,false};
skystate = 1;
skybox = new Material[3]{Resources.Load("sunrise") as Material,
Resources.Load("sunset") as Material,
Resources.Load("night") as Material};
RenderSettings.skybox = skybox[skystate];
cam = Camera.main;
player = Instantiate(Resources.Load<GameObject>("prefabs/player"),transform.position,Quaternion.identity);
arrow = Instantiate(Resources.Load<GameObject>("prefabs/Arrow"),Vector3.zero,Quaternion.identity);
arrig = arrow.GetComponent<Rigidbody>();
arrow.SetActive(false);
arrtrail = arrow.transform.gameObject.GetComponent<TrailRenderer>();
}
// Update is called once per frame
void Update()
{
//切换天空盒
if(Input.GetKeyDown(KeyCode.Q)){
skystate = (skystate + 1) % 3;
RenderSettings.skybox = skybox[skystate];
}
}
public void Reset(){
//重置胜负变量
for(int i = 0;i<3;i++){
placeState[i] = true;
winflag[i] = false;
hiddenflag[i] = false;
}
//重置天空盒
skystate = 1;
RenderSettings.skybox = skybox[skystate];
//重启弓弩,玩家归位,箭矢隐藏
player.transform.Find("FirstPersonCharacter").Find("Crossbow").gameObject.SetActive(true);
arrow.SetActive(false);
//重置关卡数据
// GameObject root = GameObject.Find("Root");
// foreach (Transform child in root.transform)
// {
// child.gameObject.SetActive(true);
// child.GetComponent<LevelSet>().Reset();
// child.gameObject.SetActive(false);
// }
//重置触发站台状态
GameObject.Find("startplace0").GetComponent<standplaceaction>().Reset();
GameObject.Find("startplace1").GetComponent<standplaceaction>().Reset();
GameObject.Find("startplace2").GetComponent<standplaceaction>().Reset();
//重置gui关卡计数
Singleton<UserGUI>.Instance.guifinishlevel = 0;
}
//在动画箭处生成实体箭
public void ArrowSpawn(Vector3 pos, Quaternion rot,float drawstrength,Vector3 forw){
if(arrow.activeSelf == false){
arrow.SetActive(true);
arrtrail.Clear();
arrow.transform.position = pos;
arrow.transform.rotation = rot;
arrig.velocity = 50 * drawstrength * forw;
arrtrail.enabled = true;
}
}
//启动关卡
public void ShootingStart(int number){
if(placeState[number]){
GameObject root = GameObject.Find("Root");
GameObject level = root.transform.Find(number.ToString()).gameObject;
level.SetActive(true);
foreach (Transform child in level.transform)
{
child.gameObject.SetActive(true);
}
Singleton<UserGUI>.Instance.state = 1;
}
}
//暂时关闭关卡
public void ShootingEnd(int number){
GameObject root = GameObject.Find("Root");
GameObject level = root.transform.Find(number.ToString()).gameObject;
level.GetComponent<LevelSet>().InitialTime();
level.SetActive(false);
Singleton<UserGUI>.Instance.state = 0;
}
//禁用关卡
public void Shutdown(int number){
GameObject root = GameObject.Find("Root");
root.transform.Find(number.ToString()).gameObject.SetActive(false);
//让射击位失活,站上去不再启动关卡
GameObject.Find("startplace"+number.ToString()).GetComponent<standplaceaction>().ShutdownLevel();
Singleton<UserGUI>.Instance.state = 0;
placeState[number] = false;
CheckIfEnd();
}
//射中隐藏靶子
public void HitHiddenTarget(string name){
Debug.Log(name + " hitted!");
hiddenflag[(int)char.GetNumericValue(name[12])] = true;
}
//成功完成一关
public void FinishLevel(int number){
Singleton<UserGUI>.Instance.AddFinishLevel();
Singleton<UserGUI>.Instance.state = 0;
winflag[number] = true;
Shutdown(number);
}
//检查是否结束游戏
public void CheckIfEnd(){
bool ifend = true;
bool ifwin = true;
bool iffoundhidden = true;
foreach (bool state in placeState){
if(state == true){
ifend = false;
break;
}
}
if(ifend == true){
foreach (bool flag in winflag){
if(flag == false){
ifwin = false;
break;
}
}
}
//所有关卡被关闭,且所有关卡被成功完成
if(ifend && ifwin){
//判断是否所有隐藏靶子被找到
foreach (bool flag in hiddenflag){
if(flag == false){
iffoundhidden = false;
break;
}
}
player.transform.Find("FirstPersonCharacter").Find("Crossbow").gameObject.SetActive(false);
Singleton<UserGUI>.Instance.SetWinSentence(iffoundhidden);
Singleton<UserGUI>.Instance.state = 2;
}
//所有关卡被关闭,但有关卡未被成功完成
else if (ifend){
player.transform.Find("FirstPersonCharacter").Find("Crossbow").gameObject.SetActive(false);
Singleton<UserGUI>.Instance.state = 3;
}
}
}
2、shootaction:附着于十字弩对象,主要负责修改弩动画
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class shootaction : MonoBehaviour
{
private Animator ani;
public float fulltime;
private float holdtime;
private Transform crossbowtrans;
private Transform arrowtrans;
// Start is called before the first frame update
void Start()
{
ani = GetComponent<Animator>();
holdtime = 0;
ani.SetFloat("drawstrength", 0);
fulltime = 1;
crossbowtrans = transform;
//获取动画箭transform
arrowtrans = crossbowtrans.Find("箭");
}
private void FixedUpdate()
{
//检测是否按下左键
if(Input.GetButton("Fire1"))
{
//由于动画状态切换有延迟,为保证按下即可开始蓄力,不判断是否处于Fill状态,而是判断是否不在Fire状态
if(!ani.GetCurrentAnimatorStateInfo(0).IsName("Fire")){
arrowtrans.gameObject.SetActive(true);
ani.SetBool("Holding",true);
}
//修改drawstrength
if(holdtime < fulltime && !ani.GetCurrentAnimatorStateInfo(0).IsName("Fire"))
holdtime += Time.deltaTime;
ani.SetFloat("drawstrength", holdtime / fulltime);
if(holdtime != 0){
//修改动画时间,使得短按长按的动画时间不同,可以在放开鼠标的同时就发射。
ani.speed = fulltime / holdtime;
}
else{
ani.speed = 1;
}
}
else
{
ani.SetBool("Holding", false);
holdtime = 0;
}
}
// Update is called once per frame
//Fire动画结束时被调用,提供实体箭所需的位置、朝向、速度等信息,并隐藏动画箭
public void Shoot()
{
Vector3 pos = arrowtrans.position;
Quaternion rot = crossbowtrans.rotation;
Vector3 forw = crossbowtrans.forward;
Singleton<SceneController>.Instance.ArrowSpawn(pos,rot,ani.GetFloat("drawstrength"),forw);
arrowtrans.gameObject.SetActive(false);
}
}
3、arrowaction:附着于实体箭,主要负责箭的碰撞检测
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SocialPlatforms.Impl;
public class arrowaction : MonoBehaviour
{
//箭的拖尾
private TrailRenderer trail;
// Start is called before the first frame update
private SceneController sc;
void Awake(){
trail = transform.gameObject.GetComponent<TrailRenderer>();
sc = Singleton<SceneController>.Instance;
}
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void OnCollisionEnter(Collision collision)
{
// 检查碰撞对象是否是地面
if (collision.gameObject.CompareTag("Ground"))
{
// 在碰撞到地面时禁用箭
DisableArrow();
}
//检测碰撞对象是否为普通靶子
if (collision.gameObject.CompareTag("Target"))
{
Debug.Log("Hit Target!");
//将对应关卡的剩余靶子数-1
if(collision.gameObject.transform.parent != null){
collision.gameObject.transform.parent.parent.GetComponent<LevelSet>().lasttargetnum -= 1;
}
//让被射中靶子消失
collision.gameObject.transform.parent.gameObject.SetActive(false);
DisableArrow();
}
//检测碰撞对象是否为隐藏靶子
if (collision.gameObject.CompareTag("HiddenTarget"))
{
if(sc == null)
sc = Singleton<SceneController>.Instance;
Debug.Log(collision.gameObject.name);
sc.HitHiddenTarget(collision.gameObject.name);
collision.gameObject.SetActive(false);
DisableArrow();
}
}
private void DisableArrow()
{
//禁用拖尾
trail.enabled = false;
gameObject.SetActive(false);
}
}
4、LevelSet:附着于关卡对象(孩子有多个普通靶子的空对象,每关对应一个),负责控制本关的参数。
using System.Collections;
using System.Collections.Generic;
using UnityEditor.SearchService;
using UnityEngine;
public class LevelSet : MonoBehaviour
{
//设定总靶数和剩余靶数
[SerializeField]
public int targetnum;
public int lasttargetnum;
//机会数
[SerializeField]
private int chancenum;
private int lastchance;
//时间
[SerializeField]
public float time;
private float lasttime;
//触发器,用于触发gui时间的更新
private float trigger;
//关卡编号
[SerializeField]
private int number;
private SceneController sc;
private UserGUI gui;
// Start is called before the first frame update
void Awake(){
lastchance = chancenum;
}
//每次启用都被调用
void OnEnable()
{
Debug.Log("Start!");
lasttime = time;
lasttargetnum = targetnum;
sc = Singleton<SceneController>.Instance;
trigger = 0;
gui = Singleton<UserGUI>.Instance;
gui.UpdateTime(lasttime);
gui.UpdateChance(lastchance);
}
// Update is called once per frame
void Update()
{
lasttime -= Time.deltaTime;
trigger += Time.deltaTime;
if(trigger >= 1){
gui.UpdateTime(lasttime);
trigger = 0;
//所有靶子被击倒,成功完成该关
}
if(lasttargetnum <= 0){
//在禁用关卡前就重置关卡,若在按Reset按钮的重新启用后再调用该函数,会因update函数先于该Reset函数执行而出错
Reset();
sc.FinishLevel(number);
}
//关卡超时
if(lasttime <= 0){
lastchance--;
//立即重置时间,避免重复减少机会数
InitialTime();
gui.UpdateChance(lastchance);
//机会数为0,禁用关卡
if(lastchance <= 0){
if (sc == null)
sc = Singleton<SceneController>.Instance;
//与上一个Reset原因相同
Reset();
sc.Shutdown(number);
return;
}
if (sc == null)
sc = Singleton<SceneController>.Instance;
//暂时关闭关卡
sc.ShootingEnd(number);
}
}
public void InitialTime(){
lasttime = time;
}
public void Reset(){
lastchance = chancenum;
//被主动disable的子对象对象无法跟随父对象一起enable,因此需手动重启
foreach (Transform child in transform)
{
child.gameObject.SetActive(true);
}
}
}
5、standplaceaction:附着于射击位,负责检测玩家的进入和退出,并据此启动/暂时关闭对应关卡
using System.Collections;
using System.Collections.Generic;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
public class standplaceaction : MonoBehaviour
{
private SceneController sc;
//射击位编号,对应关卡位编号
[SerializeField]
private int number;
//射击位被激活和未激活的材质,分别为白色和红色
private Material active;
private Material unactive;
//修改材质用
private Renderer ren;
//用于使射击位赋活/失活
private bool isactive;
// Start is called before the first frame update
void Start()
{
sc = Singleton<SceneController>.Instance;
active = Instantiate(Resources.Load<Material>("Material/active"));
unactive = Instantiate(Resources.Load<Material>("Material/unactive"));
ren = GetComponent<Renderer>();
ren.material = unactive;
isactive = true;
}
// Update is called once per frame
void Update()
{
}
//检测Player进入射击位
private void OnTriggerEnter(Collider other)
{
if(isactive){
if (other.CompareTag("Player"))
{
ren.material = active;
if(sc == null)
sc = Singleton<SceneController>.Instance;
sc.ShootingStart(number);
}
}
}
//检测Player离开射击位
private void OnTriggerExit(Collider other){
if(isactive){
if (other.CompareTag("Player"))
{
ren.material = unactive;
if(sc == null)
sc = Singleton<SceneController>.Instance;
sc.ShootingEnd(number);
}
}
}
//使射击位失活
public void ShutdownLevel(){
isactive = false;
ren.material = unactive;
}
//使射击位赋活
public void Reset(){
isactive = true;
}
}
6、UserGUI:负责关卡信息与游戏结束信息显示
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
public int state;//0为普通状态显示,1为关卡中显示,2为胜利显示,3为失败显示
private int width = Screen.width / 5;
private int height = Screen.height / 15;
private GUIStyle titleStyle;
private int guiChance;
private int guiTime;
public int guifinishlevel;
private string winsentence;
// Start is called before the first frame update
void Start()
{
state = 0;
titleStyle = new GUIStyle();
titleStyle.normal.textColor = Color.black;
titleStyle.normal.background = null;
titleStyle.fontSize = 50;
titleStyle.alignment = TextAnchor.MiddleCenter;
guiChance = 0;
guiTime = 20;
guifinishlevel = 0;
winsentence = "But there might be something else...";
}
// Update is called once per frame
void Update()
{
}
void OnGUI(){
if(state == 0){
GUI.Label(new Rect(0,Screen.height - height,width,height),"Finish Level: "+ guifinishlevel.ToString() + "/3");
}
if (state == 1){
GUI.Label(new Rect(Screen.width / 2,0,width,height),"Left Time: "+ guiTime.ToString());
GUI.Label(new Rect(Screen.width / 2 - width,0,width,height),"Left Chance: "+guiChance.ToString());
}
if(state == 2){
GUI.Label(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 0.1f, width, height), "You Win!", titleStyle);
GUI.Label(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 0.1f + height, width, 2 * height), winsentence);
bool button = GUI.Button(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 3 / 7, width, height), "Reset");
if(button){
state = 0;
Singleton<SceneController>.Instance.Reset();
}
}
if(state == 3){
GUI.Label(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 0.1f, width, height), "You Lose!", titleStyle);
bool button = GUI.Button(new Rect(Screen.width / 2 - width * 0.5f, Screen.height * 3 / 7, width, height), "Reset");
if(button){
Singleton<SceneController>.Instance.Reset();
}
}
}
//更新关卡剩余时间
public void UpdateTime(float lasttime){
guiTime = (int)lasttime;
}
//更新关卡剩余机会显示
public void UpdateChance(int lastchance){
guiChance = lastchance;
}
//增加完成关卡数
public void AddFinishLevel(){
guifinishlevel++;
}
//设置隐藏靶相关语句
public void SetWinSentence(bool iffoundhidden){
if(iffoundhidden)
winsentence = "And you found all the hidden target!";
else
winsentence = "But there might be something else...";
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。