事實上, 我使用了 react、preact、vue 和 omi 框架 分別開發貪吃蛇遊戲,我發現 omi 的開發體驗最棒,而且其最後的源代碼很是地簡潔,讓我一步一步帶着大家開發這款簡單的遊戲。css
閱讀本文大概花費 10 分鐘,你能夠從中學會:html
class Snake {
constructor() {
this.body = [3, 1, 2, 1, 1, 1]
this.dir = 'right'
}
move(eating) {
const b = this.body
if (!eating) {
b.pop()
b.pop()
}
switch (this.dir) {
case 'up':
b.unshift(b[0], b[1] - 1)
break
case 'right':
b.unshift(b[0] + 1, b[1])
break
case 'down':
b.unshift(b[0], b[1] + 1)
break
case 'left':
b.unshift(b[0] - 1, b[1])
break
}
}
turnUp() {
if (this.dir !== 'down')
this.dir = 'up'
}
turnRight() {
if (this.dir !== 'left')
this.dir = 'right'
}
turnDown() {
if (this.dir !== 'up')
this.dir = 'down'
}
turnLeft() {
if (this.dir !== 'right')
this.dir = 'left'
}
}
複製代碼
蛇的轉向有個邏輯,就是不能反方向後退,好比正在向上移動,不能直接直接向下轉向,因此在 turnUp
,turnRight
,turnDown
,turnLeft
中都有對應的條件判斷。前端
import Snake from './snake'
class Game {
constructor() {
this.map = []
this.size = 16
this.loop = null
this.interval = 500
this.paused = false
this._preDate = Date.now()
this.init()
}
init() {
this.snake = new Snake
for (let i = 0; i < this.size; i++) {
const row = []
for (let j = 0; j < this.size; j++) {
row.push(0)
}
this.map.push(row)
}
}
tick() {
this.makeFood()
const eating = this.eat()
this.snake.move(eating)
this.mark()
}
mark() {
const map = this.map
for (let i = 0; i < this.size; i++) {
for (let j = 0; j < this.size; j++) {
map[i][j] = 0
}
}
for (let k = 0, len = this.snake.body.length; k < len; k += 2) {
this.snake.body[k + 1] %= this.size
this.snake.body[k] %= this.size
if (this.snake.body[k + 1] < 0) this.snake.body[k + 1] += this.size
if (this.snake.body[k] < 0) this.snake.body[k] += this.size
map[this.snake.body[k + 1]][this.snake.body[k]] = 1
}
if (this.food) {
map[this.food[1]][this.food[0]] = 1
}
}
start() {
this.loop = setInterval(() => {
if (Date.now() - this._preDate > this.interval) {
this._preDate = Date.now()
if (!this.paused) {
this.tick()
}
}
}, 16)
}
stop() {
clearInterval(this.loop)
}
pause() {
this.paused = true
}
play() {
this.paused = false
}
reset() {
this.paused = false
this.interval = 500
this.snake.body = [3, 1, 2, 1, 1, 1]
this.food = null
this.snake.dir = 'right'
}
toggleSpeed() {
this.interval === 500 ? (this.interval = 150) : (this.interval = 500)
}
makeFood() {
if (!this.food) {
this.food = [this._rd(0, this.size - 1), this._rd(0, this.size - 1)]
for (let k = 0, len = this.snake.body.length; k < len; k += 2) {
if (this.snake.body[k + 1] === this.food[1]
&& this.snake.body[k] === this.food[0]) {
this.food = null
this.makeFood()
break
}
}
}
}
eat() {
for (let k = 0, len = this.snake.body.length; k < len; k += 2) {
if (this.snake.body[k + 1] === this.food[1]
&& this.snake.body[k] === this.food[0]) {
this.food = null
return true
}
}
}
_rd(from, to) {
return from + Math.floor(Math.random() * (to + 1))
}
}
複製代碼
能夠看到上圖使用了 16*16 的二維數組來存儲蛇、食物、地圖信息。蛇和食物佔據的格子爲 1,其他爲 0。vue
[
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
複製代碼
因此上面表明了一條長度爲 5 的蛇和 1 個食物,你能在上圖中找到嗎?react
import { define, rpx } from 'omi'
define('my-game', ['map'], _ => (
<div class="game"> {_.store.data.map.map(row => { return <p> {row.map(col => { if (col) { return <b class='s'></b> } return <b></b> })} </p> })} </div>
), rpx(require('./_index.css')))
複製代碼
帶有 class
爲 s
的格式是黑色的,好比食物、蛇的身體,其他的會灰色底色。['map']
的做用後面細講,['map']
表明依賴 store.data.map,map 更新會自動更新視圖。git
import { define, rpx } from 'omi'
import '../game'
define('my-index', ['paused'], ({store}) => (
<div class="container"> <h1>OMI SNAKE</h1> <my-game></my-game> <div class="ctrl"> <div class="btn cm-btn cm-btn-dir up" onClick={store.turnUp}><i></i><em></em><span>Up</span></div> <div class="btn cm-btn cm-btn-dir down" onClick={store.turnDown}><i></i><em></em><span>Down</span></div> <div class="btn cm-btn cm-btn-dir left" onClick={store.turnLeft}><i></i><em></em><span >Left</span></div> <div class="btn cm-btn cm-btn-dir right" onClick={store.turnRight}><i></i><em></em><span >Right</span></div> <div class="btn cm-btn space" onClick={store.toggleSpeed}><i></i><span >加速/減速</span></div> <div class="btn reset small" onClick={store.reset}><i ></i><span >Reset</span></div> <div class="btn pp small" onClick={store.pauseOrPlay}><i></i><span >{store.data.paused ? 'Play' : 'Pause'}</span></div> </div> </div> ), rpx(require('./_index.css'))) 複製代碼
import Game from '../models/game'
const game = new Game
const { snake, map } = game
game.start()
class Store {
data = {
map,
paused: false
}
turnUp() {
snake.turnUp()
}
turnRight() {
snake.turnRight()
}
turnDown() {
snake.turnDown()
}
turnLeft() {
snake.turnLeft()
}
pauseOrPlay = () => {
if (game.paused) {
game.play()
this.data.paused = false
} else {
game.pause()
this.data.paused = true
}
}
reset() {
game.reset()
}
toggleSpeed() {
game.toggleSpeed()
}
}
export default new Store
複製代碼
會發現, store 很薄,只負責中轉 View 的 action,到 Model,以及更改this.data.paused
數據會自動更新 View,其中 this.data.map
的更改是在 model 中進行(game.js 的 tick 方法)。github
由於定義組件時聲明瞭依賴:web
define('my-index', ['paused'], ...
複製代碼
define('my-game', ['map'], ...
複製代碼
怎麼控制主幀率和局部幀率。通常狀況下,咱們認爲 60 FPS 是流暢的,因此咱們定時器間隔是有 16ms,核心循環裏的計算量越小,就越接近 60 FPS:json
this.loop = setInterval(() => {
//
}, 16)
複製代碼
可是有些計算沒有必要 16 秒計算一次,這樣會下降幀率,因此能夠記錄上一次執行的時間用來控制幀率:canvas
this.loop = setInterval(() => {
//執行在這裏是大約 60 FPS
if (Date.now() - this._preDate > this.interval) {
//執行在這裏是大約 1000/this.interval FPS
this._preDate = Date.now()
//暫停判斷
if (!this.paused) {
//核心循環邏輯
this.tick()
}
}
}, 16)
複製代碼
你可使用基於 requestAnimationFrame
的 raf-interval 來替代 setInterval
,用於提升性能:
this.loop = setRafInterval(() => {
//執行在這裏是大約 60 FPS
if (Date.now() - this._preDate > this.interval) {
//執行在這裏是大約 1000/this.interval FPS
this._preDate = Date.now()
//暫停判斷
if (!this.paused) {
//核心循環邏輯
this.tick()
}
}
}, 16)
複製代碼
├─ build //web 編譯出的文件,用於生產環境
├─ config
├─ public
├─ scripts
├─ src
│ ├─ assets
│ ├─ components //存放全部頁面的組件
│ ├─ models //存放全部模型
│ ├─ stores //存放頁面的 store
│ └─ index.js //入口文件,會 build 成 index.html
複製代碼
那麼是 MVC、MVP 仍是 MVVM?
從貪吃蛇源碼能夠看出:視圖(components)和模型(models)是分離的,沒有相互依賴關係,可是在 MVC 中,視圖依賴模型,耦合度過高,致使視圖的可移植性大大下降,因此必定不是 MVC 架構。
在 MVP 模式中,視圖不直接依賴模型,由 Presenter 負責完成 Model 和 View 的交互。MVVM 和 MVP 的模式比較接近。ViewModel 擔任這 Presenter 的角色,而且提供 UI 視圖所須要的數據源,而不是直接讓 View 使用 Model 的數據源,這樣大大提升了 View 和 Model 的可移植性,好比一樣的 Model 切換使用 Flash、HTML、WPF 渲染,好比一樣 View 使用不一樣的 Model,只要 Model 和 ViewModel 映射好,View 能夠改動很小甚至不用改變。
從貪吃蛇源碼能夠看出,View(components) 裏直接使用了 Presenter(stores) 的 data 屬性進行渲染,data 屬性來自於 Model(models) 的屬性,並無出現 Model 到 ViewModel 的映射。因此必定不是 MVVM 架構。
因此上面的貪吃蛇屬於 MVP !只不過是進化版的 MVP,由於 M 裏的 map 的變動會自定更是 View,從 M->P->V的迴路是自動化的,代碼裏看不到任何邏輯。僅僅須要聲明依賴:
define('my-game', ['map'] ...
複製代碼
這樣也規避了 MVVM 最大的問題: M 到 VM 映射的開銷。
一、複用性
Model 和 View 之間解耦,Model 或 View 中的一方發生變化,Presenter 接口不變,另外一方就不必對上述變化作出改變,那麼 Model 層的業務邏輯具備很好的靈活性和可重用性。
二、靈活性
Presenter 的 data 變動自動映射到視圖,使得 Presenter 很薄很薄,View 屬於被動視圖。並且基於 Presenter 的 data 可使用任何平臺、任何框架、任何技術進行渲染。
三、測試性
假如 View 和 Model 之間的緊耦合,在 Model 和 View 同時開發完成以前對其中一方進行測試是不可能的。出於一樣的緣由,對 View 或 Model 進行單元測試很困難。如今,MVP模式解決了全部的問題。MVP 模式中,View 和 Model 之間沒有直接依賴,開發者可以藉助模擬對象注入測試二者中的任一方。
rpx(responsive pixel)最初來源於小程序的 wxss,可是知道其原理後也能夠用於 web。 rpx 能夠根據屏幕寬度進行自適應。規定屏幕寬爲750rpx。如在 iPhone6 上,屏幕寬度爲375px,共有750個物理像素,則750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
設備 | rpx 轉 px (屏幕寬度/750) | px 轉 rpx (750/屏幕寬度) |
---|---|---|
iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
rpx 單元很是有利於前端開發的總體工做流程,由於設計人員的設計草圖是按照750的寬度設計的,因此前端頁面能夠直接使用草圖導出標尺進行 rpx 佈局。
由於設備寬度只能在運行時知道,因此須要在運行時動態計算 rpx 到 px 的映射。
export function rpx(css) {
return css.replace(/([1-9]\d*|0)(\.\d*)*rpx/g, (a, b) => {
return (window.innerWidth * Number(b)) / 750 + 'px'
})
}
複製代碼
從上圖能夠看到使用的 web components shadow dom 進行渲染,最新的兩個版本的現代瀏覽器都支持。Edge 和 Internet Explorer 11 須要引入 web components polyfills。
若是你要兼容 IE8+, 只要改 package.json 裏的一行代碼即可以:
"alias": {
"omi": "omio"
}
複製代碼
Omio - Omi for old browsers with same api of omi(IE8+)