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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
use std::sync::mpsc;
use slog;
use globe;
use specs;
use cell_dweller;
use types::*;
// TODO: make a proper test harness using `App`, `piston::window::NoWindow`,
// and some custom systems to drive the tests.
struct Walker {
movement_input_sender: mpsc::Sender<cell_dweller::MovementEvent>,
world: specs::World,
dispatcher: specs::Dispatcher<'static, 'static>,
guy_entities: Vec<specs::Entity>,
}
impl Walker {
pub fn new(walker_count: u16) -> Walker {
// Log to nowhere.
let drain = slog::Discard;
let root_log = slog::Logger::root(drain, o!("pk_version" => env!("CARGO_PKG_VERSION")));
// Create Specs `World`.
let mut world = specs::World::new();
// Register all component types.
world.register::<::cell_dweller::CellDweller>();
world.register::<::Spatial>();
world.register::<::globe::Globe>();
world.register::<::net::NetMarker>();
// Initialize common resources.
world.add_resource(TimeDeltaResource(0.0));
// Create systems.
let chunk_sys = globe::ChunkSystem::new(&root_log);
let (movement_input_sender, movement_input_receiver) = mpsc::channel();
let mut movement_sys =
cell_dweller::MovementSystem::new(&mut world, movement_input_receiver, &root_log);
movement_sys.init(&mut world);
// Stop the player from getting stuck on cliffs; we want to test what
// happens when they walk really aggressively all around the world, not what
// happens when they fall into a hole and don't move anywhere.
movement_sys.set_step_height(100);
let physics_sys = cell_dweller::PhysicsSystem::new(
&root_log,
0.1, // Seconds between falls
);
// Make a dispatcher and add all our systems.
let dispatcher = specs::DispatcherBuilder::new()
.add(movement_sys, "cd_movement", &[])
.add(physics_sys, "cd_physics", &[])
.add(chunk_sys, "chunk", &[])
.build();
// Use an Earth-scale globe to make it likely we're constantly
// visiting new chunks.
let globe = globe::Globe::new_earth_scale_example();
// First add the globe to the world so we can get a handle on its entity.
let globe_spec = globe.spec();
let globe_entity = world.create_entity().with(globe).build();
// Find globe surface and put player character on it.
use grid::{GridPoint3, Dir};
use globe::chunk::Material;
let mut guy_pos = GridPoint3::default();
guy_pos = {
let mut globes = world.write::<globe::Globe>();
let globe = globes.get_mut(globe_entity).expect(
"Uh oh, where did our Globe go?",
);
globe.find_lowest_cell_containing(guy_pos, Material::Air)
};
let guy_entities: Vec<_> = (0..walker_count).map(|_| {
world
.create_entity()
.with(cell_dweller::CellDweller::new(
guy_pos,
Dir::default(),
globe_spec,
Some(globe_entity),
))
.with(::Spatial::new_root())
.build()
}).collect();
Walker {
movement_input_sender: movement_input_sender,
world: world,
dispatcher: dispatcher,
guy_entities: guy_entities,
}
}
// TODO: track how many steps have actually been taken somehow?
pub fn tick_lots(&mut self, ticks: usize) {
// Start our CellDweller moving forward indefinitely.
use cell_dweller::MovementEvent;
self.movement_input_sender
.send(MovementEvent::StepForward(true))
.unwrap();
use rand;
use rand::Rng;
let mut rng = rand::thread_rng();
for _ in 0..ticks {
for guy_entity in &self.guy_entities {
// Set our new character as the currently controlled cell dweller.
self.world
.write_resource::<cell_dweller::ActiveCellDweller>()
.maybe_entity = Some(guy_entity.clone());
// Maybe turn left or right.
let f: f32 = rng.gen();
if f < 0.02 {
// Turn left.
self.movement_input_sender
.send(MovementEvent::TurnLeft(true))
.unwrap();
self.movement_input_sender
.send(MovementEvent::TurnRight(false))
.unwrap();
} else if f < 0.01 {
// Turn right.
self.movement_input_sender
.send(MovementEvent::TurnLeft(false))
.unwrap();
self.movement_input_sender
.send(MovementEvent::TurnRight(true))
.unwrap();
} else {
// Walk straight.
self.movement_input_sender
.send(MovementEvent::TurnLeft(false))
.unwrap();
self.movement_input_sender
.send(MovementEvent::TurnRight(false))
.unwrap();
}
self.world.write_resource::<TimeDeltaResource>().0 = 0.1;
self.dispatcher.dispatch(&mut self.world.res);
self.world.maintain();
}
}
}
}
#[test]
fn random_walk_one_walker() {
use grid::GridPoint3;
let mut walker = Walker::new(1);
walker.tick_lots(1000);
// Walking should have taken us away from the origin.
assert_eq!(walker.guy_entities.len(), 1);
let guy_entity = walker.guy_entities.first().unwrap();
let cd_storage = walker.world.read::<::cell_dweller::CellDweller>();
let cd = cd_storage.get(guy_entity.clone()).unwrap();
assert_ne!(cd.pos, GridPoint3::default());
}
#[test]
fn random_walk_three_walkers() {
use grid::GridPoint3;
let mut walker = Walker::new(3);
walker.tick_lots(1000);
// Walking should have taken us away from the origin.
assert_eq!(walker.guy_entities.len(), 3);
for guy_entity in &walker.guy_entities {
let cd_storage = walker.world.read::<::cell_dweller::CellDweller>();
let cd = cd_storage.get(guy_entity.clone()).unwrap();
assert_ne!(cd.pos, GridPoint3::default());
}
}
#[cfg(feature = "nightly")]
pub mod benches {
use test::Bencher;
use super::*;
#[bench]
// # History for random walks:
//
// - Earth-scale globe, with initial implementation of unloading most
// distant chunks when you have too many loaded.
// - 185,350,057 ns/iter (+/- 128,603,998)
//
// NOTE: avoid premature fiddly optimisation through clever caching, or anything that makes
// the design harder to work with; rather go for the optimisations that push all this
// forward in general and make interfaces _more elegant_.
fn bench_random_walk_one_walker(b: &mut Bencher) {
let mut walker = Walker::new(1);
// Start by moving everyone away from the origin.
walker.tick_lots(1000);
b.iter(|| { walker.tick_lots(10); });
}
// These multi-walker tests are to make sure we don't get pathological performance
// from having multiple "points of interest" on the globe. (Thrashing loading and unloading
// chunks as each walker moves.)
//
// No prizes for guessing how this test was born... :)
#[bench]
fn bench_random_walk_two_walkers(b: &mut Bencher) {
let mut walker = Walker::new(2);
// Start by moving everyone away from the origin.
walker.tick_lots(1000);
b.iter(|| { walker.tick_lots(10); });
}
#[bench]
fn bench_random_walk_three_walkers(b: &mut Bencher) {
let mut walker = Walker::new(3);
// Start by moving everyone away from the origin.
walker.tick_lots(1000);
b.iter(|| { walker.tick_lots(10); });
}
}