Mechanic · combat · rail shooter, shoot 'em up
Lock-on shot
A charged shot that locks onto an enemy before it fires and then homes in on it.
On this page
A lock-on shot finds its own target. In Star Fox 64 holding A charges the shot and sends a search shot ahead; the first enemy it passes near is locked, and the next press of A fires a shot that steers toward it1.
- Search and homeStar Fox 64 (1997). Holding A charges to 21 and sends search shots; a press at 10 or more, or with an enemy locked, fires a shot that steers toward the lock.1
Games
| Game | Year | How | Note |
|---|---|---|---|
| Star Fox 64 | 1997 | Search and home | holding A charges to 21 and sends search shots; a press at 10 or more, or with an enemy locked, fires a shot that steers toward the lock |
Every claim about this mechanic, by game (1)
References
- ^ abIn the US 1.1 version of Star Fox 64, holding A counts a charge counter up by 1 a player update to a cap of 21, and an update without A held counts it down by 1 (
Player_UpdateLockOn). A fresh press of A, whose own update first adds 1 to the counter, fires the charged shot (PLAYERSHOT_LOCK_ON) if an enemy is locked on or the counter is then above 10 (so 10 or more before the press). Either path fires only if the player's charged-shot slot (gPlayerShots[14 - player->num]) is free, holds a shot of another type, or holds a charged shot that has lost its target (the locked path) or grown past scale 1 (the counter path); a press that fires nothing sets the counter to 0. Once the counter is above 20, while A is held, nothing is locked, and Z and R aren't both held, it launches a search shot every fourth game frame, and the first targetable enemy within 300 units of that shot on each axis (200 in all-range mode) is locked on for a count of 20, which counts down only while A is up. Each update while its target stays locked, the charged shot turns toward it (PlayerShot_UpdateLockOnShot).Codesonicdcer/sf64revisedHow we know
3186bool Player_UpdateLockOn(Player* player) { ⋮ 3190 if (gInputHold->button & A_BUTTON) { 3191 gChargeTimers[player->num]++; 3192 if (gChargeTimers[player->num] > 21) { 3193 gChargeTimers[player->num] = 21; ⋮ 3201 ((gGameFrameCount % 4) == 0) && Player_CanLockOn(player->num)) { 3202 if (gChargeTimers[player->num] > 20) { ⋮ 3206 Player_SetupArwingShot(player, &gPlayerShots[i], 0.0f, 0.0f, PLAYERSHOT_LOCK_SEARCH, ⋮ 3217 if (gChargeTimers[player->num] != 0) { 3218 gChargeTimers[player->num]--;
src/engine/fox_play.c lines 3186–3220 at 4235c59 · upstream ↗ 3164bool Player_CanLockOn(s32 playerNum) { ⋮ 3169 if ((actor->obj.status == OBJ_ACTIVE) && (actor->lockOnTimers[playerNum] != 0)) { 3170 return false;
src/engine/fox_play.c lines 3164–3172 at 4235c59 · upstream ↗ 3222 if (gInputPress->button & A_BUTTON) { 3223 for (i = 0; i < ARRAY_COUNT(gActors); i++) { 3224 if ((gActors[i].obj.status == OBJ_ACTIVE) && (gActors[i].lockOnTimers[player->num] != 0)) { 3225 if ((gPlayerShots[14 - player->num].obj.status == SHOT_FREE) || 3226 (gPlayerShots[14 - player->num].obj.id != PLAYERSHOT_LOCK_ON) || 3227 ((gPlayerShots[14 - player->num].obj.id == PLAYERSHOT_LOCK_ON) && 3228 (gPlayerShots[14 - player->num].unk_60 != 0))) { ⋮ 3230 Player_SetupArwingShot(player, &gPlayerShots[14 - player->num], 0.0f, 0.0f, PLAYERSHOT_LOCK_ON, ⋮ 3239 break; ⋮ 3243 if (gChargeTimers[player->num] > 10) { 3244 if ((gPlayerShots[14 - player->num].obj.status == SHOT_FREE) || 3245 (gPlayerShots[14 - player->num].obj.id != PLAYERSHOT_LOCK_ON) || ⋮
src/engine/fox_play.c lines 3222–3260 at 4235c59 · upstream ↗ 69 gControllerPress[i].button = 70 (gControllerHold[i].button ^ sPrevController[i].button) & gControllerHold[i].button;
src/sys/sys_joybus.c lines 69–70 at 4235c59 · upstream ↗ 1758 if (gLevelMode == LEVELMODE_ON_RAILS) { 1759 lockRange = 300.0f; 1760 } else { 1761 lockRange = 200.0f; ⋮ 1765 if ((actor->obj.status == OBJ_ACTIVE) && (actor->info.targetOffset != 0.0f) && 1766 ((actor->lockOnTimers[shot->sourceId] == 0) && (fabsf(shot->obj.pos.x - actor->obj.pos.x) <= lockRange) && 1767 (fabsf(shot->obj.pos.y - (actor->obj.pos.y + actor->info.targetOffset)) <= lockRange) && 1768 (fabsf(shot->obj.pos.z - actor->obj.pos.z) <= lockRange))) { 1769 actor->lockOnTimers[shot->sourceId] = 20;
src/engine/fox_beam.c lines 1758–1769 at 4235c59 · upstream ↗ 2848 } else if (this->lockOnTimers[TEAM_ID_FOX] != 0) { 2849 if (!(gControllerHold[gMainController].button & A_BUTTON)) { 2850 this->lockOnTimers[TEAM_ID_FOX]--; ⋮ 2852 gChargeTimers[0] = 0;
src/engine/fox_enmy.c lines 2848–2852 at 4235c59 · upstream ↗ 2174 if (shot->unk_60 == 0) { ⋮ 2177 (actor->lockOnTimers[shot->sourceId] != 0)) { 2178 var_a3 = 1; 2179 actor->lockOnTimers[shot->sourceId] = 2; 2180 sLockOnPos = actor->obj.pos; ⋮ 2188 var_a3 = 1; ⋮ 2193 } else { ⋮ 2200 if (var_a3 != 0) { ⋮ 2218 Math_SmoothStepToAngle(&shot->vec_2C.y, sp58, 1.0f, shot->unk_50, 0.0f); 2219 Math_SmoothStepToAngle(&shot->vec_2C.x, sp5C, 1.0f, shot->unk_50, 0.0f); ⋮ 2221 } else { 2222 shot->unk_60 = 1;
src/engine/fox_beam.c lines 2174–2222 at 4235c59 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
Sources
Code
- sonicdcer and sf64 contributors. 2026. “sf64: a decompilation of Star Fox 64 (sonicdcer/sf64).” Commit 4235c59. sonicdcer/sf64 @ 4235c59 · licence, all uses