这个是一个非常跟热点的小游戏,思聪吃热狗。这个游戏的话,我感觉思路还挺简单的,天上会掉热狗和障碍物,
思聪在下面张开嘴巴,进行左右移动,接热狗。如果吃到的是热狗就得一分,如果思聪吃到的不是热狗,是障碍物,就结束游戏。 如果要衍生的话,其实可以将热狗的下落方向不定的,就像俄罗斯方块那样,思聪的嘴巴也可以除了向上接热狗,还可以所有咬热狗那种。 感觉很简单很好玩的游戏哇,先放游戏的效果。 我们看动图可以发现,其实这个里面除了热狗障碍物,还有一个是得5分的大能量。 先放作者的github地址:https://github.com/sl1673495/ig-wxz-and-hotdog 接下来我们一起分析代码 项目入口为//index.js中会初始化游戏import Scheduler from './modules/scheduler'function initGame() { new Scheduler()}initGame()
utils文件夹中是工具函数
//ig-wxz-and-hotdog\app\utils\index.jsexport const isUndef = (v) => v === null || v === undefinedexport const noop = () => {}export * from './constant'export * from './dom'export * from './device'export { default as eventEmitter } from './event'
//ig-wxz-and-hotdog\app\utils\constant.jsexport const PLYAYER_OPTIONS = { img: require('@/assets/images/sicong.jpg'), width: 70, height: 70,}export const DIALOG_OPTIONS = { width: 250, height: 170,}// 坠落到底事件export const CHECK_FALL_EVENT = 'checkFall'
//定义的一些设备//ig-wxz-and-hotdog\app\utils\device.jsconst ua = window.navigator.userAgentconst dpr = window.devicePixelRatioconst w = window.screen.widthconst h = window.screen.height// iPhone X、iPhone XSconst isIPhoneX = /iphone/gi.test(ua) && dpr && dpr === 3 && w === 375 && h === 812// iPhone XS Maxconst isIPhoneXSMax = /iphone/gi.test(ua) && dpr && dpr === 3 && w === 414 && h === 896// iPhone XRconst isIPhoneXR = /iphone/gi.test(ua) && dpr && dpr === 2 && w === 414 && h === 896const needSafe = isIPhoneX || isIPhoneXSMax || isIPhoneXRexport const safeHeight = needSafe ? 45 : 0export const screenHeight = window.innerHeight - safeHeightexport const screenWidth = Math.max(window.innerWidth, 300)
//ig-wxz-and-hotdog\app\utils\dom.jsimport { isUndef } from './index'const supportsPassive = (function () { let support = false try { const opts = Object.defineProperty({}, 'passive', { get: function () { support = true } }) window.addEventListener('test', null, opts) } catch (e) { } return support})()export const addEvent = ( node, event, fn, options = {}) => { let { capture, passive } = options capture == isUndef(capture) ? false : capture passive = isUndef(passive) ? true : passive if (typeof node.addEventListener == 'function') { if (supportsPassive) { node.addEventListener(event, fn, { capture, passive, }) } else { node.addEventListener(event, fn, capture) } } else if (typeof node.attachEvent == 'function') { node.attachEvent('on' + event, fn); }}export const removeEvent = function (node, event, fn) { if (typeof node.removeEventListener == 'function') { node.removeEventListener(event, fn); } else if (typeof node.detatchEvent == 'function') { node.detatchEvent('on' + event, fn); }}export const removeNode = (node) => node.parentNode.removeChild(node)
事件函数
//ig-wxz-and-hotdog\app\utils\event.jsclass EventEmitter { constructor() { this._event = {} this._listeners = [] } on(name, callback) { (this._event[name] || (this._event[name] = [])).push(callback) } emit(name, payload) { const cbs = this._event[name] || [] for (let i = 0, len = cbs.length; i < len; i++) { cbs[i](payload) } if (this._listeners.length) { for (let { trigger, callback } of this._listeners) { if (trigger(name)) { callback() } } } } remove(name) { this._event[name] = null } clear() { this._event = {} } // 监听某些事件时使用 listen(condition, callback) { let trigger if (condition instanceof RegExp) { trigger = eventName => condition.test(eventName) } else if (typeof condition === 'string') { trigger = eventName => eventName.includes(condition) } this._listeners.push({ trigger, callback }) }}export default new EventEmitter()
//ig-wxz-and-hotdog\app\store\index.js//游戏中的状态管理/** * 全局状态管理 */class ReactiveStore { constructor() { this._store = {} this._listeners = {} } // currying createAction(key) { const set = (val) => { this.set(key, val) } const get = () => { return this.get(key) } const subscribe = (fn) => { return this.subscribe(key, fn) } return { get, set, subscribe, } } // set的时候触发subscribe的方法 set(key, val) { this._store[key] = val const listeners = this._listeners[key] if (listeners) { listeners.forEach(fn => fn()) } } get(key) { return this._store[key] } // 订阅某个key的set执行fn回调 subscribe(key, cb) { (this._listeners[key] || (this._listeners[key] = [])).push(cb) // return unsubscribe return () => { const cbs = this._listeners[key] const i = cbs.findIndex(f => cb === f) cbs.splice(i, 1) } }}const store = new ReactiveStore()const { set: setScore, get: getScore, subscribe: subscribeScore } = store.createAction('score')const { set: setSeconds, get: getSeconds, subscribe: subscribeSeconds } = store.createAction('seconds')export { setScore, getScore, subscribeScore, setSeconds, getSeconds, subscribeSeconds,}
//ig-wxz-and-hotdog\app\modules\bounus-point.js/** * 得分提示 */import { PLYAYER_OPTIONS, safeHeight, addEvent, removeNode } from '@/utils'const { height: playerHeight } = PLYAYER_OPTIONSexport default class BounusPoint { constructor(x, bounus) { this.$el = null this.left = x this.bottom = safeHeight + playerHeight this.bounus = bounus this.init() this.initEvent() } init() { const el = document.createElement('div') el.style.cssText = ` position: fixed; z-index: 2; width: auto; height: 20px; text-align: center; left: ${this.left}px; bottom: ${this.bottom}px; font-weight: 700; font-size: 18px; animation:bounus 1s; ` const text = document.createTextNode(`+${this.bounus}`) el.appendChild(text) document.body.appendChild(el) this.$el = el } initEvent() { addEvent(this.$el, 'animationend', () => { removeNode(this.$el) }) }}
//动态创建弹框//ig-wxz-and-hotdog\app\modules\dialog.js/** * 游戏结束对话框 */import { screenWidth, DIALOG_OPTIONS, addEvent, removeNode, noop } from '@/utils'import { getScore,} from 'store'const { width, height } = DIALOG_OPTIONSexport default class Dialog { constructor(onLeftClick, onRightclick) { this.onLeftClick = onLeftClick ? () => { this.destory() onLeftClick() } : noop this.onRightClick = onRightclick || noop this.initDialog() } initDialog() { const dialog = document.createElement('div') dialog.style.cssText = ` position: fixed; z-index: 2; width: ${width}px; height: ${height}px; padding-top: 20px; border: 2px solid black; text-align: center; left: ${screenWidth / 2 - width / 2}px; top: 200px; font-weight: 700; ` const endText = createText('游戏结束', 'font-size: 30px;') const scoreText = createText(`${getScore()}分`, 'font-size: 30px;') const restartBtn = createButton('replay', this.onLeftClick, 'left: 20px;') const starBtn = createButton('❤star', this.onRightClick, 'right: 20px;') dialog.appendChild(endText) dialog.appendChild(scoreText) dialog.appendChild(restartBtn) dialog.appendChild(starBtn) document.body.appendChild(dialog) this.$el = dialog } destory() { removeNode(this.$el) }}const createText = (text, extraCss) => { const p = document.createElement('p') p.style.cssText = ` font-weight: 700; text-align: center; margin-bottom: 8px; ${extraCss} ` const textNode = document.createTextNode(text) p.appendChild(textNode) return p}const createButton = (text, fn, extraCss) => { const button = document.createElement('div') button.style.cssText = ` position: absolute; width: 90px; bottom: 20px; border: 2px solid black; font-weight: 700; font-size: 20px; ${extraCss} ` const textNode = document.createTextNode(text) button.appendChild(textNode) addEvent(button,'click', fn) return button}
这个是下落部分的
//ig-wxz-and-hotdog\app\modules\fall.js/** * 掉落物 */import { screenWidth, screenHeight, PLYAYER_OPTIONS, CHECK_FALL_EVENT, removeNode, eventEmitter,} from '@/utils'// 每次下落的距离const INTERVAL_DISTANCE = 15const CUP = { width: 50, height: 100, img: require('@/assets/images/cup.jpg'), bounus: 5,}const HOT_DOG = { width: 20, height: 50, img: require('@/assets/images/hotdog.jpg'), bounus: 1,}const { height: playerHeight,} = PLYAYER_OPTIONSexport default class Fall { constructor() { this.img = null this.bounus = 0 this.width = 0 this.height = 0 this.posY = 0 this.moveTimes = 0 this.randomFallItem() this.calcTimePoint() this.initFall() this.startFall() } randomFallItem() { const fallItem = Math.random() <= 0.08 ? CUP : HOT_DOG const { img, bounus, width, height } = fallItem this.img = img this.bounus = bounus this.width = width this.height = height } // 计算开始碰撞的时间点 calcTimePoint() { const { width, height } = this // 从生成到落到人物位置需要的总移动次数 this.timesToPlayer = Math.floor((screenHeight - playerHeight - height) / INTERVAL_DISTANCE) // 从生成到落到屏幕底部需要的总移动次数 this.timesToEnd = Math.floor(screenHeight / INTERVAL_DISTANCE) } initFall() { this.posX = getScreenRandomX(this.width) const { width, height, posX } = this const fall = document.createElement('img') this.$el = fall fall.src = this.img fall.style.cssText = ` position: fixed; width: ${width}px; height: ${height}px; left: ${posX}px; transform: translateY(0px); z-index: 0; ` document.body.appendChild(fall) } updateY() { this.moveTimes++ // 进入人物范围 生成高频率定时器通知外部计算是否碰撞 if (this.moveTimes === this.timesToPlayer) { if (!this.emitTimer) { this.emitTimer = setInterval(() => { eventEmitter.emit(CHECK_FALL_EVENT, this) }, 4) } } // 到底部了没有被外部通知销毁 就自行销毁 if (this.moveTimes === this.timesToEnd) { this.destroy() return } const nextY = this.posY + INTERVAL_DISTANCE this.$el.style.transform = `translateY(${nextY}px)` this.posY = nextY } destroy() { this.emitTimer && clearInterval(this.emitTimer) this.fallTimer && clearInterval(this.fallTimer) removeNode(this.$el) } startFall() { this.fallTimer = setInterval(() => { this.updateY() }, 16) }}function getScreenRandomX(width) { return Math.random() * (screenWidth - width)}
//ig-wxz-and-hotdog\app\modules\player.js/** * 人物 */import { screenWidth, safeHeight, addEvent, PLYAYER_OPTIONS, isUndef } from '@/utils'const { width: playerWidth, height: playerHeight, img } = PLYAYER_OPTIONSexport default class Player { constructor() { // 初始化位置 屏幕正中 this.posX = screenWidth / 2 - playerWidth / 2 this.initPlayer() this.initMoveEvent() } //初始化图像 initPlayer() { const el = this.$el = document.createElement('img') el.src = img el.style.cssText = ` position: fixed; bottom: ${safeHeight}px; width: ${playerWidth}px; height: ${playerHeight}px; transform: translateX(${ screenWidth / 2 - playerWidth / 2}px); z-index: 1; ` document.body.appendChild(el) } //移动事件 initMoveEvent() { const body = document.body addEvent( body, 'touchstart', e => { setPositionX(this, e) }) const moveEvent = 'ontouchmove' in window ? 'touchmove' : 'mousemove' addEvent( body, moveEvent, e => { e.preventDefault() setPositionX(this, e) }, { passive: false } ) }}//设置位置const setPositionX = (player, e) => { let x = e.pageX if (isUndef(x)) { x = e.touches[0].clientX } const { $el } = player $el.style.transform = `translateX(${checkScreenLimit(x - (playerWidth / 2))}px)` player.posX = x}//设置位置限制const checkScreenLimit = (x) => { const leftLimit = 0 - (playerWidth / 2) const rightLimit = screenWidth - (playerWidth / 2) return x < leftLimit ? leftLimit : x > rightLimit ? rightLimit : x}
分数
//ig-wxz-and-hotdog\app\modules\score-board.js/** * 计分板 */import { setScore, getScore, subscribeScore, getSeconds} from 'store'class Score { constructor() { this.$el = null this.initScore() subscribeScore(this.renderScore.bind(this)) } initScore() { const score = document.createElement('div') score.style.cssText = ` position: fixed; z-index: 2; width: 100px; height: 50px; line-height: 50px; text-align: center; right: 0; top: 0; font-size: 30px; font-weight: 700; ` this.$el = score document.body.appendChild(score) } addScore(bounus) { const seconds = getSeconds() if (seconds !== 0) { setScore(getScore() + bounus) } } renderScore() { this.$el.innerText = getScore() }}export default Score
计时板
//ig-wxz-and-hotdog\app\modules\time-board.js/** * 计时板 */import { subscribeSeconds, getSeconds } from 'store'export default class TimeBoard { constructor() { this.$el = null this.initTimerBoard() subscribeSeconds(this.renderTimerText.bind(this)) } initTimerBoard() { const board = document.createElement('div') board.style.cssText = ` position: fixed; z-index: 2; width: 200px; height: 50px; line-height: 50px; text-align: center; left: 0; top: 0; font-size: 30px; font-weight: 700; ` document.body.appendChild(board) this.$el = board } renderTimerText() { this.$el.innerText = createTimerText(getSeconds()) }}const createTimerText = (seconds) => `剩余时间${seconds}秒`
后记,我没有看懂到底怎么写的