欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Cocos Creator 获得手机陀螺仪(Gyrometer)数据

程序员文章站 2022-04-18 19:55:27
...

接触 Cocos Creator 已经一年多, 体验是酸甜苦辣俱全, 不过仍然要夸一下这东西确实神作, 可以让我这种网页小白靠着Unity开发经验直接上手. 到目前为止的 Cocos Creator 编辑器版本(v2.4.3), 对移动端的运动传感器支持仍然是有限, 只提供了一个加速度数据接口, 加速度数据可以提供重力方向, 动作力度, 轨迹识别, 算法好一点的还能粗略做个惯性导航. 但是, 可惜, 缺少了关键的陀螺仪传感器接口, 应用程序无法得知手机的3D姿态, 在某些应用中这是个必备的数据, 例如网页运行 VR游戏 和 Panorama(全景图片). 于是需要自己开发脚本组件实现.

根据以前开发 Android 原生VR应用的经验, 大概推测 Cocos Creator 不提供陀螺仪数据接口的原因, 可能是为了引擎稳定性和兼容性更好? 因为不是所有手机都有陀螺仪. 首先要搞清楚加速度传感器(Accelerometer)和陀螺仪传感器(Gyrometer)是两个独立的硬件设备, 大部分低端手机是只有加速度传感器没有陀螺仪传感器(陀螺仪传感器更贵), 壕们可能很少见到这样的手机, 然而这种手机我就用过好几个. 奇葩的是这两个数据又是放在同一个HTML5事件里传回的, 所以在自己开发的组件里, 首先要实现检测用户设备是否支持陀螺仪数据接口, 然后再实现功能和填各种坑.

加速度数据和陀螺仪数据在 HTML5 中统一由事件 DeviceMotionEvent 传出, 具体用法可以直接参考网上资料, DeviceMotionEvent - Web API 接口参考 | MDN (mozilla.org). 看了文档才知道, 事件传回数据中只包含了采样数据和时间戳, 如果设备没有陀螺仪传感器, 事件对象中的 "rotationRate" 是不存在的, JS代码中就是 "undefined", 通过检测 "rotationRate" 是否"undefined"可以判断陀螺仪设备是否存在.

如果是运行在 iPhone 手机上, 因为 iOS 系统对传感器权限的严格控制, DeviceMotionEvent 需要用户授权才能传出数据, 通过调试代码发现 iOS 系统的 DeviceMotionEvent 有个 requestPermission 静态函数, 需要先调用一下这个静态函数, 弹出提示用户授权对话框, 用户选择允许后, 页面才能获得 DeviceMotion 事件和数据. Android 系统目前没有这种限制(估计以后也会有).

iPhone 上还有更严格的权限限制, requestPermission 必须在某个UI交互事件的回调函数中调用才能弹出请求授权对话框, 例如某个按钮的 OnClick, 如果在页面或场景初始化代码中悄无声息的 requestPermission 没有任何效果. Cocos Creator 程序可以在 Button 组件的 OnClick 事件处理代码中调用 requestPermission, 就可以在 iPhone 上成功弹出授权对话框. 解决授权问题扒了很多资料, 一直扒到了 DeviceOrientation Event Specification (w3c.github.io), 总算查清这个问题, 如果想看通俗易懂的总结可以看 在IOS中DeviceMotion及DeviceOrientation事件不触发的问题_greenwishing的专栏-****博客.

其它小坑, 比如运行需要 HTTPS 协议, iPhone 手机上重力向量的方向需要颠倒, iPhone 手机上传感器刷新时间的单位要转换等, 一一踩平...

搞清楚了以上问题和要点, 就可以自己实现一个运动传感器功能组件了, 展示的代码是被做成了 DeviceMotionEvent 组件, 以方便使用. 在编辑器里直接拖放脚本到 Node 对象即可, 多个 Node 对象可以各自拥有一个 DeviceMotionEvent 而互不影响.

/**
 * DeviceMotionEvent component
 * 
 * MatrixLife's component for getting device motion data in Cocos Creator.
 *
 * E-Mail:  aaa@qq.com
 * Version: 0.1?
 * Done-At: 2020.11.28
 */

