Skip to content

Commit efb424e

Browse files
committed
Improve weather system
1 parent b7b512e commit efb424e

12 files changed

Lines changed: 721 additions & 479 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# 1. 365-Day Seasonal Cycle
2+
3+
Date: 2025-12-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The application previously operated on a simple day/night cycle with no concept of long-term progression or seasons. The weather was static unless manually changed. A request was made to simulate a full year.
12+
13+
## Decision
14+
15+
We implemented a `day` (1-365) and `year` counter in the `WeatherSystem`. The year is divided into four distinct seasons based on day ranges:
16+
* **Winter**: Days 1-90
17+
* **Spring**: Days 91-180
18+
* **Summer**: Days 181-270
19+
* **Autumn**: Days 271-365
20+
21+
## Consequences
22+
23+
* **Positive**: Adds depth and variety to the gameplay loop; creates a sense of time progression.
24+
* **Negative**: Adds state complexity to the `WeatherSystem`.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# 2. Automatic Probabilistic Weather Transitions
2+
3+
Date: 2025-12-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
Weather needed to change dynamically without user input, but purely random selection (noise) would lack realism (e.g., snow in summer).
12+
13+
## Decision
14+
15+
We implemented a probabilistic approach where the `currentSeason` determines the likelihood of specific weather types. The system checks for a weather change every 2-4 in-game hours.
16+
17+
* **Winter**: High chance of Snow.
18+
* **Summer**: High chance of Sun.
19+
* **Spring/Autumn**: Mixed probabilities.
20+
21+
## Consequences
22+
23+
* **Positive**: Weather feels organic and season-appropriate.
24+
* **Negative**: Predictability is reduced; rigorous testing requires time acceleration or mocking.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# 3. 10-Minute Fixed Day Duration
2+
3+
Date: 2025-12-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The speed of the day/night cycle impacts the pacing of the game.
12+
13+
## Decision
14+
15+
The time scale is fixed such that 1 in-game day equals 10 minutes of real time. This is achieved by setting the time increment `hoursPerSec` to `0.04` (24 hours / 600 seconds).
16+
17+
## Consequences
18+
19+
* **Positive**: Provides a balanced cycle that is long enough to enjoy but short enough to see transitions during a play session.
20+
* **Negative**: Fixed scaling might not suit all players (though this is standard for this genre).
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# 4. Removal of Manual Weather Controls
2+
3+
Date: 2025-12-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
With the introduction of the automatic seasonal system, manual overrides (keyboard shortcuts 1, 2, 3) conflicted with the simulation logic (e.g., manually setting Sun during a calculated Snow period would be immediately overwritten or break immersion).
12+
13+
## Decision
14+
15+
We removed the manual keyboard event listeners and the corresponding on-screen UI instructions.
16+
17+
## Consequences
18+
19+
* **Positive**: Ensures the integrity of the seasonal simulation; cleans up the UI.
20+
* **Negative**: Developers and users lose the ability to instantly force weather states for debugging or screenshot purposes. A "Debug Mode" may need to be reintroduced later.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# 5. Sync Start Date with Real World
2+
3+
Date: 2025-12-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
Players want the in-game season to initially match the real-world season to enhance immersion. For example, if playing in December, the game should start in Winter.
12+
13+
## Decision
14+
15+
We query the system's current date (`new Date()`) at startup and calculate the day of the year (1-366). We initialize the game's `day` counter with this value.
16+
17+
## Consequences
18+
19+
* **Positive**: Immediate immersion boost; the game feels "live" and connected to reality.
20+
* **Negative**: A new player starting in Winter might face harder weather conditions (snow) immediately compared to starting in Summer.

