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

国庆期间本来是想找份工作的,结果目前没有合适的。只好闭门造车。。。感慨世事万千,生命的神奇。废话不多说,三句就够了!

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Ubuntu的发音 Ubuntu,源于非洲祖鲁人和科萨人的语言,发作 oo-boon-too 的音。了解发音是有意...
    萤火虫de梦阅读 99,215评论 9 467
  • 古代十大兵器排名: 中国古代十大兵器排行第十名:诸葛亮——孔明扇诸葛亮乃是三国时期最伟大的军事家,神机妙算,简直是...
    晓寒深处明月人倚楼阅读 4,359评论 0 5
  • 我的彩铅画从这张雪人拉开帷幕…… 第一幅作品:雪人!应该是简单,但当时画了好几次,最后出来这个圈还是歪扭~ 第二幅...
    毛丢丢呀毛丢丢阅读 499评论 0 0
  • 很久很久没有写字了。不是不想写,只是不知道从何写起。日复一日,年复一年,天天一成不变的生活,仿佛行尸走肉般迷茫...
    猫绝阅读 550评论 0 50
  • 放羊和砍柴的故事告诉我们,面同一件事物,心态不一样,结果就会不一样! 你是砍柴的,他是放羊的,你和他聊了一天,他的...
    沙漠之骆驼刺阅读 62评论 0 0