cc.Class({
    extends: cc.Component,

    statics:
    {
        EVENT_INITIALIZE:   'MATRIXLIFE_DEVICEMOTION_INITIALIZE_EVENT',
        EVENT_UNINITIALIZE: 'MATRIXLIFE_DEVICEMOTION_UNINITIALIZE_EVENT',
    },

    properties:
    {
        _SampleInterval:  0,
        _HasAcceleration: false,
        _HasGravityState: false,
        _HasGyrometer:    false,

        _LastEventTimeStamp: 0.0,
        _Acceleration:       null,
        _GravityState:       null,
        _GyroState:          null,
        _GravityNeedInvert:  false,

        _DeviceMotionFirstInstance: null,
        _DeviceMotionEventInstance: null,

        HasAcceleration:
        {
            type: cc.Boolean,
            get()
            {
                return this._HasAcceleration;
            },
        },

        HasGravityState:
        {
            type: cc.Boolean,
            get()
            {
                return this._HasGravityState;
            },
        },

        HasGyrometer:
        {
            type: cc.Boolean,
            get()
            {
                return this._HasGyrometer;
            },
        },

        Acceleration:
        {
            type: cc.Vec3,
            get()
            {
                return this._Acceleration;
            },
        },

        GravityState:
        {
            type: cc.Vec3,
            get()
            {
                return this._GravityState;
            },
        },

        GyroState:
        {
            type: cc.Quat,
            get()
            {
                return this._GyroState;
            },
        },
    },

    _OnDeviceMotionFirst(e)
    {
        var ua_text = navigator.userAgent;
        if((ua_text.indexOf('iPhone') >= 0) || (ua_text.indexOf('Mac OS X') >= 0))
        {
            this._SampleInterval = e.interval;
            this._GravityNeedInvert = true;
        }
        else
        {
            this._SampleInterval = e.interval / 1000.0;
            this._GravityNeedInvert = false;
        }

        this._LastEventTimeStamp = e.timeStamp;
        if(e.acceleration && e.acceleration.x && e.acceleration.y && e.acceleration.z)
        {
            this._HasAcceleration = true;
            this._Acceleration.x = e.acceleration.x;
            this._Acceleration.y = e.acceleration.y;
            this._Acceleration.z = e.acceleration.z;
        }
        if(e.accelerationIncludingGravity && e.accelerationIncludingGravity.x && e.accelerationIncludingGravity.y && e.accelerationIncludingGravity.z)
        {
            this._HasGravityState = true;
            this._GravityState.x = e.accelerationIncludingGravity.x;
            this._GravityState.y = e.accelerationIncludingGravity.y;
            this._GravityState.z = e.accelerationIncludingGravity.z;
        }

        var gv_init = new cc.Vec3(this._GravityState.x, this._GravityState.y, this._GravityState.z);
        if(this._GravityNeedInvert)
        {
            gv_init.x = -(gv_init.x);
            gv_init.y = -(gv_init.y);
            gv_init.z = -(gv_init.z);
        }
        cc.Vec3.normalize(gv_init, gv_init);
        cc.Quat.rotationTo(this._GyroState, gv_init, cc.Vec3.UP);
        if(e.rotationRate && e.rotationRate.alpha && e.rotationRate.beta && e.rotationRate.gamma)
        {
            this._HasGyrometer = true;
            var rot_x = new cc.Quat();
            var rot_y = new cc.Quat();
            var rot_z = new cc.Quat();
            cc.Quat.rotateX(rot_x, cc.Quat.IDENTITY, e.rotationRate.alpha * this._SampleInterval * Math.DEG_TO_RAD);
            cc.Quat.rotateY(rot_y, cc.Quat.IDENTITY, e.rotationRate.beta  * this._SampleInterval * Math.DEG_TO_RAD);
            cc.Quat.rotateZ(rot_z, cc.Quat.IDENTITY, e.rotationRate.gamma * this._SampleInterval * Math.DEG_TO_RAD);
            cc.Quat.multiply(this._GyroState, this._GyroState, rot_z);
            cc.Quat.multiply(this._GyroState, this._GyroState, rot_x);
            cc.Quat.multiply(this._GyroState, this._GyroState, rot_y);
        }

        this.node.emit('MATRIXLIFE_DEVICEMOTION_INITIALIZE_EVENT', this);
        window.removeEventListener('devicemotion', this._DeviceMotionFirstInstance);
        window.addEventListener('devicemotion', this._DeviceMotionEventInstance);
    },

    _OnDeviceMotionEvent(e)
    {
        var dt = (e.timeStamp - this._LastEventTimeStamp) / 1000.0;
        this._LastEventTimeStamp = e.timeStamp;

        if(this._HasAcceleration)
        {
            this._Acceleration.x = e.acceleration.x;
            this._Acceleration.y = e.acceleration.y;
            this._Acceleration.z = e.acceleration.z;
        }
        if(this._HasGravityState)
        {
            this._GravityState.x = e.accelerationIncludingGravity.x;
            this._GravityState.y = e.accelerationIncludingGravity.y;
            this._GravityState.z = e.accelerationIncludingGravity.z;
        }
        if(this._HasGyrometer)
        {
            var rot_x = new cc.Quat();
            var rot_y = new cc.Quat();
            var rot_z = new cc.Quat();
            cc.Quat.rotateX(rot_x, cc.Quat.IDENTITY, e.rotationRate.alpha * dt * Math.DEG_TO_RAD);
            cc.Quat.rotateY(rot_y, cc.Quat.IDENTITY, e.rotationRate.beta  * dt * Math.DEG_TO_RAD);
            cc.Quat.rotateZ(rot_z, cc.Quat.IDENTITY, e.rotationRate.gamma * dt * Math.DEG_TO_RAD);
            cc.Quat.multiply(this._GyroState, this._GyroState, rot_z);
            cc.Quat.multiply(this._GyroState, this._GyroState, rot_x);
            cc.Quat.multiply(this._GyroState, this._GyroState, rot_y);
        }
    },

    TryInitialize()
    {
        if(window.DeviceMotionEvent)
        {
            if(window.DeviceMotionEvent.requestPermission)
            {
                var _THIS = this;
                window.DeviceMotionEvent.requestPermission().then(
                    function(ps)
                    {
                        window.addEventListener('devicemotion', _THIS._DeviceMotionFirstInstance);
                    },
                );
            }
            else
            {
                window.addEventListener('devicemotion', this._DeviceMotionFirstInstance);
            }
        }
    },

    TryUninitialize()
    {
        if(window.DeviceMotionEvent)
        {
            window.removeEventListener('devicemotion', this._DeviceMotionFirstInstance);
            window.removeEventListener('devicemotion', this._DeviceMotionEventInstance);
        }

        this._SampleInterval = 0;
        this._HasAcceleration = false;
        this._HasGravityState = false;
        this._HasGyrometer = false;

        this.node.emit('MATRIXLIFE_DEVICEMOTION_UNINITIALIZE_EVENT', this);
    },

    // LIFE-CYCLE CALLBACKS:

    onDestroy()
    {
        this.TryUninitialize();
    },

    onLoad()
    {
        if(Math.DEG_TO_RAD == undefined) Math.DEG_TO_RAD = Math.PI / 180.0;
        if(Math.RAD_TO_DEG == undefined) Math.RAD_TO_DEG = 180.0 / Math.PI;
        if(cc.Quat.IDENTITY == undefined) cc.Quat.IDENTITY = new cc.Quat(0.0, 0.0, 0.0, 1.0);

        this._Acceleration = new cc.Vec3(0.0, 0.0, 0.0);
        this._GravityState = new cc.Vec3(0.0, 9.8, 0.0);
        this._GyroState = cc.Quat.clone(cc.Quat.IDENTITY);

        this._DeviceMotionFirstInstance = this._OnDeviceMotionFirst.bind(this);
        this._DeviceMotionEventInstance = this._OnDeviceMotionEvent.bind(this);
    },
});

然后返回 Cocos Creator, 新建一个空场景, 可以写个简易测试看看效果(比较简单就不放代码了). 运行请求传感器数据的页面需要服务器提供 HTTPS 连接, 如果是在 Windows 系统上, 可以利用 Windows 专业版自带的 IIS 在内网开一个 HTTPS 站点, 如果没有 IIS 可以下载安装免费的 IIS Express. 找来 IIS 是因为它自带一个给开发者测试用的SSL证书, 避免了搞证书问题头疼. 最后, 用主流的传感器齐全的中高档手机测试, 成功收到所有运动数据:

Cocos Creator 获得手机陀螺仪(Gyrometer)数据