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

特效很棒还差点

上级 8eb3a4a9
......@@ -9,7 +9,7 @@ import { drawBubbleGrid } from './renderer/bubbleGrid.js'
import { drawBubble3D, BUBBLE_RADIUS } from './renderer/bubble.js'
import { drawShooter } from './renderer/shooter.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 { SCREEN_WIDTH, SCREEN_HEIGHT } from './constants.js'
......@@ -195,10 +195,11 @@ function loop() {
const teamBStates = states.filter(s => getPlayerTeam(s.playerId ?? 1) === 'B')
const totalSlots = Math.max(teamAStates.length + teamBStates.length, 1)
// 人数变化时重新计算缩放
// 人数变化时重新计算缩放和爆炸质量
if (totalSlots !== _lastPlayerCount) {
_lastPlayerCount = totalSlots
applyScaler(totalSlots)
setExplosionQuality(totalSlots) // 根据人数调整爆炸效果质量
// 清理消失玩家的爆炸列表
for (const pid of playerExplosions.keys()) {
if (!states.find(s => (s.playerId ?? 1) === pid)) {
......
/**
* 爆炸粒子特效 —— 与 minigame-1/js/effects/explosion.js 1:1 复刻
* 去除 wx 依赖,使用浏览器 Canvas 2D API
* 优化:批量渲染 + 动态质量调整,支持4人同屏
*/
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 {
......@@ -22,7 +63,7 @@ class CircleParticle {
update() {
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.y += this.vy
this.life++
......@@ -30,17 +71,6 @@ class CircleParticle {
this.radius = Math.max(0.5, this.radius * 0.97)
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 {
this.life = 0
this.maxLife = 18 + Math.floor(Math.random() * 14)
this.alive = true
this.isSpark = true
}
update() {
......@@ -71,20 +102,6 @@ class SparkParticle {
this.alpha = Math.max(0, 1 - this.life / this.maxLife)
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 {
this.color = colorHex
this.r = BUBBLE_RADIUS * 0.2
this.maxR = BUBBLE_RADIUS * 2.4
this.life = -delay // 负值表示延迟未开始
this.maxLife = 14
this.life = -delay
this.maxLife = 24
this.alpha = 0
this.alive = true
}
update() {
this.life++
if (this.life <= 0) return // 等待延迟
if (this.life <= 0) return
const t = this.life / this.maxLife
this.r = BUBBLE_RADIUS * 0.2 + (this.maxR - BUBBLE_RADIUS * 0.2) * t
this.alpha = 0.9 * (1 - t)
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 {
this.particles = []
this.rings = []
// 支持直接传 hex 字符串(大屏从 state.explosions[].colorHex 拿到的)
const colorHex = typeof color === 'string' && color.startsWith('#')
? color
: (BUBBLE_COLORS[color] || '#ffffff')
const R = BUBBLE_RADIUS
const cfg = QUALITY_CONFIG[currentQuality]
// 闪光帧数(缩短,一闪而过)
this.flashLife = isFloating ? 2 : 4
this.flashLife = isFloating ? 4 : 10
this._x = x
this._y = y
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++) {
const angle = (i / circleCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.8
const speed = isFloating
? 1.5 + Math.random() * 2
: 2.5 + Math.random() * 3.5
? (1.5 + Math.random() * 3)
: (2.5 + Math.random() * 5)
const vx = Math.cos(angle) * speed
const vy = isFloating
? Math.sin(angle) * speed * 0.4 + 1.5
? (Math.sin(angle) * speed * 0.4 + 1.5)
: Math.sin(angle) * speed
const radius = R * (isFloating ? 0.15 : 0.14 + Math.random() * 0.18)
const maxLife = 10 + Math.floor(Math.random() * 8) // 10~18帧,约0.17~0.3s
const radius = R * (isFloating ? 0.2 : 0.18 + Math.random() * 0.28)
const maxLife = cfg.maxLifeBase + Math.floor(Math.random() * 20)
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) {
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 {
}
render(ctx) {
// 闪光光晕(径向渐变
// ── 闪光光晕(简单实心圆,性能更好
if (this.flashLife > 0) {
const t = this.flashLife / 10
ctx.save()
ctx.globalAlpha = t * 0.8
const grad = ctx.createRadialGradient(
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.globalAlpha = t * 0.6
ctx.fillStyle = '#ffffff'
ctx.beginPath()
ctx.arc(this._x, this._y, BUBBLE_RADIUS * 1.6, 0, Math.PI * 2)
ctx.fillStyle = grad
ctx.arc(this._x, this._y, BUBBLE_RADIUS * 0.8, 0, Math.PI * 2)
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.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) {
}
}
/** 同屏最大爆炸实例数,超出丢弃避免卡顿 */
const MAX_EXPLOSIONS = 12
/**
* 单玩家最大爆炸实例数,超出丢弃避免卡顿
* 按玩家独立计算,避免4人同屏时互相抢占额度
* 考虑到一次消除可能有3-10个泡泡,悬空掉落可能更多,设置足够大的值
*/
const MAX_EXPLOSIONS_PER_PLAYER = 50
/**
* 根据状态中的新爆炸事件追加 Explosion 实例
* 确保每个消除的泡泡都有爆炸效果
*/
export function appendExplosionsFromState(explosionList, newExplosions) {
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(小游戏序列化传来的十六进制),回退到颜色索引
const c = colorHex || color || 1
explosionList.push(new Explosion(x, y, c, false))
......
minigame-1 @ 8ab3c7c4
Subproject commit d88c2a7b167e135b0e3a33f7f72a42d5d7eb9b5b
Subproject commit 8ab3c7c4b7f885eea0ef9c4d9f51f3fa08188665
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论