国庆期间本来是想找份工作的,结果目前没有合适的。只好闭门造车。。。感慨世事万千,生命的神奇。废话不多说,三句就够了!
00.png1、前期准备
1、PC平台
2、资源(UI素材,粒子特效,动画等)
3、导入SteamVR
4、那个运行HTC Vive设备最少970显卡
注意:全部选择
2、导入3D视角
2.png3、导入模型资源
需要那个手柄控制,就放置在那个手柄下
04.png4、基于设备调整好模型与手柄之间的角度、距离
10.png5、针对箭头,挂载脚本
设置箭头的位置和控制箭头的父物体,脚本在父物体挂载
05.png6、设置弓与箭的触发器
06.png 07.png7、实例化一个箭头
08.png箭头与弓是分离的,所以在手柄控制器中,放置在string里面来达到收纳箭头,控制箭头的位置信息
10.png 12.png8、拉动弓箭
8.1箭头控制器应该拿到弓玄的起始位置
13.png8.2弓箭的起始位置与拉动位置
14.png 15.png9、箭的发射
箭头所在的脚本:
16.png箭头控制器里面的方法:
17.png 18.png射箭:
19.png上面就是开发一款虚拟与现实最简单的应用(国外的开发牛人提供的素材)
箭头控制器源码:
using UnityEngine;
using System.Collections;
using System;
public class ArrowsManager : MonoBehaviour {
public float dir;
// 实例化对象
public static ArrowsManager instance;
void Awake()
{
instance = this;
}
// 过渡游戏对象
private GameObject curArrow;
// 实际的箭头
public GameObject arrowPf;
// 获得VR设备(因为箭头要在控制手柄上,所以必须要有手柄对象)
public SteamVR_TrackedObject trackObj;
// 拥有箭头位置的对象,也就是箭头在手柄内部位置
public GameObject stringAttachPoint;
// 开始点
public GameObject arrowStartPoint;
// 弓玄的起始位置
public GameObject StringStartPoint;
// 判断是否触发
private bool isAttached;
void Update () {
AttachArrow();
PullString(); // 判断拉弓
}
// 射箭
private void Fire()
{
curArrow.transform.parent = null;
// 拿到当前箭头的刚体组件
var r = curArrow.GetComponent<Rigidbody>();
r.useGravity = true; // 使用重力
r.velocity = curArrow.transform.forward * 50f * dir; //设置刚体的速度
// 将弓玄string还原
stringAttachPoint.transform.position = StringStartPoint.transform.position;
curArrow = null; // 射出去了,当前箭头就为空
isAttached = false; // 射出去后,就不会在触发了。
}
// 箭头的实时位置
void AttachArrow()
{
if (curArrow == null)
{ // 实例化箭头
curArrow = Instantiate(arrowPf);
// 设置箭头的父控件
arrowPf.transform.parent = trackObj.transform;
// 设置箭头的地方坐标
curArrow.transform.localPosition = new Vector3(0, 0, 0.256f);
// 设置角度
// Quaternion.identity就是指Quaternion(0,0,0,0),就是每旋转前的初始角度,是一个确切的值,
// 而transform.rotation是指本物体的角度,是一个属性变量
curArrow.transform.localRotation = Quaternion.identity;
}
}
/*
*触发器触发后调整箭头位置
*/
public void AttachBowToArrow()
{
// 当前箭头的父控件 = 手柄的位置
curArrow.transform.parent = stringAttachPoint.transform;
// 当前箭头的本地坐标就是开始箭头的本地坐标(开始箭头的坐标通过赋值对象的坐标来获取)
curArrow.transform.localPosition = arrowStartPoint.transform.localPosition;
// 当前箭头的旋转 = 开始箭头的旋转
curArrow.transform.rotation = arrowStartPoint.transform.rotation;
isAttached = true; // 标志位,触发了,其实也就调用了拉动弓玄方法
}
/*
*拉动弓玄
*/
public void PullString()
{
if (isAttached) // 如果触发,再调整箭头的位置
{
// InverseTransformPoint:变换位置从自身坐标到世界坐标(弓玄的本地坐标转换成世界坐标的X(就是拉动玄的长度))
// 获得转换后的vector的X值
dir = StringStartPoint.transform.InverseTransformPoint(trackObj.transform.position).x;
print(StringStartPoint.transform.InverseTransformPoint(trackObj.transform.position));
// 拿到初始弓玄与手柄设备的差值
// float dis = (StringStartPoint.transform.position - trackObj.transform.position).magnitude;
// 箭头的实际位置 = 起始位置+上面的差值
if (dir < 0)
{
dir = 0;
}
dir = dir > 0.4f ? 0.4f : dir;
stringAttachPoint.transform.localPosition = StringStartPoint.transform.localPosition + new Vector3(dir, 0, 0);
// 获得输入的VR手柄设备
var device = SteamVR_Controller.Input((int)ArrowsManager.instance.trackObj.index);
// 如果扣动扳机(如果处于攻击),发射弓箭
if (device.GetTouch(SteamVR_Controller.ButtonMask.Trigger))
{
Fire(); // 开火
}
}
}
}
箭头挂载的脚本:
using UnityEngine;
using System.Collections;
using System;
public class Arrows : MonoBehaviour {
private bool isFire;
private bool isAttached;
void Update () {
if (isFire)
{ //当前的朝向 当前的位置+当前刚体的速率
// LookAt: 朝向,是一个相对坐标
transform.LookAt(transform.position + transform.GetComponent<Rigidbody>().velocity);
}
}
// 触发器(API)
void OnTriggerEnter(Collider c)
{
AttackArrow();
AttackEnemy(c);
}
// 根据传入的碰撞器标签,来攻击怪物
private void AttackEnemy(Collider c)
{
if (c.tag == "Enemy")
{ // 拿到碰撞器所在物体的《怪物》脚本执行TakeDamage方法
c.gameObject.GetComponent<Enmy>().TakeDamage();
}
}
public void Fire()
{
isFire = true;
}
// 攻击
public void AttackArrow()
{
// 获得输入的VR手柄设备
var device = SteamVR_Controller.Input((int)ArrowsManager.instance.trackObj.index);
// 如果扣动扳机(如果处于攻击)
if (isAttached == false && device.GetTouch(SteamVR_Controller.ButtonMask.Trigger))
{
// 拿到Arrowsmanager,调用箭头的位置
ArrowsManager.instance.AttachBowToArrow();
isAttached = true;
}
}
}
怪物生成脚本:
using UnityEngine;
using System.Collections;
public class EnemySpawner : MonoBehaviour {
// 起始路点
public PathNode m_startNode;
// 保存所有的从XML读取的数据
ArrayList m_enemyList;
// 存储敌人出场顺序
public TextAsset xmldata;
// 出场敌人的序列号
int m_index = 0;
// 距离下一个敌人的出场时间
float m_timer = 0;
int liveEnemy;
void Start () {
ReadXML();
// 获取初始敌人
SpawnData date = (SpawnData)m_enemyList[m_index];
m_timer = date.wait;
}
void Update () {
SpawnEnemy();
}
// 每个一定时间生成一个敌人
void SpawnEnemy()
{
if (m_index >= m_enemyList.Count)
{
return;
}
// 更新时间,等待下一个敌人
m_timer -= Time.deltaTime;
if (m_timer > 0)
{
return;
}
// 获取下一个敌人的数据
SpawnData data = (SpawnData)m_enemyList[m_index];
// 如果下一个敌人是下一波,需要等待前一波敌人全部销毁
if (GameManager.Instance.wave < data.wave)
{
if (liveEnemy > 0)
{
return;
}
else
{
GameManager.Instance.wave = data.wave; // 更新wave数值
}
}
m_index++;
if (m_index < m_enemyList.Count)
{
m_timer = ((SpawnData)m_enemyList[m_index]).wait;// 更新等待的时间
}
// 读取敌人的模型
GameObject enemymodel = Resources.Load<GameObject>(data.enemyname);
// Debug.Log(" 调试 "+m_startNode.transform.position);
// 实例化敌人的模型,并转向第一个路点
Vector3 dir = m_startNode.transform.position - this.transform.position;
// 预设物,位置,旋转角度
GameObject enmeyObj = (GameObject)Instantiate(enemymodel,this.transform.position, Quaternion.LookRotation(dir));
// 添加Enemy
Enmy eney = enmeyObj.AddComponent<Enmy>();
// 设置敌人出发点
eney.curNode = m_startNode;
Debug.Log(m_startNode.transform.position+" 设置敌人出发点 ");
// 根据data.level设置敌人数值,本示例只是简单的根据波数增加敌人的生命
eney.m_life = data.level * 3;
eney.m_maxlife = data.level * 3;
// 更新存活敌人数量
liveEnemy++;
// 为敌人指定死亡动作,当敌人死亡回调减少敌人数量
OnEnmyDeath(eney, (Enmy e) =>
{
liveEnemy--;
});
}
// 定义了动作的函数
void OnEnmyDeath(Enmy eney, System.Action<Enmy> onDeath)
{
eney.onDeath = onDeath;
}
void OnDrawGizmos()
{
Gizmos.DrawIcon(transform.position, "spawner.tif");
}
void ReadXML()
{
m_enemyList = new ArrayList();
XMLParser xmlparse = new XMLParser();
XMLNode node = xmlparse.Parse(xmldata.text);
// 取得XML数据 = 传入XML文件路径
XMLNodeList list = node.GetNodeList("ROOT>0>table");
for (int i = 0; i < list.Count; i++)
{
string wave = node.GetValue("ROOT>0>table>" + i + ">@wave");
string enemyname = node.GetValue("ROOT>0>table>" + i + ">@enemyname");
string level = node.GetValue("ROOT>0>table>" + i + ">@level");
string wait = node.GetValue("ROOT>0>table>" + i + ">@wait");
SpawnData data = new SpawnData();
data.wave = int.Parse(wave);
data.enemyname = enemyname;
data.level = int.Parse(level);
data.wait = float.Parse(wait);
m_enemyList.Add(data);
}
}
// xml数据
public class SpawnData
{ // 波数
public int wave = 1;
public string enemyname = "";
public int level = 1;
public float wait = 1.0f;
}
}
using UnityEngine;
using System.Collections;
using System;
public class Enmy : MonoBehaviour {
public PathNode curNode; // 怪物的起始点
public float speed = 2; // 怪物的速度
internal int m_life;
internal int m_maxlife;
public System.Action<Enmy> onDeath;
void Start () {
ShowEffect();
}
void Update () {
RorateTo();
MoveTo();
}
public void MoveTo()
{
Vector3 pos1 = this.transform.position; // 当前怪物所在位置
Vector3 pos2 = Vector3.zero;
if (curNode!=null)
{
pos2 = curNode.transform.position; // 起始点的位置
}
// 两者之间的距离差值
float dis = Vector2.Distance(new Vector2(pos1.x, pos1.z), new Vector2(pos2.x, pos2.z));
// 判断目的地
if (dis < 0.3f) // 到达目的地
{
if (curNode.next == null)
{
DestroyMe();
}
else {
curNode = curNode.next; // 如果还有下个点,那么当前点就是起始点
}
}
transform.Translate(new Vector3(0, 0, speed * Time.deltaTime)); // 移动
}
// internal : 只能在程序集中访问的意思
internal void TakeDamage()
{ // 加载粒子资源
var p = Resources.Load("CFX2_SoulsEscape Rainbow");
// 实例化(预制物,预制物位置,预制物角度)
Instantiate(p, transform.position, Quaternion.identity);
DestroyMe(); // 摧毁自己
}
// 例子特效
void ShowEffect()
{
var p = Resources.Load("CFX2_EnemyDeathSkull");
Instantiate(p, transform.position, Quaternion.identity);
}
// 旋转视角
public void RorateTo()
{ // 拿到当前对象的欧拉值的Y轴角度
//http://wiki.ceeger.com/script:unityengine:classes:transform:transform.eulerangles
float cur = this.transform.eulerAngles.y;
// 朝向当前点的方向
transform.LookAt(curNode.transform);
// 移向目标(从当前的欧拉Y值,相对于父级的y轴变换旋转角度,目标速度*时间)
float next = Mathf.MoveTowardsAngle(cur, this.transform.localEulerAngles.y, 120 * Time.deltaTime);
//// 为当前对象赋值欧拉角
this.transform.eulerAngles = new Vector3(0, next, 0);
}
private void DestroyMe()
{
onDeath(this);
Destroy(gameObject);
}
}
怪物路径控制器脚本
using UnityEngine;
using System.Collections;
public class PathManager : MonoBehaviour {
public ArrayList PathNode;
void Start () {
}
void Update () {
}
[ContextMenu("BuildPath")]
void BuildPath() // 编译路径
{
PathNode = new ArrayList(); // 初始化数组
GameObject[] objs = GameObject.FindGameObjectsWithTag("pathnode"); // 找到所有Pathnode节点
for (int i = 0; i < objs.Length; i++)
{
PathNode node = objs[i].GetComponent<PathNode>(); // 取出每一个节点
PathNode.Add(node);
}
}
public void OnDrawGizmos() // 窗口可见时,每一帧调用这个函数
{
if (PathNode == null) return;
Gizmos.color = Color.blue;
foreach (PathNode item in PathNode)
{
if (item.next != null) // 只要有下一个点
{
Gizmos.DrawLine(item.transform.position, item.next.transform.position); //画线
}
}
}
}
怪物路径
using UnityEngine;
using System.Collections;
public class PathNode : MonoBehaviour
{
public PathNode parent;// PathNode类型的起始点
public PathNode next; // 下一个点
void Start()
{
}
void Update()
{
}
public void SetNext(PathNode node) // 设置下一个点
{
if (next != null) // 如果下个点不存在
{
next.parent = null; // 那么起始点也不存在
}
next = node; //如果下个点存在,那么下个点就是传入的这个点
node.parent = this; // 起始点就是当前点
}
// 在窗口可见时,每一帧都会调用这个函数。在其中进行Gizmos的绘制,也就是辅助编辑的线框体
void OnDrawGizmos() // 画图 当绘制Gizmos
{
Gizmos.DrawIcon(this.transform.position, "Node.tif");
}
}
关于怪物路径的编辑器的拓展工具条脚本(不用挂载,只需要放置在Editor文件下,没有就创建)
using UnityEngine;
using UnityEditor;
using System.Collections;
public class PathTool : ScriptableObject
{
static PathNode parent; // 静态起始点
[MenuItem("PathTool/Creat PathNode")]
static void GreatePathNoce()
{
// 创建一个新的路点
GameObject go = new GameObject();
go.AddComponent<PathNode>(); // 添加PathNode脚本
go.name = "pathnode";
// 设置标签
go.tag = "pathnode";
// 使该路点处于选择状态 (这个将绝不返回预设物或者不可修改的物体)
Selection.activeTransform = go.transform;
}
[MenuItem("PathTool/Set Parent %q")]
static void SetParent() // 设置起始点
{
// Selection.activeGameObject 返回激活的游戏物体。(在检查面板中显示)
// SelectionMode.Unfiltered 返回整个选择,
// Selection.GetTransforms(SelectionMode.Unfiltered).Length
// 允许对选择类型进行精细的控制,使用SelectionMode枚举类型。
if (!Selection.activeGameObject || Selection.GetTransforms(SelectionMode.Unfiltered).Length > 1)
{
return;
}
// 如果选择的游戏对象的标签 = 点标签
if (Selection.activeGameObject.tag.CompareTo("pathnode") == 0)
{ // 那么起始点 = 选中游戏对象的所在脚本
parent = Selection.activeGameObject.GetComponent<PathNode>();
Debug.Log("设置" + parent.name + "起始点.");
}
}
[MenuItem("PathTool/Set Next")]
static void SetNextChild()
{
// 没有选择激活得游戏物体,并且没有起始点,并且所有选择的长度>1
if (!Selection.activeGameObject || parent == null || Selection.GetTransforms(SelectionMode.Unfiltered).Length > 1)
{
return;
}
// 如果选择的激活的游戏对象的标签 == pathNode
if (Selection.activeGameObject.tag.CompareTo("pathnode") == 0)
{
parent.SetNext(Selection.activeGameObject.GetComponent<PathNode>()); // 那么设置下一个点
parent = null;
Debug.Log("设置" + Selection.activeGameObject.name + "所选择激活的游戏对象的名字");
}
}
}
20.png
网友评论