1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
use specs;
use specs::{ReadStorage, WriteStorage};
use specs::Entities;
use slog::Logger;
use grid::PosInOwningRoot;
use super::{Globe, ChunkOrigin};
use cell_dweller::CellDweller;
// NOTE: this is currently all pretty awful. See comments throughout.
/// Loads and unloads `Chunk`s for a `Globe`.
///
/// The `Chunk`s may be loaded from disk, or generated fresh if
/// they have never existed before.
pub struct ChunkSystem {
log: Logger,
// When we go higher than this many chunks loaded...
max_chunks_loaded_per_globe: usize,
// ...we will unload chunks to leave only this many.
// This is a kind of hysteresis. I haven't yet validated
// that this actually improves performance _at all_.
//
// TODO: instead have a budget for chunks, have the chunk
// creation logic know what it is and _target_ it when creating
// new chunks, so that we never end up with thrashing by
// creating too many then immediately deleting them repeatedly
// each frame. (You should only go over if you won't do it again
// immediately after cleaning up.) Then also complain loudly
// if there's not enough budget left for the really essential chunks
// for a given CellDweller.
cull_chunks_down_to: usize,
}
impl ChunkSystem {
pub fn new(parent_log: &Logger) -> ChunkSystem {
ChunkSystem {
log: parent_log.new(o!()),
// TODO: accept as arguments.
//
// There appears to be at least ~110
// loaded at a minimum the way I have it at the moment;
// have to be super careful to get these numbers right
// so we don't unnecessarily churn chunks.
//
// TODO: how to make sure this is automatically right?
// (See comments above; have a budget, work to that.)
// These values were originally 200 and 150 before I bumped
// it up to allow for 3 players. This solution really
// isn't going to fly very long. It's time to decouple what
// chunk _views_ exist from what chunks exist, so we can
// load the essential chunks for each player character,
// and the desirable views for each client. (Etc.)
max_chunks_loaded_per_globe: 300,
cull_chunks_down_to: 250,
}
}
fn unload_excess_chunks_if_necessary<'a>(
&mut self,
globe: &mut Globe,
globe_entity: specs::Entity,
cds: &specs::ReadStorage<'a, CellDweller>,
) {
use super::globe::GlobeGuts;
if globe.chunks().len() <= self.max_chunks_loaded_per_globe {
// We're under the limit; nothing to do.
return;
}
// Get all the CellDweller positions relative to the globe.
// We don't care which CellDweller is which, so just store
// them as a Vec of points.
use specs::Join;
let cd_positions: Vec<_> = cds.join()
// Only consider CellDwellers from this globe.
.filter(|cd| cd.globe_entity == Some(globe_entity))
.map(|cd| {
cd.real_transform_without_setting_clean().translation.vector
})
.collect();
// There are no cell dwellers, so no interesting terrain.
// (If a tree falls in a forest...)
if cd_positions.len() == 0 {
return;
}
// Unload the chunks that are most distant from their nearest CellDweller.
//
// TODO: Don't allocate memory all the time here.
// At very least use a persistent scratch buffer instead
// of allocating every time!
let mut chunk_distances: Vec<(ChunkOrigin, f64)> = globe
.chunks()
.keys()
.map(|chunk_origin| {
// TODO: don't use chunk origin; use the middle cell,
// or otherwise whatever the closest corner is.
// Or even a bounding sphere.
// (Cache this per Chunk).
let chunk_origin_pos = globe.spec().cell_bottom_center(*chunk_origin.pos());
let distance_from_closest_cd = cd_positions.iter()
// TODO: norm_squared; it'll be quicker.
.map(|cd_pos| (cd_pos - chunk_origin_pos.coords).norm())
.min_by(|a, b| a.partial_cmp(b).expect("Really shouldn't be possible to get NaN etc. here"))
.expect("We already ensured there is at least one CellDweller");
(*chunk_origin, distance_from_closest_cd)
})
.collect();
// Farthest away chunks come first.
chunk_distances.sort_by(|a, b| {
b.1.partial_cmp(&a.1).expect(
"All chunk origins and CellDwellers should be real distances from each other!",
)
});
let chunks_to_remove = self.max_chunks_loaded_per_globe - self.cull_chunks_down_to;
chunk_distances.truncate(chunks_to_remove);
for (chunk_origin, _distance) in chunk_distances {
globe.remove_chunk(chunk_origin);
}
}
fn ensure_essential_chunks_for_cell_dweller_present<'a>(
&mut self,
cd: &CellDweller,
globes: &mut specs::WriteStorage<'a, Globe>,
) {
if let Some(globe_entity) = cd.globe_entity {
// Get the associated globe, complaining loudly if we fail.
// TODO: this is becoming a common pattern; factor out.
let globe = match globes.get_mut(globe_entity) {
Some(globe) => globe,
None => {
warn!(
self.log,
"The globe associated with this CellDweller is not alive! Can't proceed!"
);
return;
}
};
// TODO: throttle, and do in background.
// (Except that the truly essential chunks really do need to be loaded _now_.)
// TODO: see remarks in `Chunk::list_accessible_chunks`
// about this actually being an inappropriate way to approach
// this problem; we'll load a bunch of chunks we don't need to yet
// in a desperate attempt to not miss the ones we do need.
// Load all the chunks that we could possibly try
// to move into from this chunk within two steps.
//
// Takes into account that a single user action could lead to
// multiple cell jumps, e.g., stepping up a small ledge.
//
// TODO: this is all a bit finicky and fragile.
// TODO: this is also just plain wrong.
// You don't need the neighbouring chunks of the neighbouring chunks.
// You just need all the chunks containing neighbouring cells of
// neighbouring cells. No wonder there are so many chunks loaded
// at the moment. :)
let cd_pos_in_owning_root = PosInOwningRoot::new(cd.pos, globe.spec().root_resolution);
let chunk_origin = globe.origin_of_chunk_owning(cd_pos_in_owning_root);
globe.ensure_chunk_present(chunk_origin);
let accessible_chunks = {
use super::globe::GlobeGuts;
let chunk = globe.chunks().get(&chunk_origin).expect(
"We just ensured this chunk is loaded.",
);
// TODO: Gah, such slow!
chunk.accessible_chunks.clone()
};
for accessible_chunk_origin in accessible_chunks {
globe.ensure_chunk_present(accessible_chunk_origin);
// Repeat this from each immediately accessible chunk.
let next_level_accessible_chunks = {
use super::globe::GlobeGuts;
let chunk = globe.chunks().get(&accessible_chunk_origin).expect(
"We just ensured this chunk is loaded.",
);
// TODO: Gah, such slow!
chunk.accessible_chunks.clone()
};
for next_level_accessible_chunk_origin in next_level_accessible_chunks {
globe.ensure_chunk_present(next_level_accessible_chunk_origin);
}
}
}
}
}
impl<'a> specs::System<'a> for ChunkSystem {
type SystemData = (Entities<'a>, WriteStorage<'a, Globe>, ReadStorage<'a, CellDweller>);
fn run(&mut self, data: Self::SystemData) {
use specs::Join;
let (entities, mut globes, cds) = data;
// If we have too many chunks loaded, then unload some of them.
for (mut globe, globe_entity) in (&mut globes, &*entities).join() {
self.unload_excess_chunks_if_necessary(&mut globe, globe_entity, &cds);
}
// Make sure the chunks under/near the player are present.
for cd in cds.join() {
self.ensure_essential_chunks_for_cell_dweller_present(cd, &mut globes);
}
}
}