流沙俄罗斯,模仿SandBlast做的

看论坛里有大神在做这个游戏的优化,我也试着做了一个
图片都是大部分都是imageFX画的,然后自己简单切下图
这个游戏,我用node.js做了个简单服务器,有登录,排行榜功能

预览:

沙盘的核心代码:

import { _decorator, Color, Component, ImageAsset, Size, Sprite, SpriteFrame, Texture2D, UI, UITransform, Vec2 } from 'cc';
import { debug } from 'db://assets/framework/tool/Log';
const { ccclass, property, requireComponent } = _decorator;

/*
 * @Author: garyxuan
 * @Version: V1.0
 * @Date: 2025-09-01 16:55:49
 * @Description: 像素渲染组件
 */
@ccclass('PixelSprite')
@requireComponent(Sprite)
export class PixelSprite extends Component {

    @property(Size)
    size: Size = new Size(0, 0);

    public _texture: Texture2D = null;
    public _sprite: Sprite = null;
    private _pixelData: Uint8Array = null;
    private _autoUpdate: boolean = true;
    private _isDirty: boolean = false;

    // 设置是否下一帧自动更新
    // 默认是自动更新
    // 手动更新需要调用updateAllData
    public setAutoUpdate(bAutoUpdate: boolean) {
        this._autoUpdate = bAutoUpdate;
    }

    protected onLoad(): void {
        this.reSize()
    }

    public reSize() {
        this._texture = new Texture2D();
        this._texture.reset({
            width: this.size.width,
            height: this.size.height,
            format: Texture2D.PixelFormat.RGBA8888
        });

        let spriteFrame = new SpriteFrame();
        spriteFrame.texture = this._texture;
        spriteFrame.packable = false;

        if (!this.node.getComponent(Sprite)) {
            this.node.addComponent(Sprite);
        }
        this.node.getComponent(Sprite).spriteFrame = spriteFrame;
        this._sprite = this.node.getComponent(Sprite);
        // 初始化像素数据数组
        this._pixelData = new Uint8Array(this._texture.width * this._texture.height * 4);

        // 从纹理中读取当前的像素数据
        this.readTexturePixels();
    }

    public updateData(colors: Color[][], x: number, y: number, update: boolean = false, force: boolean = false) {
        if (!this._texture || !this._pixelData) {
            return;
        }

        let h = colors.length;
        let w = colors[0].length;

        // 边界检查
        if (x + w > this._texture.width || y + h > this._texture.height) {
            debug("updateData: 坐标超出纹理范围");
            return;
        }

        // 遍历每个像素,写入到像素缓冲区
        for (let i = 0; i < h; i++) {
            for (let j = 0; j < w; j++) {
                const pixelX = j + x;
                const pixelY = i + y;
                const idx = (pixelY * this._texture.width + pixelX) * 4;
                const color = colors[i][j];

                // 检查颜色是否有效
                if (color && (force || (color.r !== undefined && color.g !== undefined && color.b !== undefined && color.a !== undefined))) {
                    this._pixelData[idx] = color.r;
                    this._pixelData[idx + 1] = color.g;
                    this._pixelData[idx + 2] = color.b;
                    this._pixelData[idx + 3] = color.a;
                }
            }
        }

        // 标记脏数据,等待刷新
        this._isDirty = true;

        // 如果需要更新,则立即更新到GPU
        if (update) {
            this._isDirty = false;
            this._texture.uploadData(this._pixelData);
        }
    }

    public updateAllData() {
        if (!this._pixelData || !this._texture) {
            return;
        }
        this._isDirty = false;
        this._texture.uploadData(this._pixelData)
    }

    protected lateUpdate(dt: number): void {
        if (this._autoUpdate && this._isDirty) {
            this.updateAllData()
            this._isDirty = false;
        }
    }

    /**
     * 从纹理中读取当前的像素数据(跨平台兼容)
     */
    private readTexturePixels() {
        if (!this._texture || !this._pixelData) {
            return;
        }

        try {
            // 跨平台检测
            if (typeof window === 'undefined' || !window.document) {
                // 原生平台:暂时初始化为白色
                this.readTexturePixelsNative();
            } else {
                // Web平台:使用canvas方法
                this.readTexturePixelsWeb();
            }
        } catch (error) {
            debug("读取纹理像素数据失败:", error);
            // 如果读取失败,初始化为白色
            this._pixelData.fill(255);
        }
    }

