提交 0d709a43 authored 作者: lidongxu's avatar lidongxu

特效很棒还差点

上级 8eb3a4a9
...@@ -9,7 +9,7 @@ import { drawBubbleGrid } from './renderer/bubbleGrid.js' ...@@ -9,7 +9,7 @@ import { drawBubbleGrid } from './renderer/bubbleGrid.js'
import { drawBubble3D, BUBBLE_RADIUS } from './renderer/bubble.js' import { drawBubble3D, BUBBLE_RADIUS } from './renderer/bubble.js'
import { drawShooter } from './renderer/shooter.js' import { drawShooter } from './renderer/shooter.js'
import { drawGameInfo, drawTeamResultOverlay } from './renderer/gameinfo.js' import { drawGameInfo, drawTeamResultOverlay } from './renderer/gameinfo.js'
import { updateAndDrawExplosions, appendExplosionsFromState, Explosion } from './renderer/explosion.js' import { updateAndDrawExplosions, appendExplosionsFromState, Explosion, setExplosionQuality } from './renderer/explosion.js'
import { drawIdleScreen } from './renderer/idleScreen.js' import { drawIdleScreen } from './renderer/idleScreen.js'
import { SCREEN_WIDTH, SCREEN_HEIGHT } from './constants.js' import { SCREEN_WIDTH, SCREEN_HEIGHT } from './constants.js'
...@@ -195,10 +195,11 @@ function loop() { ...@@ -195,10 +195,11 @@ function loop() {
const teamBStates = states.filter(s => getPlayerTeam(s.playerId ?? 1) === 'B') const teamBStates = states.filter(s => getPlayerTeam(s.playerId ?? 1) === 'B')
const totalSlots = Math.max(teamAStates.length + teamBStates.length, 1) const totalSlots = Math.max(teamAStates.length + teamBStates.length, 1)
// 人数变化时重新计算缩放 // 人数变化时重新计算缩放和爆炸质量
if (totalSlots !== _lastPlayerCount) { if (totalSlots !== _lastPlayerCount) {
_lastPlayerCount = totalSlots _lastPlayerCount = totalSlots
applyScaler(totalSlots) applyScaler(totalSlots)
setExplosionQuality(totalSlots) // 根据人数调整爆炸效果质量
// 清理消失玩家的爆炸列表 // 清理消失玩家的爆炸列表
for (const pid of playerExplosions.keys()) { for (const pid of playerExplosions.keys()) {
if (!states.find(s => (s.playerId ?? 1) === pid)) { if (!states.find(s => (s.playerId ?? 1) === pid)) {
......
/** /**
* 爆炸粒子特效 —— 与 minigame-1/js/effects/explosion.js 1:1 复刻 * 爆炸粒子特效 —— 与 minigame-1/js/effects/explosion.js 1:1 复刻
* 去除 wx 依赖,使用浏览器 Canvas 2D API * 去除 wx 依赖,使用浏览器 Canvas 2D API
* 优化:批量渲染 + 动态质量调整,支持4人同屏
*/ */
import { BUBBLE_RADIUS, BUBBLE_COLORS } from './bubble.js' import { BUBBLE_RADIUS, BUBBLE_COLORS } from './bubble.js'
// ─── 全局配置:根据同屏玩家数动态调整质量 ─────────────────────────────────────
const QUALITY_CONFIG = {
// 单人/双人 - 高质量
low: {
circleCount: 12,
glintCount: 5,
sparkCount: 5,
ringCount: 2,
maxLifeBase: 25
},
// 三人 - 中等质量
medium: {
circleCount: 8,
glintCount: 3,
sparkCount: 3,
ringCount: 2,
maxLifeBase: 20
},
// 四人 - 性能优先
high: {
circleCount: 6,
glintCount: 2,
sparkCount: 2,
ringCount: 1,
maxLifeBase: 18
}
}
let currentQuality = 'medium'
/**
* 设置爆炸质量等级(根据同屏玩家数调用)
* @param {number} playerCount 当前游戏人数 1-4
*/
export function setExplosionQuality(playerCount) {
if (playerCount <= 2) currentQuality = 'low'
else if (playerCount === 3) currentQuality = 'medium'
else currentQuality = 'high'
}
// ─── 圆形粒子 ───────────────────────────────────────────────────────────────── // ─── 圆形粒子 ─────────────────────────────────────────────────────────────────
class CircleParticle { class CircleParticle {
...@@ -22,7 +63,7 @@ class CircleParticle { ...@@ -22,7 +63,7 @@ class CircleParticle {
update() { update() {
this.vx *= 0.92 this.vx *= 0.92
this.vy = this.vy * 0.92 + 0.2 // 摩擦 + 重力 this.vy = this.vy * 0.92 + 0.2
this.x += this.vx this.x += this.vx
this.y += this.vy this.y += this.vy
this.life++ this.life++
...@@ -30,17 +71,6 @@ class CircleParticle { ...@@ -30,17 +71,6 @@ class CircleParticle {
this.radius = Math.max(0.5, this.radius * 0.97) this.radius = Math.max(0.5, this.radius * 0.97)
if (this.life >= this.maxLife) this.alive = false if (this.life >= this.maxLife) this.alive = false
} }
render(ctx) {
if (!this.alive || this.alpha <= 0) return
ctx.save()
ctx.globalAlpha = this.alpha
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
ctx.fillStyle = this.color
ctx.fill()
ctx.restore()
}
} }
// ─── 火花线条粒子 ───────────────────────────────────────────────────────────── // ─── 火花线条粒子 ─────────────────────────────────────────────────────────────
...@@ -58,6 +88,7 @@ class SparkParticle { ...@@ -58,6 +88,7 @@ class SparkParticle {
this.life = 0 this.life = 0
this.maxLife = 18 + Math.floor(Math.random() * 14) this.maxLife = 18 + Math.floor(Math.random() * 14)
this.alive = true this.alive = true
this.isSpark = true
} }
update() { update() {
...@@ -71,20 +102,6 @@ class SparkParticle { ...@@ -71,20 +102,6 @@ class SparkParticle {
this.alpha = Math.max(0, 1 - this.life / this.maxLife) this.alpha = Math.max(0, 1 - this.life / this.maxLife)
if (this.life >= this.maxLife) this.alive = false if (this.life >= this.maxLife) this.alive = false
} }
render(ctx) {
if (!this.alive || this.alpha <= 0) return
ctx.save()
ctx.globalAlpha = this.alpha
ctx.strokeStyle = this.color
ctx.lineWidth = 1.8
ctx.lineCap = 'round'
ctx.beginPath()
ctx.moveTo(this.x, this.y)
ctx.lineTo(this.prevX, this.prevY)
ctx.stroke()
ctx.restore()
}
} }
// ─── 冲击波圆环 ─────────────────────────────────────────────────────────────── // ─── 冲击波圆环 ───────────────────────────────────────────────────────────────
...@@ -96,32 +113,20 @@ class ShockRing { ...@@ -96,32 +113,20 @@ class ShockRing {
this.color = colorHex this.color = colorHex
this.r = BUBBLE_RADIUS * 0.2 this.r = BUBBLE_RADIUS * 0.2
this.maxR = BUBBLE_RADIUS * 2.4 this.maxR = BUBBLE_RADIUS * 2.4
this.life = -delay // 负值表示延迟未开始 this.life = -delay
this.maxLife = 14 this.maxLife = 24
this.alpha = 0 this.alpha = 0
this.alive = true this.alive = true
} }
update() { update() {
this.life++ this.life++
if (this.life <= 0) return // 等待延迟 if (this.life <= 0) return
const t = this.life / this.maxLife const t = this.life / this.maxLife
this.r = BUBBLE_RADIUS * 0.2 + (this.maxR - BUBBLE_RADIUS * 0.2) * t this.r = BUBBLE_RADIUS * 0.2 + (this.maxR - BUBBLE_RADIUS * 0.2) * t
this.alpha = 0.9 * (1 - t) this.alpha = 0.9 * (1 - t)
if (this.life >= this.maxLife) this.alive = false if (this.life >= this.maxLife) this.alive = false
} }
render(ctx) {
if (!this.alive || this.alpha <= 0 || this.life <= 0) return
ctx.save()
ctx.globalAlpha = this.alpha
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2)
ctx.strokeStyle = this.color
ctx.lineWidth = 2.5
ctx.stroke()
ctx.restore()
}
} }
// ─── 单颗泡泡爆炸 ───────────────────────────────────────────────────────────── // ─── 单颗泡泡爆炸 ─────────────────────────────────────────────────────────────
...@@ -138,37 +143,64 @@ export class Explosion { ...@@ -138,37 +143,64 @@ export class Explosion {
this.particles = [] this.particles = []
this.rings = [] this.rings = []
// 支持直接传 hex 字符串(大屏从 state.explosions[].colorHex 拿到的)
const colorHex = typeof color === 'string' && color.startsWith('#') const colorHex = typeof color === 'string' && color.startsWith('#')
? color ? color
: (BUBBLE_COLORS[color] || '#ffffff') : (BUBBLE_COLORS[color] || '#ffffff')
const R = BUBBLE_RADIUS const R = BUBBLE_RADIUS
const cfg = QUALITY_CONFIG[currentQuality]
// 闪光帧数(缩短,一闪而过) this.flashLife = isFloating ? 4 : 10
this.flashLife = isFloating ? 2 : 4
this._x = x this._x = x
this._y = y this._y = y
this._colorHex = colorHex this._colorHex = colorHex
// ── 圆形粒子(大幅减少数量,缩短寿命) // ── 圆形粒子
const circleCount = isFloating ? 3 : 5 const circleCount = isFloating ? Math.floor(cfg.circleCount * 0.6) : cfg.circleCount
for (let i = 0; i < circleCount; i++) { for (let i = 0; i < circleCount; i++) {
const angle = (i / circleCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.8 const angle = (i / circleCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.8
const speed = isFloating const speed = isFloating
? 1.5 + Math.random() * 2 ? (1.5 + Math.random() * 3)
: 2.5 + Math.random() * 3.5 : (2.5 + Math.random() * 5)
const vx = Math.cos(angle) * speed const vx = Math.cos(angle) * speed
const vy = isFloating const vy = isFloating
? Math.sin(angle) * speed * 0.4 + 1.5 ? (Math.sin(angle) * speed * 0.4 + 1.5)
: Math.sin(angle) * speed : Math.sin(angle) * speed
const radius = R * (isFloating ? 0.15 : 0.14 + Math.random() * 0.18) const radius = R * (isFloating ? 0.2 : 0.18 + Math.random() * 0.28)
const maxLife = 10 + Math.floor(Math.random() * 8) // 10~18帧,约0.17~0.3s const maxLife = cfg.maxLifeBase + Math.floor(Math.random() * 20)
this.particles.push(new CircleParticle(x, y, colorHex, vx, vy, radius, maxLife)) this.particles.push(new CircleParticle(x, y, colorHex, vx, vy, radius, maxLife))
} }
// ── 冲击波圆环(主消除才有,只保留1圈) // ── 白色 & 黄色小圆粒子(高光碎片)
const glintCount = isFloating ? Math.floor(cfg.glintCount * 0.4) : cfg.glintCount
for (let i = 0; i < glintCount; i++) {
const angle = Math.random() * Math.PI * 2
const speed = 3 + Math.random() * 4
const glintColor = Math.random() < 0.5 ? '#ffffff' : '#ffe566'
this.particles.push(
new CircleParticle(x, y, glintColor,
Math.cos(angle) * speed, Math.sin(angle) * speed,
R * 0.12, 18 + Math.floor(Math.random() * 12))
)
}
// ── 火花线条粒子
const sparkCount = isFloating ? Math.floor(cfg.sparkCount * 0.4) : cfg.sparkCount
for (let i = 0; i < sparkCount; i++) {
const angle = Math.random() * Math.PI * 2
const speed = 5 + Math.random() * 7
const sparkColor = i % 2 === 0 ? '#ffffff' : colorHex
this.particles.push(
new SparkParticle(x, y, sparkColor,
Math.cos(angle) * speed, Math.sin(angle) * speed)
)
}
// ── 冲击波圆环
if (!isFloating) { if (!isFloating) {
this.rings.push(new ShockRing(x, y, colorHex, 0)) this.rings.push(new ShockRing(x, y, colorHex, 0))
if (cfg.ringCount >= 2) {
this.rings.push(new ShockRing(x, y, '#ffffff', 5))
}
} }
} }
...@@ -192,30 +224,57 @@ export class Explosion { ...@@ -192,30 +224,57 @@ export class Explosion {
} }
render(ctx) { render(ctx) {
// 闪光光晕(径向渐变 // ── 闪光光晕(简单实心圆,性能更好
if (this.flashLife > 0) { if (this.flashLife > 0) {
const t = this.flashLife / 10 const t = this.flashLife / 10
ctx.save() ctx.save()
ctx.globalAlpha = t * 0.8 ctx.globalAlpha = t * 0.6
const grad = ctx.createRadialGradient( ctx.fillStyle = '#ffffff'
this._x, this._y, 0,
this._x, this._y, BUBBLE_RADIUS * 1.6
)
grad.addColorStop(0, '#ffffff')
grad.addColorStop(0.4, this._colorHex)
grad.addColorStop(1, 'rgba(0,0,0,0)')
ctx.beginPath() ctx.beginPath()
ctx.arc(this._x, this._y, BUBBLE_RADIUS * 1.6, 0, Math.PI * 2) ctx.arc(this._x, this._y, BUBBLE_RADIUS * 0.8, 0, Math.PI * 2)
ctx.fillStyle = grad ctx.fill()
ctx.globalAlpha = t * 0.4
ctx.fillStyle = this._colorHex
ctx.beginPath()
ctx.arc(this._x, this._y, BUBBLE_RADIUS * 1.4, 0, Math.PI * 2)
ctx.fill() ctx.fill()
ctx.restore() ctx.restore()
} }
// 冲击波圆环 // ── 冲击波圆环(批量渲染)
for (const r of this.rings) r.render(ctx) ctx.save()
ctx.lineWidth = 2.5
for (const r of this.rings) {
if (!r.alive || r.alpha <= 0 || r.life <= 0) continue
ctx.globalAlpha = r.alpha
ctx.strokeStyle = r.color
ctx.beginPath()
ctx.arc(r.x, r.y, r.r, 0, Math.PI * 2)
ctx.stroke()
}
ctx.restore()
// 粒子 // ── 粒子批量渲染(圆形粒子 + 火花线条)
for (const p of this.particles) p.render(ctx) ctx.save()
for (const p of this.particles) {
if (!p.alive || p.alpha <= 0) continue
ctx.globalAlpha = p.alpha
if (p.isSpark) {
ctx.strokeStyle = p.color
ctx.lineWidth = 1.8
ctx.lineCap = 'round'
ctx.beginPath()
ctx.moveTo(p.x, p.y)
ctx.lineTo(p.prevX, p.prevY)
ctx.stroke()
} else {
ctx.fillStyle = p.color
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2)
ctx.fill()
}
}
ctx.restore()
} }
} }
...@@ -234,16 +293,25 @@ export function updateAndDrawExplosions(ctx, explosionList) { ...@@ -234,16 +293,25 @@ export function updateAndDrawExplosions(ctx, explosionList) {
} }
} }
/** 同屏最大爆炸实例数,超出丢弃避免卡顿 */ /**
const MAX_EXPLOSIONS = 12 * 单玩家最大爆炸实例数,超出丢弃避免卡顿
* 按玩家独立计算,避免4人同屏时互相抢占额度
* 考虑到一次消除可能有3-10个泡泡,悬空掉落可能更多,设置足够大的值
*/
const MAX_EXPLOSIONS_PER_PLAYER = 50
/** /**
* 根据状态中的新爆炸事件追加 Explosion 实例 * 根据状态中的新爆炸事件追加 Explosion 实例
* 确保每个消除的泡泡都有爆炸效果
*/ */
export function appendExplosionsFromState(explosionList, newExplosions) { export function appendExplosionsFromState(explosionList, newExplosions) {
if (!newExplosions || !newExplosions.length) return if (!newExplosions || !newExplosions.length) return
for (const { x, y, colorHex, color } of newExplosions) {
if (explosionList.length >= MAX_EXPLOSIONS) break // 如果新爆炸数量超过限制,优先保留前面的(确保每个泡泡都有爆炸)
const availableSlots = Math.max(0, MAX_EXPLOSIONS_PER_PLAYER - explosionList.length)
const explosionsToAdd = newExplosions.slice(0, availableSlots)
for (const { x, y, colorHex, color } of explosionsToAdd) {
// 优先用 colorHex(小游戏序列化传来的十六进制),回退到颜色索引 // 优先用 colorHex(小游戏序列化传来的十六进制),回退到颜色索引
const c = colorHex || color || 1 const c = colorHex || color || 1
explosionList.push(new Explosion(x, y, c, false)) explosionList.push(new Explosion(x, y, c, false))
......
minigame-1 @ 8ab3c7c4
Subproject commit d88c2a7b167e135b0e3a33f7f72a42d5d7eb9b5b Subproject commit 8ab3c7c4b7f885eea0ef9c4d9f51f3fa08188665
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论