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
//! Scenes and prefabs: worlds and entities kept in files under `assets/`.
//!
//! A scene is a whole world (the [`WorldReflect::save`] format): a level, a
//! menu, a test setup. A prefab is one entity's components, spawned as many
//! times as you like: a tree, an enemy, a building. Both are JSON, so they
//! diff well, and agents and the editor write them like any other file.
use serde_json::{Value, json};
use crate::{Ctx, Entity, Transform2D, Vec2, WorldReflect, assets};
const PREFAB_FORMAT: &str = "crabby-prefab";
const PREFAB_VERSION: u64 = 1;
impl Ctx {
/// Replaces the world with the scene in `assets/<path>` (written by
/// [`save_scene`](Self::save_scene), the editor, or
/// [`WorldReflect::save`]). Entities keep their ids, so references
/// between them hold. Components must be [reflected](WorldReflect::reflect).
///
/// ```no_run
/// # use ponko::*;
/// Game::new("Crabs", 800, 600).run(|ctx| {
/// ctx.world.reflect::<Transform2D>();
/// ctx.load_scene("levels/beach.json").expect("level");
/// });
/// ```
///
/// # Errors
///
/// If the file is missing or isn't a scene, or names a component that
/// isn't reflected. The world is unchanged then.
pub fn load_scene(&mut self, path: &str) -> Result<(), String> {
let file = assets::resolve(path)?;
let text = assets::read_to_string(&file)?;
let scene: Value = serde_json::from_str(&text).map_err(|e| format!("{path}: {e}"))?;
self.load_scene_assets(&scene).map_err(|e| format!("{path}: {e}"))?;
self.world.load(&scene).map_err(|e| format!("{path}: {e}"))
}
/// Saves the world (its reflected components and resources) as a scene
/// in `assets/<path>`, over the file if it exists. Not in web builds.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// ctx.save_scene("levels/beach.json").expect("saved");
/// # }
/// ```
///
/// # Errors
///
/// If the file can't be written.
pub fn save_scene(&mut self, path: &str) -> Result<(), String> {
let text = serde_json::to_string_pretty(&self.world.save()).map_err(|e| e.to_string())?;
assets::write(path, text.as_bytes()).map(|_| ())
}
/// Spawns the prefab in `assets/<path>` with its position at `at` (its
/// rotation and scale as saved). The file is read once, then kept.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// for x in 0..10 {
/// ctx.spawn_prefab("prefabs/palm.json", vec2(x as f32 * 64.0, 400.0)).expect("palm");
/// }
/// # }
/// ```
///
/// # Errors
///
/// If the file is missing or isn't a prefab, or a component isn't
/// reflected or doesn't fit; nothing is spawned then.
pub fn spawn_prefab(&mut self, path: &str, at: Vec2) -> Result<Entity, String> {
if !self.prefabs.contains_key(path) {
let file = assets::resolve(path)?;
let text = assets::read_to_string(&file)?;
let prefab: Value = serde_json::from_str(&text).map_err(|e| format!("{path}: {e}"))?;
let components = components_of(&prefab).map_err(|e| format!("{path}: {e}"))?;
self.load_scene_assets(&components)
.map_err(|e| format!("{path}: {e}"))?;
self.prefabs.insert(path.to_string(), components);
}
let components = self.prefabs[path].clone();
spawn_at(&mut self.world, components, at).map_err(|e| format!("{path}: {e}"))
}
/// Saves entity `e` (its reflected components) as a prefab in
/// `assets/<path>`, for [`spawn_prefab`](Self::spawn_prefab). Its
/// position isn't kept: each spawn says where. Not in web builds.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx, palm: Entity) {
/// ctx.save_prefab(palm, "prefabs/palm.json").expect("saved");
/// # }
/// ```
///
/// # Errors
///
/// If `e` is dead or the file can't be written.
pub fn save_prefab(&mut self, e: Entity, path: &str) -> Result<(), String> {
let mut components = self.world.entity_json(e).map_err(|e| e.to_string())?;
if let Some(t) = components.get_mut("Transform2D") {
t["position"] = json!([0.0, 0.0]);
}
let prefab = json!({ "format": PREFAB_FORMAT, "version": PREFAB_VERSION, "components": components });
let text = serde_json::to_string_pretty(&prefab).map_err(|e| e.to_string())?;
assets::write(path, text.as_bytes())?;
self.prefabs.insert(path.to_string(), components);
Ok(())
}
}
/// A prefab file's components, checked.
fn components_of(prefab: &Value) -> Result<Value, String> {
if prefab["format"] != PREFAB_FORMAT {
return Err(format!("not a prefab (expected \"format\": \"{PREFAB_FORMAT}\")"));
}
if prefab["version"].as_u64() != Some(PREFAB_VERSION) {
return Err(format!(
"unsupported prefab version {} (this engine reads {PREFAB_VERSION})",
prefab["version"]
));
}
match &prefab["components"] {
Value::Object(c) if !c.is_empty() => Ok(prefab["components"].clone()),
_ => Err("a prefab needs a non-empty \"components\" object".into()),
}
}
/// Spawns `components` with the position set to `at` (adding a transform if
/// there's none).
fn spawn_at(world: &mut crate::World, mut components: Value, at: Vec2) -> Result<Entity, String> {
match components.get_mut("Transform2D") {
Some(t) => t["position"] = json!([at.x, at.y]),
None => components["Transform2D"] = serde_json::to_value(Transform2D::at(at)).map_err(|e| e.to_string())?,
}
world.spawn_json(components).map_err(|e| e.to_string())
}