    /**
     * Web平台读取纹理像素数据
     */
    private readTexturePixelsWeb() {
        if (typeof document === 'undefined') {
            this._pixelData.fill(255);
            return;
        }

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        if (!ctx) {
            this._pixelData.fill(255);
            return;
        }

        canvas.width = this._texture.width;
        canvas.height = this._texture.height;

        // 尝试从ImageAsset读取像素数据
        if (this._texture.image) {
            const imageAsset = this._texture.image;

            // 检查是否是ImageAsset类型
            if (imageAsset && (imageAsset as any)._nativeData) {
                const nativeImage = (imageAsset as any)._nativeData;
                if (nativeImage && typeof nativeImage === 'object' && nativeImage.tagName === 'IMG') {
                    ctx.drawImage(nativeImage, 0, 0);
                    const data = ctx.getImageData(0, 0, this._texture.width, this._texture.height).data;

                    // 将读取到的数据复制到我们的像素数据数组中
                    for (let i = 0; i < data.length; i++) {
                        this._pixelData[i] = data[i];
                    }

                    debug("成功从ImageAsset._nativeData读取像素数据");
                    return;
                }
            }

            // 如果_native不可用,尝试其他方法
            if (imageAsset && imageAsset.data) {
                // 直接使用ImageAsset的data属性
                const imageData = imageAsset.data;
                if (imageData instanceof Uint8Array || imageData instanceof ArrayBuffer) {
                    const dataArray = imageData instanceof ArrayBuffer ? new Uint8Array(imageData) : imageData;

                    // 将数据复制到我们的像素数据数组中
                    for (let i = 0; i < Math.min(this._pixelData.length, dataArray.length); i++) {
                        this._pixelData[i] = dataArray[i];
                    }

                    debug("成功从ImageAsset.data读取像素数据");
                    return;
                }
            }
        }

        // 如果所有方法都失败,初始化为白色
        // debug("无法读取纹理像素数据,初始化为白色");
        this._pixelData.fill(255);
    }

    /**
     * 原生平台读取纹理像素数据
     */
    private readTexturePixelsNative() {
        // 原生平台暂时初始化为白色
        // 如果需要真正的像素读取,需要使用原生插件或C++扩展
        debug("原生平台暂不支持像素读取,初始化为白色");
        this._pixelData.fill(255);
    }

    /**
     * 获取当前的像素数据
     * @returns Uint8Array 当前的像素数据
     */
    public getPixelData(): Uint8Array | null {
        return this._pixelData;
    }

    /**
     * 设置单个像素颜色
     * @param x 像素x坐标
     * @param y 像素y坐标
     * @param color 颜色
     * @param update 是否立即更新
     */
    setPixel(x: number, y: number, color: Color, update: boolean = false) {
        if (!this._texture || !this._pixelData) {
            return;
        }

        if (x < 0 || x >= this._texture.width || y < 0 || y >= this._texture.height) {
            return;
        }

        const idx = (y * this._texture.width + x) * 4;
        this._pixelData[idx] = color.r;
        this._pixelData[idx + 1] = color.g;
        this._pixelData[idx + 2] = color.b;
        this._pixelData[idx + 3] = color.a;

        this._isDirty = true;

        if (update) {
            this._isDirty = false;
            this._texture.uploadData(this._pixelData);
        }
    }

    /**
     * 获取单个像素颜色
     * @param x 像素x坐标
     * @param y 像素y坐标
     * @returns 颜色对象
     */
    getPixel(x: number, y: number): Color | null {
        if (!this._texture || !this._pixelData) {
            return null;
        }

        if (x < 0 || x >= this._texture.width || y < 0 || y >= this._texture.height) {
            return null;
        }

        const idx = (y * this._texture.width + x) * 4;
        return new Color(
            this._pixelData[idx],
            this._pixelData[idx + 1],
            this._pixelData[idx + 2],
            this._pixelData[idx + 3]
        );
    }

