本文默认你看完了pixijs的官网介绍。pixi.nodejs.cn/8.x/guides
其中包含内容。
- 小熊精灵动画,根据键盘左右移动,移动动画。
- 文字从屏幕上方往下掉落及阴影。
- 小熊精灵部位与文字灯笼碰撞检测。
- 碰撞爆炸气泡动画及计数累计文字漂浮。
效果图
一、template
<template>
<div class="game-container">
<div
class="pixijs"
ref="pixiDom"
>
</div>
<div
class="top-home-icon"
@click="() => router.push('/ModulesPanel')"
>
<img
class="image"
src="@renderer/assets/img/game/public/home_btn.png"
/>
</div>
</div>
</template>
二、 vue变量及props内容
import { shallowRef, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { Application, Assets, Text, Sprite, Container, DisplayObject, Graphics, Texture, AnimatedSprite, TextStyle, Filter } from 'pixi.js'
import { DropShadowFilter } from '@pixi/filter-drop-shadow'
import * as PIXI from 'pixi.js'
import yx_bg from '@renderer/assets/img/game/jiezi/stage/yx_bg.png'
import start_btn_png from '@renderer/assets/img/game/jiezi/stage/start_btn.png'
import gress_bg from '@renderer/assets/img/game/public/pro.png'
import text_bg from '@renderer/assets/img/game/jiezi/stage/text_bg.png'
import t_bg from '@renderer/assets/img/game/jiezi/stage/t_bg.png'
import bubble from '@renderer/assets/img/game/jiezi/stage/bubble.png'
import dui from '@renderer/assets/audio/public/dui.mp3'
import cuo from '@renderer/assets/audio/public/cuo.mp3'
import { exImgMap, keyboard, arrTurnObject, initRandomArr, moveValue } from '@renderer/common/utils/util'
Assets.load('@/renderer/assets/font/HYTuanTuanYuanYuan-U') // 加载字体样式文件
const router = useRouter()
const emits = defineEmits(['gameSubmit'])
Assets.add({ alias: 'start_btn_png', src: start_btn_png })
Assets.add({ alias: 'text_bg', src: text_bg })
const sprites = exImgMap(import.meta.glob('@renderer/assets/img/game/jiezi/stage/sprite/*.png', { eager: true }))
const spritesImg = Array.from(sprites.values()) // 小熊精灵雪碧图
const gameOver = ref(false)
const currentBearDirection = ref('right') // 当前小熊精灵朝向
const rtText = ref('')
const duiAndCuoAudio = new Audio(dui) // 击中文字提示音
const props = defineProps({
isStart: {
// 是否渲染开始按钮(刚打开游戏时需要)
type: Boolean,
default: false
},
knowledgeType: {
// 是否是组合知识点 1单一2组合
type: Number,
default: 1
},
fontType: {
type: String,
default: 'shengpang'
},
allDropTextTotalArr: {
type: Array<{ src: string, answer: boolean }>,
default: () => []
},
topic: {
// 当前题目内容及配置
type: Object,
default: () => { }
},
currentMinorIndex: {
// 当前小题下标
type: Number,
default: 0
},
currentTopicConfig: {
// 当前题目内容配置
type: Object,
default: () => { }
}
})
const canvasConfig = {
width: window.innerWidth > 1920 ? 1920 : window.innerWidth,
height: window.innerHeight, // > 1080 ? 1080 : window.innerHeight,
ratioW: window.innerWidth < 1920 ? window.innerWidth / 1920 : 1,
ratioH: window.innerHeight / 1080 // window.innerHeight < 1080 ? window.innerHeight / 1080 : 1
}
const left = keyboard("ArrowLeft")
// up = keyboard("ArrowUp"),
const right: any = keyboard("ArrowRight")
let bearSpriteContainer: Container, bearSprite: AnimatedSprite | any, progressBar: Graphics, rightContainer: Container<DisplayObject>
const gameParamsConfing = ref({
gameTotalTime: 60, // 游戏总时长
timeLine: 60, // 时间轴
textMaxNum: 5, // 屏幕中文字最多显示个数
columnCount: 5, // 屏幕分列数
columnGap: 10, // 列间隔
columnWidth: canvasConfig.width - 186 * canvasConfig.ratioW / 5, // 每列宽度
createTextNodeInterval: [100, 500, 900, 300, 200], // 执行创建节点间隔 毫秒
textBallSpeed: 2, // 文字球掉落速度 2px级
})
const basketPositionRight = ref({ // 朝右位置,默认基于小熊container定位
x: 127,
y: 83,
w: 180,
h: 114,
})
const createColIndexArr = ref<number[]>([]) // 生成列下标数组
const currentTime = ref(60) // 时间轴时间
const columnCount = ref(5) // 页面分为5列
const columnWidth = ref(canvasConfig.width - 186 * canvasConfig.ratioW / columnCount.value) // 每列宽度
const pixiDom = shallowRef<HTMLDivElement>()
let app: Application // pixi
const catchTextNum = ref(0) // 接住多少正确字累计。用于判断小熊状态
const bearStatus = ref(0) // 精灵小熊状态
// const allDropTextTotalArr = ref<{ src: string, answer: boolean }[]>([]) // 一局游戏需要掉落的文字的总数
const catchTextArr = ref<boolean[]>([]) // 已接住文字答案数组 // true or false
const createTextNodeIndex = ref(0) // 创建文字节点下标
let textImgArr: Container[] = []
let textures_right: Texture<PIXI.Resource>[] = [] // 朝右小熊精灵
let textures_left: Texture<PIXI.Resource>[] = [] // 朝左小熊精灵
三、初始化方法
const initCustomGame = async () => {
console.log(props.allDropTextTotalArr)
app = new Application({
width: canvasConfig.width,
height: canvasConfig.height,
antialias: true, // 抗锯齿
resolution: 1, // 设备像素比 // window。devicePixelRatio
backgroundAlpha: 1, // 背景透明度
resizeTo: window,
hello: true // 在控制台输出当前渲染器的版本和类型信息
})
// 设置最大小帧率为 60fps
app.ticker.minFPS = 60
app.ticker.maxFPS = 60
createColIndexArr.value = initRandomArr(5) // 设置随机生成位置
drawBearSprite() // 创建精灵
addScreenBackground() // 添加背景图
createProgressBar() // 创建时间倒计时进度条
pixiDom.value?.appendChild(app.view as any)
textImgArr = []
// columnWidth.value = (app.renderer.width - (canvasConfig.ratioW * 186)) / columnCount.value
createRightHintText() // 绘制右侧文字
// 设置游戏循环
if (!props.isStart) {
app.ticker.add(delta => gameLoop(delta))
}
}
onMounted(async () => {
await initCustomGame()
})
四、绘制背景图
const addScreenBackground = async () => {
// 绘制背景图
// const texture = await Assets.load(yx_bg)
const background = Sprite.from(yx_bg)
background.width = canvasConfig.width
background.height = canvasConfig.height
background.zIndex = 1
app.stage.addChild(background)
}
五、创建小熊动画精灵及键盘移动监听
const boardListen = () => {
left.press = () => {
if (!right.isDown) {
console.log(bearSpriteContainer.children[1])
console.log(bearSpriteContainer.children[1].getBounds())
bearSpriteContainer.children[1].x = -120 // 此时变更定位是基于已定位置
bearSprite.textures = textures_left
currentBearDirection.value = 'left'
bearSprite.x -= 1
bearSprite.play()
}
}
right.press = () => {
if (!left.isDown) {
bearSpriteContainer.children[1].x = 0 // 此时变更定位是基于已定位置
currentBearDirection.value = 'right'
bearSprite.textures = textures_right
bearSprite.x += 1
bearSprite.play()
}
}
left.release = () => {
if (!right.isDown) {
bearSprite.gotoAndStop(0) // 回到第一帧
} else {
bearSpriteContainer.children[1].x = 0 // 此时变更定位是基于已定位置
bearSprite.textures = textures_right
bearSprite.play()
}
}
right.release = () => {
if (!left.isDown) {
bearSprite.gotoAndStop(0) // 回到第一帧
} else {
bearSpriteContainer.children[1].x = -120 // 此时变更定位是基于已定位置
bearSprite.textures = textures_left
bearSprite.play()
}
}
}
const changeBearTexture = async (baseTexture) => {
// 改变小熊精灵纹理
textures_right = []
textures_left = []
const keyArr = ['x', 'y', 'w', 'h']
const rightPosition = [[119, 81, 320, 320], [481, 81, 320, 320], [843, 81, 320, 320], [1204, 81, 320, 320], [1566, 81, 320, 320], [1928, 81, 320, 320]]
const leftPosition = [[119, 422, 320, 320], [481, 422, 320, 320], [843, 422, 320, 320], [1204, 422, 320, 320], [1566, 422, 320, 320], [1928, 422, 320, 320]]
rightPosition.forEach((v) => {
const { x, y, w, h } = arrTurnObject(v, keyArr)
textures_right.push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(x, y, w, h)))
})
leftPosition.forEach((v) => {
const { x, y, w, h } = arrTurnObject(v, keyArr)
textures_left.push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(x, y, w, h)))
})
}
const drawBearSprite = async () => {
// 创建小熊精灵
await changeBearTexture(loadTexture(spritesImg[bearStatus.value]))
bearSpriteContainer = new Container()
app.stage.addChild(bearSpriteContainer)
const bear = new PIXI.AnimatedSprite(textures_right)
bearSpriteContainer.addChild(bear)
bearSprite = bearSpriteContainer.children[0] // 容器的children顺序默认按添加顺序排序。如需更改使用setChildIndex api修改
// bearSprite.anchor.set(0.5, 0.5) // 锚点位置。值范围为 0~1,表示相对图片宽高位置的百分比,比如设置为 (0.5, 0.5) 就是取宽高一半的位置作为旋转中心,也就是图片的中点
bearSprite.zIndex = 9
bearSprite.animationSpeed = 0.2 // 动画速度
// bearSprite.gotoAndPlay(0) // 从第几帧开始播放
// 设置位置
bearSprite.height = 320 * canvasConfig.ratioH
bearSprite.width = 320 * canvasConfig.ratioW
bearSpriteContainer.x = canvasConfig.width / 2 - bearSprite.width / 2
bearSpriteContainer.y = canvasConfig.height - bearSprite.height - (100 * canvasConfig.ratioH)
const border = new Graphics();
border.lineStyle(1, 0x000000, 0.001); // 设置边框样式,不能设置为完全透明,否则将无法获取Graphics的xy
border.beginFill(0xffffff, 0); // 透明填充
const { x, y, w, h } = basketPositionRight.value
border.drawRect(x, y, w * canvasConfig.ratioW, h * canvasConfig.ratioH) // 绘制一个矩形,大小与精灵相同
bearSpriteContainer.addChild(border)
boardListen() // 监听键盘事件
}
六、掉落文字气泡及移动更新方法
const createTextNode = async () => {
// const index: number = parseInt(Math.random() * 5)
try {
if (!props.allDropTextTotalArr[createTextNodeIndex.value]?.src || textImgArr.length >= gameParamsConfing.value.textMaxNum) return
if (textImgArr.length < gameParamsConfing.value.textMaxNum) {
const textContainer: Container<DisplayObject> | any = new Container()
textContainer.height = 128 * canvasConfig.ratioH
textContainer.width = 128 * canvasConfig.ratioW
textContainer.ly = gameParamsConfing.value.textBallSpeed // 可考虑固定掉落速度,防止较快的与之重叠 // (1 * Math.random() + 0.8)
textContainer.zIndex = 9
const text_bgTure = PIXI.Texture.from(text_bg)
const textBg = new Sprite(text_bgTure)
const textPath = PIXI.Texture.from(props.allDropTextTotalArr[createTextNodeIndex.value]?.src) // 取文字路径
textContainer.answer = props.allDropTextTotalArr[createTextNodeIndex.value].answer
const texture = new Sprite(textPath)
texture.width = texture.height = 50
textBg.width = textBg.height = 128
texture.x = texture.y = textBg.width / 2 - texture.width / 2
textContainer.addChild(textBg)
textContainer.addChild(texture)
const m = createColIndexArr.value[createTextNodeIndex.value % 5] // Math.random()
textContainer.colIndex = m // 设置它的生成位置。当它remove,后续补入
let x = m * columnWidth.value + (columnWidth.value / 2 - textContainer.width / 2) // 128文字容器宽度 186右侧文字描述宽度
textContainer.x = x
textContainer.y = -textContainer.height
createTextNodeIndex.value += 1
// 应用阴影滤镜
const dropShadowFilter = new DropShadowFilter({
distance: 10, // 阴影偏移距离
rotation: 75,// 阴影角度
color: 0x74c6d8, // 阴影颜色
alpha: 0.75, // 阴影透明度
// blur: 4.0, // 阴影模糊度
// quality: 1, // 阴影质量
resolution: app.renderer.resolution, // 分辨率
shadowOnly: false, // 是否只显示阴影
})
textContainer.filters = [dropShadowFilter] // 为容器添加阴影
// const border = new Graphics();
// border.lineStyle(3, 0x0000FF); // 设置边框样式,这里设置为蓝色,线条宽度为 10
// // border.alpha = 0.5; // 设置边框透明度
// border.drawRect(0, 0, textContainer.width, textContainer.height); // 绘制一个矩形,大小与精灵相同
// textContainer.addChild(border)
textImgArr.push(textContainer)
app.stage.addChild(textContainer)
}
} catch (err) { console.log(err) }
}
const updateTextPosition = (sprite, ind: number) => {
//@ts-ignore
sprite.y += sprite.ly // 随机下落速度
// const rect1 = bearSprite?.getBounds() // bearSprite?.getBounds()
const rect1 = bearSpriteContainer.children[1].getBounds()
const rect2 = sprite?.getBounds() // sprite?.getBounds()
if (rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.height + rect1.y > rect2.y) {
catchTextArr.value.push(sprite.answer)
if (sprite.answer) { // 接住了正确就累计加一
catchTextNum.value += 1
duiAndCuoAudio.src = dui
duiAndCuoAudio.load()
duiAndCuoAudio.play().catch(err => {
console.log(err)
})
createCatchTextNum() // 创建文字累加效果
} else {
duiAndCuoAudio.src = cuo
duiAndCuoAudio.load()
duiAndCuoAudio.play().catch(err => {
console.log(err)
})
}
// 爆炸效果
createTextBoom(sprite)
app.stage.removeChild(sprite)
textImgArr.splice(ind, 1)
if (catchTextNum.value % 5 === 0 && bearStatus.value < 3) { // 累计改变小熊纹理
bearStatus.value += 1
changeBearTexture(loadTexture(spritesImg[bearStatus.value]))
}
updateProgressBar() // 更新进度条
}
if (sprite.y > app.renderer.height - (bearSpriteContainer.height / 2)) {
app.stage.removeChild(sprite)
textImgArr.splice(ind, 1)
updateProgressBar() // 更新进度条
}
}
七、游戏循环gameLoop方法
let lastTime = 0
const gameLoop = async (ticker) => {
const now = Date.now()
if (now - lastTime >= gameParamsConfing.value.createTextNodeInterval[createTextNodeIndex.value % 5]) { // 控制生成时间间隔
// 执行游戏逻辑
console.log('gameloop:' + createTextNodeIndex.value)
await createTextNode()
if (currentTime.value <= 0 && !textImgArr.length) { // 时间结束 且 文字全部掉完
gameOver.value = true
app.ticker.stop() // 停止循环
const correctRatio = (catchTextArr.value.filter(Boolean).length / props.currentTopicConfig.ratioAccuracy) * 100 // 整数百分比
const mistakeRatio = (catchTextArr.value.filter(i => i === false).length / props.currentTopicConfig.ratioDisturbnum) * 100
if (correctRatio >= props.currentTopicConfig.trueMin && mistakeRatio <= props.currentTopicConfig.falseMax) {
emits('gameSubmit', true)
} else {
emits('gameSubmit', false)
}
}
lastTime = now
}
for (let i = 0; i < textImgArr.length; i++) {
updateTextPosition(textImgArr[i], i)
}
// 移动限制,防止精灵跑出画布
if (left.isDown) {
bearSpriteContainer.x <= 0 ? bearSpriteContainer.x = 0 : bearSpriteContainer.x -= 12
}
if (right.isDown) {
bearSpriteContainer.x + bearSpriteContainer.width >= (canvasConfig.width - (canvasConfig.ratioW) * 186) ? bearSpriteContainer.x = (canvasConfig.width - (canvasConfig.ratioW) * 186) - bearSpriteContainer.width : bearSpriteContainer.x += 12
}
}
八、创建进度条以及更新进度条方法
const createProgressBar = async () => {
const gressBg = Sprite.from(gress_bg)
gressBg.width = 450 * canvasConfig.ratioW
gressBg.height = 35 * canvasConfig.ratioH
gressBg.anchor.set(0.5, 0.5)
gressBg.zIndex = 1
gressBg.x = canvasConfig.width / 2 // - 225
gressBg.y = canvasConfig.height - (65 * canvasConfig.ratioH)
app.stage.addChild(gressBg)
progressBar = new Graphics()
progressBar.beginFill('#f4981bFF')
progressBar.drawRoundedRect(0, 0, 435 * canvasConfig.ratioW, 31 ** canvasConfig.ratioH, 10)
progressBar.endFill()
progressBar.x = canvasConfig.width / 2 - (218 * canvasConfig.ratioW)
progressBar.y = canvasConfig.height - (75 * canvasConfig.ratioH)
app.stage.addChild(progressBar)
}
const updateProgressBar = () => {
currentTime.value -= 1
progressBar.scale.x = progressBar.scale.x ? (gameParamsConfing.value.gameTotalTime - createTextNodeIndex.value) / gameParamsConfing.value.gameTotalTime : 0
}
九、创建碰撞数字累计动画
const createCatchTextNum = async () => {
// 创建累计数字动效
const textStyle = new TextStyle({
fontFamily: 'HYTuanTuanYuanYuan-U',
fontSize: 46,
// fontStyle: 'italic',
fontWeight: 'bold', // 加粗
fill: ['#FAD9611F', '#F76B1C'], // 填充颜色
align: 'left',
strokeThickness: 3, // 描边宽度
stroke: '#FFF',
padding: 20
})
const scoreText = new Text(`+${catchTextNum.value}`, textStyle)
// scoreText.anchor.set(0.5, 0.5)
// scoreText.rotation = 5 * (Math.PI / 180)
const bearPosition = bearSprite.getBounds()
// scoreText.x = bearPosition.x + bearPosition.width * 0.25 // currentBearDirection.value === 'right' ? bearPosition.x + bearPosition.width * 0.25 : bearPosition.x + bearPosition.width * 0.75
// scoreText.y = bearPosition.y
scoreText.alpha = 1 // 初始透明度为完全不透明
const textContainer = new Container()
textContainer.width = textContainer.height = 200
textContainer.x = currentBearDirection.value === 'right' ? bearPosition.x + bearPosition.width * 0.25 : bearPosition.x + bearPosition.width * 0.75
textContainer.y = bearPosition.y
textContainer.addChild(scoreText)
app.stage.addChild(textContainer)
// const border = new Graphics();
// border.lineStyle(3, 0x0000FF); // 设置边框样式,这里设置为蓝色,线条宽度为 10
// // border.alpha = 0.5; // 设置边框透明度
// border.drawRect(0, 0, textContainer.width, textContainer.height); // 绘制一个矩形,大小与精灵相同
// scoreText.addChild(border)
// 向上移动并渐变消失
const moveUpAndFade = () => {
scoreText.y -= 0.5 // 向上移动
scoreText.alpha -= 0.02 // 透明度减少
if (scoreText.alpha <= 0) {
scoreText.alpha = 0
app.stage.removeChild(scoreText)
}
}
app.ticker.add(moveUpAndFade)
}
十、创建爆炸动画
const createTextBoom = async (sprite) => { // 创建爆炸气泡
const baseTexture = PIXI.BaseTexture.from(bubble)
const bubbleTexture: any = []
bubbleTexture.push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(0, 0, 200, 200)))
bubbleTexture.push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(216, 0, 200, 200)))
bubbleTexture.push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(432, 0, 200, 200)))
const bubbleSprite = new AnimatedSprite(bubbleTexture)
bubbleSprite.anchor.set(0.5)
// const bearPosition = bearSprite.getBounds()
// bubbleSprite.x = currentBearDirection.value === 'right' ? bearPosition.x + bearPosition.width * 0.7 : bearPosition.x + bearPosition.width / 3
bubbleSprite.x = sprite.x + sprite.width / 2
bubbleSprite.y = sprite.y + sprite.height / 2
bubbleSprite.animationSpeed = 0.1 // 动画速度
bubbleSprite.loop = false // 设置循环为 false
app.stage.addChild(bubbleSprite)
bubbleSprite.play()// 播放动画
const clearBoom = setTimeout(() => {
app.stage.removeChild(bubbleSprite)
clearTimeout(clearBoom)
}, 500)
}
十一、创建绘制右侧文字内容
const createRightHintText = () => {
// 创建右侧提示文字内容
rtText.value = props.topic?.description.slice(0, 16) || '本关要接住的文字为'
rightContainer = new Container()
const textStyle = new TextStyle({
fontFamily: 'SourceHanSerifCN-Medium',
fontSize: 32,
fill: '#AC9558FF',
wordWrap: true,
wordWrapWidth: 40,
align: 'center'
})
let dispose_text = ''
for (let i = 0; i < rtText.value.length; i++) {
dispose_text += rtText.value[i] + '\n'
}
const text = new Text(dispose_text, textStyle)
rightContainer.addChild(text)
const tb = new Sprite(Texture.from(t_bg))
tb.y = text.getBounds().height
tb.anchor.set(0.5)
tb.scale.x = tb.scale.y = 0.6
tb.x = 15
rightContainer.addChild(tb)
const tishiImg = +props.currentTopicConfig.rightAnswers1 === 1 ? props.currentTopicConfig.tishi[0] : props.currentTopicConfig.tishi[1]
const wz = new Sprite(Texture.from(tishiImg))
wz.y = text.getBounds().height
wz.anchor.set(0.5)
wz.scale.x = wz.scale.y = +props.currentTopicConfig.rightAnswers1 === 1 ? 0.5 : 0.6
wz.x = 15
rightContainer.addChild(wz)
// tb.y = text.getBounds().height
// tb.anchor.set(0.5)
// tb.scale.x = tb.scale.y = 0.6
// tb.x = 15
rightContainer.addChild(tb)
rightContainer.x = canvasConfig.width - ((canvasConfig.width / 1920) * 186) / 2 - 10
rightContainer.y = (canvasConfig.height - text.getBounds().height) / 2
app.stage.addChild(rightContainer)
}
十二、游戏开始方法
const startViwe = () => {
clearResetGameData() // 开始前先重置数据
start_btn.on('pointertap', () => { // pointertap集成点击和触摸事件
app.stage.removeChild(start_btn)
app.ticker.add(delta => gameLoop(delta))
app.ticker.start() // 开始循环
})
}