block-crush · nhánh dung/task-feature · 30/10/2026

Cơ chế đổi skin của bàn chơi

"Đổi màu" trong game là đổi skin (skinId). Core đổi id ngay khi đặt khối; view đổi hình chậm hơn một nhịp để khớp hiệu ứng nâng cấp.

1. Cùng một bàn, skin khác nhau

Mỗi skin có thư mục Art/feature/skin_<id>/skins/ với board.png, bg.png và cube_1..7.png. Dưới đây là ảnh thật của ba skin.

board skin 1000
cube_1cube_2cube_3cube_4cube_5cube_6cube_7
Skin 1000 (mặc định)
board 885x885 · cube 106x106
board skin 1005
cube_1cube_2cube_3cube_4cube_5cube_6cube_7
Skin 1005
board 885x885 · cube 106x106
board skin 2011
cube_1cube_2cube_3cube_4cube_5cube_6cube_7
Skin 2011
board 894x894 · có thêm crown.png 94x70, shadow.png, fonts/, effects/

Khối nhỏ bên dưới mỗi bàn là cube_1..7. Mọi cube cao 106 px, bằng kCubeStdPx, nên một hệ số kCell/106 = 120/106 khớp ô của bàn.

2. Luồng đổi, theo thứ tự chạy

Core đổi id
executeSkinChange (endless_game_skin.cpp): curSkinId = nextRandomSkinId, rồi rút sẵn skin kế. Cứ kSkinNeedNum = 25 tiến độ thì đổi. Pool mặc định là 4000..4019.
View giữ skin cũ
skinPending_ = true. Trong refresh(), setDisplaySkin bị bỏ qua khi đang chờ (BoardPlay.cpp:1754), nên bàn vẫn hiện skin cũ trong lúc nâng cấp chạy.
Hết nhịp 0,7 s game
executePendingSkinChange (BoardPlay.cpp:2110): setDisplaySkin(curSkinId), rồi reskinAll().
Dựng lại cả bàn và khay
reskinAll() gọi refresh(), không thay ảnh từng ô, để cỡ ảnh, z chồng 3D và layout khay đúng theo skin mới. Khối đang kéo chỉ thay ảnh (reskinBlocks).
Thay phần trang trí
refresh() gọi applySkinDecor (BoardView.cpp:521): xoá SkinBg, SkinBoard, SkinCrown cũ rồi dựng cái mới.
Phát hiệu ứng
fx:skin_change. Ván Travel phát playMilestoneLinesEffect, còn lại phát playSkinChangeEffects.

3. Mỗi thành phần đổi thế nào

Thành phầnNguồnCách đặt
Ảnh khốicube_1..7.png theo icons[] trong skins.jsonblockId kẹp vào 1..7. Skin 1000 trả chuỗi rỗng, dùng ic_block.
Nền mànbg.pngKéo giãn kín màn, không giữ tỉ lệ (gốc gán SpriteFrame vào Widget 4 cạnh = 0). Ẩn WoodBgL/R. Ảnh dải 20x1707 hay 960x1707 đều xử lý như nhau.
Bànboard.pngKhung, lưới và ô gộp một ảnh. Phóng 120/106, tức 885 thành khoảng 1002 px. Khi có, ẩn chessboard2, GridBorder, gezi, 64 QiCell. boardHoleOffset dò độ lệch pha lưới rồi dời ảnh.
Vương miệncrown.png (chỉ một số skin)Ẩn Crown mặc định, neo đáy ảnh mới vào đáy vương miện cũ.
Âm xoá dòngsound_clear.wavNạp sẵn bằng AudioEngine::preload để lần phát đầu không chờ đọc file.
Âm đặt khốiputAudioKey trong skins.jsonĐọc lúc đặt khối (BoardFx.cpp:1550).
Màu hiệu ứng xoámatchColors[k], ba màu RGBA mỗi khốiSkin không khai thì trích màu từ chính cube của skin (DEC-238, giả định).
Font số điểmskinfont score / highscoreDựng lại chữ số khi skin đổi.

Mọi file decor đi qua decorPath(skinId, name) (SkinCatalog.cpp:136). File không tồn tại thì trả chuỗi rỗng và phần đó quay về hình mặc định: qipannew + QiPanxin cho bàn, nền gỗ cho màn.

