06-pet-nhan-vat.md · Cập nhật lần cuối: 2026-09-06 23:06 +07 · sha256 nguồn bdc3f6250a6d06 — Pet & nhân vật (đàn mèo leader/follower, bầy pet boids, Spine)
Nguồn framework:
docs/framework/05-characters-pets-enemies.md§2.1, §2.2, §3.1–3.3, §4, §5;docs/framework/04-sound.md§3.5 (cờis_Shoot_Mute);docs/framework/08-architecture-and-scale.md§2.4 (Spine, atlas). Cấp bằng chứng: L1 ANALYSIS — dựng lại clean-room từ disasm + metadata + quan sát runtime; không phải source gốc. Mỗi dòng bên dưới mang nhãn bằng chứng riêng.
Mục tiêu
Muốn màn hình idle đông và sống: một mèo người chơi, vài mèo AI bám theo như đoàn rắn, một bầy pet
lượn quanh không giẫm lên nhau — mà không cần pathfinding hay NavMesh. CatGunner làm bằng hai class
flocking chạy nhiều nhất toàn game (Pet_Ingame_manager 20 038 lời gọi / 40 s, Cat_manager 9 577)
cộng một Cat ~100 field. Trang này gom đúng các hằng số và công thức đã đọc được, và nói rõ chỗ nào
artist cần chuẩn bị asset Spine.
Game gốc làm gì
Mèo — leader + follower
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
Cat_manager.Cat_list[0] là mèo người chơi, còn lại là AI follower — ghi trong tooltip tiếng Hàn của chính source |
"0번 = 메인 플레이어 / 나머지 = AI 팔로워" |
metadata-proven | doc 05 §2.1, dump.cs:593380 |
Cat_manager chỉ có 3 tham số đội hình |
FormationRadius @0x28, SeparationRadius @0x2C, SeparationStrength @0x30 |
metadata-proven | doc 05 §2.1 |
Cat_manager 11 method |
Awake, Start, Update, ComputeSeparation, SetupFormation, AddFollower, RemoveFollower, Balance_Reload, Warp, Skin_Reload, .ctor |
metadata-proven | doc 05 §2.1 |
| Separation chạy mỗi frame | Update 4 776× và ComputeSeparation 4 776× / ~40 s |
runtime-observed | doc 05 §2.1 |
Cat_manager.is_Shoot_Mute static bool @0x0 — Mute_Panel ghi khi panel mở |
19 Mute_Panel trong scene |
metadata-proven + scene-census | doc 04 §2/§3.5 |
Cat (TypeDefIndex 8154) là class lớn nhất của gameplay |
~100 field, 50+ method, máy trạng thái Cat.AIState riêng |
metadata-proven | doc 05 §2.1, dump.cs:593002 |
| Follower đi theo breadcrumb (vệt đường leader) | static readonly List<Vector3> Breadcrumbs, RecordDist = 0.5, MaxCrumbs = 100, CrumbLookahead = 5 |
metadata-proven | doc 05 §2.1 |
| Follower có lịch sử vị trí có độ trễ | Queue<Cat.PosRecord> _leaderHistory, HistoryDuration = 2 giây, GetLaggedLeaderPos() |
metadata-proven | doc 05 §2.1 |
| Chống rung trạng thái | [Range(0.05,0.3)] HysteresisMargin, [Range(0.1,1)] MinStateDuration, _stateTimer, EvaluateDesiredState(leader, distToLeader) |
metadata-proven | doc 05 §2.1 |
| Làm mượt vận tốc | [Range(2,20)] VelocitySmoothSpeed, _smoothVelocity, DecelerationRange, MinSpeedRatio |
metadata-proven | doc 05 §2.1 |
| Fidget khi đứng yên | AIFidget(), _fidgetTimer, _fidgetInterval, _fidgetOffset |
metadata-proven | doc 05 §2.1 |
| Hệ spread (tản đạn) động | SpreadAngleMin, SpreadAngleMax, [Range(0.1,0.6)] SpreadGrowthRate, SpreadDecayMultiplier, _spreadAngle; UpdateSpreadState RVA 0x2AE80B0; tooltip gốc: góc tản nhỏ nhất = tốc độ đánh chậm nhất |
metadata-proven | doc 05 §2.1/§3.2 |
| Field debug read-only ngay trong inspector | [Header("Spread Debug (Read Only)")] Debug_SpreadAngle / Debug_SpreadMaxAngle |
metadata-proven | doc 05 §3.2 |
| Hằng số camera / hình học | CameraYAngle = -45, CamPitchSin = 0.57735026 (= 1/√3, nghiêng 30°), HandBasicAngle = 130, BulletSpawnY = 2, MonsterMoveRadius = 14 |
metadata-proven | doc 05 §2.1 |
| Auto-move riêng cho từng nội dung | HandleAutoMove, FindNearestTreeByGrid, FindNearestTreeSpeedRun, FindNearestOreMine, FindBossTarget, OverlapSphereNearestOre |
metadata-proven | doc 05 §2.1 |
| Ngắm bằng raycast | FindNearestEnemyByRaycast (RayCount 8..72) → ResolveAttackTarget → Attack |
metadata-proven | doc 05 §3.1 |
| Tốc độ đánh | Cat.CalculateAtkCoolMax(baseAtkSpeed, upgradeValue_Disable, isMainStage, isRaid = false) RVA 0x2AE5CB8 |
metadata-proven | doc 05 §2.1 |
Cat_manager.Balance_Reload chỉ chạy khi cân bằng lại (không mỗi frame) |
Start 9×, Balance_Reload 9× |
runtime-observed | doc 05 §2.1 |
Đợt 3 — 21/67 method của Cat đã dựng lại (ctor, cctor, Awake, Start, Skin_Reload, Balance_Reload, cooldown, Update, ngắm, hướng mặt); OnDrawGizmosSelected (editor-only) cố ý bỏ |
42 160 B / 67 method, 5 batch | disasm-proven | doc 05 §2.1 (G352, G344) |
Mặc định ctor của Cat (khởi tạo field bị công cụ decompile bỏ rơi) |
moveSpeed = 5, Attack_Length = 5, Raycast_Cool_Max = 0.1, RayCount = 36, AutoOreSearchRadius = 25, OreOverlapBufferSize = 64, AutoNearbyRadius = 8 (+ 27 khởi tạo khác) |
disasm-proven | doc 05 §2.1 (G344, G352) |
Hằng static của Cat (thiếu ⇒ ngắm/hướng mặt lệch 45°) |
BoxHalfExtents (0.3, 0.4, 0.3), LeftAngle (40, −45, 0), RightAngle (−40, 135, 0), CamForward = Euler(40, −45, 0)·forward = (−0.5417, −0.6428, 0.5417) (đính chính G400 — bản trước thiếu pitch 40°, và đó chính là lý do đạn không trúng, xem 04), CamCorrection = Euler(0, 45, 0), MonsterMapCenter (−100, 0, 0), ScreenSpeedK = 1.2 |
disasm-proven (re-derived) | doc 05 §2.1 (G352, G400) |
Singleton Cat.instance chỉ là mèo người chơi: Awake gán if (!is_AI) instance = this — follower AI không ghi đè |
— | disasm-proven | doc 05 §2.1 (G352) |
| Góc tản tối đa là ánh xạ log theo tốc độ đánh | SpreadMaxAngle = log(AtkCool_Max / 0.05) / log(40) (chuẩn hoá dải cool 0.05..2 s) |
disasm-proven | doc 05 §2.1 (G352) |
Pipeline cân bằng rẽ theo chế độ (Cat.Balance_Reload): Mine = damage phẳng 1, CriPer = 0; Boss / Hunt / Raid = damage súng thô (không nhân upgrade vĩnh viễn), Raid bỏ cả Rebirth; Main nhân đôi moveSpeed khi is_Clear_Stage |
4 nhánh | disasm-proven | doc 05 §2.1 (G352) |
Tốc độ đánh nhanh dần theo tiến độ: CalculateAtkCoolMax trừ 30 mỗi stage; IsOnScreen mở (true) khi không có camera; FindNearestTreeByGrid cắt tỉa theo dải Z của lưới, cây boss cạnh tranh ở Level 4 |
−30 / stage | disasm-proven | doc 05 §2.1 (G352) |
Cat_manager ctor: warpOffsets = {(−6, 0, −6.4), (6, 0, −6.4)} (thiếu ⇒ NRE Warp), FormationRadius = 10 / SeparationRadius = 1.8 / SeparationStrength = 3 — nhưng scene serialize FormationRadius = 2.5 ⇒ scene thắng; Pet_manager.AutoMerge_Degree = 6, Pet._currentAnim = "" |
— | disasm-proven + scene-YAML | doc 05 §2.1, doc 11 §1.1 (G306) |
Follower đi theo VỆT leader, không đuổi theo điểm: lịch sử leader static dùng chung (2 s), mỗi follower đọc ở độ trễ riêng 0.15–0.6 s rút theo instance id ⇒ đoàn rắn tất định, không cần offset đội hình phức tạp |
2 s / 0.15–0.6 s | disasm-proven | doc 05 §2.1/§3.1c (G364) |
Hai lớp chống rung độc lập: nới băng trạng thái đang giữ (HysteresisMargin) và MinStateDuration |
2 | disasm-proven | doc 05 §3.1c (G364) |
AIAssist xích ở 0.85 × FollowMaxDist; ở chế độ Hunt không có leader — mỗi mèo một trạm trên vành bán kính 14 |
0.85 / 14 | disasm-proven | doc 05 §2.1 (G364) |
Save mới chỉ thấy MỘT mèo — đúng: Cat_manager.Balance_Reload bật một mèo cho mỗi súng đang trang bị; save mới trang bị 1 ⇒ 1 mèo; xem follower cần ≥ 2 súng trang bị (chơi thật) |
Selected_items = [0, −1, −1] |
disasm-proven + scene-YAML | doc 05 §2.1 (G364) |
Cat hoàn tất (65/67, chỉ gizmo editor-only bỏ): finder quặng mở rộng dần, buffer non-alloc 64, Warp dời rigidbody, damage kỳ vọng boss trộn crit |
65/67 | disasm-proven | doc 05 §2.1 (G369) |
Pet — bầy boids
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
Pet_Ingame_manager là class chạy nhiều thứ hai toàn game |
20 038 lời gọi / 40 s, 11 method | runtime-observed | doc 05 §2.2 |
| Vòng lặp mỗi bước vật lý | FixedUpdate 4 006× → GetMainCat 4 007×, ComputePetSeparation 4 006×, ComputeCatAvoidance 4 006×, MovePets 4 006× |
runtime-observed | doc 05 §2.2 |
GetMainCat() = Cat_list[0] nếu có |
— | disasm-proven | doc 05 §2.2, _evidence/Pet_Ingame_manager/README.md |
| Tốc độ mèo được làm mượt hàm mũ | _smoothedCatSpeed += (inst - _smoothedCatSpeed) * 0.12f |
disasm-proven | doc 05 §2.2/§3.3 |
| Tham số bầy (offset đã xác minh) | FormationRadius @0x28, FormationSpreadDeg @0x2C, StartMoveDistance @0x30, StopMoveDistance @0x34, MinSpeed @0x38, MaxSpeed @0x3C, FullSpeedDistance @0x40, CatSpeedMultiplier @0x44, PetSeparationRadius @0x48, PetSeparationStrength @0x4C, CatAvoidRadius @0x50, CatAvoidStrength @0x54 |
metadata-proven | doc 05 §2.2 |
Pet giữ 3 vector lực [HideInInspector] |
SeparationForce @0x40, CatAvoidanceForce @0x4C, FormationOffset @0x58, IsMoving @0x64, _currentAnim @0x68 |
metadata-proven | doc 05 §2.2, dump.cs:597811 |
Pet._anim là SkeletonAnimation (Spine), không phải Animator |
5 method: Reload(int SkinNum), Warp(Vector3), SetAnimation(string), SetFacing(Vector3), .ctor |
metadata-proven | doc 05 §2.2 |
| Spine runtime 4.1.24, định dạng JSON | 13 skeleton + 13 atlas, đường ảnh gốc ./Pet/ |
asset-census | doc 05 §2.2, asset_lane.md §0 |
| 13 pet | Acorn, Bee, BlackCat, Chicken, Dove, Frog, Ghost, Griffin, Pigeon, PillowGhost, PinkRabbit, Slime, Slime2 — tất cả "spine": "4.1.24" |
asset-census | doc 05 §2.2 |
| Tên dự án nội bộ đọc từ header skeleton | BardCat | asset-census | doc 05 §2.2 |
Không có NavMeshAgent trong Cat / Pet / Monster / Boss — mọi di chuyển thủ công qua Rigidbody + lực |
package com.unity.ai.navigation có trong manifest nhưng không dùng ở các class này |
metadata-proven (negative) | doc 05 §5 |
| Sprite mèo nằm trong atlas riêng | atlas Cat 125 sprite, 4096×4096 ASTC 6x6 |
asset-census | doc 08 §2.4 |
Công thức / pseudocode
// ---- L1 ANALYSIS (có RVA / evidence) ----
// Pet_Ingame_manager.FixedUpdate — disasm-proven, chạy 4 006x/40s
Cat mainCat = GetMainCat(); // Cat_manager.instance.Cat_list[0]
if (mainCat == null) return;
Vector3 pos = mainCat.transform.position;
float inst = Vector3.Distance(pos, _prevMainCatPos) / Time.fixedDeltaTime;
_smoothedCatSpeed += (inst - _smoothedCatSpeed) * 0.12f; // lọc thông thấp bậc 1 (~8 bước vật lý)
_prevMainCatPos = pos;
ComputePetSeparation();
ComputeCatAvoidance();
MovePets(mainCat);
// Ba lực — thuật toán đã giải mã, CHƯA xác minh byte-exact (G131, xem UNKNOWN)
// 1. tách nhau giữa pet (từng cặp i<j, chỉ pet đang active)
d = Pets[i].pos.xy - Pets[j].pos.xy; dist = |d|
if (0.001 <= dist <= PetSeparationRadius) {
f = normalize(d) * ((1 - dist / PetSeparationRadius)^2 * PetSeparationStrength)
Pets[i].SeparationForce += f; Pets[j].SeparationForce -= f
}
// 2. né mèo (mỗi pet, với mỗi mèo đang active)
d = pet.pos.xy - cat.pos.xy; dist = |d|
if (0.001 <= dist <= CatAvoidRadius)
pet.CatAvoidanceForce += normalize(d) * ((1 - dist / CatAvoidRadius)^2 * CatAvoidStrength)
// 3. MovePets
FormationOffset + SeparationForce + CatAvoidanceForce
+ hysteresis Start/StopMoveDistance
+ kẹp [MinSpeed, MaxSpeed], FullSpeedDistance
+ CatSpeedMultiplier * _smoothedCatSpeed
-> rb; điều khiển Spine (IsMoving / _currentAnim)
// ---- Auto-move của mèo (batch C, disasm-proven, dự đoán khớp thiết bị — G356/G358) ----
HandleAutoMove (mode-aware):
Main : cây gần nhất theo lưới; đi tới, DỪNG CÁCH 8 đơn vị; ~14.4 u/s (tới cây cách 27 u trong ~1.4 s)
SpeedRun : khoá mục tiêu bắn Raid : mục tiêu = boss
Monster : đứng trên VÀNH đấu trường bán kính 14; lùi khi vào trong MinEnemyDist tới +0.5
ScreenSpeedMultiplier: stick.x / ScreenSpeedK ; stick.z * ScreenSpeedK // triệt tiêu foreshortening iso (CamPitchSin 0.577) — BẤT ĐỐI XỨNG
Breadcrumbs (static, dùng CHUNG cả đàn): leader thả 1 vệt / 0.5 u, giữ 100; follower nhắm vệt thứ 5 phía trước
// ---- AI follower (batch D, disasm-proven — G364) ----
RecordLeaderHistory: leader đẩy (pos, t) vào lịch sử STATIC dùng chung, cắt còn 2 s
GetLaggedLeaderPos : follower đọc vị trí leader tại t - _lagSeconds, _lagSeconds ∈ [0.15, 0.6] s rút 1 lần/instance (tất định theo instance id)
EvaluateDesiredState: nới băng trạng thái ĐANG GIỮ thêm HysteresisMargin // chống rung lớp 1
HandleAI : giữ trạng thái ít nhất MinStateDuration // chống rung lớp 2
AIAssist : xích ở 0.85 × FollowMaxDist
HandleAIMonster : không leader — mỗi mèo một TRẠM trên vành đấu trường bán kính 14
// ---- Finder & tiện ích (batch E, disasm-proven — G369) ----
FindBossTarget : không tìm — một mục tiêu duy nhất
FindNearestOreMine : bán kính -> bán kính × hệ số fallback -> quét toàn Ore_list (mở rộng dần)
OverlapSphereNearestOre: non-alloc vào buffer 64 ô cấp sẵn trong ctor
Warp : dời cả rigidbody, không chỉ transform
ExpectedDamage_Boss_return: bỏ Power_return; trộn thường/chí mạng theo tỉ lệ crit = kỳ vọng mỗi phát
// ---- Cấu trúc đàn mèo (tóm tắt từ metadata) ----
Cat_manager (tầng ĐÀN) : Update -> ComputeSeparation (mỗi frame); SetupFormation; Add/RemoveFollower; Warp; Skin_Reload
Cat (tầng CÁ THỂ) : AIState Follow / Chase / AttackStay / Reposition / Assist / Fidget (suy từ tên method, enum chưa trích)
breadcrumb : leader thả vệt mỗi RecordDist = 0.5; giữ MaxCrumbs = 100; follower nhắm điểm thứ CrumbLookahead = 5
lịch sử trễ : _leaderHistory, HistoryDuration = 2 s, GetLaggedLeaderPos()
chống rung : đổi trạng thái chỉ khi vượt HysteresisMargin và đã ở trạng thái >= MinStateDuration
spread : bắn -> _spreadAngle tăng theo SpreadGrowthRate; ngừng -> giảm theo SpreadDecayMultiplier
// ---- Gợi ý framework ----
IFlockManager : đội hình + lực tách nhau + thêm/bớt thành viên (chạy mỗi frame / bước vật lý)
IFlockMember : máy trạng thái riêng, nhận lực, tự di chuyển
HysteresisThreshold { margin; minStateDuration } // dùng cho MỌI ngưỡng chuyển trạng thái
LowPass(k = 0.12) cho mọi tín hiệu tốc độ đầu vào // hằng số cảm-giác đã ship
Falloff(d, r) = (1 - d/r)^2, tính trong 2D // mềm ở rìa, đẩy mạnh khi sát
"Bể" thực thể trang trí — pool cố định, rải chỗ ngẫu nhiên, sắp lớp hai trục (doc 05 §4 mục 11–13)
Đây là một hệ khác với đàn boids ở trên: không có lực, không có vật lý, thực thể chỉ bơi lượn trong một khung UI và là hình ảnh của kho đồ. Bể cá 20 ô của game gốc dùng đúng ba mẫu sau, và cả ba đều rẻ tới mức đáng chép nguyên.
1) Pool cố định bằng đúng trần kho — không Instantiate/Destroy lúc chạy.
Awake: for each slot in pool: slot.Init(this); slot.Deactivate() # pool dựng sẵn trong scene
Thêm: it = GetInactiveItem(); if (it == null) return # HẾT CHỖ = trần kho tự thực thi
it.Reload(grade, invIndex); it.SetPosition(FindEmptyPosition(it)); SortByGrade()
Xoá: it.Deactivate() # chỉ tắt object, không huỷ
Không GC spike khi người chơi rút liên tục, và trần kho không cần một if riêng — kích thước
mảng chính là trần. Đổi lại: pool và hằng trần phải cùng đọc một const.
Kèm theo: chỉ số kho là dữ liệu, tham chiếu item là tạm bợ. Sau mỗi lần xoá, gán lại chỉ số
cho mọi item đang sống bằng cách bắt cặp theo hạng với một mảng used (mỗi ô kho dùng đúng một
lần); item không khớp được thì để -1. Đừng nhớ item bằng tham chiếu qua nhiều frame.
2) Rải chỗ trống: "thử N lần, giữ chỗ thoáng nhất" — không lưới, không giải va chạm.
best = randomPoint(); bestDist = -1 # SEED bằng điểm ngẫu nhiên, KHÔNG bằng (0,0)
for i in range(maxAttempts): # 30
p = randomPoint()
d = distanceToNearestActive(p, exclude = self)
if d >= minDistance: return p # 100 px — đủ thoáng, NHẬN NGAY
if d > bestDist: bestDist, best = d, p
return best # không có nhánh thất bại
Ba tính chất khiến nó đáng chép: luôn trả về một điểm hợp lệ kể cả khi bể chật (chất lượng suy giảm
mượt thay vì hỏng); chi phí có trần cứng O(maxAttempts × N); hai tham số là núm chỉnh cảm giác,
không phải hằng số kỹ thuật. Kèm khuôn kẹp khung: if (min > max) { min = max = 0 } — thực thể to
hơn khung thì ghim về tâm, đừng để Random.Range ngược biên.
3) Sắp lớp bằng MỘT thang order mang HAI trục + một dải "luôn trên cùng" nằm ngoài Clamp.
order = Clamp(round(yOrigin - y) + step * (maxGrade - grade), 0, 2000) # step = 10
if isDragging: order = 9999 # vật thể người chơi đang cầm
elif isAnimatingToTarget: order = 9998 # vật thể đang trong hoạt ảnh
Trục hạng chiếm bước 10, trục Y chiếm bước 1 ⇒ hạng vẫn thắng, nhưng trong cùng hạng ai ở
thấp thì nổi lên trên (phối cảnh nhìn ngang). Hai giá trị đặc biệt nằm ngoài Clamp nên không
bao giờ bị trục nội dung đuổi kịp.
Và một quyết định thẩm mỹ nghe rất ngược: sắp hierarchy theo hạng giảm dần ⇒ thực thể bậc CAO (sprite to) bị bậc THẤP vẽ đè. Lý do: một con to không được nuốt mất mặt tiền của cả đàn nhỏ — người chơi vẫn phải đếm được tài sản của mình. Thực thể đang tắt thì đẩy xuống cuối hierarchy để không chiếm chỗ sibling.
4) Bơi trang trí, rẻ mà có nhịp: tốc độ Random(15, 35) px/s, đổi hướng mỗi Random(2, 5) s,
hướng mới x = ±1 (50/50) và y ∈ [-0.3, 0.3] rồi chuẩn hoá ⇒ luôn bơi gần như ngang, chỉ chếch
nhẹ. Chạm mép ngang thì đảo x và lật hình; chạm mép dọc thì đảo y mà không lật (không con
cá nào lộn ngược khi chạm đáy). Cập nhật order mỗi frame theo Y.
Bẫy đã gặp
- Toán NEON của 3 method flocking chưa xác minh byte-exact (G131). Thuật toán đã giải mã nhưng ánh xạ lane vector → thành phần và cách cộng dồn lực chưa được oracle xác nhận. Flock-oracle đã chạy nhưng pet không active trong phiên early-game nên mọi lực = 0 — bằng chứng phủ định hợp lệ, không phải xác nhận. Không ship ở mức 1:1 khi chưa xác minh (doc 05 §5).
- IDA đặt sai biên hàm cho
Cat_manager.ComputeSeparation/SetupFormation(decompile ra thânUpdate/Awake) — phảidel_func + add_functại đúng RVA trước khi đọc (G130). - Dùng tốc độ tức thời thay vì tốc độ đã làm mượt ⇒ pet giật mỗi khi người chơi nhả joystick.
Hệ số
0.12là hằng số cảm-giác quan trọng nhất của hệ pet (doc 05 §3.3). - Follower đi thẳng tới leader thay vì đi theo vệt ⇒ đi xuyên tường và trông như đám xúm lại thay vì
đoàn rắn;
CrumbLookahead = 5là thứ cho phép cắt góc nhẹ (doc 05 §3.1). - Không có hysteresis ⇒ follower đứng đúng ranh giới khoảng cách nhấp nháy giữa hai trạng thái mỗi
frame; tên field tiếng Hàn
버벅거림 방지("chống giật") cho thấy đây là bản vá cho bug thật (doc 05 §3.1). is_Shoot_Mutelà bool toàn cục, không phải counter — hai panelMute_Panelcùng mở, một cái đóng ⇒ cờ vềfalsedù panel kia còn mở. An toàn trong SOT vì UI một-panel-một-lúc; framework nên dùng counter (doc 04 §3.5).- Đổi tên field Spine / lệch version runtime — SOT dùng spine-unity 4.1 và SkeletonData export bởi Spine 4.1.24; runtime phải khớp
version editor đã export (
quy tắc dự án (nội bộ)R2). - Khởi tạo field bị công cụ decompile bỏ rơi.
warpOffsetsnull ⇒ NRE ngayWarp; các hằng static củaCatthiếu ⇒ hướng mặt lệch 45° mà không ném gì. Đọc.ctor/.cctortrước khi dựng method (G306, G352; doc 08 §3.6). - Ctor default ≠ giá trị ship với field serialize:
FormationRadiusctor = 10, scene = 2.5. Với field public/serialize, scene thắng; ctor chỉ áp khiAddComponentlúc chạy (G306, G364; doc 11 §1.1).SeparationRadius 1.8/Strength 3thì hai nguồn trùng nhau. Bẫy này lặp lại và tốn hơn ở một chỗ khác: bảng tỉ lệ gacha của hệ Cá — ctor{40,30,20,10}, scene{60,30,10,0}— đã bị chép từ ctor vào tài liệu và publish trước khi bị bắt (G1682). Luật đọc:public/[SerializeField]có trong scene ⇒ scene thắng;privatekhông[SerializeField]⇒ ctor là giá trị thật, scene không cứu được;const⇒ ctor thắng. Kiểm loại field trước khi tin con số. - Setter khởi động coroutine thừa hưởng stub của coroutine.
Bounce_Position.Bounce_Start(giật lùi súng) được coi là cosmetic, nhưngCat.Attackgọi nó mỗi phát và coroutine bên trong làreturn null⇒ ship setter một mình là giết đường bắn (G369).
Checklist khi tự làm
- [ ] Tách manager (quan hệ giữa cá thể) khỏi cá thể (hành vi riêng); manager ~10 method là đủ.
- [ ]
list[0]= người chơi, phần còn lại = AI — một quy ước duy nhất, ghi thành tooltip. - [ ] Follower đi theo breadcrumb (
RecordDist,MaxCrumbs,CrumbLookahead) + lịch sử leader có độ trễ. - [ ] Mọi ngưỡng chuyển trạng thái có
HysteresisMargin+MinStateDuration; mọi ngưỡng khoảng cách có cặpStart/Stop. - [ ] Tín hiệu tốc độ đầu vào đi qua lọc thông thấp
smoothed += (inst - smoothed) * 0.12. - [ ] Lực boids suy giảm bình phương
(1 - d/r)^2, tính trong 2D cho game nhìn từ trên. - [ ] Fidget khi đứng yên để đàn không thành tượng.
- [ ] Spread tăng theo nhịp bắn, giảm khi ngừng; spread max phụ thuộc tốc độ đánh (cân bằng tự nhiên).
- [ ] Field debug read-only trong inspector cho mọi hệ cảm-giác (
[Header("... Debug (Read Only)")]). - [ ] Pet animate bằng Spine
SkeletonAnimation(khôngAnimator), 13 skeleton JSON + 13 atlas, cùng một version runtime. - [ ] Artist: mỗi pet = 1 skeleton JSON + 1 atlas, tên project trong header; sprite mèo vào atlas
Cat. - [ ] Không dùng NavMesh cho đàn/bầy —
Rigidbody+ lực là đủ và rẻ hơn. - [ ] "Bể" thực thể trang trí: pool cố định bằng đúng trần kho,
GetInactiveItem()trảnull= hết chỗ; khôngInstantiatelúc chạy. - [ ] Chỉ số kho gán lại sau mỗi lần xoá (bắt cặp theo hạng + mảng
used); đừng nhớ item bằng tham chiếu qua nhiều frame. - [ ] Rải chỗ bằng "thử N lần, giữ chỗ thoáng nhất", seed
bestbằng điểm ngẫu nhiên; không có nhánh thất bại. - [ ] Một thang
sortingOrdermang hai trục (hạng × bước 10, Y × bước 1) + dải "luôn trên cùng" ngoàiClamp. - [ ] Đọc
.ctorđể lấy hằng khởi tạo của cảpool/Itemtrước khi dựng method — fieldprivatekhông có[SerializeField]thì scene không cứu.
Điều chưa biết (UNKNOWN)
- Toán NEON của
ComputePetSeparation/ComputeCatAvoidance/MovePets— CHƯA XÁC MINH BYTE-EXACT (G131). Cần phiên có nhiều pet ĐANG hoạt động và tụ lại (mở khoá Pet ở Stage_Value ≥ 110, triệu hồi nhiều pet). Cat_manager.ComputeSeparation— CHƯA dựng lại, cần sửa biên hàm IDA (G130) rồi disasm RVA0x2AEF830.Cat— hoàn tất 65/67 (A/B/C/D/E; G352–G369). ~~Thân mèo không vẽ trên save đã nạp~~ — ĐÃ ĐÓNG (G374):Skin_Bone_return/Skin_Color_return/is_Unlock(skin 0 luôn mở) là stub ⇒ sprite thân null + tay trong suốt; đã dựng, chờ build kế. Hành vi follower chưa quan sát được trên thiết bị vì cần save có ≥ 2 súng trang bị.- Tham số đội hình
Cat_manager— ĐÃ ĐỌC (scene:FormationRadius 2.5,SeparationRadius 1.8,SeparationStrength 3; doc 11 §1.1). Tham số boidsPet_Ingame_manager(PetSeparationRadius,StartMoveDistance…) — vẫn UNKNOWN, chưa trích từ scene. - Ánh xạ 13 skeleton Spine → pet nào trong game (
Pet.Skin_list/_skinNum) — UNKNOWN; cần tríchPet_managertừ scene. Cat.AIStatecó bao nhiêu trạng thái và giá trị — UNKNOWN một phần; suy được từ tên method, enum chưa trích.- Nơi
Cat_manager.is_Shoot_Muteđược đọc (Cat.AttackRVA0x2AE9624/Cat_manager.Update) — UNKNOWN, chưa dựng lại (doc 04 §5). Pet_manager(43 method) — logic mở khoá / khoá slot pet (IsPetLocked,LockOwned,UnlockGrouplà khoá do người chơi tự đặt, UNLOCK_GATES §3) — thân chưa khảo sát.- Hai GUID script đông nhất trong scene (7 991 và 4 957 instance) không ánh xạ được sang
Assembly-CSharp; ứng viên gồm spine-unity — Không được đoán (doc 03 §5, doc 08 §5).
Đọc thêm
../framework/05-characters-pets-enemies.md— toàn bộ bằng chứng mèo / pet / boids../framework/04-sound.md§3.5 —Mute_Panelvà cờis_Shoot_Mute../framework/06-juice-game-feel.md— chống rung / làm mượt ở tầng cảm-giác../framework/08-architecture-and-scale.md§2.4/§2.6 — Spine 4.1, atlasCat, package spine-unity[liveops catalog: UNLOCK_GATES.md]— Pet mở ởStage_Value_Max ≥ 110(Stage 12-1); khoá slot pet do người chơi đặtgap list (nội bộ)— G130, G131