commit ebf36351d5e6cbda5294b978a86a744dac805ce3
Author: jens <jens.se@icloud.com>
AuthorDate: Tue Aug 25 16:25:15 2026 +0200
Commit: jens <jens.se@icloud.com>
CommitDate: Tue Aug 25 16:25:15 2026 +0200
Spread fallback player spawns into a grid instead of a single line
P_SpawnFallbackPlayer (used for players 5+, since vanilla maps only
ever define 4 player starts) placed every extra player along a single
diagonal line offset from one start point (24 units per player in both
x and y). With up to 25 players that line ran long enough for players
to end up stacked closely enough to visibly overlap, and for later
players in the line to run into wall geometry.
Replaced with a grid (5 columns, expanding downward in rows, 48-unit
spacing - comfortably more than twice MT_PLAYER's 16-unit radius)
anchored at a single shared origin point for every fallback player
this level, rather than one grid per available start: most maps put
all 4 coop starts in the same small room, so independent per-start
grids were still landing on top of each other in testing. Verified
with 25 local splitscreen players in E1M1 - no more players seeing
another player's model fill their entire view at point-blank range.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
src/doom/p_setup.c | 76 ++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 60 insertions(+), 16 deletions(-)
diff --git a/src/doom/p_setup.c b/src/doom/p_setup.c
index 09748543..6b24a89d 100644
--- a/src/doom/p_setup.c
+++ b/src/doom/p_setup.c
@@ -117,6 +117,18 @@ mapthing_t* deathmatch_p;
mapthing_t playerstarts[MAXPLAYERS];
boolean playerstartsingame[MAXPLAYERS];
+// State for P_SpawnFallbackPlayer's grid, reset alongside
+// playerstartsingame[] at level load: which of the 4 vanilla player
+// starts it's using as the grid's shared origin (picked once, the
+// first time it's needed, and cached rather than re-searched every
+// call), and how many fallback players have been placed in the grid so
+// far. A single shared grid rather than one per start avoids two
+// starts' independent grids landing on top of each other when the
+// vanilla starts themselves are close together, as they usually are
+// (most maps put all 4 coop starts in the same small room).
+static int fallback_base_start = -1;
+static int fallback_grid_count;
+
@@ -370,6 +382,8 @@ void P_LoadThings (int lump)
{
playerstartsingame[i] = false;
}
+ fallback_base_start = -1;
+ fallback_grid_count = 0;
mt = (mapthing_t *)data;
for (i=0 ; i<numthings ; i++, mt++)
@@ -431,36 +445,66 @@ void P_LoadThings (int lump)
W_ReleaseLumpNum(lump);
}
+// Grid used to spread fallback players out from their shared origin
+// point - see P_SpawnFallbackPlayer. 48 map units comfortably clears
+// twice MT_PLAYER's 16-unit radius, so adjacent grid cells don't
+// overlap.
+#define FALLBACK_GRID_SPACING 48
+#define FALLBACK_GRID_COLS 5
+
//
// P_SpawnFallbackPlayer
// Spawns playernum at a fallback start position, reusing one of the
// map's own defined player starts (doomednum 1-4, tracked in
-// playerstartsingame[]) with a small position offset so extras don't
-// stack on top of each other. Used both by the loop above (players 5-10
-// at level load) and by a player joining local splitscreen mid-level
-// (see G_AddLocalPlayer in g_game.c). Returns false if the map has no
-// player start at all to fall back to.
+// playerstartsingame[]). Vanilla only ever defines 4 of these, but
+// MAXPLAYERS is now up to 25 (see doomstat.h), so most players calling
+// this are "extra" - arranged in a grid of rows spreading out from
+// whichever start is used first (cached in fallback_base_start, same
+// one for every fallback player this level, not one grid per start:
+// most maps put all 4 coop starts in the same small room, so
+// independent per-start grids would still often land on top of each
+// other) rather than lining every extra player up single-file, which
+// both keeps a large player count from packing solidly enough to
+// visibly overlap and keeps it from running in a straight line into
+// nearby wall geometry the way a single long offset eventually would.
+// Used both by the loop above (players 5+ at level load) and by a
+// player joining local splitscreen mid-level (see G_AddLocalPlayer in
+// g_game.c). Returns false if the map has no player start at all to
+// fall back to.
//
boolean P_SpawnFallbackPlayer (int playernum)
{
- int j;
+ int j, k, row, col;
mapthing_t spawnthing;
- for (j = 0; j < 4; j++)
+ if (fallback_base_start < 0)
{
- if (playerstartsingame[j])
+ for (j = 0; j < 4; j++)
{
- spawnthing = playerstarts[j];
- spawnthing.type = playernum + 1;
- spawnthing.x += (playernum - j) * 24;
- spawnthing.y += (playernum - j) * 24;
- playerstarts[playernum] = spawnthing;
- P_SpawnPlayer(&spawnthing);
- return true;
+ if (playerstartsingame[j])
+ {
+ fallback_base_start = j;
+ break;
+ }
}
}
- return false;
+ if (fallback_base_start < 0)
+ return false;
+
+ // Grid starts one cell out from the real start (k=0 would otherwise
+ // land exactly on top of the real player already standing there).
+ k = fallback_grid_count++;
+ row = (k + 1) / FALLBACK_GRID_COLS;
+ col = (k + 1) % FALLBACK_GRID_COLS;
+
+ spawnthing = playerstarts[fallback_base_start];
+ spawnthing.type = playernum + 1;
+ spawnthing.x += (col - FALLBACK_GRID_COLS/2) * FALLBACK_GRID_SPACING;
+ spawnthing.y += row * FALLBACK_GRID_SPACING;
+ playerstarts[playernum] = spawnthing;
+ P_SpawnPlayer(&spawnthing);
+ return true;
}