docs/adr/0006-geometry-merging.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# 6. Geometry Merging for Chunk Performance
2+
3+
Date: 2025-12-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The game generates an infinite world using chunks. Each chunk contained hundreds of individual meshes for buildings, windows, road markings, and vegetation. This led to a very high number of draw calls (thousands per frame) and significant GPU overhead, preventing the game from maintaining a steady 60 FPS, especially on lower-end devices.
12+
13+
## Decision
14+
15+
We refactored the chunk generation system (`world.js` and `createCityChunk`) to implement **Geometry Merging**.
16+
17+
Instead of creating a `THREE.Mesh` for every single building part:
18+
1. We pass a `geoms` accumulator object to all building functions.
19+
2. Each function pushes `THREE.BufferGeometry` instances into arrays keyed by material type (e.g., `geoms.concrete`, `geoms.glass`).
20+
3. At the end of chunk generation, we use `BufferGeometryUtils.mergeGeometries` to combine all geometries of the same material into a single mesh.
21+
4. The final chunk contains only ~10-15 meshes (one per unique material) instead of hundreds.
22+
23+
## Consequences
24+
25+
### Positive
26+
* **Drastically Reduced Draw Calls**: From ~500+ per chunk to ~15 per chunk.
27+
* **Improved Frame Rate**: Significant CPU and GPU optimization.
28+
* **Consistent Visuals**: The visual output remains identical.
29+
30+
### Negative
31+
* **Loss of Individual Object Identity**: Individual buildings are no longer separate objects. We cannot easily interact with (e.g., click, move, destroy) a specific building instance anymore without complex raycasting logic on the merged mesh.
32+
* **Increased Memory during Generation**: All geometries must be held in memory arrays before merging.
33+
* **Slightly Slower Chunk Generation**: The merge operation itself is CPU intensive, but it happens once per chunk load and saves frame time forever after.
34+
35+
## Compliance
36+
* Implemented in `src/world.js`.

docs/adr/0007-object-pooling.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# 7. Object Pooling for Traffic
2+
3+
Date: 2025-12-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The infinite runner nature of the game requires constant spawning and despawning of traffic cars as new chunks load and old ones unload. Creating new `THREE.Group` hierarchies and geometries for complex cars triggers frequent Garbage Collection (GC) pauses and frame drops (stutter) whenever the player crosses a chunk boundary.
12+
13+
## Decision
14+
15+
We implemented an **Object Pooling** system for the `TrafficSystem`.
16+
17+
1. A `CarPool` dictionary organizes inactive car objects by type (`sedan`, `taxi`, etc.).
18+
2. When a chunk loads, cars are retrieved from the pool (`getCarFromPool`). If empty, a new car is created.
19+
3. When a chunk unloads, cars are hidden and returned to the pool (`returnCarToPool`) instead of being disposed.
20+
4. The `utils.js` disposal logic is bypassed for pooled objects during normal gameplay.
21+
22+
## Consequences
23+
24+
### Positive
25+
* **Reduced Stutter**: Elimination of instantiation lag when generating chunks.
26+
* **Reduced GC Pressure**: Memory usage stabilizes as objects are reused rather than constantly allocated and discarded.
27+
* **Smoother Gameplay**: Consistent framerate across chunk boundaries.
28+
29+
### Negative
30+
* **Memory Overhead**: Unused cars remain in memory (hidden) rather than being fully freed.
31+
* **State Management Risk**: If a car is not properly reset (e.g., position, rotation, damage state) when retrieved from the pool, "ghost" behaviors could occur. We explicitly reset visibility and scene presence.
32+
33+
## Compliance
34+
* Implemented in `src/traffic.js`.

docs/adr/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Architecture Decision Records
2+
3+
* [1. 365-Day Seasonal Cycle](0001-365-day-seasonal-cycle.md)
4+
* [2. Automatic Probabilistic Weather Transitions](0002-automatic-probabilistic-weather-transitions.md)
5+
* [3. 10-Minute Fixed Day Duration](0003-10-minute-fixed-day-duration.md)
6+
* [4. Removal of Manual Weather Controls](0004-removal-of-manual-weather-controls.md)
7+
* [5. Sync Start Date with Real World](0005-sync-start-date-with-real-world.md)
8+
* [6. Geometry Merging](0006-geometry-merging.md)
9+
* [7. Object Pooling](0007-object-pooling.md)

