Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Weex】纯 Weex 开发一个小游戏 #4

Open
zwwill opened this issue Nov 10, 2017 · 6 comments
Open

【Weex】纯 Weex 开发一个小游戏 #4

zwwill opened this issue Nov 10, 2017 · 6 comments

Comments

@zwwill
Copy link
Owner

zwwill commented Nov 10, 2017

前言

作为一个移动端初学者、爱好者,能使用前端技术开发原生游戏一直是一件渴望而不可及的事情,暂且不说游戏逻辑的复杂度,算法的健壮性,单单是场景、画布、布局就让我们无处下手。

几年前曾经参与 Appcan 技术的技术孵化和推广,尝试使用 Hybrid 技术写过一个小游戏,由于此游戏结构场景比较简单,所以未使用大型的游戏引擎,Cocos2d-x游戏引擎,所有逻辑全部手工。同样也是可「三端同构」,但本质上还是一个 H5小游戏,只是可以安装在手机上,执行环境是一个 Webview,所以,H5可以做的,他都可以做,H5不能做到,他未必不能做,如摄像头、陀螺仪等。但缺点也很致命,执行效率完全受限于原生控件 Webview,要知道对于一个游戏来讲,流畅度是第一要义。

总的来讲,使用 Hybrid 技术开发游戏的方案虽然可行,但是,效果并不是我想要的。

自从 ReactNative 开源以来,一直想着要使用 ReactNative 开发游戏。个人原因,一直未付诸实践。直到上周有网友问我,「Weex是否能拿来做游戏开发」,试试就知道,那就先拿 Weex 开刀,来挑战下 game app 同构的能力,给还没上车的朋友带波节奏。

准备工作

如果你还未入门,没关系,就当看个热闹了,知道 Weex 能不能快速开发游戏就可以了。

如果你想先入门,以下几篇文章你可以当作是导读。

扫雷游戏 Demo

官方提供的 WeexPlayground 中也提供了一个游戏 demo 扫雷,如下图

此 demo 是为了实践以下三件事:

  1. 界面指示器
  2. 动态数据绑定
  3. 更复杂的事件

总体表现还是不错的。更多细节,可详读《Weex版扫雷游戏开发》

我的小游戏

别人的东西再炫酷也始终是别人的,不自己动手码一个说话都不硬气!

没有实践就没有发言权,此处献上源码的 Github 链接:https://github.com/zwwill/just-do-8,欢迎「Star」「Fork」,支持瞎搞 ψ(`∇´)ψ

先来感受下最终的效果

界面

体验

可以直接使用 Weex Playground 扫码体验 Weex Playground下载地址

规则

规则很简单,会玩「俄罗斯方块」和「2048」就一定会玩这款小游戏

一期功能

由于要快速产出,界面随便就别太在意了,另外很多功能还没有开发,如,全球排名、分享、游戏设置等,这些都放在后面慢慢迭代吧(如果有第二版的话( ̄. ̄))

源码分析

接下来是一大波源码分析,不感冒?那就直接跳过。
由于篇幅有限,此处只做简要介绍,详细请见工程源码,地址请爬楼

项目结构

只有三个文件(一个场景两个组件)。我来逐一讲解下每个文件的职能。

index.vue

**【index.vue】**是一个场景文件,用于根据状态切换场景,以及监听处理所有的手势

【模版 | 简码】

<template>
  <div class="wrapper" @swipe="onSwipe" @click="onClick" @panstart="onPanstart" @panend="onPanend" @horizontalpan="onHorizontalpan">
    <!-- 此处省略一堆代码 -->
    <stoneMap v-if="stoneMapShow" ref="rStoneMap" class="stone-map" @screenLock="onScreenLock" @screenUnlock="onScreenUnlock" @over="onGameover" @win="onGameWin"></stoneMap>
    <!-- 此处省略一堆代码 -->
  </div>
</template>