    /**
     * 清空纹理(设置为透明)
     * @param update 是否立即更新
     */
    clear(update: boolean = false) {
        if (!this._pixelData) {
            return;
        }

        this._pixelData.fill(0);
        this._isDirty = true;

        if (update) {
            this._isDirty = false;
            this._texture.uploadData(this._pixelData);
        }
    }

    /**
     * 绘制像素网格,支持放大显示
     * @param blockSize 每个小方块的像素大小
     * @param borderColor 分界线颜色
     * @param blockColors 每个小方块的颜色数组
     * @param pixelScale 像素放大倍数(例如:10x10图片放大100倍变成1000x1000)
     * @param update 是否立即更新
     */
    drawPixelGrid(blockSize: number = 1, borderColor: Color = new Color(0, 0, 0, 255), blockColors: Color[][] = null, update: boolean = true) {
        if (!this._texture || !this._pixelData) {
            return;
        }
        // 清空纹理
        this._pixelData.fill(0);

        const originalWidth = this._texture.width;
        const originalHeight = this._texture.height;


        // 计算网格参数
        const gridCols = Math.floor(originalWidth / blockSize);
        const gridRows = Math.floor(originalHeight / blockSize);

        // 绘制每个小方块
        for (let row = 0; row < gridRows; row++) {
            for (let col = 0; col < gridCols; col++) {
                // 计算方块在纹理中的位置
                const startX = col * blockSize;
                const startY = row * blockSize;

                // 获取方块颜色
                let blockColor: Color;
                if (blockColors && blockColors[row] && blockColors[row][col]) {
                    blockColor = blockColors[row][col];
                } else {
                    // 默认随机颜色
                    blockColor = new Color(
                        Math.random() * 255,
                        Math.random() * 255,
                        Math.random() * 255,
                        255
                    );
                }

                // 绘制方块内部(留出边框空间)
                for (let y = startY + 1; y < startY + blockSize - 1; y++) {
                    for (let x = startX + 1; x < startX + blockSize - 1; x++) {
                        if (x < originalWidth && y < originalHeight) {
                            const idx = (y * originalWidth + x) * 4;
                            this._pixelData[idx] = blockColor.r;
                            this._pixelData[idx + 1] = blockColor.g;
                            this._pixelData[idx + 2] = blockColor.b;
                            this._pixelData[idx + 3] = blockColor.a;
                        }
                    }
                }
            }
        }

        // 绘制网格线
        this.drawGridLines(blockSize, borderColor);

        // 标记脏数据
        this._isDirty = true;

        // 如果需要更新,则立即更新到GPU
        if (update) {
            this._isDirty = false;
            this._texture.uploadData(this._pixelData);
        }

    }

    /**
     * 设置指定网格方块的颜色
     * @param gridX 网格X坐标
     * @param gridY 网格Y坐标
     * @param color 颜色
     * @param blockSize 方块大小
     * @param update 是否立即更新到GPU
     */
    setGridBlockColor(gridX: number, gridY: number, color: Color, blockSize: number = 1, update: boolean = true) {
        if (!this._texture || !this._pixelData) {
            return;
        }

        const textureWidth = this._texture.width;
        const textureHeight = this._texture.height;
        const gridCols = Math.floor(textureWidth / blockSize);
        const gridRows = Math.floor(textureHeight / blockSize);

        // 边界检查
        if (gridX < 0 || gridX >= gridCols || gridY < 0 || gridY >= gridRows) {
            debug("网格坐标超出范围");
            return;
        }

        // 计算方块在纹理中的位置
        const startX = gridX * blockSize;
        const startY = gridY * blockSize;

        // 绘制方块内部(留出边框空间)
        for (let y = startY + 1; y < startY + blockSize - 1; y++) {
            for (let x = startX + 1; x < startX + blockSize - 1; x++) {
                if (x < textureWidth && y < textureHeight) {
                    const idx = (y * textureWidth + x) * 4;
                    this._pixelData[idx] = color.r;
                    this._pixelData[idx + 1] = color.g;
                    this._pixelData[idx + 2] = color.b;
                    this._pixelData[idx + 3] = color.a;
                }
            }
        }

        // 标记脏数据
        this._isDirty = true;

        // 如果需要更新,则立即更新到GPU
        if (update) {
            this._isDirty = false;
            this._texture.uploadData(this._pixelData);
        }
    }