src/main.js

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,9 @@ async function init() {
105105

106106
console.log('Game Initialized with Infinite World + Populated Chunks');
107107

108-
// Weather Controls
109-
window.addEventListener('keydown', (e) => {
110-
if (weatherSystem) { // Safety check
111-
if (e.key === '1') weatherSystem.setSunny();
112-
if (e.key === '2') weatherSystem.setRain();
113-
if (e.key === '3') weatherSystem.setSnow();
114-
}
115-
});
116108

117-
// Instructions for weather
118-
const weatherInfo = document.createElement('div');
119-
weatherInfo.style.position = 'absolute';
120-
weatherInfo.style.top = '20px';
121-
weatherInfo.style.right = '20px';
122-
weatherInfo.style.color = '#fff';
123-
weatherInfo.style.fontFamily = 'monospace';
124-
weatherInfo.innerHTML = '[1] Sunny [2] Rain [3] Snow';
125-
document.body.appendChild(weatherInfo);
109+
110+
126111

127112
// Version Display
128113
const verDiv = document.createElement('div');
@@ -133,7 +118,7 @@ async function init() {
133118
verDiv.style.background = 'rgba(0,0,0,0.5)';
134119
verDiv.style.padding = '5px';
135120
verDiv.style.fontFamily = 'monospace';
136-
verDiv.innerHTML = 'v6.1.0: Mega City & Life Update';
121+
verDiv.innerHTML = 'v6.2.0: Performance & Weather Update';
137122
document.body.appendChild(verDiv);
138123

139124
animate(() => {

src/traffic.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ export class TrafficSystem {
1010
this.roadWidth = roadWidth;
1111
this.chunkCars = new Map();
1212
this.cars = []; // Flat list for easy loop
13-
// this.carSpeed = 10; // Removed global speed
14-
// No init() call here, wait for ChunkManager
13+
14+
// Object Pooling
15+
this.carPool = {
16+
sedan: [],
17+
taxi: [],
18+
suv: [],
19+
truck: [],
20+
bus: [],
21+
sport: []
22+
};
1523
}
1624

1725
// [NEW] Speed definitions
@@ -27,6 +35,25 @@ export class TrafficSystem {
2735
}
2836
}
2937

38+
getCarFromPool(type) {
39+
if (this.carPool[type] && this.carPool[type].length > 0) {
40+
const car = this.carPool[type].pop();
41+
car.visible = true;
42+
return car;
43+
}
44+
return createCarMesh(type);
45+
}
46+
47+
returnCarToPool(carMesh, type) {
48+
if (!carMesh) return;
49+
// Reset transform? Not strictly needed as spawnCarInChunk overwrites.
50+
carMesh.visible = false;
51+
this.scene.remove(carMesh); // Detach from scene logic inside unloadChunk handles this too, but good to ensure.
52+
53+
if (!this.carPool[type]) this.carPool[type] = [];
54+
this.carPool[type].push(carMesh);
55+
}
56+
3057
loadChunk(cx, cz, biome = 'city') {
3158
const chunkCarsList = [];
3259
const numCars = 3; // Cars per chunk
@@ -43,7 +70,9 @@ export class TrafficSystem {
4370

4471
for (let i = 0; i < numCars; i++) {
4572
const type = getRandomCarType();
46-
const carGroup = createCarMesh(type);
73+
74+
// USE POOL
75+
const carGroup = this.getCarFromPool(type);
4776

4877
// Optimization: Cache local bounding box to avoid per-frame traversal
4978
// This box is in local space (relative to car origin)
@@ -83,7 +112,13 @@ export class TrafficSystem {
83112
cx: cx,
84113
cz: cz
85114
});
115+
} else {
116+
// Spawn failed (invalid spot), return to pool immediately
117+
this.returnCarToPool(carGroup, type);
86118
}
119+
} else {
120+
// Spawn failed (collision?), return to pool
121+
this.returnCarToPool(carGroup, type);
87122
}
88123
}
89124
this.chunkCars.set(`${cx},${cz}`, chunkCarsList);
@@ -102,8 +137,10 @@ export class TrafficSystem {
102137
// It remains in this.cars so traffic avoids it, which is correct.
103138
return;
104139
}
105-
this.scene.remove(car.mesh);
106-
disposeCar(car.mesh); // Dispose resources
140+
141+
// RETURN TO POOL instead of Dispose
142+
this.returnCarToPool(car.mesh, car.type);
143+
107144
// Remove from flat list
108145
const idx = this.cars.indexOf(car);
109146
if (idx > -1) this.cars.splice(idx, 1);

0 commit comments

Comments
 (0)