前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >VR开发--虚拟与现实游戏(VR-狩猎)

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

作者头像
孙寅
发布2020-06-02 17:42:25
6500
发布2020-06-02 17:42:25
举报
文章被收录于专栏:宜达数字宜达数字

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

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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、前期准备
  • 2、导入3D视角
  • 3、导入模型资源
  • 4、基于设备调整好模型与手柄之间的角度、距离
  • 5、针对箭头,挂载脚本
  • 6、设置弓与箭的触发器
  • 7、实例化一个箭头
  • 8、拉动弓箭
  • 9、箭的发射
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档