vue3 自定義渲染器 + canvas 寫一個《貪吃蛇?》

灬都是個謎發表於2023-03-06

前言

由於 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>

相關文章