消消方块阵换皮表情
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
6.3 KiB

1 week ago
import {
_decorator,
Component,
EffectAsset,
Filter,
Material,
Sprite,
Texture2D,
UITransform,
Vec3,
} from 'cc';
const { ccclass, property } = _decorator;
/** smoothstep(edge0, edge1, x),与 GLSL 一致 */
function smoothstep(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
/**
* UVShader .r
* 使 Sprite CastleDamageMask.effect effectAsset
*/
@ccclass('CastleDamageSprite')
export class CastleDamageSprite extends Component {
@property({ type: EffectAsset, tooltip: '拖入 assets/gameplay/CastleSiege/CastleDamageMask' })
effectAsset: EffectAsset = null;
@property({ tooltip: '损伤图分辨率,越大边缘越细、CPU 更新越重' })
maskSize = 256;
@property({ tooltip: '单次命中半径(相对较短边,约 0.02~0.15)' })
holeRadiusUv = 0.06;
@property({ tooltip: '洞边缘软化(UV)' })
holeSoftnessUv = 0.015;
@property({ tooltip: '边缘锯齿:越大轮廓越不规则' })
jaggedness = 0.12;
@property({ tooltip: '若挖洞位置与命中点上下颠倒,可勾选' })
flipMaskV = true;
private _sprite: Sprite = null;
private _ui: UITransform = null;
private _material: Material = null;
private _maskTex: Texture2D = null;
private _pixels: Uint8Array = null;
private _dirty = false;
private _hitSeed = 0;
onLoad() {
this._sprite = this.getComponent(Sprite);
this._ui = this.node.getComponent(UITransform);
if (!this._sprite || !this._ui) {
console.warn('[CastleDamageSprite] 需要同节点上有 Sprite、UITransform');
return;
}
if (!this.effectAsset) {
console.warn('[CastleDamageSprite] 请指定 effectAsset(CastleDamageMask)');
return;
}
this._buildMaskBuffer();
this._initMaterial();
}
onDestroy() {
if (this._sprite) {
this._sprite.customMaterial = null;
}
this._maskTex?.destroy();
this._material?.destroy();
}
lateUpdate() {
if (!this._dirty || !this._maskTex || !this._pixels) {
return;
}
this._maskTex.uploadData(this._pixels);
this._maskTex.updateImage();
this._dirty = false;
}
/** 重置为完好(全白遮罩) */
resetDamage() {
if (!this._pixels) {
return;
}
this._pixels.fill(255);
this._dirty = true;
}
/**
* Canvas/UI
*/
applyDamageAtWorld(worldPos: Vec3 | Readonly<Vec3>) {
const uv = this.worldToCastleUV(worldPos);
if (!uv) {
return;
}
this.stampHoleUv(uv.u, uv.v, this.holeRadiusUv, this.holeSoftnessUv);
}
/** 直接用 UV(0~1)挖洞,便于调试 */
stampHoleUv(u: number, v: number, radiusUv?: number, softUv?: number) {
if (!this._pixels) {
return;
}
const r = radiusUv ?? this.holeRadiusUv;
const soft = softUv ?? this.holeSoftnessUv;
const w = this.maskSize;
const h = this.maskSize;
const px = Math.floor(u * (w - 1));
const py = this.flipMaskV ? Math.floor((1 - v) * (h - 1)) : Math.floor(v * (h - 1));
const rPx = Math.max(2, r * Math.min(w, h) * 0.5);
const softPx = Math.max(1, soft * Math.min(w, h) * 0.5);
const margin = rPx + softPx + 3;
const seed = this._hitSeed++;
const x0 = Math.max(0, Math.floor(px - margin));
const x1 = Math.min(w - 1, Math.ceil(px + margin));
const y0 = Math.max(0, Math.floor(py - margin));
const y1 = Math.min(h - 1, Math.ceil(py + margin));
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
const dx = x - px;
const dy = y - py;
const angle = Math.atan2(dy, dx);
const j =
1 -
this.jaggedness *
(0.5 + 0.5 * Math.sin(angle * 9 + seed * 0.37) + 0.15 * Math.sin(angle * 17));
const rEff = rPx * j;
const dist = Math.sqrt(dx * dx + dy * dy);
const m = smoothstep(rEff - softPx, rEff + softPx, dist);
const idx = (y * w + x) * 4;
const newR = Math.min(this._pixels[idx], Math.floor(255 * m));
this._pixels[idx] = newR;
this._pixels[idx + 1] = newR;
this._pixels[idx + 2] = newR;
this._pixels[idx + 3] = 255;
}
}
this._dirty = true;
}
worldToCastleUV(worldPos: Vec3 | Readonly<Vec3>): { u: number; v: number } | null {
if (!this._ui) {
return null;
}
const lp = this._ui.convertToNodeSpaceAR(worldPos as Vec3);
const w = this._ui.contentSize.width;
const h = this._ui.contentSize.height;
if (w <= 0 || h <= 0) {
return null;
}
const ax = this._ui.anchorX;
const ay = this._ui.anchorY;
const left = -w * ax;
const bottom = -h * ay;
let u = (lp.x - left) / w;
let v = (lp.y - bottom) / h;
if (u < 0 || u > 1 || v < 0 || v > 1) {
return null;
}
return { u, v };
}
private _buildMaskBuffer() {
const n = this.maskSize * this.maskSize * 4;
this._pixels = new Uint8Array(n);
this._pixels.fill(255);
this._maskTex = new Texture2D();
this._maskTex.setFilters(Filter.LINEAR, Filter.LINEAR);
this._maskTex.reset({
width: this.maskSize,
height: this.maskSize,
format: Texture2D.PixelFormat.RGBA8888,
mipmapLevel: 0,
});
this._maskTex.uploadData(this._pixels);
this._maskTex.updateImage();
}
private _initMaterial() {
const mat = new Material();
mat.initialize({ effectAsset: this.effectAsset });
mat.setProperty('damageMask', this._maskTex);
this._sprite.customMaterial = mat;
this._material = mat;
}
}