这个是一个非常跟热点的小游戏,思聪吃热狗。这个游戏的话,我感觉思路还挺简单的,天上会掉热狗和障碍物,
思聪在下面张开嘴巴,进行左右移动,接热狗。如果吃到的是热狗就得一分,如果思聪吃到的不是热狗,是障碍物,就结束游戏。
如果要衍生的话,其实可以将热狗的下落方向不定的,就像俄罗斯方块那样,思聪的嘴巴也可以除了向上接热狗,还可以所有咬热狗那种。
感觉很简单很好玩的游戏哇,先放游戏的效果。
我们看动图可以发现,其实这个里面除了热狗障碍物,还有一个是得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-hotdogapputilsindex.js
export const isUndef = (v) => v === null || v === undefined
export const noop = () => {}
export * from './constant'
export * from './dom'
export * from './device'
export { default as eventEmitter } from './event'
//ig-wxz-and-hotdogapputilsconstant.js
export const PLYAYER_OPTIONS = {
img: require('@/assets/images/sicong.jpg'),
70,
height: 70,
}
export const DIALOG_OPTIONS = {
250,
height: 170,
}
// 坠落到底事件
export const CHECK_FALL_EVENT = 'checkFall'
//定义的一些设备
//ig-wxz-and-hotdogapputilsdevice.js
const ua = window.navigator.userAgent
const dpr = window.devicePixelRatio
const w = window.screen.width
const h = window.screen.height
// iPhone X、iPhone XS
const isIPhoneX = /iphone/gi.test(ua) && dpr && dpr === 3 && w === 375 && h === 812
// iPhone XS Max
const isIPhoneXSMax = /iphone/gi.test(ua) && dpr && dpr === 3 && w === 414 && h === 896
// iPhone XR
const isIPhoneXR = /iphone/gi.test(ua) && dpr && dpr === 2 && w === 414 && h === 896
const needSafe = isIPhoneX || isIPhoneXSMax || isIPhoneXR
export const safeHeight = needSafe ? 45 : 0
export const screenHeight = window.innerHeight - safeHeight
export const screenWidth = Math.max(window.innerWidth, 300)
//ig-wxz-and-hotdogapputilsdom.js
import { 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-hotdogapputilsevent.js
class 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-hotdogappstoreindex.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-hotdogappmodulesounus-point.js
/**
* 得分提示
*/
import { PLYAYER_OPTIONS, safeHeight, addEvent, removeNode } from '@/utils'
const { height: playerHeight } = PLYAYER_OPTIONS
export 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;
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-hotdogappmodulesdialog.js
/**
* 游戏结束对话框
*/
import { screenWidth, DIALOG_OPTIONS, addEvent, removeNode, noop } from '@/utils'
import {
getScore,
} from 'store'
const { width, height } = DIALOG_OPTIONS
export 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}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;
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-hotdogappmodulesfall.js
/**
* 掉落物
*/
import {
screenWidth,
screenHeight,
PLYAYER_OPTIONS,
CHECK_FALL_EVENT,
removeNode,
eventEmitter,
} from '@/utils'
// 每次下落的距离
const INTERVAL_DISTANCE = 15
const CUP = {
50,
height: 100,
img: require('@/assets/images/cup.jpg'),
bounus: 5,
}
const HOT_DOG = {
20,
height: 50,
img: require('@/assets/images/hotdog.jpg'),
bounus: 1,
}
const {
height: playerHeight,
} = PLYAYER_OPTIONS
export 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}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-hotdogappmodulesplayer.js
/**
* 人物
*/
import { screenWidth, safeHeight, addEvent, PLYAYER_OPTIONS, isUndef } from '@/utils'
const { playerWidth, height: playerHeight, img } = PLYAYER_OPTIONS
export 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;
${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-hotdogappmodulesscore-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;
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-hotdogappmodules ime-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;
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}秒`
后记,我没有看懂到底怎么写的