4. Tổng hợp sprite liên quan

Hai nhóm: sprite đổi theo skin (đọc từ Art/feature/skin_<id>/skins/) và sprite cố định (nằm ngoài thư mục skin, dùng làm mặc định hoặc dự phòng). Ảnh là file thật trong Content/res.

Đổi theo skin

cube_1..7.png
đổi theo skin
cube_1..7.png
106x106. Ảnh khối, 7 ảnh mỗi skin (91 skin có đủ 7 ảnh)
board.png
đổi theo skin
board.png
885x885 (skin 2011: 894). Gộp khung + lưới + ô; 89 skin có
bg.png
đổi theo skin
bg.png
Skin 1000: dải 20x1707. Skin 2011: 960x1707. Kéo giãn kín màn; 98 skin có
bg.png (2011, thu nhỏ)
đổi theo skin
bg.png (2011, thu nhỏ)
960x1707 gốc
crown.png
đổi theo skin
crown.png
94x70. Thay vương miện, chỉ 32 skin có
fonts/score.fnt + .png
đổi theo skin
fonts/score.fnt + .png
Font số điểm hiện tại. Còn highscore và combo
effects/xc_011_tex.png
đổi theo skin
effects/xc_011_tex.png
Atlas DragonBones của hiệu ứng xoá dòng (gameplay_eliminate, xc_<id>)

Cố định, và mặc định khi skin thiếu file

ic_block.png
cố định
ic_block.png
120x120. Khối của skin 1000, và ảnh dự phòng khi skin thiếu cube
base_ic_collection_block.png
cố định
base_ic_collection_block.png
120x120. Nền đào của ô gem Travel (id ≥ 100) ở skin mặc định
icon_block_big.png
cố định
icon_block_big.png
120x120. Khối vạn năng (id 998), vừa đúng 1 ô
BlockStyle/Default/shadow.png
cố định
BlockStyle/Default/shadow.png
120x120. Bóng khối trong khay, lệch phải-xuống 20 px
icon_crown_0.png
cố định
icon_crown_0.png
66x46. Vương miện mặc định cạnh điểm cao nhất

Có trong skin nhưng chưa thấy dùng

shadow.png
cố định
shadow.png
106x106. 51 skin có file này, nhưng tôi không thấy code nào nạp nó
SpriteSố skin có (trong 100 thư mục)Nơi nạp
bg.png98applySkinDecor, BoardView.cpp:521
cube_1..7.png91makeBlock qua skinIconPath
board.png89applySkinDecor
shadow.png51chưa thấy nơi nạp (bóng khay dùng BlockStyle/Default/shadow.png)
crown.png32applySkinDecor
fonts/*.fntchưa đếmskinFontNumber: score, highscore, combo, jiafen
effects/*chưa đếmeffectBase: gameplay_eliminate, gameplay_ready_eliminate, gameplay_eliminatePrompt

Phép tính cỡ: ảnh khối được cubeDisplaySize ép về một ô 120 px (cube 106 px giữ tỉ lệ, neo giữa ô), còn board.png nhân 120/106. 885 × 1,132 ≈ 1002 px, đúng cỡ khung mặc định.

5. Skin chưa có trên máy

Kho skin
skins.json có 294 skin: 39 skin builtin (nhúng app, họ 1xxx) và 255 skin tải từ server. Trong thư mục app có 100 thư mục skin_*.
Yêu cầu tải
setDisplaySkin gọi requestSkin: skin chưa có được chen lên đầu hàng tải.
Hiện tạm
fallbackSkinId lấy 1000 + skinId mod 39 (ASM-009), chỉ đổi ảnh, id trong core giữ nguyên. Khi tải xong thì đổi ảnh thật.

6. Chưa kiểm

Số liệu trên đọc từ code và từ file trong Content/res/Art/feature. Tôi chưa chạy game để xem một lần đổi skin thật. Tôi cũng chưa đọc boardHoleOffset đến cùng và chưa so từng skin với bản gốc.

Skin 1000 có board.png riêng (xanh navy trong ảnh ở mục 1). Vì vậy bộ qipannew + QiPanxin chỉ hiện khi decorPath trả rỗng. Tôi chưa xác nhận skin 1000 chạy đường nào trong game thật.