Appearance
飞船---摩擦力和碰撞(w、s、d)
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="collisionAndFriction" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, nextTick } from 'vue'
const requestID = ref<any>()
const isRunning = ref(false)
const eventListeners = ref<any>([])
const f = 0.97
let vr = 0
let vx = 0
let vy = 0
let ax = 0
let ay = 0
let speed = 0
let angle = 0
const onTrigger = async () => {
if (!isRunning.value) {
eventListeners.value = []
isRunning.value = true
await nextTick()
onRunning()
} else {
isRunning.value = false
destroy()
eventListeners.value = []
}
}
function SpaceShip () {
this.x = 0
this.y = 0
this.width = 25
this.height = 25
this.rotation = 0
this.showFlame = false
}
SpaceShip.prototype.draw = function (context) {
context.save()
context.beginPath()
context.translate(this.x, this.y)
context.rotate(this.rotation)
context.strokeStyle = "#dc6aff"
context.moveTo(10, 0)
context.lineTo(-10, 10)
context.lineTo(-5, 0)
context.lineTo(-10, -10)
context.lineTo(10, 0)
context.closePath()
context.stroke()
if (this.showFlame === true) {
context.save()
context.beginPath()
context.strokeStyle = "#f00"
context.moveTo(-7.5, -5)
context.lineTo(-15, 0)
context.lineTo(-7.5, 5)
context.stroke()
context.restore()
}
context.restore()
}
const onRunning = async() => {
await nextTick()
const canvas: any = document.getElementById('collisionAndFriction')
const ctx = canvas.getContext('2d')
const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
canvas.width = width
canvas.height = height
const ship = new SpaceShip()
ship.x = canvas.width / 2
ship.y = canvas.height / 2
function keyDown (event) {
event.preventDefault()
switch (event.keyCode) {
case 68:
vr = -3
break
case 65:
vr = 3
break
case 87:
speed = 0.5
ship.showFlame = true
break
}
}
function keyUp(event) {
event.preventDefault()
vr = 0
speed = 0
ship.showFlame = false
}
eventListeners.value.push(keyDown, keyUp)
window.addEventListener('keydown', keyDown)
window.addEventListener('keyup', keyUp)
const runAnimate = () => {
ctx.clearRect(0, 0, width, height)
requestID.value = requestAnimationFrame(runAnimate)
ship.rotation += vr * Math.PI / 180
angle = ship.rotation
ax = Math.cos(angle) * speed
ay = Math.sin(angle) * speed
vx += ax
vy += ay
vx *= f
vy *= f
ship.x += vx
ship.y += vy
if (ship.x + ship.width / 2 > canvas.width) {
ship.x = canvas.width - ship.width
vx *= -1
} else if (ship.x < ship.width / 2) {
ship.x = ship.width / 2
vx *= -1
}
if (ship.y + ship.height / 2 > canvas.height) {
ship.y = canvas.height - ship.height / 2
vy *= -1
} else if (ship.y < ship.height / 2) {
ship.y = ship.height / 2
vy *= -1
}
ship.draw(ctx)
}
runAnimate()
}
const destroy = () => {
cancelAnimationFrame(requestID.value)
eventListeners.value.forEach(listener => {
window.removeEventListener('keydown', listener)
window.removeEventListener('keyup', listener)
})
}
onUnmounted(() => {
destroy()
})
</script>
A 星寻路算法---随机迷宫生成算法
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<div id="aStartRandomMazeBox">
<canvas v-if="isRunning" id="aStartRandomMaze" class="stage"></canvas>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, nextTick } from 'vue'
const requestID = ref<any>()
const isRunning = ref(false)
const eventListeners = ref<any>([])
const onTrigger = async () => {
if (!isRunning.value) {
eventListeners.value = []
isRunning.value = true
await nextTick()
onRunning()
} else {
isRunning.value = false
destroy()
eventListeners.value = []
}
}
const onRunning = async() => {
await nextTick()
const canvas: any = document.getElementById('aStartRandomMaze')
const ctx = canvas.getContext('2d')
const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
canvas.width = width
canvas.height = height
const row = 19
const col = 35
const size = 9
// 取区域随机数 x>=min && x<max
const randInt = (min, max) => {
const _max = max || 0
const _min = min || 0
// 计算范围的绝对值
const step = Math.abs(_max - _min)
// 如果只有一个参数,st = 0
const st = (max === undefined) ? 0 : _min
// 生成随机整数
const result = st + (Math.ceil(Math.random() * step)) - 1
return result
}
// 普里姆算法生成连通图的二维数组 row 行 column 列
const primMaze = (c, r) => {
const init = (c, r) => {
const tempArr = new Array(2 * r + 1)
// 全部置1
for (let i = 0; i < tempArr.length; i++) {
const cols = 2 * c + 1
tempArr[i] = new Array(cols).fill(1)
}
// 中间格子置0
// 在迷宫生成算法中,1 表示墙,0 表示通道
// 这种初始化方式可以创建一个“棋盘格”式的迷宫框架,其中每隔一个单元格是一个通道(0),其余部分是墙(1)
// 这种结构为后续的迷宫生成算法(如深度优先搜索或Prim算法)提供了一个基础框架
for (let i = 0; i < r; i++) {
for (let j = 0; j < c; j++) {
tempArr[2 * i + 1][2 * j + 1] = 0
}
}
return tempArr
}
const process = (arr) => {
// acc存放已访问队列
const acc: any = []
// noAcc存放没有访问队列
const noAcc: any = []
// 迷宫的实际行数和列数
// arr.length 右移一位,相当于整除 2
// 位运算 >> 是一种高效的整数除法操作,特别适合除以 2 的幂
// 在这个场景中,使用 >> 1 比使用传统的除法操作(如 / 2)更高效,且结果是整数
const r = arr.length >> 1
const c = arr[0].length >> 1
// 单元格总数
const total = r * c
for (let i = 0; i < total; i++) {
// 初始化未访问队列
noAcc[i] = 0
}
// 表示上下左右四个方向在 《一维数组》 中的偏移量,用于在 《一维数组》 中快速找到相邻单元格的位置
const offs = [-c, c, -1, 1]
// 表示上下左右四个方向在 《二维数组》 中的行偏移量,用于在 《二维数组》 中计算相邻单元格的行列坐标
const offR = [-1, 1, 0, 0]
// 表示上下左右四个方向在 《二维数组》 中的列偏移量, 用于在 《二维数组》 中计算相邻单元格的行列坐标
const offC = [0, 0, -1, 1]
// 随机从noAcc取出一个位置
let pos = randInt(total, undefined)
// 标记为已访问
noAcc[pos] =
// 将起始点加入已访问队列
acc.push(pos)
while (acc.length < total) {
// 初始化变量
let ls = -1
let offPos = -1
// 找出pos位置在二维数组中的坐标
let pr = pos / c | 0 // 当前行
let pc = pos % c// 当前列
let co = 0
let o = 0
// 随机取上下左右四个单元
while (++co < 5) {
o = randInt(0, 5)
ls = offs[o] + pos
var tpr = pr + offR[o]
var tpc = pc + offC[o]
if (tpr >= 0 && tpc >= 0 && tpr <= r - 1 && tpc <= c - 1 && noAcc[ls] == 0) {
offPos = o
break
}
}
if (offPos < 0) {
pos = acc[randInt(acc.length, undefined)]
} else {
pr = 2 * pr + 1
pc = 2 * pc + 1
// 相邻空单元中间的位置置0
arr[pr + offR[offPos]][pc + offC[offPos]] = 0
pos = ls
noAcc[pos] = 1
acc.push(pos)
}
}
}
let a = init(c, r)
process(a)
// 返回一个二维数组,行的数据为2r+1个,列的数据为2c+1个
return a
}
// 栅格线条
const drawGrid = (context, color, stepX, stepY) =>{
context.strokeStyle = color
context.lineWidth = 0.5
for (let i = stepX + 0.5; i < context.canvas.width; i += stepX) {
context.beginPath()
context.moveTo(i, 0)
context.lineTo(i, context.canvas.height)
context.stroke()
}
for (let i = stepY + 0.5; i < context.canvas.height; i += stepY) {
context.beginPath()
context.moveTo(0, i)
context.lineTo(context.canvas.width, i)
context.stroke()
}
}
// 方块创造方法
const createRect = (x, y, r, c) => {
ctx.beginPath()
ctx.fillStyle = c
ctx.rect(x, y, r, r)
ctx.fill()
}
// 定义点对象【a*点对象】
function Point(x, y) {
this.x = x
this.y = y
this.parent = null
this.f = 0
this.g = 0
this.h = 0
//当前点状态,0:表示在openlist 1:表示closelist,-1表示还没处理
this.state = -1
//flag表明该点是否可通过
this.flag = 0
}
// 把普通二维数组(全部由1,0表示)的转换成a*所需要的点数组
function convertArrToAS (arr) {
const r = arr.length
const c = arr[0].length
const a = new Array(r)
for (let i = 0; i < r; i++) {
a[i] = new Array(c)
for (let j = 0; j < c; j++) {
const pos = new Point(i, j)
pos.flag = arr[i][j]
a[i][j] = pos
}
}
return a
}
// A*算法,pathArr表示最后返回的路径
function findPathA (pathArr, start, end, row, col) {
// 添加数据到排序数组中
function addArrSort (descSortedArr, element, compare) {
let left = 0
let right = descSortedArr.length - 1
let mid = (left + right) >> 1
while (left <= right) {
mid = (left + right) >> 1
if (compare(descSortedArr[mid], element) === 1) {
left = mid + 1
} else if (compare(descSortedArr[mid], element) == -1) {
right = mid - 1
} else {
break
}
}
for (let i = descSortedArr.length - 1; i >= left; i--) {
descSortedArr[i + 1] = descSortedArr[i]
}
descSortedArr[left] = element
}
// 判断两个点是否相同
function pEqual (p1, p2) {
return p1.x == p2.x && p1.y == p2.y
}
// 获取两个点距离,采用曼哈顿方法
function posDist (pos, pos1) {
return (Math.abs(pos1.x - pos.x) + Math.abs(pos1.y - pos.y))
}
function between (val, min, max) {
return (val >= min && val <= max)
}
// 比较两个点f值大小
function compPointF (pt1, pt2) {
return pt1.f - pt2.f
}
// 处理当前节点
function processCurPoint (arr, openList, row, col, currPoint, destPoint) {
// 获取上下左右
const ltx = currPoint.x - 1
const lty = currPoint.y - 1
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
const cx = ltx + i
const cy = lty + j
if ((cx === currPoint.x || cy === currPoint.y) && between(ltx, 0, row - 1) && between(lty, 0, col - 1)) {
const tp = arr[cx][cy]
if (tp.flag === 0 && tp.state !== 1) {
if (pEqual(tp, destPoint)) {
tp.parent = currPoint
return true
}
if (tp.state === -1) {
tp.parent = currPoint
tp.g = 1 + currPoint.g
tp.h = posDist(tp, destPoint)
tp.f = tp.h + tp.f
tp.state = 0
addArrSort(openList, tp, compPointF)
} else {
const g = 1 + currPoint.g
if (g < tp.g) {
tp.parent = currPoint
tp.g = g
tp.f = tp.g + tp.h
openList.quickSort(compPointF)
}
}
}
}
}
}
return false
}
// 定义openList
const openList = []
// 定义closeList
const closeList: any = []
start = pathArr[start[0]][start[1]]
end = pathArr[end[0]][end[1]]
// 添加开始节点到openList
addArrSort(openList, start, compPointF)
let finish = false
while ((openList.length > 0)) {
const currPoint: any = openList.pop()
currPoint.state = 1
closeList.push(currPoint)
finish = processCurPoint(pathArr, openList, row, col, currPoint, end)
if (finish) {
break
}
}
if (finish) {
const farR: any = []
var tp = end.parent
farR.push(end)
while (tp != null) {
farR.push(tp)
tp = tp.parent
}
return farR
} else {
return null
}
}
// 定位屏幕坐标到数组位置
function mapSCPos(i, j) {
return [i / size | 0, j / size | 0]
}
// 检测数组中的位置是否存在方块
function mapHasRect(map, i, j) {
return (map[i][j])
}
// 迷宫
const mapArr = primMaze(col, row)
const startRect = {
x: function() {
for (var i = 0, len = mapArr.length; i < len; i++) {
for (var j = 0, len1 = mapArr[i].length; j < len1; j++) {
if (!mapArr[i][j]) {
return j * size
}
}
}
}(),
y: function() {
for (var i = 0, len = mapArr.length; i < len; i++) {
for (var j = 0, len1 = mapArr[i].length; j < len1; j++) {
if (!mapArr[i][j]) {
return i * size
}
}
}
}(),
pos: function() {
return [this.x, this.y]
}
}
const endRect: any = {
hasCreate: false,
x: null,
y: null,
pos: function() {
return [this.x, this.y]
}
}
let startPoint = mapSCPos(startRect.pos()[1], startRect.pos()[0])
let endPoint
let path: any = null
let next: any = null
const mouseClick = (event) => {
// 标准的获取鼠标点击相对于canvas画布的坐标公式
const x = event.offsetX
const y = event.offsetY
var endRectPos = mapSCPos(y, x) // [i, j]
endRect.x = endRectPos[1] * size
endRect.y = endRectPos[0] * size
if (mapHasRect(mapArr, endRectPos[0], endRectPos[1])) {
console.log('这个位置已经有方块啦!')
} else {
endRect.pos()
endRect.hasCreate = true
}
console.log(endRect.hasCreate)
}
eventListeners.value.push(mouseClick)
const parent = document.getElementById('aStartRandomMazeBox') as any
parent.addEventListener('click', mouseClick)
const runAnimate = () => {
ctx.clearRect(0, 0, width, height)
requestID.value = requestAnimationFrame(runAnimate)
drawGrid(ctx, 'lightgray', size, size)
// 根据地图二维数组创建色块
for (let i = 0, len = mapArr.length; i < len; i++) {
for (let j = 0, len1 = mapArr[i].length; j < len1; j++) {
if (mapArr[i][j]) {
createRect(j * size, i * size, size, '#fd8e00')
}
}
}
// 绘制开始方块
createRect(startRect.x, startRect.y, size, 'red')
if (endRect.hasCreate) {
// 绘制跟随方块
createRect(endRect.pos()[0], endRect.pos()[1], size, 'blue')
endPoint = mapSCPos(endRect.pos()[1], endRect.pos()[0])
if (path === null) {
const ASmap = convertArrToAS(mapArr)
path = findPathA(ASmap, startPoint, endPoint, ASmap.length, ASmap[0].length)
} else {
next = path.pop()
startRect.y = next.x * size
startRect.x = next.y * size
if (path.length === 0) {
startPoint = mapSCPos(startRect.pos()[1], startRect.pos()[0])
path = null
endRect.hasCreate = false
}
}
}
}
runAnimate()
}
const destroy = () => {
cancelAnimationFrame(requestID.value)
const parent = document.getElementById('aStartRandomMazeBox') as any
if(parent) {
eventListeners.value.forEach(listener => {
parent.removeEventListener('click', listener)
})
}
}
onUnmounted(() => {
destroy()
})
// 迷宫的二维结构与一维表示
// 假设有一个迷宫,其二维结构如下:
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// 这是一个 5×5 的迷宫,其中每个格子可以用二维坐标 (row, col) 表示。例如,左上角的格子是 (0, 0),右下角的格子是 (4, 4)。
// 为了简化计算,还可以用一维数组来表示迷宫的格子。假设迷宫的格子用一维数组表示如下:
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
// 其中,每个数字对应一个格子的位置。例如,0 表示 (0, 0),5 表示 (1, 0),24 表示 (4, 4)。
// 偏移量的设置
// 偏移量的目的是从当前格子快速找到它的上下左右相邻格子。在代码中,偏移量定义如下:
// var offs = [-c, c, -1, 1]; // 一维数组中的偏移量
// var offR = [-1, 1, 0, 0]; // 二维数组中的行偏移量
// var offC = [0, 0, -1, 1]; // 二维数组中的列偏移量
// 1. 一维偏移量 offs
// 在一维数组中,相邻格子的位置可以通过偏移量快速计算:
// ·向上:当前格子的索引减去 c(列数)。
// ·向下:当前格子的索引加上 c。
// ·向左:当前格子的索引减去 1。
// ·向右:当前格子的索引加上 1。
// 例如,假设当前格子是 (1, 1),在一维数组中的索引是 6(假设 c = 5):
// ·向上:6 - 5 = 1(索引为 1,对应 (0, 1))。
// ·向下:6 + 5 = 11(索引为 11,对应 (2, 1))。
// ·向左:6 - 1 = 5(索引为 5,对应 (1, 0))。
// ·向右:6 + 1 = 7(索引为 7,对应 (1, 2))。
// 2. 二维偏移量 offR 和 offC
// 在二维数组中,相邻格子的行和列坐标可以通过偏移量计算:
// ·向上:行坐标减去 1,列坐标不变。
// ·向下:行坐标加上 1,列坐标不变。
// ·向左:行坐标不变,列坐标减去 1。
// ·向右:行坐标不变,列坐标加上 1。
// 例如,当前格子是 (1, 1):
// ·向上:(1 - 1, 1) = (0, 1)。
// ·向下:(1 + 1, 1) = (2, 1)。
// ·向左:(1, 1 - 1) = (1, 0)。
// ·向右:(1, 1 + 1) = (1, 2)。
// 图解
// 假设当前格子是 (1, 1),其一维索引为 6,迷宫如下:
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// | | X | | | | <- 当前格子 (1, 1)
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// | | | | | |
// +---+---+---+---+---+
// 通过偏移量,可以找到相邻格子:
// ·向上:(0, 1),一维索引为 6 - 5 = 1。
// ·向下:(2, 1),一维索引为 6 + 5 = 11。
// ·向左:(1, 0),一维索引为 6 - 1 = 5。
// ·向右:(1, 2),一维索引为 6 + 1 = 7。
// 总结
// 偏移量的设置是为了:
// 1、快速计算相邻格子的索引:在一维数组中,通过 offs 可以直接找到相邻格子的位置。
// 2、快速计算相邻格子的坐标:在二维数组中,通过 offR 和 offC 可以快速计算相邻格子的行和列坐标。
// 3、简化逻辑:通过统一的偏移量数组,可以方便地处理上下左右四个方向的移动,避免重复代码。
// 这种设计使得迷宫生成算法能够高效地随机选择相邻格子,从而生成复杂的迷宫结构。
</script>