VR开发--虚拟与现实游戏(VR-狩猎)

2020-06-02 17:42:25 浏览数 (1)

00.png

1、前期准备

1、PC平台 2、资源(UI素材,粒子特效,动画等) 3、导入SteamVR 4、那个运行HTC Vive设备最少970显卡

01.png

注意:全部选择

2、导入3D视角

2.png

3、导入模型资源

需要那个手柄控制,就放置在那个手柄下

04.png

4、基于设备调整好模型与手柄之间的角度、距离

10.png

5、针对箭头,挂载脚本

设置箭头的位置和控制箭头的父物体,脚本在父物体挂载

05.png

6、设置弓与箭的触发器

06.png

07.png

7、实例化一个箭头

08.png

箭头与弓是分离的,所以在手柄控制器中,放置在string里面来达到收纳箭头,控制箭头的位置信息

10.png

12.png

8、拉动弓箭

8.1箭头控制器应该拿到弓玄的起始位置

13.png

8.2弓箭的起始位置与拉动位置

14.png

15.png

9、箭的发射

箭头所在的脚本:

16.png

箭头控制器里面的方法:

17.png

18.png

射箭:

19.png

上面就是开发一款虚拟与现实最简单的应用(国外的开发牛人提供的素材) 箭头控制器源码:

代码语言:javascript复制
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(); // 开火
                }
        }
    }
}

箭头挂载的脚本:

代码语言:javascript复制
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;
        }   
    }
}

怪物生成脚本:

代码语言:javascript复制
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; 
    }
}
代码语言:javascript复制
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);
    } 
}

怪物路径控制器脚本

代码语言:javascript复制
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); //画线
            }
        }
    }
}

怪物路径

代码语言:javascript复制
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文件下,没有就创建)

代码语言:javascript复制
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

0 人点赞