    /**
     * 清除指定网格方块的颜色
     * @param gridX 网格X坐标
     * @param gridY 网格Y坐标
     * @param blockSize 方块大小
     * @param update 是否立即更新
     */
    clearGridBlockColor(gridX: number, gridY: number, blockSize: number = 1, update: boolean = true) {
        if (!this._texture || !this._pixelData) {
            return;
        }

        const textureWidth = this._texture.width;
        const textureHeight = this._texture.height;
        const gridCols = Math.floor(textureWidth / blockSize);
        const gridRows = Math.floor(textureHeight / blockSize);

        // 边界检查
        if (gridX < 0 || gridX >= gridCols || gridY < 0 || gridY >= gridRows) {
            return;
        }

        const startX = gridX * blockSize;
        const startY = gridY * blockSize;

        for (let y = startY + 1; y < startY + blockSize - 1; y++) {
            for (let x = startX + 1; x < startX + blockSize - 1; x++) {
                if (x < textureWidth && y < textureHeight) {
                    const idx = (y * textureWidth + x) * 4;
                    this._pixelData[idx] = 0;
                    this._pixelData[idx + 1] = 0;
                    this._pixelData[idx + 2] = 0;
                    this._pixelData[idx + 3] = 0;
                }
            }
        }

        this._isDirty = true;

        if (update) {
            this._isDirty = false;
            this._texture.uploadData(this._pixelData);
        }
    }

    /**
     * 获取指定网格方块的颜色
     * @param gridX 网格X坐标
     * @param gridY 网格Y坐标
     * @param blockSize 方块大小
     * @returns 颜色对象
     */
    getGridBlockColor(gridX: number, gridY: number, blockSize: number = 1): Color | null {
        if (!this._texture || !this._pixelData) {
            return null;
        }

        const textureWidth = this._texture.width;
        const textureHeight = this._texture.height;
        const gridCols = Math.floor(textureWidth / blockSize);
        const gridRows = Math.floor(textureHeight / blockSize);

        // 边界检查
        if (gridX < 0 || gridX >= gridCols || gridY < 0 || gridY >= gridRows) {
            return null;
        }

        // 计算方块中心位置
        const centerX = gridX * blockSize + Math.floor(blockSize / 2);
        const centerY = gridY * blockSize + Math.floor(blockSize / 2);

        if (centerX < textureWidth && centerY < textureHeight) {
            const idx = (centerY * textureWidth + centerX) * 4;
            return new Color(
                this._pixelData[idx],
                this._pixelData[idx + 1],
                this._pixelData[idx + 2],
                this._pixelData[idx + 3]
            );
        }

        return null;
    }

    /**
     * 绘制网格线(支持放大模式)
     * @param blockSize 方块大小
     * @param borderColor 边框颜色
     * @param pixelData 可选的像素数据(放大模式时使用)
     * @param width 可选的宽度(放大模式时使用)
     * @param height 可选的高度(放大模式时使用)
     */
    private drawGridLines(blockSize: number, borderColor: Color, pixelData?: Uint8Array, width?: number, height?: number) {
        // 使用传入的参数或默认值
        const targetPixelData = pixelData || this._pixelData;
        const targetWidth = width || this._texture.width;
        const targetHeight = height || this._texture.height;

        if (!targetPixelData) {
            return;
        }

        // 绘制垂直线
        for (let x = 0; x < targetWidth; x += blockSize) {
            for (let y = 0; y < targetHeight; y++) {
                const idx = (y * targetWidth + x) * 4;
                targetPixelData[idx] = borderColor.r;
                targetPixelData[idx + 1] = borderColor.g;
                targetPixelData[idx + 2] = borderColor.b;
                targetPixelData[idx + 3] = borderColor.a;
            }
        }

        // 绘制水平线
        for (let y = 0; y < targetHeight; y += blockSize) {
            for (let x = 0; x < targetWidth; x++) {
                const idx = (y * targetWidth + x) * 4;
                targetPixelData[idx] = borderColor.r;
                targetPixelData[idx + 1] = borderColor.g;
                targetPixelData[idx + 2] = borderColor.b;
                targetPixelData[idx + 3] = borderColor.a;
            }
        }
    }

}

