Phaser刮刮乐:监测擦除像素占比的方法及现有实现问题排查
问题描述
我想用Phaser开发刮刮乐游戏,需求是用户刮擦卡片覆盖层显示下方内容,当刮擦面积达到画布70%时自动完全显示内容。目前功能无法正常运行,尝试通过ImageData获取像素数组但返回全零数组,当前使用calculateScratchRatio函数实现进度计算,代码如下:
import BaseScene from "./BaseScene"; const SKY_IMAGE = "sky"; const SKY_IMAGE_BLACK = "skyblack"; const BOARD = "board"; const HEADER_ACT = "header_act"; const KEY_BRUSH = "brush"; const BGGAME = "bg-game"; export default class Scratch extends BaseScene { constructor(config) { super("Scratch", { ...config }); this.config = config; this.isDown = false; this.renderTexture = null; this.brush = null; this.erasedPixels = 0; this.screenCenter = [config.width / 2, config.height / 2]; } create() { super.create(); this.cover = this.make.image({ key: SKY_IMAGE_BLACK, add: false, }); this.board = this.make.image({ key: BOARD, add: false, }); this.ScratchOff(); this.add.image(...this.screenCenter, BOARD).setScale(0.7); console.log(this.board.getBounds()); const headerinfo = this.add .image(this.screenCenter[0] - 160, 130, "header_act") .setScale(0.7); let helloWorld = this.add .text(0, 0, "Hello World") .setFont("20px Arial") .setColor("#ffffff"); const container = this.add.container(headerinfo.x, headerinfo.y); container.add(helloWorld); } ScratchOff() { this.add .image(this.screenCenter[0] - 160, this.screenCenter[1], SKY_IMAGE) .setScale(0.7); this.cover.setOrigin(0, 0); const width = this.cover.width; const height = this.cover.height; console.log(width, height); const rt = this.add.renderTexture( this.screenCenter[0] - 160, this.screenCenter[1], width * 0.7, height * 0.71 ); this.isRenderTextureErased = false; this.erasureThreshold = 0.99; rt.setOrigin(0.5, 0.5); rt.draw(this.cover); rt.setInteractive(); rt.on(Phaser.Input.Events.POINTER_DOWN, this.handlePointerDown, this); rt.on(Phaser.Input.Events.POINTER_MOVE, this.handlePointerMove, this); rt.on(Phaser.Input.Events.POINTER_UP, () => (this.isDown = false)); this.brush = this.make.image({ key: KEY_BRUSH, add: false, }); this.renderTexture = rt; } handlePointerDown(pointer) { this.isDown = true; this.handlePointerMove(pointer); } handlePointerMove(pointer) { if (!this.isDown) { return; } const x = pointer.x - this.renderTexture.x + this.renderTexture.width * 0.5; const y = pointer.y - this.renderTexture.y + this.renderTexture.height * 0.5; this.renderTexture.erase(this.brush, x, y); const result = this.calculateScratchRatio(x, y); console.log("result", result); } calculateScratchRatio(x, y) { const texture = this.textures.get(SKY_IMAGE_BLACK); console.log(texture); if (!texture) { console.error(`Texture with key '${SKY_IMAGE_BLACK}' not found.`); return 0; } console.log(texture); const canvas = document.createElement("canvas"); canvas.width = texture.source[0].width; console.log("canvas.width", canvas.width); canvas.height = texture.source[0].height; const context = canvas.getContext("2d"); context.drawImage(texture.source[0].image, 0, 0); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; console.log(imageData, pixels); let erasedCount = 0; for (let i = 3; i < pixels.length; i += 4) { const alpha = pixels[i + 3]; if (alpha < 128) { erasedCount++; } } const totalPixels = canvas.width * canvas.height; const scratchRatio = (erasedCount / totalPixels) * 100; return Math.round(scratchRatio); } }
问题原因与修复
当前calculateScratchRatio函数的核心问题是:它读取的是原始纹理SKY_IMAGE_BLACK的像素数据,而不是用户刮擦后RenderTexture上的实时像素状态,所以永远无法检测到擦除操作带来的像素变化,自然返回错误结果。
修改后的calculateScratchRatio函数,应该直接从RenderTexture获取画布数据:
calculateScratchRatio() { // 获取RenderTexture对应的画布 const canvas = this.renderTexture.getCanvas(); if (!canvas) return 0; const context = canvas.getContext("2d"); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; let erasedCount = 0; const totalPixels = canvas.width * canvas.height; // 遍历像素,统计透明(alpha < 128)的像素数量 for (let i = 3; i < pixels.length; i += 4) { if (pixels[i] < 128) { // 修正之前的索引错误 erasedCount++; } } const scratchRatio = (erasedCount / totalPixels) * 100; const roundedRatio = Math.round(scratchRatio); // 达到70%时自动清除全部覆盖层 if (roundedRatio >= 70 && !this.isRenderTextureErased) { this.isRenderTextureErased = true; this.renderTexture.clear(); } return roundedRatio; }
同时,在handlePointerMove中调用时,不需要传x/y参数:
const result = this.calculateScratchRatio();
其他监测刮擦进度的方法
累计擦除面积
每次调用erase方法时,计算笔刷覆盖的近似面积(比如笔刷是圆形,按π*r²计算),累加总面积。当累加值达到RenderTexture总面积*0.7时,触发自动清除。这种方法无需遍历像素,性能大幅提升,适合大多数场景。区域采样估算
不遍历全部像素,而是在画布上随机选取多个采样点(比如200个),统计透明点的比例,以此估算整体刮擦进度。精度可通过调整采样点数量平衡,性能比全量遍历好很多。使用BitmapData(Phaser旧版本)
若使用Phaser 3.x之前的版本,可使用BitmapData替代RenderTexture,它提供了更直接的像素操作API,比如getPixelAlpha方法,可快速获取指定位置的透明度。
内容的提问来源于stack exchange,提问作者georgio bejjani

