前言
由於 canvas
畫圖太麻煩,所有用 PixiJS
代替
DEMO
原始碼
實現原理
main.ts
import { createRenderer } from 'vue'
import { isOn } from '@vue/shared'
import { useResizeObserver } from '@vueuse/core'
import * as PIXI from 'pixi.js'
import patchEvents from './patchEvent'
import App from './App.vue'
import { set } from './utils'
// 建立畫布
const app = new PIXI.Application({ backgroundColor: '#242424' })
app.stage.sortableChildren = true
const canvas = app.view as HTMLCanvasElement
canvas.id = 'canvas'
document.body.appendChild(canvas)
// 設定 canvas 為滿尺寸
useResizeObserver(canvas, ([entry]) => {
app.renderer.resize(entry.contentRect.width, entry.contentRect.height)
})
// 自定義渲染器
const renderer = createRenderer<PIXI.DisplayObject | null, PIXI.Container>({
createElement(type, isSVG, isCustomizedBuiltIn, props) {
return new PIXI[type]() as PIXI.Container
},
insert(el, parent, anchor) {
if (!el || !parent) return
const i = parent.children.indexOf(anchor)
if (i > -1) parent.addChildAt(el, i)
else parent.addChild(el)
},
remove(el) {
el?.removeFromParent()
},
patchProp(el, key, preVal, nextVal) {
if (!el) return
if (typeof el[key] === 'function') {
el[key](...nextVal)
} else if (isOn(key)) {
patchEvents(el, key, nextVal)
} else {
set(el, key, nextVal, ':')
}
},
createText(text) {
return new PIXI.Text(text)
},
createComment(text) {
const comment = new PIXI.Text(`<!-- ${text} -->`)
comment.visible = false
return comment
},
setText(node: PIXI.Text, text) {
node.text = text
},
setElementText() {},
parentNode(node) {
return node?.parent
},
nextSibling(node) {
return node?.parent.children[node.parent.getChildIndex(node) + 1]
}
})
renderer.createApp(App).mount(app.stage)
App.vue
<template>
<!-- 頂部 -->
<Graphics :zIndex="1">
<Graphics :beginFill="[0x2d333b]" :drawRect="[0, 0, width, 55]" endFill :alpha="0.75" />
<Text text="? SNAKE" :x="12" :style:lineHeight="55" style:fill="white" />
<Sprite :texture="Texture.from('https://huodoushigemi.github.io/docx2vue/assets/github-540f5a2f.svg')" :x="256" :y="5" :width="45" :height="45" cursor="pointer" @click="toGithub" />
</Graphics>
<!-- 網格 -->
<template v-for="i in maxX">
<Graphics :x="i * size" :lineStyle="[1]" :moveTo="[0, 0]" :lineTo="[0, maxY * size]" />
</template>
<template v-for="i in maxY">
<Graphics :y="i * size" :lineStyle="[1]" :moveTo="[0, 0]" :lineTo="[maxX * size, 0]" />
</template>
<!-- 食物 -->
<Text text="?" :x="food[0] * size" :y="food[1] * size" :style:fontSize="size / 1.4" :style:lineHeight="size" />
<!-- 蛇身 -->
<template v-for="p in snake">
<Graphics :x="p[0] * size" :y="p[1] * size" :beginFill="[0]" :drawRect="[0, 0, size, size]" endFill />
</template>
<Text text="按空格 暫停/繼續" :x="maxX * size * 0.45" :y="maxY * size * 0.8" :style:fill="0xcccccc80" />
<Text v-if="!isActive" text="暫停中……" :x="maxX * size * 0.45" :y="maxY * size * 0.85" :style:fill="0xcccccc80" />
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useIntervalFn, useWindowSize } from '@vueuse/core'
import { Texture } from 'pixi.js'
type Point = [x: number, y: number]
// 網格數量
const num = 10
const { width, height } = useWindowSize()
const size = computed(() => Math.min(width.value, height.value) / num)
// 邊界
const maxX = computed(() => (width.value / size.value) >> 0)
const maxY = computed(() => (height.value / size.value) >> 0)
// 蛇身
const snake = ref<Point[]>([
[2, num >> 1],
[1, num >> 1],
[0, num >> 1]
])
// 食物
const food = ref<Point>(genFood())
// 方向
let direction = 'ArrowRight'
let nextDirection = direction
window.addEventListener('keydown', e => {
// 按空格 暫停/繼續
if (e.key == ' ') return isActive.value ? pause() : resume()
// ? ↑ ↓ ← →
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return
if (direction == 'ArrowUp' && e.key == 'ArrowDown') return
if (direction == 'ArrowDown' && e.key == 'ArrowUp') return
if (direction == 'ArrowLeft' && e.key == 'ArrowRight') return
if (direction == 'ArrowRight' && e.key == 'ArrowLeft') return
nextDirection = e.key
})
// loop
const { resume, pause, isActive } = useIntervalFn(() => {
direction = nextDirection
const head = snake.value[0]
let next!: Point
if (direction == 'ArrowUp') next = [head[0], head[1] - 1]
if (direction == 'ArrowDown') next = [head[0], head[1] + 1]
if (direction == 'ArrowLeft') next = [head[0] - 1, head[1]]
if (direction == 'ArrowRight') next = [head[0] + 1, head[1]]
// 撞牆
if (!isRange(next)) return pause(), alert('撞牆身亡,遊戲結束')
// 自盡
if (snake.value.slice(0, -1).some(e => isSamePoint(e, next))) return pause(), alert('咬到自己,遊戲結束')
snake.value.unshift(next)
// 吃到食物
if (isSamePoint(next, food.value)) {
food.value = genFood()
} else {
snake.value.pop()
}
}, 250)
function toGithub() {
const a = document.createElement('a')
a.href = 'https://github.com/huodoushigemi/vue-canvas-snake.git'
a.target = '_blank'
a.click()
}
// p1 == p2
function isSamePoint(p1: Point, p2: Point) {
return p1[0] == p2[0] && p1[1] == p2[1]
}
// 是否越界
function isRange(p: Point) {
return p[0] >= 0 && p[0] < maxX.value && p[1] >= 0 && p[1] < maxY.value
}
// 生成隨機數
function random(max: number, min = 0) {
return (min + Math.random() * (max - min)) >> 0
}
// 生成食物
function genFood() {
let point!: Point
do point = [random(maxX.value), random(maxY.value)]
while (snake.value.some(e => isSamePoint(e, point)))
return point
}
</script>