通过控制单个逻辑像素上的颜色就实现了游戏中的效果
主要使用setGridBlockColor这个接口


绘制形状

游戏的数字也是基于这个

import { _decorator, Color, Component, Director, director, Size } from 'cc';
import { PixelSprite } from "db://assets/scripts/util/PixelSprite";
import { debug } from '../../framework/tool/Log';
import { DEV } from 'cc/env';

const { ccclass, property, executeInEditMode } = _decorator;

/**
 * 像素数字显示组件
 * 基于PixelSprite实现像素风格的数字显示
 */
@ccclass('PixelSpriteNum')
@executeInEditMode()
export class PixelSpriteNum extends PixelSprite {

    @property
    _blockSize: number = 10; //单格子的像素大小
    @property({ displayName: DEV && '🔤 单像素格子大小' })
    get blockSize() {
        return this._blockSize;
    }
    set blockSize(value: number) {
        this._blockSize = value;
        this.showNumber(this._number);
    }

    @property
    _number: number = 0;
    @property({ displayName: DEV && '🔤 数字' })
    get number() {
        return this._number;
    }
    set number(value: number) {
        this._number = value;
        this.showNumber(this._number);
    }

    @property
    _digitSpacing: number = 1; // 数字之间的间隔(像素列数)
    @property({ displayName: DEV && '🔤 数字之间的间隔' })
    get digitSpacing() {
        return this._digitSpacing;
    }
    set digitSpacing(value: number) {
        this._digitSpacing = value;
        this.showNumber(this._number);
    }


    @property
    _useCommaFormat: boolean = false; // 是否使用逗号格式化(每三位一个逗号)
    @property({ displayName: DEV && '🔤 是否使用逗号格式化' })
    get useCommaFormat() {
        return this._useCommaFormat;
    }
    set useCommaFormat(value: boolean) {
        this._useCommaFormat = value;
        this.showNumber(this._number);
    }

    @property
    _digitColor: Color = new Color(255, 255, 255, 255);  // 数字颜色(白色)
    @property({ displayName: DEV && '🔤 数字颜色' })
    get digitColor() {
        return this._digitColor;
    }
    set digitColor(value: Color) {
        this._digitColor = value;
        this.showNumber(this._number);
    }

