顾名思义,整个射箭
游戏视频链接:3D游戏设计 — 射箭 大作业 修改版_哔哩哔哩bilibili
弩(玩家所操控的角色)的运动Crossbow_Move.cs
(本代码挂载在弩上)
在设计之初,这份c#的功能是接受键盘上WASD和方向键↑↓←→,将其转化为弩的运动。
弩会(朝着镜头方向)前进后退,但当镜头偏上或偏下(有y轴分量)时,弩不会朝着天上移动;
在后续实验中发现:若先让弩运动,再计算镜头MainCamera的位置并赋值,则镜头会发生明显的颤抖。因此,直接让方向键同时控制弩和镜头的各自运动。
但是这样也有个小问题:发生碰撞的时候弩应当停在原地(加入box collision组件即可),但此时镜头仍然会根据方向键的输入而移动。因此,最后同时保留了方向键控制镜头移动和位置的计算和赋值,这样才能既消除颤抖,又使镜头真正跟随弩的移动。
(镜头位置的计算和赋值放在了CameraFollow.cs中)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
//功能:设计弩的移动(和静止)
public class Crossbow_Move: MonoBehaviour
{
public float m_speed = 10f; //这个定义为公有变量,方便之后修改物体速度
private float ypos; //用来阻止弩的竖直方向移动上下
private Vector3 offset; //用来修改移动方向,使之在水平线上
private Transform crossbow; //用来记录弩的transform对象变量
private Transform camr;
void Start()
{
//其实本代码套在弩上,crossbow就是this.transform
crossbow = GameObject.FindGameObjectWithTag("Player").transform;
crossbow.localEulerAngles = Vector3.zero;
camr = GameObject.FindGameObjectWithTag("MainCamera").transform;
Vector3 offset = new Vector3(0f, 1f, 0f);
camr.position = crossbow.position + offset;
ypos = crossbow.position.y;//初始y轴高度。日后都得是这个高度哦~
}
void Update()
{
//以下三行是用于消除镜头移动中莫名的颤抖
GetComponent<Rigidbody>().velocity = Vector3.zero;
GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezeAll;
GetComponent<Rigidbody>().constraints = RigidbodyConstraints.None;
//以下是方向键/WASD移动
offset = crossbow.forward;
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))//如果按下了↑
{
this.transform.Translate(new Vector3(0, -offset.y * m_speed * Time.deltaTime, m_speed * Time.deltaTime));
camr.Translate(new Vector3(0, -offset.y * m_speed * Time.deltaTime, m_speed * Time.deltaTime));
}
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S) )//如果按下了↓
{
this.transform.Translate(new Vector3(0, offset.y * m_speed * Time.deltaTime, -1 * m_speed * Time.deltaTime));
camr.Translate(new Vector3(0, offset.y * m_speed * Time.deltaTime, -1 * m_speed * Time.deltaTime));
}
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D) )//如果按下了→
{
this.transform.Translate(new Vector3(m_speed * Time.deltaTime, 0, 0));
camr.Translate(new Vector3(m_speed * Time.deltaTime, 0, 0));
}
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))//如果按下了←
{
this.transform.Translate(new Vector3(-1 * m_speed * Time.deltaTime, 0, 0));
camr.Translate(new Vector3(-1 * m_speed * Time.deltaTime, 0, 0));
}
//这三行也控制弩只在同一水平线上
Vector3 true_pos = this.transform.position;
//true_pos.z = (true_pos.z < 0) ? true_pos.z : 0;//这一行限制弩的活动区域
true_pos.x = (float)Math.Round(true_pos.x,5);
true_pos.y = (float)Math.Round(ypos, 0);
true_pos.z = (float)Math.Round(true_pos.z,5);
//this.transform.position = Vector3.Lerp(this.transform.position, true_pos, m_speed * Time.deltaTime);
this.transform.position = true_pos;
}
void OnCollisionEnter(Collision col)
{
m_speed = 3f;//发生碰撞时暂时降低速度,防止穿模
}
void OnCollisionExit(Collision col)
{
m_speed = 5f;//结束碰撞时恢复速度
}
}
镜头位置/朝向移动代码:CameraFollow.cs
(本代码挂载在MainCamera上)
主要功能:使镜头位置跟随弩的运动而运动;使镜头和弩的朝向都随着鼠标移动而转向。
在这份代码中,RotateSpeed参数控制了镜头转向的灵敏度;
加入了限制,使得镜头上移和下移都不能超过40°。
镜头位置是弩(玩家)位置抬高1个单位(0,1,0) ,创造类似于第一人称视角的效果。
using UnityEngine;
using System.Collections;
using System;
//功能:
//1.镜头跟随着弩的移动而移动;
//2.镜头与弩的朝向随着鼠标移动而变化
public class CameraFollow: MonoBehaviour
{
private Vector3 offset = new Vector3(0f, 1f, 0f); //相机相对于弩的偏移量
private Transform crossbow; //用来记录弩的对象变量
private Vector3 pos; //
public int RotateSpeed = 3; //镜头旋转速度,类似于游戏内置的灵敏度
void Start()
{
crossbow = GameObject.FindGameObjectWithTag("Player").transform; //寻找弩
//crossbow.localEulerAngles = Vector3.zero;
}
void Update()
{
//通过鼠标位置来获得下一瞬间的弩朝向/镜头朝向angle
Vector3 angle = Vector3.zero;
angle.x = transform.localEulerAngles.x - Input.GetAxis("Mouse Y") * RotateSpeed;
if ((angle.x > 40) && (angle.x < 180)) { angle.x = 40; }//对于弩的上下朝向设置限制
if ((angle.x < 320) && (angle.x > 180)) { angle.x = 320 ;}
angle.y = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * RotateSpeed;
//弩和镜头的朝向都设置为angle
crossbow.localEulerAngles = angle;
this.transform.localEulerAngles = angle;
//在移动时调整相机相对于弩之间的位置。如果只靠这个移动camera会导致卡顿,需要搭配crowmove中的相机移动方式
Vector3 true_pos = crossbow.position;
true_pos.x = (float)Math.Round(true_pos.x, 5);
true_pos.y = (float)Math.Round(true_pos.y, 0);
true_pos.z = (float)Math.Round(true_pos.z, 5);
pos = true_pos + offset;
this.transform.position = pos;
}
}
射击代码:Click_to_shoot.cs
(本代码挂载在弩上)
主要功能:使鼠标左键控制弩发射箭。
射箭这一动作存在着CD,冷却时间中不允许发射下一根箭;
箭的飞行速度取决于弩的蓄力时间长度,蓄力时间越长,动作机animator中的参数shoot_force值越大,最大不超过1f。
此外,设置了允许射击的范围:靶子位置.z>0,而当弩(玩家)位置.z<0时才允许设计。
接下来依次放出 动作机结构 / 模型包 / 代码
动作机的大致结构:
1.射箭cd转好时,点击鼠标左键,则先使holding state参数变为true,再开始蓄力。蓄力是不断修改shoot_force的值,最大为1。
2.蓄力达到最大值时,动画会进入Hold状态,在松开鼠标时进入Shoot状态;若还未蓄力完成就松开,则动画会直接从Fill进入Shoot状态。
3.松开鼠标、进入Shoot时,利用预制体Prefab中的箭矢模型,先设置箭的位置、速度、朝向,再实例化一支箭,
弩的模型/动画/状态机都可以用下图的包中导入:
代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:点击/长按/发射等动作的控制
public class click_to_shoot : MonoBehaviour
{
private Animator ani; //记录动作机,用于调整其中的参数
private float force; //力度。会同步给animator中的shoot_force
private GameObject arrow; //记录预制体,用于实例化
private GameObject crossbow; //记录弩
private Vector3 offset = new Vector3(0, 0.5f, 0); //记录偏移量,用于箭的位置的生成
private bool cd = false; //记录发射的冷却
private bool legal = true; //记录弩是否在选定区域内
private int quantity = 5;
GUIStyle style;
void Start()
{ //各种初始化
ani = GetComponent<Animator>();
force = 0f;
arrow = (GameObject)Resources.Load("Prefab/Arrow");
crossbow = GameObject.FindGameObjectWithTag("Player");
style = new GUIStyle();
style.fontSize = 45;
style.normal.textColor = Color.black;
}
void FixedUpdate()
{
//接下来的if_else作用为:判定玩家是否处于可设计射击位置,只有是的时候才能执行动作/生成箭矢
if ( (this.transform.position.z < 0) && ( (this.transform.position.x + 10) % 30 <= 20 ))
{
if(legal == false)
{
ani.SetFloat("shoot_force", 0f);
force = 0f;
quantity = 5;
}
legal = true;
}
else
legal = false;
if ( Input.GetMouseButton(0) && !cd && legal && (quantity > 0))//没在cd的时候,按下鼠标左键
{
ani.SetBool("Holding_state",true);//正在蓄力
if (force < 1f)//蓄力的上限是1,范围是 range( 0, 1, 0.05 )
{
force += 0.05f;
ani.SetFloat("shoot_force", force);
}
}
else if (!Input.GetMouseButton(0) && !cd && legal)//没在cd,松开鼠标
{
//生成一支箭
if ( (force > 0.04f) && (quantity > 0) )//区分没点和刚松手
{
cd = true; //进入cd
quantity -= 1;
//指定位置和方向,生成箭
arrow.transform.position = offset + crossbow.transform.position;
arrow.transform.rotation = crossbow.transform.rotation;
Invoke("inite", 0.1f); //0.1秒后生成一支箭(为了贴合弩的动画)
Invoke("unenable", 0.9f); //等待0.5s后,冷却完毕,允许下一次生成箭
}
ani.SetBool("Holding_state", false);
ani.SetTrigger("Crossrow_Trigger");
}
}
public void OnGUI()
{
string text = "Arrows : " + quantity.ToString();
GUI.Label(new Rect(10, 100, Screen.width, 50), text, style);
}
void inite()
{
Instantiate(arrow); //生成箭
}
void unenable()
{
ani.SetFloat("shoot_force", 0f); //生成之后再将力归零
force = 0f;
cd = false; //cd结束
}
}
箭靶移动代码:targetMove.cs
(本代码挂载在箭靶上)
主要功能:控制靶子往返运动
当达到左边界时转向右运动,右边界同理。左右边界都是事先计算好的。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:让靶子做往返匀速运动
public class targetMove : MonoBehaviour
{
private Vector3 offset = new Vector3(10,0,0);//偏移量
private Vector3 left_boundary;
private Vector3 right_boundary;
private int dir;
private Vector3 leftmove = new Vector3(-5, 0, 0);
private Vector3 rightmove = new Vector3(5, 0, 0);
void Start()
{
dir = 1;//代表初始运动方向向左
left_boundary = this.transform.position - offset;
right_boundary = this.transform.position + offset;
}
void Update()
{
if (dir>0)//向左运动
{
GetComponent<Rigidbody>().velocity = leftmove;
if (this.transform.position.x <= left_boundary.x)
dir = 1 - dir;
}
else
{
GetComponent<Rigidbody>().velocity = rightmove;
if (this.transform.position.x >= right_boundary.x)
dir = 1 - dir ;
}
}
}
箭矢飞行/插在箭靶上:arrowSpeed.cs
(本代码需要挂载在预制体箭矢模型上)
主要功能:
1.在箭矢没有击中任何东西时,让子弹箭矢飞;
2.在箭矢击中靶子(特定物体)时,使箭矢静止;
3.在箭矢击中其他东西时,使箭矢垂直落下。
此外,在OnGUI中设置了分数的显示。分数是通过勾股定理计算距离得到的。
我的靶子本质上是个厚度为0.2的Cube。每当发生碰撞时,都先获取碰撞点P,然后与碰撞到的靶子的中心点计算距离(斜边),最后利用斜边和厚度计算出碰撞点到靶子中心的距离。这个距离对应着击中靶子的环数,能够依此知道得分。
下面放出代码和靶子图案(直接拖动到Cube上即可)
score是个自制的命名空间,在ScoreManager.cs中实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using score;
//控制箭矢在运动全过程中的状态
public class arrowSpeed : MonoBehaviour
{
private int stopflag = 0;
public int speed = 10;
private Animator ani;
private Transform crossbow;
private float force;
Vector3 closestPoint;
Vector3 dir;
Vector3 down;
GameObject my_target;
ScoreManager scrmng;
GUIStyle style;
void Start()
{
crossbow = GameObject.FindGameObjectWithTag("Player").transform;
force = crossbow.GetComponent<Animator>().GetFloat("shoot_force") * 20f;
//Debug.Log("force:" + force.ToString());
style = new GUIStyle();
style.fontSize = 45;
style.normal.textColor = Color.black;
dir = crossbow.forward;
dir.x = speed * dir.x;
dir.y = speed * dir.y;
dir.z = speed * dir.z;
down.y = -5;
scrmng = new ScoreManager();
}
public void OnGUI()
{
string text = "Total Scores : " + scrmng.getTotalScore().ToString();
string text2 = "Scores : " + scrmng.getThisScore().ToString();
GUI.Label(new Rect(10, 10, Screen.width, 50), text, style);
GUI.Label(new Rect(10, 55, Screen.width, 50), text2, style);
}
void Update()
{
if (0==stopflag)//没发生碰撞
{
GetComponent<Rigidbody>().velocity = dir * force;
GetComponent<Rigidbody>().useGravity = true;
}
else if (2 <= stopflag)//没打中靶子,受重力作用,且下坠
{
GetComponent<Rigidbody>().velocity = down;
GetComponent<Rigidbody>().useGravity = true;
}
else if (1 == stopflag)//打中靶子,与其保持相对静止,记录分数
{
GetComponent<Rigidbody>().velocity = my_target.GetComponent<Rigidbody>().velocity;
GetComponent<Rigidbody>().useGravity = false;
}
//else //没打中靶子,跟随物体移动或静止
//{
// GetComponent<Rigidbody>().velocity = my_target.GetComponent<Rigidbody>().velocity;
//}
}
void OnCollisionEnter(Collision col)
{
//Vector3 location = this.transform.position;
//closestPoint = collision.collider.ClosestPoint(location);
my_target = col.gameObject;//my_target是指撞到的物体。一般是靶子target,也有可能是箭/地面
if ( (my_target.tag == "Target" ))
{
stopflag += 1;
closestPoint = col.collider.ClosestPoint(this.transform.position);
float distance = Vector3.Distance(closestPoint, my_target.transform.position);
scrmng.setDistance(distance);
}
else
{
stopflag += 2;
}
}
}
靶子图案来源于百度图片。
得分管理代码:ScoreManager.cs
(本代码不需要挂载)
此处创建了命名空间score,这个命名空间被arrowSpeed中的OnGUI()调用,从而计算和显示分数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using score;
namespace score{
public class ScoreManager{
public float distance = 0f;
public static int this_score = 0;
public static int total_score = 0;
public void Start ()
{
}
public void setDistance(float dd)
{
distance = (float)Math.Sqrt( dd * dd - 0.01f);
//Debug.Log("distance : " + distance.ToString());
this_score = 10 - (int)(distance / 0.2f);
this_score = ( (this_score > 0) ? this_score : 0);
//Debug.Log("this_score : " + this_score.ToString());
total_score += this_score;
//Debug.Log("score : " + total_score.ToString());
}
public int getThisScore()
{ return this_score; }
public int getTotalScore()
{ return total_score; }
}
}
应对bug的代码:TreeStop.cs
十分惭愧,在下的项目中,树木模型不能静止在原地,有时候会向上飘,有时候会沉入地下。为此,特地编写了一个简易的代码,让树(以及别的小东西)静止在原地。
原理:速度不断设置为0;记录物体的初始位置和朝向,不断赋值给物体本身
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TreeStop : MonoBehaviour
{
private Vector3 pos;
private Vector3 fwd;
// Start is called before the first frame update
void Start()
{
pos = this.transform.position;
fwd = this.transform.forward;
}
// Update is called once per frame
void Update()
{
GetComponent<Rigidbody>().velocity = Vector3.zero;
this.transform.position = pos;
this.transform.forward = fwd;
}
}
天空盒切换代码:SkyBoxController.cs
(本代码需要挂载在MainCamera上)
仅切换 “镜头内看到的” 天空盒skybox。在使用本代码时,需要在挂载后手动加入天空盒资源。
加入方式如下:选中挂载了代码的MainCamera,在Inspector内找到Materials队列,点+加入资源。天空盒资源在assret store中一搜一大把,我用的是Fantasy Skybox FREE。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:每15秒切换一次天空盒
public class SkyBoxController : MonoBehaviour
{
public Material[] materials = null;
int index;
void Start()
{
InvokeRepeating("ChangeSkyBox", 0, 15f);
}
void ChangeSkyBox()
{
RenderSettings.skybox = materials[index];
index++;
index %= materials.Length;
}
}
总结
项目gitee仓库:出了点小问题没法上传,慢慢修。
我在本次开发过程中,既写出了属于自己的游戏,也对过往学习的知识整了一次查缺补漏和运用,受益匪浅。