我们监听了 Weex 的一堆事件来「合成」我们需要的【切换】【左右滑动】【下降】等主要游戏操作。如@swipe@click@panstart@panend@horizontalpan,同时给<stoneMap />组件注册@screenLock@screenUnlock@over@win等事件,用于游戏场景切换。

  • @swipeswipe的属性direction提供在屏幕上滑动时触发的方向,本项目用到updown,官方给的说法是『direction的值可能为upleftbottomright』但实际上我得到的却是down而不是bottom,具体请客还在和Weex的开发团队进行沟通,确认后会更新上来。另外要注意的是@swipe@click@panstart@panend@horizontalpan这些事件同时使用时会出现冲突问题,Android 平台下问题比较多,具体大家在做的时候需要做好兼容
  • @click:常规的click事件
  • @panStart@PanEnd、@horizontalpan:用于计算左右滑动距离,每滑动40个显示像素就向<stoneMap />组件发起滑块左右滑动的指令

具体事件的使用姿势,大家可以详读官方文档

每一个事件方法的功能实现和视觉此处就略去了。

stoneMap.vue

**【stoneMap.vue】**就像是「大内总管」,一切闲杂喽啰的事都归他管。主要管理的数字块的布局、状态、游戏分值等

【简码】

<template>
    <div class="u-slider">
        <!-- 此处省略一些记录分值等无关紧要的代码 -->
        <template v-for="i in stones">
            <stone :ref="i.id" :id="i.id" :p0="i.p0" :num0="i.s"></stone>
        </template>
    </div>
</template>
<script>
   export default {
        components: {
            stone: stone
        },
        data() {
            return {
                MAX_H: 9,
                stones: [],
                map: [],
                // 此处省略一些无关紧要的data
            }
        },
        mounted() {
            // 绘制画布矩阵
            for (let _i = 0; _i < this.MAX_H; _i++) {
                this.map.push(['', '', '', '', '', '']);
            }
            // 开始游戏
            this.pushStones();
        },
        methods: {
            /**
             * 事件控制
             * */
            action(_action) { /* ... */ },
            /**
             * 新增三个单元数字块
             * */
            pushStones() { /* ... */ },
            /**
             * 滑块切换
             * */
            actionChange() { /* ... */ },
            /**
             * 滑块左右滚动
             * */
            actionSliderMove(_d) { /* ... */ },
            /**
             * 单元块位置移动+权重加码
             * */
            actionDown() { /* ... */ },
            /**
             * 重新计算map并更新
             * */
            mapUpdate() { /* ... */ },
            /**
             * 计算map
             * */
            mapCalculator: (function () { /* ... */ })(),
            /**
             * 整理数字块,堆积下降
             * */
            stonesTrim() { /* ... */ },
            /**
             * 单元块位置移动+权重加码
             * */
            sChange(_id, _p, _score) { /* ... */ }
        }
    }
</script>
  • this.stones:用于管理所有实例进来的数字块,将他们投影到界面上
  • this.map:是一个6*9的逻辑网,标记 this.stones 中的的数字块的逻辑位置

此处主要介绍下事件的控制分发和逻辑网的计算,讲解在注释中

【action() | 简码】

/**
 * 事件的控制分发
 * */
action(_action) {
    if (!!this.actionLock) return;
    switch (_action) {
        case 'click':
        case 'up':
            // click 和 up 触发上方三个活动数字块的互相切换
            this.actionChange();
            break;
        case 'left':
        case 'right':
            // left 和 right 触发上方三个活动数字块的的整体平移
            this.actionSliderMove(_action);
            break;
        case 'down':
        case 'bottom':
            // down 触发上方三个活动数字块进场
            // bottom 起到兼容的作用
            this.actionDown();
            break;
        default:
            break;
    }
}

【mapCalculator() | 全码】

/**
 * 计算map
 * */