    // 数字像素模板(5x7像素网格)
    private digitTemplates: number[][][] = [
        // 0
        [
            [0, 1, 1, 1, 0],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [0, 1, 1, 1, 0]
        ],
        // 1
        [
            [0, 0, 1, 0, 0],
            [0, 1, 1, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 1, 1, 1, 0]
        ],
        // 2
        [
            [0, 1, 1, 1, 0],
            [1, 0, 0, 0, 1],
            [0, 0, 0, 0, 1],
            [0, 0, 0, 1, 0],
            [0, 0, 1, 0, 0],
            [0, 1, 0, 0, 0],
            [1, 1, 1, 1, 1]
        ],
        // 3
        [
            [0, 1, 1, 1, 0],
            [1, 0, 0, 0, 1],
            [0, 0, 0, 0, 1],
            [0, 0, 1, 1, 0],
            [0, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [0, 1, 1, 1, 0]
        ],
        // 4
        [
            [0, 0, 0, 1, 0],
            [0, 0, 1, 1, 0],
            [0, 1, 0, 1, 0],
            [1, 0, 0, 1, 0],
            [1, 1, 1, 1, 1],
            [0, 0, 0, 1, 0],
            [0, 0, 0, 1, 0]
        ],
        // 5
        [
            [1, 1, 1, 1, 1],
            [1, 0, 0, 0, 0],
            [1, 1, 1, 1, 0],
            [0, 0, 0, 0, 1],
            [0, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [0, 1, 1, 1, 0]
        ],
        // 6
        [
            [0, 1, 1, 1, 0],
            [1, 0, 0, 0, 0],
            [1, 0, 0, 0, 0],
            [1, 1, 1, 1, 0],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [0, 1, 1, 1, 0]
        ],
        // 7
        [
            [1, 1, 1, 1, 1],
            [0, 0, 0, 0, 1],
            [0, 0, 0, 1, 0],
            [0, 0, 1, 0, 0],
            [0, 1, 0, 0, 0],
            [0, 1, 0, 0, 0],
            [0, 1, 0, 0, 0]
        ],
        // 8
        [
            [0, 1, 1, 1, 0],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [0, 1, 1, 1, 0],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [0, 1, 1, 1, 0]
        ],
        // 9
        [
            [0, 1, 1, 1, 0],
            [1, 0, 0, 0, 1],
            [1, 0, 0, 0, 1],
            [0, 1, 1, 1, 1],
            [0, 0, 0, 0, 1],
            [0, 0, 0, 0, 1],
            [0, 1, 1, 1, 0]
        ],
        // // +
        // [
        //     [0, 0, 0, 0, 0],
        //     [0, 0, 1, 0, 0],
        //     [0, 0, 1, 0, 0],
        //     [1, 1, 1, 1, 1],
        //     [0, 0, 1, 0, 0],
        //     [0, 0, 1, 0, 0],
        //     [0, 0, 0, 0, 0]
        // ],
        // // -
        // [
        //     [0, 0, 0, 0, 0],
        //     [0, 0, 0, 0, 0],
        //     [0, 0, 0, 0, 0],
        //     [1, 1, 1, 1, 1],
        //     [0, 0, 0, 0, 0],
        //     [0, 0, 0, 0, 0],
        //     [0, 0, 0, 0, 0]
        // ]
    ];

    // 逗号像素模板(3x7像素网格)
    private commaTemplate: number[][] = [
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 1, 0],
        [0, 1, 0],
        [1, 0, 0]
    ];


    protected onEnable(): void {
        // 设置默认尺寸
        this.size = new Size(5 * 5 * this.blockSize, 7); // 默认显示1位数字
        // 在启用时渲染一次,编辑器和运行时都生效
        this.showNumber(this._number);
    }

    protected onLoad(): void {
        super.onLoad();
        // 设置默认尺寸
        this.size = new Size(5 * this.blockSize, 7); // 默认显示1位数字
        this.showNumber(this._number);
    }

    /**
     * 显示多位数字
     * @param number 要显示的数字
     * @param maxDigits 最大显示位数
     */
    public showNumber(number: number, maxDigits: number = 1): void {
        // debug("showNumber", number, maxDigits);
        let displayChars: (number | string)[] = [];

        if (this.useCommaFormat) {
            // 使用逗号格式化
            displayChars = this.getFormattedChars(number);
        } else {
            // 使用原来的数字显示方式
            const digits = this.getDigits(number, maxDigits);
            displayChars = digits;
        }

        const digitWidth = 5; // 每个数字的宽度
        const commaWidth = 3; // 逗号的宽度
        const digitHeight = 7; // 每个数字的高度

        // 计算总宽度:考虑数字、逗号和间隔
        let totalGridWidth = 0;
        for (let i = 0; i < displayChars.length; i++) {
            if (typeof displayChars[i] === 'string' && displayChars[i] === ',') {
                totalGridWidth += commaWidth;
            } else {
                totalGridWidth += digitWidth;
            }
            // 添加间隔(除了最后一个字符)
            if (i < displayChars.length - 1) {
                totalGridWidth += this.digitSpacing;
            }
        }
        const totalGridHeight = digitHeight;

        // 初始化一个二维颜色数组,用于表示所有数字的像素网格
        const allDigitsColors: Color[][] = [];
        for (let row = 0; row < totalGridHeight; row++) {
            allDigitsColors[row] = [];
            for (let col = 0; col < totalGridWidth; col++) {
                allDigitsColors[row][col] = new Color(0, 0, 0, 0); // 默认设置为透明
            }
        }

        // 遍历每个字符,将其模板转换为颜色并填充到总的颜色网格中
        let currentX = 0;
        for (let i = 0; i < displayChars.length; i++) {
            const char = displayChars[i];
            let template: number[][];
            let charWidth: number;

            if (typeof char === 'string' && char === ',') {
                // 绘制逗号
                template = this.commaTemplate;
                charWidth = commaWidth;
            } else {
                // 绘制数字
                template = this.digitTemplates[char as number];
                charWidth = digitWidth;
            }

            for (let row = 0; row < digitHeight; row++) {
                for (let col = 0; col < charWidth; col++) {
                    if (template[row][col] === 1) {
                        allDigitsColors[row][currentX + col] = this.digitColor;
                    }
                    // 如果模板中为0,则保持透明(已在初始化时设置)
                }
            }

            // 移动到下一个字符的位置
            currentX += charWidth;
            // 添加间隔(除了最后一个字符)
            if (i < displayChars.length - 1) {
                currentX += this.digitSpacing;
            }
        }

        if (totalGridWidth * this.blockSize != this.size.width ||
            totalGridHeight * this.blockSize != this.size.height) {

            // 根据总的像素网格尺寸和blockSize设置Sprite的实际尺寸
            this.size = new Size(totalGridWidth * this.blockSize, totalGridHeight * this.blockSize);

            // 重置尺寸
            this.reSize()
        }


        // 使用drawPixelGrid方法一次性绘制所有数字
        // 这里的backgroundColor参数是整个网格的背景色,对于showNumber,我们通常希望背景是透明的
        this.drawPixelGrid(this.blockSize, new Color(0, 0, 0, 0), allDigitsColors, true);
    }

    /**
     * 获取数字的各位数字数组
     * @param number 数字
     * @param maxDigits 最大位数
     * @returns 数字数组
     */
    private getDigits(number: number, maxDigits: number): number[] {
        const digits: number[] = [];
        let num = Math.abs(number);

        if (num === 0) {
            digits.push(0);
        } else {
            while (num > 0) {
                digits.unshift(num % 10);
                num = Math.floor(num / 10);
            }
        }

        // 补零到最大位数
        while (digits.length < maxDigits) {
            digits.unshift(0);
        }

        return digits;
    }

    /**
     * 获取格式化的字符数组(包含数字和逗号)
     * @param number 数字
     * @returns 字符数组
     */
    private getFormattedChars(number: number): (number | string)[] {
        const digits = this.getDigits(number, 0); // 不补零
        const result: (number | string)[] = [];

        for (let i = 0; i < digits.length; i++) {
            result.push(digits[i]);

            // 在每三位数字后添加逗号(除了最后一位)
            const positionFromEnd = digits.length - 1 - i;
            if (positionFromEnd > 0 && positionFromEnd % 3 === 0) {
                result.push(',');
            }
        }

        return result;
    }


    // /**
    //  * 填充背景色
    //  * @param color 背景颜色
    //  */
    // private fillBackground(color: Color): void {
    //     const width = this.size.width;
    //     const height = this.size.height;

    //     for (let y = 0; y < height; y++) {
    //         for (let x = 0; x < width; x++) {
    //             this.setPixel(x, y, color);
    //         }
    //     }
    // }
}

实现文字的也是同一个道理,但是只支持中文,代码太长就不贴了

在线体验:https://garyxuan.github.io/SandPuzzle-client-publish/
托管在github pages,打不开的话可能要fanqiang

3赞

链接打不开诶,我也做了一个,目前已经上架谷■了,国内的还在申请版号

额,我这个用的github pages,可能要挂■■。。

你的app叫啥,我去玩玩


无限弹框,根本关不掉。
另外,每次升级弹出的提示会弹出很多很多个,点了几十下才关掉。

估计是服务器网络的问题,服务器托管的Vercel,不是太稳定

我试了下,升级是会弹出多次,是个bug

有需要钻石的,在设置界面复制下id,留言,我给大家加下钻石

1赞

这些图都是AI画的么,请问提示词怎么写的?
这UI质量还挺好的。

我用的cursor,直接给他一个参考图,让他帮我生成的提示词,再到imageFX去生成 :grinning:

提示词自己写的话,不好写,主要是很多细节要描述清楚不简单,所以都依赖AI写提示词,再用绘图AI去生成图

流量不太好,搜不到,我自己都搜不到 :joy:

收藏一下~