02-combat-projectiles.md · Cập nhật lần cuối: 2026-09-05 08:39 +07 · sha256 nguồn 5d187ad4dc3502 — Chiến đấu & đạn (Combat / Projectiles)
Nguồn: chưng cất từ restore SOT
com.Chodun.CatGunner1.1.59 (BardCat). Mọi C# ở đây là L1 ANALYSIS — dựng lại clean-room từ ARM64 disassembly, không phải source gốc.
1. Câu hỏi tài liệu này trả lời
Một idle-shooter phải bắn liên tục, không cấp phát, va chạm với hàng nghìn mục tiêu, và hỗ trợ
nhiều "loại súng" có ngữ nghĩa va chạm khác nhau — tất cả trong một class đạn duy nhất.
Bullet.Update chạy 14 199 lần / 40 giây trong trace runtime; nó là hot path thật.
2. Stable truths
| Claim | Mức bằng chứng | Nguồn / provenance | Phạm vi |
|---|---|---|---|
Bullet là class đạn thật của game (22 method); BulletShooter không bao giờ chạy trong gameplay bình thường — là shooter legacy |
runtime-verified (negative) | [runtime trace: runtime_ranking.json]: Bullet.Update 14 199×, BulletShooter.* 0× |
Cảnh báo: đừng chọn target theo tên |
27 instance Bullet trong scene ⇒ pool cố định, không Instantiate mỗi phát |
scene-census | [index: scene_census.json] (Bullet = 27) |
Kiến trúc pooling |
Enum GunType (TypeDefIndex 8226) có đúng 7 giá trị: SINGLE=0, LASER=1, PIERCE=2, SHOTGUN=3, EXPLOSIVE=4, MISSILE=5, BLAST_SNIPER=6 |
metadata-proven | dump.cs:596519 |
Mô hình vũ khí |
EXPLOSIVE = 4 và MISSILE = 5 — suy ra từ test bit ((int)gunType & ~1) == 4 trong Bullet.Destroy, đã được metadata xác nhận độc lập |
disasm-proven + metadata-proven | Bullet.cs RVA 0x2B0954C; dump.cs:596519 |
Giá trị enum |
Chỉ số vũ khí sống trong Gun_manager.Gun_Info (nested, TypeDefIndex 8227): Name, type, InfVal Damage, AtkSpeed, ShotgunPelletCount, ShotgunSpreadAngle, ExplosionRadius, MissileExplosionRadius, MissileSpeed, MissileElevationMin/Max, BlastSniperExplosionRadius, PenetratingBulletLifetime |
metadata-proven | dump.cs:596533 |
Schema tuning vũ khí |
Bên bắn là Cat, không phải Gun: Cat.TrySpawnBulletEx(pos, dir, GunType, expRadius, lifetime, dmg, dmgStr, isCri) (RVA 0x2AEDFB0), Cat.TryFireShotgun(...) (0x2AEE134), Cat.TryFireMissile(spawnPos, target, spreadAngle, ...) (0x2AEE32C), Cat.ApplySpread(dir, maxAngle) (0x2AEDF20) |
metadata-proven | dump.cs:593002 (class Cat) |
Đường bắn |
Dispatch va chạm nằm trong OnTriggerEnter (RVA 0x2B089C8); 5 method Hit* bị compiler inline vào nó |
disasm-proven | G130 trong gap list (nội bộ); [disasm: Bullet.c] |
Đọc disasm |
Đạn thẳng bay bằng Rigidbody.linearVelocity; chỉ missile được di chuyển thủ công trong Update |
disasm-proven | Bullet.SetupLinear RVA 0x2B07CF8, Bullet.Update RVA 0x2B0816C |
Chọn mô hình chuyển động |
Quỹ đạo missile là parabol y = lerp(y0,y1,t) + (1-t)·t·(arcHeight·4), đỉnh tại t = 0.5 |
disasm-proven | Bullet.Update RVA 0x2B0816C |
Công thức bay |
_msArcHeight = khoảng cách ngang (x,z) × arcHeightMultiplier (mặc định 0.4f) |
disasm-proven | Bullet.SpawnMissile RVA 0x2B07E80 |
Tỉ lệ cung |
Điểm va chạm cho đạn thẳng được tua ngược một bước vật lý: transform.position - rb.linearVelocity * Time.fixedDeltaTime, rồi other.ClosestPoint(...) |
disasm-proven | Bullet.OnTriggerEnter RVA 0x2B089C8 |
Chống lệch VFX |
Chống trúng lặp bằng HashSet<int> _hitIds chứa Collider.GetInstanceID() |
disasm-proven | field @0xD0; dùng trong OnTriggerEnter, HitLaser, HitPierce, Explode |
Pierce/AoE |
AoE = Physics.OverlapSphere(center, explosionRadius, enemyLayer), bỏ qua collider không activeInHierarchy và collider đã có trong _hitIds |
disasm-proven | Bullet.Explode RVA 0x2B08404 |
Nổ |
PIERCE: xuyên tối đa 2 mục tiêu; giết được thì đi tiếp, không giết được và đã _pierceCount >= 2 thì dừng |
disasm-proven (nhánh có rủi ro đảo, cần runtime-verify) | Bullet.HitPierce RVA 0x2B08E70; _evidence/Bullet/README.md |
Ngữ nghĩa xuyên |
LASER: gây damage mỗi enemy đúng 1 lần, sinh FX 1 lần, không bao giờ chết vì trúng enemy; chỉ chết khi trúng non-enemy |
disasm-proven | Bullet.HitLaser RVA 0x2B08D1C |
Ngữ nghĩa laser |
BLAST_SNIPER: cú trúng đầu tiên nổ; mục tiêu sống sót ⇒ dừng, mục tiêu chết ⇒ chuyển sang hành vi pierce |
disasm-proven | Bullet.HitBlastSniper RVA 0x2B09124 |
Ngữ nghĩa lai |
Damage KHÔNG đi thẳng vào Enemy: Bullet.Hit* → Gamemanager.Damage_Collider → Damage_Tag → Enemy.Damaged |
disasm-proven + runtime-verified | _evidence/Gamemanager/combat_damage.cs.evidence; trace: Damage_Collider 207×, Damage_Tag 207×, Enemy.Damaged 202× |
Kiến trúc pipeline |
Phân loại mục tiêu bằng tag: "Enemy" nhận Damage của đạn; "Ore" nhận cố định (InfVal)1 mỗi lần trúng |
disasm-proven | Damage_Tag RVA 0x2A503F0; literal @0x5E8A1E0 / 0x5E91930 |
Mục tiêu không phải quái |
Gamemanager.is_Enemy (static @0x168) được ghi lại sau mỗi lần Damage_Collider |
disasm-proven | RVA 0x2A50048 |
Trạng thái chia sẻ |
Enemy.Damaged và Enemy.Die đều có guard if (!Gamemanager.is_gaming) return; |
disasm-proven (đã sửa bug reconstruct) | G132b trong gap list (nội bộ); Enemy.cs |
Bài học then chốt — xem §3.5 |
Enemy.Type enum: Tree=0, Tree_SpeedRun=1, BossMonster=2, Monster=3, Ore=4, RaidBoss=5 |
disasm-proven | [reconstructed: Enemy.cs] |
Phân loại mục tiêu |
Chỉ Type.Monster nhận knockback: damageRatio = (float)(Damage / Health_Max) → Monster.Apply_Knockback(damageRatio) |
disasm-proven | Enemy.Damaged RVA 0x2AFEF9C |
Game feel (xem doc 06) |
Vòng đời đạn dùng Invoke("Destroy", t) / Invoke("Deactivate", delay), KHÔNG dùng coroutine |
disasm-proven | SpawnEx RVA 0x2B07AD0, DieAndWait RVA 0x2B08858 |
Vòng đời |
Thời gian sống đạn thẳng = min(50 / max(Speed, 0.1), lifetime); Speed mặc định 45f ⇒ ~1.11 s |
disasm-proven | .ctor + SpawnEx |
Tuning |
Scene ghi đè Speed = 70 (ctor 45 chỉ là mặc định — quy tắc G902) ⇒ đạn thẳng sống 50/70 ≈ 0.71 s; _deactivateDelay = 1f |
scene-YAML + disasm-proven | G385 | Giá trị ship |
Thiết lập trigger đạn ↔ địch (đọc từ scene + binary): Bullet layer 8, SphereCollider trigger r 0.6, Rigidbody không kinematic, interpolate, va chạm DISCRETE; Enemy_Coll layer 7, BoxCollider tĩnh ~20×12×14; EnemyColl không có method (chỉ giữ enemyScript+0x20); Damage_Tag đọc tag của enemyScript (932 Enemy / 1000 Ore / 1 boss Untagged); Bullet.Update không raycast quét — chỉ trigger. Ma trận va chạm SOT: Bullet(8)↔Enemy(7) bật (SOT[8] = 0xFFFFF881, SOT[7] = 0xFFFFF7FF), Cat(6)↔Enemy(7), Bullet(8)↔Monster(9) |
scene-YAML + disasm-proven + build-measured | G385, G383 | Cặp trigger hợp lệ |
Bullet.OnTriggerEnter (0x2B089C8) đối chiếu từng lệnh với bản dựng lại: cổng is_Die, _hitIds.Contains, ClosestPoint, CompareTag, dispatch 7 nhánh theo gunType (SINGLE = 0) — khớp |
disasm-proven | G385 | Loại trừ code |
Đạn thẳng đi bằng rb.linearVelocity (SetupLinear 0x2B07CF8: isKinematic = false, đặt vị trí + hướng, velocity = forward × Speed, SetActive, particle); SpawnEx hẹn Invoke("Destroy", min(50/Speed, lifetime)); ResetState (0x2B07C18) bật lại collider; Update thoát ngay trừ khi _isMissile — missile kinematic, dời bằng transform. Bản dựng lại khớp; xuyên hầm khó xảy ra (box ~20×12×14 vs 1.4 u/bước) |
disasm-proven | G391 | Loại trừ code |
Enemy/Tree/Ore..ctor là tail-call 8 byte vào base ctor ⇒ không có khởi tạo bị bỏ rơi; Enemy.Damaged (0x2AFEF9C) có đúng 2 thoát sớm (is_Die, !is_gaming); Enemy.Spawn đặt is_Die = false; Tree không có Damaged (damage đi Enemy.Damaged → Die → Die_Reload → Tree.Die_Reload) |
disasm-proven | G391 | Loại trừ code |
Ba cổng "sức mạnh" theo chế độ (G395): Power_Boss_return = Σ mèo active ExpectedDamage_Boss_return() × 100 / (int)(AtkCool_Boss_return() × 100), ToPrecision(5); Power_Monster_return = như trên × hệ số mục tiêu theo loại súng; Power_SpeedRun_return (mỗi phát) = (Damage × (100 − criPct) + CriDmg × criPct) / 100 với criPct = clamp(Math.Round(CriPer × 100) [làm tròn NGÂN HÀNG], 0, 100) |
disasm-proven | G395 | Sức mạnh hiển thị theo chế độ |
GunTargetMultiplier_return: SINGLE 1 · LASER 7 · PIERCE 5 · SHOTGUN max(pellets, 1) · EXPLOSIVE 5 · MISSILE 5 · BLAST_SNIPER 10 — khác bảng MultiTargetCount của simulator offline (Laser/Pierce 10, nổ 5, sniper 5): hai bảng cho hai mục đích (hiển thị sức mạnh vs mô phỏng clear) |
disasm-proven | G395; so doc 01 §5.8 luật 8 | Hai bảng, đừng gộp |
Trong cửa sổ trace, mọi phát bắn đều là HitSingle (207×) ⇒ súng khởi đầu là SINGLE; các nhánh khác chưa được runtime-verify |
runtime-observed (negative) | runtime_ranking.json |
Giới hạn bằng chứng |
Cụm bắn của Cat đã dựng lại (batch B, đợt 4): Attack 0x2AE9624, UpdateHandRotation 0x2AE7184, TryFireMissile 0x2AEE32C, TryFireShotgun 0x2AEE134, ApplyLeftHandTowardGunPivot 0x2AED960, UpdateSpreadState 0x2AE80B0, TrySpawnBulletEx 0x2AEDFB0, CalcBulletDir 0x2AEDDF4, GetFirePosition 0x2AEDD04, CalcBulletSpawnPos 0x2AEE644, ApplySpread 0x2AEDF20, ApplyHandZ 0x2AE64E8 — 12 method, 45 marker RVA |
disasm-proven | G363 | Đường bắn hoàn chỉnh |
R1 (phủ định, đã quét 12 RVA): không có nhánh weapon_02 / AD_Type nào trong cụm bắn ⇒ không gì route qua IAdService; súng quảng cáo chỉ chạm Gun_manager (doc 10 §4.6) |
disasm-proven (negative) | G363 | Ranh giới R1 |
Pool đạn — hai lượt "cướp ô": Polling_manager.Object_Polling_Smart (0x2AC6900) + Object_struct.Object_Polling_Smart (0x2AC6934): con trỏ xoay vòng, lượt 1 lấy object inactive đầu tiên; không có ⇒ lượt 2 cướp ô Bullet đã chết đầu tiên (huỷ Invoke("Deactivate") đang chờ của nó); chỉ trả null khi mọi viên còn sống ⇒ Cat.Update hoàn lại cooldown (không mất phát bắn). Hai method này từng là return null — không viên đạn nào có thể spawn mà không một ngoại lệ (R13) |
disasm-proven | G363 | Pooling |
| Spread = bloom: tăng mỗi phát (trừ shotgun), rỉ dần khi không có mục tiêu; mục tiêu mới ⇒ reset spread và cooldown | disasm-proven | UpdateSpreadState 0x2AE80B0; G363 |
Cảm giác bắn |
Missile trượt bằng tán xạ ĐIỂM CHẠM, không phải hướng bay: điểm nổ được rải trong bán kính range × tan(spread) quanh mục tiêu |
disasm-proven | TryFireMissile; G363 |
Ngữ nghĩa spread cho đạn dẫn |
Nòng súng được chiếu dọc CamForward lên mặt phẳng y = BulletSpawnY = 2 (chiều cao màn hình cố định) ⇒ đạn xuất phát cùng độ cao dù mèo đứng đâu: spawn = pos + CamForward × ((2 − pos.y) / CamForward.y) |
disasm-proven | CalcBulletSpawnPos 0x2AEE644; G363 |
Hình học iso |
VÌ SAO ĐẠN KHÔNG TRÚNG (G424 → G400): phép chiếu trên chia cho CamForward.y. Bản dựng lại có CamForward = Euler(0, −45, 0)·forward (y = 0) thay vì Euler(40, −45, 0)·forward = (−0.5417, **−0.6428**, 0.5417) của binary ⇒ chia cho 0 ⇒ mọi viên đạn spawn ở ±∞, bay ngoài mọi collider, OnTriggerEnter không bao giờ được gọi (khớp 0 dòng trace), rồi hết hạn theo lifetime. Đường này chạy mỗi phát vì Shoot_Random_trans rỗng trên cả 3 mèo trong scene. Một hằng số sai một thành phần qua được mọi guard (compile, singleton, meta, event) và mọi loại trừ (ma trận va chạm, thứ tự script, ctor, rb.velocity) — xem doc 08 §3.6 |
disasm-proven (Internal_FromEulerRad 0x2AEF350: S0 = 40°, S1 = −45°) + build-measured (batch 20) |
G400, G405 | G424 ĐÃ ĐÓNG: SetupLinear y = 2.000 hữu hạn; 104/104 OnTriggerEnter → Damage_Tag → Enemy.Damaged trong 30 s; cây đổ (fresh TreeCount 29→26→22→14, gần sạch ở 65 s; save thật 141→121→103→85), tờ tiền + hạt lá, thanh máu trên xương rồng |
| Tiếng bắn bị chặn bởi hai cờ: Cat_manager.is_Shoot_Mute AND Setting.is_eco | disasm-proven | Attack 0x2AE9624; G363 | Mute kép (doc 04) |
| Chuỗi 100 sát thương của súng khởi đầu — xác minh từ số scene: Gun_list[0].Damage 100 × Power_balance[0] 100/100 × Rebirth 100/100 × Skin 100/100 × (Fish 0 + 100)/100 × Relic 100/100 = 100; đường TrySpawnBulletEx → Bullet.SpawnEx → OnTriggerEnter → HitSingle → Damage_Collider đã đầy đủ. Hai bẫy base-100 (Rebirth_manager.Damage_return = 0, Relic.Dmg_Value = 0) từng đưa tích về 0 | disasm-proven + scene-YAML (derived) | G363, G361; R13 | Bài học then chốt |
3. Mô hình hành vi
3.1 Kiến trúc tổng thể — một class đạn, dispatch theo enum
Gun_manager / Cat_manager (quyết định bắn)
|
v
Polling_manager.Object_Polling(Objects_Type.Bullet) -> lấy Bullet từ pool
|
v
Bullet.SpawnEx(...) hoặc Bullet.SpawnMissile(...)
Skin_Reload(gunNum) -> chọn ParticleSystem theo skin súng
Mesh_Reload(gunNum) -> bật Bullet_Mesh tương ứng
ResetState() -> xoá _hitIds, _pierceCount, cờ; bật lại collider
SetupLinear() -> rb.linearVelocity = forward * Speed
Invoke("Destroy", t)
|
v (va chạm)
Bullet.OnTriggerEnter(other)
dedup theo _hitIds
hitPos = other.ClosestPoint(rewound position)
switch (gunType) -> HitSingle | HitLaser | HitPierce | HitExplosive | HitBlastSniper
|
v
Gamemanager.Damage_Collider(coll, Damage, hitPos, Damage_str, is_Cri)
-> Damage_Tag(coll, Damage)
-> coll.GetComponent<EnemyColl>().enemyScript (Enemy)
-> tag "Enemy" ? Enemy.Damaged(Damage)
: tag "Ore" ? Enemy.Damaged((InfVal)1)
: false
-> nếu trúng: is_Cri ? Critical_Txt_Spawn(str, pos) : Damage_Txt_Spawn(str, pos)
|
v
Enemy.Damaged -> Health_Now -= Damage
-> (Monster) Apply_Knockback(Damage / Health_Max)
-> Slider_Reload()
-> Health_Now <= 0 ? Die() : _bounce.Bounce_Start()
Enemy.Die -> Gamemanager.Get_Money(Money, transform.position)
-> Die_Reload() (dispatch theo Enemy.Type)
Điểm kiến trúc quan trọng: Bullet không bao giờ gọi Enemy.Damaged trực tiếp.
Nó chỉ biết Collider. Toàn bộ "collider này là gì, nhận bao nhiêu damage" nằm ở một chỗ duy nhất
(Damage_Tag). Nhờ vậy thêm một loại mục tiêu mới (Ore, Tree, Boss) không cần chạm vào code đạn.
3.2 Ngữ nghĩa từng GunType
| GunType | Handler | Ngữ nghĩa |
|---|---|---|
SINGLE |
HitSingle |
Damage → FX → chết. Đạn 1 mục tiêu. |
SHOTGUN |
HitSingle |
Cùng handler với SINGLE. Hiệu ứng "shotgun" nằm ở phía bắn, không ở phía va chạm: Cat.TryFireShotgun bắn Gun_Info.ShotgunPelletCount viên, mỗi viên lệch góc theo Gun_Info.ShotgunSpreadAngle qua Cat.ApplySpread(dir, maxAngle). |
LASER |
HitLaser |
Trúng enemy: thêm vào _hitIds, damage, SpawnFX_Once, đi tiếp vô hạn. Trúng non-enemy: FX + chết. |
PIERCE |
HitPierce |
Trúng non-enemy ⇒ chết. Trúng enemy: _pierceCount++; tính trước kill = Damage >= e.Health_Now; damage; SpawnFX_Once; nếu _pierceCount >= 2 && !kill ⇒ chết. |
EXPLOSIVE |
HitExplosive |
Explode(hitPos) → FX → chết. |
MISSILE |
HitExplosive |
Như EXPLOSIVE, nhưng bay parabol và cũng nổ khi hết thời gian bay (t >= 1). |
BLAST_SNIPER |
HitBlastSniper |
Lần trúng đầu: _firstHitDone = true, tính survived, Explode, SpawnFX_Once; survived ⇒ chết, ngược lại tiếp tục. Các lần sau: uỷ quyền cho HitPierce. |
Điều đáng học: 7 loại vũ khí, 5 handler, 1 class. Sự khác biệt không nằm ở prefab riêng hay
class con — nó nằm ở một switch trên enum + vài field trạng thái (_hitIds, _pierceCount,
_firstHitDone, explosionRadius). Đây là lý do pool 27 viên đạn phục vụ được cả game.
3.3 Missile — parabol tường minh
// L1 ANALYSIS — RVA 0x2B0816C
_msTimer += Time.deltaTime;
float t = Mathf.Clamp01(_msTimer / _msDuration);
float x = _msStart.x + (_msEnd.x - _msStart.x) * t;
float z = _msStart.z + (_msEnd.z - _msStart.z) * t;
float linearY = _msStart.y + t * (_msEnd.y - _msStart.y);
float y = linearY + (1f - t) * (t * (_msArcHeight * 4f)); // đỉnh tại t = 0.5
transform.position = new Vector3(x, y, z);
// hướng mũi theo vector di chuyển thực tế
Vector3 move = newPos - oldPos;
if (move.sqrMagnitude > 0.0001f) transform.forward = move.normalized;
LastPos = oldPos; // dùng làm điểm gốc tính hitPos
if (t >= 1f) { Explode(pos); SpawnFX(pos); DieAndWait(); }
Hệ số 4f là chuẩn hoá: (1-t)·t đạt cực đại 0.25 tại t = 0.5, nên ×4 khiến _msArcHeight
đúng bằng độ cao đỉnh cung thực tế tính từ đường thẳng nối. Đây là cách viết parabol mà tham số
có ý nghĩa vật lý trực tiếp — đáng chuẩn hoá vào framework.
Missile là isKinematic = true; đạn thẳng là isKinematic = false. Cùng một class, hai chế độ
chuyển động, chọn bằng cờ _isMissile.
3.4 Điểm va chạm "tua ngược"
Vector3 basePoint = _isMissile
? LastPos // vị trí frame trước
: transform.position - rb.linearVelocity * Time.fixedDeltaTime; // lùi 1 bước vật lý
Vector3 hitPos = other.ClosestPoint(basePoint);
Với đạn nhanh, OnTriggerEnter được gọi khi đạn đã xuyên vào trong collider, nên
transform.position nằm bên trong mục tiêu và VFX nổ ra ở giữa thân quái trông sai.
Tua ngược đúng một bước rồi lấy ClosestPoint cho ra điểm trên bề mặt, phía đạn bay tới.
Đây là một thủ thuật game-feel rẻ tiền và hiệu quả.
3.5 Bug is_gaming — vì sao nó quan trọng
Trong quá trình dựng lại, Enemy.Damaged và Enemy.Die ban đầu thiếu guard
if (!Gamemanager.is_gaming) return;. Bug lộ ra khi đối chiếu chéo với Ore.Die_Reload
(vốn có guard rõ ràng trong disasm). Đã sửa và ghi vào G132b.
Vì sao đây là bài học framework chứ không chỉ là một lỗi vặt:
- SOT chỉ có một scene duy nhất (
Main.unity, 481 MB) chứa cả gameplay lẫn toàn bộ menu/popup. Quái, đạn và collider luôn tồn tại, kể cả khi người chơi đang mở shop. - Không có "unload scene gameplay" để bảo vệ. Thứ duy nhất ngăn combat chạy trong menu là một
cờ boolean toàn cục
Gamemanager.is_gamingđược kiểm tra ở từng entry-point. - Thiếu một guard ⇒ quái vẫn ăn damage và vẫn rơi tiền khi người chơi đang ở menu ⇒ lỗi kinh tế im lặng, không crash, cực khó phát hiện qua smoke test.
Framework rút ra: khi kiến trúc là "một scene, mọi thứ luôn sống", cờ trạng thái game phải là một
guard bắt buộc ở tầng framework (ví dụ một base class hoặc một IDamageable gate), không phải một
dòng if mà mỗi lập trình viên phải nhớ chép vào.
3.6 Ore = 1 damage cố định
if (tag == "Enemy") { e.Damaged(Damage); return true; }
if (tag == "Ore") { e.Damaged((InfVal)1); return true; }
Ore (1 000 instance trong scene) là mục tiêu đếm số lần đánh, không phải mục tiêu theo sức mạnh:
máu 3 (skin 0) hoặc 5 (skin ≠ 0), mỗi lần trúng trừ đúng 1. Thưởng cũng tách khỏi Enemy: Ore.Spawn
truyền Money = 0 cho Enemy và giữ _diaValue riêng — 1 dia (skin 0) hoặc 10 dia (skin ≠ 0),
cấp khi chết; tham số int skinNum thực chất là boolean (G343). Nghĩa là tốc độ đào phụ thuộc
tốc độ bắn, không phụ thuộc damage. Đây là một cần gạt tuning riêng, cố ý tách khỏi chuỗi
multiplier damage — một mẫu hay cho tài nguyên phụ trong idle (chống việc một stat làm phẳng mọi thứ).
4. Framework takeaway
-
Một
Projectileduy nhất + enum dispatch, thay vì cây kế thừa. Trạng thái riêng của từng ngữ nghĩa nằm gọn trong vài field;ResetState()phải xoá tất cả chúng khi lấy từ pool. Đây là hợp đồng quan trọng nhất của pooling: một object tái sử dụng phải trở về trạng thái xuất xưởng, không được mang theo_hitIdscủa lần bắn trước. -
Tách "va chạm" khỏi "sát thương".
IProjectile -> chỉ biết Collider và điểm va chạm IDamageRouter-> biết Collider nào là gì, và bao nhiêu damage IDamageable -> biết mất máu thì làm gìThêm loại mục tiêu = sửa router, không sửa đạn. -
Dedup theo
GetInstanceID()trong mộtHashSetlà đủ và rẻ cho pierce + AoE. Dùng chung một tập cho cảOnTriggerEntervàOverlapSpherekhiến vụ nổ không đánh trùng mục tiêu vừa bị đạn xuyên. -
Chuẩn hoá điểm va chạm bằng rewind +
ClosestPoint. Framework nên cung cấp sẵnResolveHitPoint(collider, previousPosition). -
Parabol tham số hoá theo độ cao đỉnh (
(1-t)·t·h·4), vì designer nghĩ bằng "cung cao bao nhiêu", không nghĩ bằng hệ số bậc hai. -
Guard trạng thái game phải ở tầng framework. Trong kiến trúc một-scene, đây là lớp phòng thủ duy nhất giữa gameplay và menu.
-
Đừng suy ra hot path từ tên class.
BulletShooternghe như trái tim của game và có 0 lần gọi;Bulletchạy 14 199 lần. Runtime ranking là oracle, tên là phỏng đoán.
5. Negative evidence & UNKNOWN
| Mục | Trạng thái |
|---|---|
Nhánh PIERCE / BLAST_SNIPER ("giết ⇒ đi tiếp, sống ⇒ dừng") |
Chưa runtime-verify. _evidence/Bullet/README.md đánh dấu đây là rủi ro đảo nhánh tinh vi nhất. Cách khoá: trang bị súng pierce trên thiết bị, hook Damage_Collider + _pierceCount. |
LASER, EXPLOSIVE, MISSILE, BLAST_SNIPER, SHOTGUN |
Chưa quan sát chạy. Cửa sổ trace 40 s chỉ có súng SINGLE (HitSingle 207×). |
Bullet.cs (22 method) |
Chưa compile-test, chưa runtime-verify. Đang ở _evidence/, không nằm trong tập shipped (G124). |
Giá trị số cụ thể của Gun_Info (Damage, AtkSpeed, ExplosionRadius… của từng khẩu) |
UNKNOWN — schema đã biết đầy đủ (metadata), nhưng dữ liệu nằm trong scene Main.unity + Gun_manager.Balance_Reload() (chưa dựng lại) và có thể bị ghi đè bởi bảng từ xa (§6). |
enemyLayer / OreLayer cụ thể |
UNKNOWN — là LayerMask serialize trên Cat, chưa trích từ scene YAML. |
Damage_Collider_list (bản batch cho AoE) |
Chữ ký đã biết (RVA 0x2A4FF04 / 0x2A5017C), thân là vòng for hiển nhiên nhưng chưa dựng lại. Explode dùng bản per-collider. |
| Không quan sát thấy | BulletShooter không hề chạy — đây là negative evidence có giá trị, đã kiểm bằng hook native offset trên toàn bộ 1 279 method mục tiêu. |
FXThrottle.CanSpawn |
Có tồn tại và được Bullet.SpawnFX gọi, nhưng thân method chưa dựng lại — xem doc 03. |
Bắn đã chạy trên thiết bị (batch 15): súng lục bắn liên tục, tracer chạm mục tiêu — nhưng trên save đã nạp không quái nào mất máu / không tiền / không Game_Win (120 s) |
MỞ (G424). G423 (save mới 0 damage) đã đóng — Relic_manager.Start/Skin_manager.Start (G374). Probe batch 16 (G381): is_gaming = true 32/32, đạn sống, mục tiêu có, không enemy nào mất máu. Batch 17 (G386): ma trận va chạm layer đã sửa về SOT — vẫn damaged = 0 ở 32/32 mẫu ⇒ cả hai hướng cài đặt đã loại trừ; code phía trigger/dispatch cũng khớp từng lệnh (G385). ĐÃ ĐÓNG (G405, batch 20): sau khi sửa CamForward, 104/104 va chạm đi trọn chuỗi OnTriggerEnter → Damage_Tag → Enemy.Damaged trong 30 s, cây đổ, tiền rơi — game chơi được. Chuỗi sát thương giờ xác minh trên thiết bị (qua số cây đổ), không chỉ từ số. |
6. Raw evidence
[evidence codegen: README.md], Bullet.cs}— dựng lại đủ 22 method[evidence codegen: combat_damage.cs.evidence]—Damage_Collider× 2 +Damage_Tag[disasm: Bullet.asm], Bullet_Update.c, Bullet_Update.decompiled.c}[disasm: Bullet.c], Bullet_fixed.c, Enemy.c}[reconstructed: Enemy.cs], EnemyColl.cs, Bullet_Mesh.cs, Gun.cs, BulletShooter.cs}[evidence codegen: rule_statics.cs.evidence]— enumGunTypeđầy đủ[runtime trace: runtime_ranking.json]— 505 method đã chạy, kèm số lần- Gaps: G124, G130, G132b, G133, G134 trong
gap list (nội bộ)