mapCalculator: (function () {
    var updateStone = function (_stones, _id, _s) {
        /** 
         * 此方法控制得分规则
         * 横竖对角线+1分
         * 十字、X型+2分
         * 8字型、9宫格分别+3分、+4分,当然,不可能存在这两种情况
         * */
        if (_stones[_id]) {
            _s != 0 && _s < 8 && (_stones[_id]['score'] == 0 ? _stones[_id]['score'] = _s : _stones[_id]['score']++);
        } else {
            _stones[_id] = {
                id: _id,
                score: _s
            }
        }
    };
    
    return function (_map) {
        let hasChange = false,
            activeStones = {},
            height = _map.length - 1,
            width = _map[0].length - 1,
            _tp_id, _s;
        // 全逻辑网遍历
        for (let y = height; y >= 0; y--) {
            for (let x = 0; x <= width; x++) {
                _tp_id = _map[y][x] || "";
                // 排除四角
                if (!_tp_id || (x == 0 || x == width) && (y == 0 || y == height)) continue; 
                
                _s = parseInt(this.$refs[_tp_id][0].num);
                let _p1, _p2;
                if (x == 0 || x == width || y == 0 || y == height) {
                    // 侧边,将其单独提炼出来是为了减少计算量三分之一的计算量
                    if (x == 0 || x == width) {
                        // 竖排
                        if (!_map[y - 1][x] || !_map[y + 1][x]) continue;
                        _p1 = this.$refs[_map[y - 1][x]][0];
                        _p2 = this.$refs[_map[y + 1][x]][0];
                    } else if (y == 0 || y == height) {
                        // 横排
                        if (!_map[y][x - 1] || !_map[y][x + 1]) continue;
                        _p1 = this.$refs[_map[y][x - 1]][0];
                        _p2 = this.$refs[_map[y][x + 1]][0];
                    }
                    if (_p1 && _p2 && _p1.num == _s && _p2.num == _s) {
                        hasChange = true;
                        updateStone(activeStones, _tp_id, ++_s);
                        updateStone(activeStones, _p1.id, 0);
                        updateStone(activeStones, _p2.id, 0);
                    }
                } else {
                    // 中间可形成九宫格区域
                    const _map_matrix = [
                        [[0, 1], [0, -1]],
                        [[-1, 1], [1, -1]],
                        [[-1, 0], [1, 0]],
                        [[-1, -1], [1, 1]]
                    ];
                    for (let _i = 0, _mm; _i < _map_matrix.length; _i++) {
                        _mm = _map_matrix[_i];
                        if (!_map[y + _mm[0][0]][x + _mm[0][1]] || !_map[y + _mm[1][0]][x + _mm[1][1]]) continue;
                        _p1 = this.$refs[_map[y + _mm[0][0]][x + _mm[0][1]]][0];
                        _p2 = this.$refs[_map[y + _mm[1][0]][x + _mm[1][1]]][0];
                        if (_p1 && _p2 && _p1.num == _s && _p2.num == _s) {
                            hasChange = true;
                            updateStone(activeStones, _tp_id, _s + 1);
                            updateStone(activeStones, _p1.id, 0);
                            updateStone(activeStones, _p2.id, 0);
                        }
                    }
                }
            }
        }

        // 存在更新块
        if (hasChange) {
            setTimeout(() => {
                for (let s in activeStones) {
                    this.sChange(s, undefined, activeStones[s].score);
                }
                // 数字块整理
                setTimeout(() => {
                    this.stonesTrim();
                }, 100)
            }, 400)
        } else {
            let _errorStone = "";
            for (let _i = 0; _i < this.map[0].length; _i++) {
                if (this.map[0][_i]) {
                    _errorStone = this.$refs[this.map[0][_i]][0].$refs['stone'];
                    break;
                }
            }
            if (!!_errorStone) {
                this.$emit('over', this.totalScore, this.highScore, _errorStone);
                if (this.totalScore > this.highScore) {
                    storage.setItem('H-SCORE', this.totalScore)
                }
            } else {
                this.$emit('screenUnlock');
                setTimeout(() => {
                    this.pushStones();
                }, 100);
            }
        }
    }
})()

【stonesTrim | 全码】

/**
 * 整理数字块,堆积下降
 * */
