做为一个移动端初学者、爱好者,能使用前端技术开发原生游戏一直是一件渴望而不可及的事情,暂且不说游戏逻辑的复杂度,算法的健壮性,单单是场景、画布、布局就让咱们无处下手。javascript
几年前曾经参与 Appcan 技术的技术孵化和推广,尝试使用 Hybrid 技术写过一个小游戏,《Hybrid混合实现app小游戏》,因为此游戏结构场景比较简单,因此未使用大型的游戏引擎,Cocos2d-x游戏引擎,全部逻辑所有手工。一样也是可「三端同构」,但本质上仍是一个 H5小游戏,只是在真机上,执行环境是一个 UIWebview,因此,H5能够作的,他均可以作,H5不能作到,他未必不能作,如摄像头、陀螺仪等。但缺点也很致命,执行效率彻底受限于原生控件 UIWebview,要知道对于一个游戏来说,流畅度是第一要义。css
总的来说,使用 Hybrid 技术开发游戏的方案虽然可行,可是,效果并非我想要的。html
自从 ReactNative 开源以来,一直想着要使用 ReactNative 开发游戏。我的缘由,一直未付诸实践。直到上周有网友问我,「Weex是否能拿来作游戏开发」,试试就知道,那就先拿 Weex 开刀,来挑战下 game app 同构的能力,给还没上车的朋友带波节奏。前端
若是你还未入门,不要紧,就当看个热闹了,知道 Weex 能不能快速开发游戏就能够了。vue
若是你想先入门,如下几篇文章你能够看成是导读。java
官方提供的 WeexPlayground 中也提供了一个游戏 demo 扫雷,以下图ios
此 demo 是为了实践如下三件事:css3
整体表现仍是不错的。更多细节,可详读《Weex版扫雷游戏开发》git
别人的东西再炫酷也始终是别人的,不本身动手码一个说话都不硬气!github
没有实践就没有发言权,此处献上源码的 Github 连接:https://github.com/zwwill/just-do-8,欢迎「Star」「Fork」,支持瞎搞 ψ(`∇´)ψ
先来感觉下最终的效果
IOS已上线 https://itunes.apple.com/cn/a...
也能够直接使用 Weex Playground 扫码体验 Weex Playground下载地址
近期将发布到应用市场,届时还望你们多多支持。
规则很简单,会玩「俄罗斯方块」和「2048」就必定会玩这款小游戏
因为要快速产出,界面随便就别太在乎了,另外不少功能尚未开发,如,全球排名、分享、游戏设置等,这些都放在后面慢慢迭代吧(若是有第二版的话( ̄. ̄))
接下来是一大波源码分析,不感冒?那就直接跳过。
因为篇幅有限,此处只作简要介绍,详细请见工程源码,地址请爬楼
只有三个文件(一个场景两个组件)。我来逐一讲解下每一个文件的职能。
【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
等事件,用于游戏场景切换。
swipe
的属性direction
提供在屏幕上滑动时触发的方向,本项目用到up
、down
,官方给的说法是『direction
的值可能为up
、left
、bottom
、right
』但实际上我获得的倒是down
而不是bottom
,具体请客还在和Weex的开发团队进行沟通,确认后会更新上来。另外要注意的是@swipe
、@click
、@panstart
、@panend
和@horizontalpan
这些事件同时使用时会出现冲突问题,Android 平台下问题比较多,具体你们在作的时候须要作好兼容click
事件<stoneMap />
组件发起滑块左右滑动的指令具体事件的使用姿式,你们能够详读官方文档
每个事件方法的功能实现和视觉此处就略去了。
【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>
此处主要介绍下事件的控制分发和逻辑网的计算,讲解在注释中
【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】就像被「大内总管」管理着的「小太监」(数字块),「小太监」的一举一动都是被「总管」支配的,包括其长相(颜色)、品级(数字)以及生死(生命周期),但状态的改变都是由本身执行,直接本身整容,本身升级,还要。。自杀。底层人民好无奈 ╮(╯_╰)╭
【简码】
<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 的太高指望,致使不少最初的方案都被「阉割」或者「整容」。
想让元素动起来,传统前端通常有两种方式
一、CSS 动画
二、JS 动画
在 Weex 上由多了一个
三、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 在手势指令上虽然能够知足游戏的基础指令要求,但细节上仍是不太理想。
总的来说,Weex 算是知足了我作小游戏的要求。若是想作大游戏,就不建议使用 Weex 了,Weex 确实作不了,但者也不是 Weex 诞生的意义。
好了,这次尝试就到这吧。为了避免让思路断掉,我又通宵了,罪过,罪过 ~ ~ ~,但愿此文对感兴趣的小伙伴有所帮助。
mark: 03:05