stonesTrim() {
    let hasChange = false,
        height = this.map.length - 1,
        width = this.map[0].length - 1,
        _tp_id, _step = 0;
    for (let x = 0; x <= width; x++) {
        _step = 0;
        for (let y = height; y >= 0; y--) {
            _tp_id = this.map[y][x] || "";
            if (!_tp_id) {
                _step++;
                continue;
            } else if (_step > 0) {
                hasChange = true;
                this.sChange(_tp_id, {y: _step});
                this.map[y + _step][x] = _tp_id;
                this.map[y][x] = "";
            }
        }
    }
    setTimeout(() => {
        this.mapUpdate();
    }, hasChange ? 200 : 0);
}

stone.vue

**【stone.vue】**就像被「大内总管」管理着的「小太监」(数字块),「小太监」的一举一动都是被「总管」支配的,包括其长相(颜色)、品级(数字)以及生死(生命周期),但状态的改变都是由自己执行,直接自己整容,自己升级,还要。。自杀。底层人民好无奈 ╮(╯_╰)╭

【简码】

<template>
    <text ref="stone" class="u-stone" :style="{color:color,visibility:visibility,backgroundColor:backgroundColor0}" v-if="show" >{{score}}</text>
</template>

<script>
    const animation = weex.requireModule('animation');
    export default {
        props: ['id', 'p0', 'num0'],
        data(){
            return {
                show: true,
                p: '0,8',
                visibility: '',
                num: -1,
                colors: ["#333","#666","#eee","#b9e3ee","#ebe94b","#46cafb","#eca48f","#decb3d","#8d1894"],
                backgroundColors: ["#222","#ddd","#999","#379dc3","#36be0d","#001cc6","#da4324","#56125a","#ffffff"]
            }
        },
        computed: {
            color: function () {
                return this.colors[this.num];
            },
            score: function () {
                this.num<0 && (this.num = this.num0 || 1);
                return this.num<9&&this.num>0?this.num:0
            },
            backgroundColor0: function () {
                return this.backgroundColors[this.num];
            }
        },
        watch: {
            p: function (val) {
                // 移动数字块
                var _x = 125*val.charAt(0)+"px",
                    _y = 125*val.charAt(2)+"px";
                // 使用animation库实现过度动画
                animation.transition(this.$refs['stone'],{
                    styles: {
                        transform: 'translate('+_x +',-'+_y+')'
                    },
                    duration: 200,
                    timingFunction: 'ease-in',
                    delay: 0
                });
            }
        },
        mounted(){
            this.initState(this.p0);
        },
        methods: {
            /**
             * 移动数字块
             * */
            move(_x, _y){ /* ... */ },
            /**
             * 更新数字块的分值,即显示数字
             * */
            scoreChange(_num){ /* ... */ },
            /**
             * 初始化数字块的位置
             * */
            initState(_p){ /* ... */ }
        }
    }
</script>

好了,辣么乐色的代码我都不好意思再唠叨了。换个话题,来讲讲这个小游戏从无到有中间的一些方案的变更吧。

各种尝试

由于对 Weex 的过高期望,导致很多最初的方案都被「阉割」或者「整容」。

动画

想让元素动起来,传统前端一般有两种方式

1、CSS 动画
2、JS 动画
在 Weex 上由多了一个
3、animation 内建模块,可执行原生动画

由于 css3 的 transition 在 Weex 的 0.16.0+ 版本才能使用,官方提供的 demo 框架引用的 SDK 版本低于此版本,方案1,无效!

Weex 上的视觉是通过解析 VDom,在调用原生控件渲染成的,完全没有 DOM ,所以 JS 动画的方案,无效!

看了只剩下 Weex 的 animation 内建动画模块了。

虽然不太喜欢,用起来也很别扭,但是没办法,有总比没有强。知促常乐吧。

来看一下 animation 的使用姿势

animation.transition(this.$refs.test, {
        styles: {
            color: '#000',
            transform: 'translate(100px, 100px) sacle(1.3)',
            backgroundColor: '#CCC'
        },
            duration: 800, // ms
            timingFunction: 'ease',
            needLayout:false,
            delay: 0 // ms
        }, function () {
            // animation finished.
        })

想实现一个多态循环的动画,还要写一个方法,想想就难受

音乐

没有声音还能算是游戏吗?!

嗯 ~ ~ ~ 好像可以算

无所谓啦~ 开心最重要 ︿( ̄︶ ̄)︿

尴尬的是 Weex 官方压根就没给咱们提供这样的 API,好在有三方的插件可用,Nat, 刚好可以用上。

Weex 提倡使用网络资源,所有我把音频文件上传到了 CDN 上,为了能快一点。。

当然不可能一路顺风!

我们来看看 Nat Audio 模块的使用方式

Nat.audio.play('http://cdn.instapp.io/nat/samples/audio.mp3')

然而 Nat.audio 只提供了 play() | pause() | stop() 三个 API。

为什么没有 replay() 重放?我想用的就是重放。这都不是事儿,使用 play() 硬着头皮上吧!

由于 Nat.audio 不支持 Web 端,每次修改都是真机调试,那个速度,唉~~~我终于理解原生小伙伴们的痛苦了。。

这也不是事儿,最气愤的就是,Nat.audio.play() 每次播放相同的音频竟然不是走的缓存!难道缓存机制还要自己做?!?!ヽ(`⌒´)ノ 我的天!

最后还是乖乖的用背地文件吧。还要写平台路径适配。。

没想到音频的槽点这么多!还要我没用 Weex 做网易云音乐。

手势指令

前文也有讲过,小游戏用到了@swipe@click@panstart@panend@horizontalpan这么多事件监听。官方也有友情提醒「horizontalpan 手势在 Android 下会与 click 事件冲突」,但实际上 ios 平台上也会有冲突。

具体的我就不再描述了。此处只想说明,Weex 在手势指令上虽然可以满足游戏的基础指令要求,但细节上还是不太理想。

IOS 过审

关于 IOS 的上线审核要求还是比较严格的。

  • 不能使用第三方插件【WXDevtool】和【ATSDK-Weex】
  • 及时不支持iPad,那么在 iPad 下运行时也不能出现视觉错乱

以上两点是我在提交审核被拒的两点。

解决方案也比较直接,【WXDevtool】和【ATSDK-Weex】 是内部做调试的工具包,直接清理掉即可
至于 iPad 适配,让我们适配那就乖乖适配下吧,没办法,谁叫人家是老大,

总结

总的来讲,Weex 算是满足了我做小游戏的要求。如果想做大型游戏,就不建议使用 Weex 了,Weex 确实做不了,但者也不是 Weex 诞生的意义。

@zwwill zwwill added this to the Y-2017 milestone Nov 10, 2017
@zwwill zwwill added the Weex label Nov 10, 2017
@zwwill zwwill modified the milestone: Y-2017 Nov 10, 2017
@zwwill zwwill changed the title 纯 Weex 开发一个小游戏 【Weex】纯 Weex 开发一个小游戏 Dec 1, 2017
@dongjian
Copy link

dongjian commented Jan 2, 2018

您好.我们是just do 8原作.请停止对UI及玩法的拷贝.当初下架just do 8 事出有因 .停止尝试上架您的模仿版 . 谢谢配合.
wechatimg156
wechatimg157

@zwwill
Copy link
Owner Author

zwwill commented Jan 3, 2018

@dongjian 你好,我已将此 app 下架,此版本仅为一个 Weex 技术的实验 demo,上线的目的仅为测试 weex 的过审率,和执行效率。

@dongjian
Copy link

dongjian commented Jan 3, 2018

谢谢配合

@zwwill
Copy link
Owner Author

zwwill commented Jan 3, 2018

@dongjian 你们是一个团队吗?看到你在上面用了“我们”

@dongjian
Copy link

dongjian commented Jan 3, 2018

是的.当初俩人一起策划的

@xueran0326
Copy link

有玩法攻略吗

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants