Ambient est un environnement d'exécution 3D universel, compatible avec n'importe quel langage qui se compile/s'exécute sur WebAssembly, conçu pour faciliter la création et le déploiement de mondes et d'expériences multijoueurs riches. « Avec Ambient, nous voulons rendre la construction multijoueur aussi simple que la construction solo », expliquent ses éditeurs.Ambient est développé par une petite équipe de cinq personnes à Stockholm, en Suède parmi lesquelles : Tobias, le PDG, Fredrik, le CPTO, Mithun & Tei, les développeurs Rust, et Magda le designer interne. Après plus d'un an de développement, ils ont annoncé la disponibilité d'Ambient 0.1. L'environnement d'exécution open source a été construit avec Rust.
Pourquoi Ambient ?
Il existe de nombreux moteurs de jeu qui optimisent la création de jeux solo, mais peu visent à faciliter le multijoueur. Nous étions curieux : qu'est-ce qui pourrait être construit si le multijoueur était aussi facile à utiliser que le solo ? Quels types d'expériences extraordinaires - avec des interactions nouvelles et intéressantes - les gens pourraient-ils envisager une fois libérés des détails fins du réseautage ?
Ambient est le début de notre réponse à ces questions : un environnement d'exécution conçu pour permettre aux développeurs de toutes sortes de créer et de partager les expériences qu'ils souhaitent créer. Cependant, le problème n'est pas seulement de bien faire la communication client-serveur. Cela inclut également tous les autres défis qui se posent dans le développement de jeux multijoueurs : servir des actifs, distribuer votre jeu, exécuter durablement votre jeu en tant que service, interagir avec vos utilisateurs, et bien plus encore. Le runtime est notre première étape vers cela, et nous sommes ravis de ce qui va suivre.
Nous rendons Ambient gratuit et open-source (avec la licence MIT) car notre objectif est d'établir une norme pour la création de jeux multijoueurs qui peuvent vivre au-delà de nous. En tant qu'entreprise, notre plan est de fournir des services à valeur ajoutée pour le runtime que nous prévoyons de monétiser (y compris l'hébergement de serveurs et d'actifs), mais le runtime lui-même sera gratuit et open source pour toujours. En tant qu'utilisateur du runtime, vous pourrez toujours choisir les services de notre part dont vous profitez et ceux que vous choisissez de mettre en œuvre vous-même.
Ambient est le début de notre réponse à ces questions : un environnement d'exécution conçu pour permettre aux développeurs de toutes sortes de créer et de partager les expériences qu'ils souhaitent créer. Cependant, le problème n'est pas seulement de bien faire la communication client-serveur. Cela inclut également tous les autres défis qui se posent dans le développement de jeux multijoueurs : servir des actifs, distribuer votre jeu, exécuter durablement votre jeu en tant que service, interagir avec vos utilisateurs, et bien plus encore. Le runtime est notre première étape vers cela, et nous sommes ravis de ce qui va suivre.
Nous rendons Ambient gratuit et open-source (avec la licence MIT) car notre objectif est d'établir une norme pour la création de jeux multijoueurs qui peuvent vivre au-delà de nous. En tant qu'entreprise, notre plan est de fournir des services à valeur ajoutée pour le runtime que nous prévoyons de monétiser (y compris l'hébergement de serveurs et d'actifs), mais le runtime lui-même sera gratuit et open source pour toujours. En tant qu'utilisateur du runtime, vous pourrez toujours choisir les services de notre part dont vous profitez et ceux que vous choisissez de mettre en œuvre vous-même.
Mise en réseau transparente
Avant tout, Ambient a été conçu à partir de zéro pour permettre des expériences en réseau comme l'exemple de cube qui sera abordé plus bas. L'état du serveur est automatiquement synchronisé avec les clients et le modèle de données est le même sur le serveur et le client. Cela signifie qu'à l'avenir, vous pourrez déplacer votre code entre le backend et le frontend, ou même l'exécuter sur les deux de manière transparente.
Indépendant du langage
L'interface d'Ambient est construite sur WebAssembly, ce qui vous permettra d'écrire du code dans n'importe quel langage qui se compile en WASM. À l'heure actuelle, Rust est notre seul langage pris en charge, mais nous travaillons activement à prendre en charge autant de langages que possible afin que vous puissiez choisir le bon outil pour le travail. Grâce à notre modèle de données, différents modules construits dans différents langages peuvent toujours communiquer entre eux via des données partagées et des messages.
Isolation
L'isolation est la clé du modèle d'exécution d'Ambient : chaque module s'exécute isolé de tous les autres modules. Si une partie de votre application tombe en panne, le reste de l'application peut continuer à fonctionner sans être affecté, ce qui augmente sa résilience. Cela permet également l'utilisation de code tiers non approuvé - le code auquel vous ne faites pas confiance devrait pouvoir s'exécuter dans son propre module isolé sans affecter la fonctionnalité globale.
Conception orientée données
Ambient est construit avec une conception orientée données à l'esprit de haut en bas. Toutes les données sont stockées et interagissent via un système de composants d'entité soutenu par une base de données d'entités centralisée sur le serveur. Cette base de données est automatiquement répliquée sur chaque client, et chaque client a la possibilité d'augmenter et d'étendre les entités avec un état local. L'utilisation d'un ECS facilite la visualisation de l'état de votre application et offre d'excellentes performances et évolutivité. [ndlr. Entity Component System (ECS) est un modèle d'architecture logicielle principalement utilisé dans le développement de jeux vidéo pour la représentation d'objets du monde du jeu. Un ECS comprend des entités composées de composants de données, avec des systèmes qui fonctionnent sur les composants des entités]
Interopérable
L'utilisation d'un ECS fournit une abstraction commune des données de votre application. Cela permet une possibilité passionnante : les modules qui ne se connaissent pas peuvent toujours interagir tant qu'ils comprennent comment interpréter les données partagées de la même manière.
Pour ce faire, nous avons introduit la possibilité de définir des composants personnalisés (le C dans ECS : données typées attachées à une entité) et des concepts (ensembles de composants décrivant des comportements et des fonctionnalités partagés). Ceux-ci peuvent être partagés par plusieurs modules et utilisés pour interagir, même sans que ces modules soient directement conscients les uns des autres. De plus, les modules peuvent diffuser des messages (groupes de composants) pour négocier un comportement plus complexe.
Par exemple, un composant points de vie : F32 peut être diminué par un module de dégâts, augmenté par un module de soins et visualisé par un module d'interface utilisateur. Tous partagent la même définition, mais sont par ailleurs complètement indépendants et ne se connaissent pas.
Exécutable unique
Ambient est un exécutable unique que vous pouvez télécharger pour Windows x64, Linux x64 ou macOS ARM, ou vous pouvez le créer vous-même pour votre plate-forme. Cet exécutable peut agir en tant que serveur ou rejoindre un serveur en tant que client graphique. Il peut même faire office des deux à la fois avec ambient run !
Pipeline et flux d'actifs
Les actifs sont automatiquement compilés et optimisés par le pipeline d'actifs Ambient personnalisable, qui prend en charge les formats de modèle les plus courants (y compris FBX et glTF), les formats d'image, les formats audio, etc.
Les ressources compilées sont toujours transmises aux clients, afin qu'ils puissent immédiatement commencer à jouer sans avoir à télécharger tout le contenu au préalable.
Fonctionnalité standard riche
Enfin, Ambient vise à fournir un riche ensemble de fonctionnalités standard pour le développement de jeux et d'applications 3D. Cela inclut, mais sans s'y limiter, un moteur de rendu physique piloté par GPU, une physique alimentée par PhysX, un système d'interface utilisateur de type React, un son spatial avec des filtres composables, une entrée utilisateur indépendante de la plate-forme, etc. Certaines de ces fonctionnalités (par exemple, l'interface utilisateur et le son) n'ont pas été exposées à l'API, mais nous y travaillons.
Aperçu rapide
Commencez par installer Ambient, puis créez un nouveau projet Ambient : ambient new
Ouvrez ensuite src/lib.rs et ajoutez ce qui suit à la fonction principale et laissez votre EDI s'importer automatiquement :
| Code Rust : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | spawn_query(player()).bind(move |players| { for _ in players { Entity::new() .with_merge(make_transformable()) .with_default(cube()) .with(translation(), rand::random()) .with(color(), rand::random()) .spawn(); } }); |
[SPOILER]
| Code : | Sélectionner tout |
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 | use ambient_api::{
components::core::{
game_objects::player_camera,
player::player,
primitives::cube,
rendering::color,
transform::{lookat_center, translation},
},
concepts::{make_perspective_infinite_reverse_camera, make_transformable},
prelude::*,
};
#[main]
pub async fn main() -> EventResult {
Entity::new()
.with_merge(make_perspective_infinite_reverse_camera())
.with_default(player_camera())
.with(translation(), Vec3::ONE * 5.)
.with(lookat_center(), vec3(0., 0., 0.))
.spawn();
spawn_query(player()).bind(move |players| {
// For each player joining, spawn a random colored box somewhere
for _ in players {
Entity::new()
.with_merge(make_transformable())
.with_default(cube())
.with(translation(), rand::random())
.with(color(), rand::random())
.spawn();
}
});
EventOk
} |
Maintenant, lancez-le avec : ambient run
Vous devriez voir quelque chose comme ceci :
Ouvrez maintenant une nouvelle fenêtre de terminal et entrez : ambient join
Vous devriez maintenant voir deux cubes. Félicitations, vous venez de créer votre première expérience multijoueur avec Ambient !
Exemples plus complexes
Nous avons déjà vu comment créer une petite expérience multijoueur avec Ambient, mais il y a beaucoup plus à faire. Voici un exemple d'une scène plus complexe, Offworld, construite dans l'éditeur à venir :
Physics est supportée nativement grâce à l'utilisation de PhysX, qui s'exécute sur le serveur et fonctionnera donc par défaut dans un environnement multijoueur :
| Code Rust : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | Entity::new() .with_merge(make_transformable()) .with_default(cube()) .with(box_collider(), Vec3::ONE * 2.) .with(dynamic(), true) .with_default(physics_controlled()) .spawn(); on(event::COLLISION, |c| { println!("Collision"); EventOk }); |
L'équipe a fourni ce src :
[SPOILER]
| Code Rust : | Sélectionner tout |
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 | use ambient_api::{ components::core::{ ecs::ids, game_objects::player_camera, physics::{ angular_velocity, box_collider, dynamic, linear_velocity, physics_controlled, visualizing, }, prefab::prefab_from_url, primitives::cube, rendering::{cast_shadows, color}, transform::{lookat_center, rotation, scale, translation}, }, concepts::{make_perspective_infinite_reverse_camera, make_transformable}, physics::raycast, prelude::*, }; #[main] pub async fn main() -> EventResult { Entity::new() .with_merge(make_perspective_infinite_reverse_camera()) .with_default(player_camera()) .with(translation(), vec3(5., 5., 4.)) .with(lookat_center(), vec3(0., 0., 0.)) .spawn(); let cube = Entity::new() .with_merge(make_transformable()) .with_default(cube()) .with_default(visualizing()) .with(box_collider(), Vec3::ONE) .with(dynamic(), true) .with_default(physics_controlled()) .with_default(cast_shadows()) .with(translation(), vec3(0., 0., 5.)) .with(scale(), vec3(0.5, 0.5, 0.5)) .with(color(), Vec4::ONE) .spawn(); Entity::new() .with_merge(make_transformable()) .with(prefab_from_url(), asset_url("assets/Shape.glb").unwrap()) .spawn(); on(event::COLLISION, |c| { // TODO: play a sound instead println!("Bonk! {:?} collided", c.get(ids()).unwrap()); EventOk }); on(event::FRAME, move |_| { for hit in raycast(Vec3::Z * 20., -Vec3::Z) { if hit.entity == cube { println!("The raycast hit the cube: {hit:?}"); } } EventOk }); loop { let max_linear_velocity = 2.5; let max_angular_velocity = 360.0f32.to_radians(); sleep(5.).await; let new_linear_velocity = (random::<Vec3>() - 0.5) * 2. * max_linear_velocity; let new_angular_velocity = (random::<Vec3>() - 0.5) * 2. * max_angular_velocity; println!("And again! Linear velocity: {new_linear_velocity:?} | Angular velocity: {new_angular_velocity:?}"); entity::set_components( cube, Entity::new() .with(translation(), vec3(0., 0., 5.)) .with(rotation(), Quat::IDENTITY) .with(linear_velocity(), new_linear_velocity) .with(angular_velocity(), new_angular_velocity) .with(color(), random::<Vec3>().extend(1.)), ); } } |
Les personnages peuvent également être chargés et animés :
| Code Rust : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | let unit_id = Entity::new() .with_merge(make_transformable()) .with( prefab_from_url(), asset_url("assets/Peasant.fbx").unwrap(), ) .spawn(); let anim = "assets/Dance.fbx/animations/main.anim"; entity::set_animation_controller( unit_id, AnimationController { actions: &[AnimationAction { clip_url: &asset_url(anim).unwrap(), looping: true, weight: 1., }], apply_base_pose: false, }, ); |
L'équipe a fourni ce src :
[SPOILER]
| Code Rust : | Sélectionner tout |
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 | use ambient_api::{ components::core::{ game_objects::player_camera, player::player, prefab::prefab_from_url, primitives::quad, rendering::color, transform::{lookat_center, scale, translation}, }, concepts::{make_perspective_infinite_reverse_camera, make_transformable}, entity::{AnimationAction, AnimationController}, player::KeyCode, prelude::*, }; #[main] pub async fn main() -> EventResult { Entity::new() .with_merge(make_perspective_infinite_reverse_camera()) .with_default(player_camera()) .with(translation(), vec3(2., 2., 3.0)) .with(lookat_center(), vec3(0., 0., 1.)) .spawn(); Entity::new() .with_merge(make_transformable()) .with_default(quad()) .with(scale(), Vec3::ONE * 10.) .with(color(), vec4(0.5, 0.5, 0.5, 1.)) .spawn(); let unit_id = Entity::new() .with_merge(make_transformable()) .with( prefab_from_url(), asset_url("assets/Peasant Man.fbx").unwrap(), ) .spawn(); entity::set_animation_controller( unit_id, AnimationController { actions: &[AnimationAction { clip_url: &asset_url("assets/Capoeira.fbx/animations/mixamo.com.anim").unwrap(), looping: true, weight: 1., }], apply_base_pose: false, }, ); query(player()).build().each_frame(move |players| { for (player, _) in players { let Some((delta, _)) = player::get_raw_input_delta(player) else { continue; }; if delta.keys.contains(&KeyCode::Key1) { entity::set_animation_controller( unit_id, AnimationController { actions: &[AnimationAction { clip_url: &asset_url( "assets/Robot Hip Hop Dance.fbx/animations/mixamo.com.anim", ) .unwrap(), looping: true, weight: 1., }], apply_base_pose: false, }, ); } if delta.keys.contains(&KeyCode::Key2) { entity::set_animation_controller( unit_id, AnimationController { actions: &[AnimationAction { clip_url: &asset_url("assets/Capoeira.fbx/animations/mixamo.com.anim") .unwrap(), looping: true, weight: 1., }], apply_base_pose: false, }, ); } if delta.keys.contains(&KeyCode::Key3) { entity::set_animation_controller( unit_id, AnimationController { actions: &[ AnimationAction { clip_url: &asset_url( "assets/Robot Hip Hop Dance.fbx/animations/mixamo.com.anim", ) .unwrap(), looping: true, weight: 0.5, }, AnimationAction { clip_url: &asset_url( "assets/Capoeira.fbx/animations/mixamo.com.anim", ) .unwrap(), looping: true, weight: 0.5, }, ], apply_base_pose: false, }, ); } } }); EventOk } |
Et bien sûr, vous pouvez créer des jeux. Voici un tic-tac-toe multijoueur en moins de 100 lignes de code :
[SPOILER]
| Code Rust : | Sélectionner tout |
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 | use ambient_api::{ components::core::{ self, game_objects::player_camera, player::player, primitives::cube, rendering::{color, outline}, transform::{lookat_center, scale, translation}, }, concepts::{make_perspective_infinite_reverse_camera, make_transformable}, }; use ambient_api::{player::KeyCode, prelude::*}; use components::cell; use palette::{FromColor, Hsl, Srgb}; #[main] pub async fn main() -> EventResult { Entity::new() .with_merge(make_perspective_infinite_reverse_camera()) .with_default(player_camera()) .with(translation(), vec3(3., 3., 2.5)) .with(lookat_center(), vec3(1.5, 1.5, 0.)) .spawn(); let mut cells = Vec::new(); for y in 0..3 { for x in 0..3 { let id = Entity::new() .with_merge(make_transformable()) .with_default(cube()) .with(translation(), vec3(x as f32, y as f32, 0.)) .with(scale(), vec3(0.6, 0.6, 0.6)) .with(color(), vec4(0.1, 0.1, 0.1, 1.)) .spawn(); cells.push(id); } } spawn_query(core::player::player()).bind(|ids| { for (id, _) in ids { entity::add_component(id, cell(), 0); } }); on(event::FRAME, move |_| { for cell in &cells { entity::remove_component(*cell, outline()); } let players = entity::get_all(player()); let n_players = players.len(); for (i, player) in players.into_iter().enumerate() { let player_color = Srgb::from_color(Hsl::from_components(( 360. * i as f32 / n_players as f32, 1., 0.5, ))); let player_color = vec4(player_color.red, player_color.green, player_color.blue, 1.); let cell = entity::get_component(player, components::cell()).unwrap(); let Some((delta, _)) = player::get_raw_input_delta(player) else { continue; }; let mut x = cell % 3; let mut y = cell / 3; let keys = &delta.keys; if keys.contains(&KeyCode::Left) || keys.contains(&KeyCode::A) { x = (x + 3 - 1) % 3; } if keys.contains(&KeyCode::Right) || keys.contains(&KeyCode::D) { x = (x + 1) % 3; } if keys.contains(&KeyCode::Up) || keys.contains(&KeyCode::W) { y = (y + 3 - 1) % 3; } if keys.contains(&KeyCode::Down) || keys.contains(&KeyCode::S) { y = (y + 1) % 3; } let cell = y * 3 + x; entity::add_component_if_required(cells[cell as usize], outline(), player_color); entity::set_component(player, components::cell(), cell); if delta.keys.contains(&KeyCode::Space) { entity::set_component(cells[cell as usize], color(), player_color); } } EventOk }); EventOk } |
Ou un minigolf multijoueur :
[SPOILER]
| Code Rust : | Sélectionner tout |
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 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | use ambient_api::{ components::core::{ app::main_scene, ecs::children, game_objects::player_camera, model::model_from_url, physics::{ angular_velocity, collider_from_url, dynamic, kinematic, linear_velocity, physics_controlled, sphere_collider, }, player::{player, user_id}, prefab::prefab_from_url, rendering::{color, fog_density, light_diffuse, sky, sun, water}, transform::{ inv_local_to_world, local_to_parent, local_to_world, mesh_to_local, mesh_to_world, rotation, scale, spherical_billboard, translation, }, ui::{font_size, text}, }, concepts::{make_perspective_infinite_reverse_camera, make_transformable}, player::MouseButton, prelude::*, }; use components::{ ball, origin, player_ball, player_camera_state, player_color, player_indicator, player_indicator_arrow, player_restore_point, player_stroke_count, player_text, player_text_container, }; use concepts::{make_player_camera_state, make_player_state}; use utils::CameraState; mod utils; const BALL_RADIUS: f32 = 0.34; fn create_environment() { make_transformable() .with_default(water()) .with(scale(), Vec3::ONE * 2000.) .spawn(); make_transformable() .with_default(sun()) .with(rotation(), Quat::from_rotation_y(-45_f32.to_radians())) .with(light_diffuse(), Vec3::ONE) .with(fog_density(), 0.) .with_default(main_scene()) .spawn(); make_transformable().with_default(sky()).spawn(); make_transformable() .with(prefab_from_url(), asset_url("assets/level.glb").unwrap()) .with(translation(), Vec3::Z * -0.25) .spawn(); make_transformable() .with(model_from_url(), asset_url("assets/fan.glb").unwrap()) .with(collider_from_url(), asset_url("assets/fan.glb").unwrap()) .with(kinematic(), ()) .with(dynamic(), true) .with(angular_velocity(), vec3(0., 90_f32.to_radians(), 0.)) .with(translation(), vec3(-35., 161., 8.4331)) .with(rotation(), Quat::from_rotation_z(180_f32.to_radians())) .spawn(); } fn make_golf_ball() -> Entity { make_transformable() .with_default(ball()) .with_default(physics_controlled()) .with(dynamic(), true) .with(sphere_collider(), BALL_RADIUS) .with(model_from_url(), asset_url("assets/ball.glb").unwrap()) } fn make_text() -> Entity { Entity::new() .with( local_to_parent(), Mat4::from_scale(Vec3::ONE * 0.02) * Mat4::from_rotation_x(-180_f32.to_radians()), ) .with(color(), vec4(1., 0., 0., 1.)) .with(font_size(), 36.) .with_default(main_scene()) .with_default(local_to_world()) .with_default(mesh_to_local()) .with_default(mesh_to_world()) } #[main] pub async fn main() -> EventResult { create_environment(); // When a player spawns, create their player state. spawn_query(user_id()).requires(player()).bind({ let player_hue = State::new(0.); move |players| { for (player, player_user_id) in players { let next_color = utils::hsv_to_rgb(&[*player_hue.read(), 0.7, 1.0]).extend(1.); *player_hue.write() += 102.5; // 80 + 22.5; pseudo random color, with 16 being unique entity::add_components(player, make_player_state()); let camera_state = make_player_camera_state().spawn(); entity::add_component(player, player_camera_state(), camera_state); make_perspective_infinite_reverse_camera() .with(user_id(), player_user_id.clone()) .with(player_camera_state(), camera_state) .with_default(player_camera()) .with_default(local_to_world()) .with_default(inv_local_to_world()) .with_default(translation()) .with_default(rotation()) .spawn(); // TODO: This is a bit... odd entity::add_component(player, player_color(), next_color * 2.2); let text = make_text() .with(color(), next_color) .with(user_id(), player_user_id.clone()) .with(text(), player_user_id.clone()) .spawn(); entity::add_component(player, player_text(), text); entity::add_component( player, player_text_container(), make_transformable() .with_default(main_scene()) .with_default(local_to_world()) .with_default(spherical_billboard()) .with(translation(), vec3(-5., 0., 5.)) .with(children(), vec![text]) .spawn(), ); entity::add_component( player, player_ball(), make_golf_ball() .with(color(), next_color) .with(user_id(), player_user_id.clone()) .with(translation(), vec3(-5., 0., 20.)) .spawn(), ); entity::add_component( player, player_indicator(), make_transformable() .with(color(), next_color) .with(user_id(), player_user_id.clone()) .with(model_from_url(), asset_url("assets/indicator.glb").unwrap()) .spawn(), ); entity::add_component( player, player_indicator_arrow(), make_transformable() .with(color(), next_color) .with(user_id(), player_user_id.clone()) .with( model_from_url(), asset_url("assets/indicator_arrow.glb").unwrap(), ) .spawn(), ); } } }); let flag = make_transformable() .with(model_from_url(), asset_url("assets/flag.glb").unwrap()) .with(collider_from_url(), asset_url("assets/flag.glb").unwrap()) .with(dynamic(), true) .with(kinematic(), ()) .with(origin(), vec3(-35., 205., 0.3166)) .spawn(); // Update the flag every frame. query(translation()) .requires(ball()) .build() .each_frame(move |balls| { let flag_origin = entity::get_component(flag, origin()).unwrap_or_default(); let mut min_distance = std::f32::MAX; for (_, ball_position) in &balls { let distance = ball_position.distance(flag_origin); if distance < min_distance { min_distance = distance; } } if min_distance < 5. { entity::set_component( flag, translation(), flag_origin + Vec3::Z * (5. - min_distance), ); } else { entity::set_component(flag, translation(), flag_origin); } }); // Update player cameras every frame. query(player_camera_state()) .requires(player_camera()) .build() .each_frame({ move |cameras| { for (id, camera_state) in &cameras { let camera_state = CameraState(*camera_state); let (camera_translation, camera_rotation) = camera_state.get_transform(); entity::set_component(*id, translation(), camera_translation); entity::set_component( *id, rotation(), camera_rotation * Quat::from_rotation_x(90.), ); } } }); // When a player despawns, clean up their objects. let player_objects_query = query(user_id()).build(); despawn_query(user_id()).requires(player()).bind({ move |players| { let player_objects = player_objects_query.evaluate(); for (_, player_user_id) in &players { if let Some((id, _)) = player_objects .iter() .find(|(_, object_user_id)| *player_user_id == *object_user_id) { entity::despawn(*id); } } } }); query(( player_ball(), player_text(), player_text_container(), player_indicator(), player_indicator_arrow(), player_camera_state(), )) .requires(player()) .build() .each_frame(move |players| { for ( player, ( player_ball, player_text, player_text_container, player_indicator, player_indicator_arrow, player_camera_state, ), ) in &players { let Some((delta, new)) = player::get_raw_input_delta(*player) else { continue; }; let player_camera_state = CameraState(*player_camera_state); let ball_position = entity::get_component(*player_ball, translation()).unwrap_or_default(); player_camera_state .set_position(ball_position) .rotate(delta.mouse_position / 250.) .zoom(delta.mouse_wheel / 25.); let mut force_multiplier = time() % 2.0; if force_multiplier > 1.0 { force_multiplier = 1.0 - (force_multiplier - 1.0); } entity::set_component( *player_text_container, translation(), ball_position + Vec3::Z * 2., ); // TODO: This can be removed after #114 is resolved. let player_color = entity::get_component(*player, player_color()).unwrap_or_default(); entity::set_component(*player_ball, color(), player_color); entity::set_component(*player_indicator, color(), player_color); entity::set_component(*player_indicator_arrow, color(), player_color); let camera_rotation = Quat::from_rotation_z(player_camera_state.get_yaw()); let camera_direction = camera_rotation * -Vec3::Y; entity::set_component(*player_indicator, translation(), ball_position); entity::set_component(*player_indicator, rotation(), camera_rotation); entity::set_component(*player_indicator, scale(), vec3(1.0, force_multiplier, 1.0)); entity::set_component(*player_indicator_arrow, rotation(), camera_rotation); entity::set_component( *player_indicator_arrow, translation(), ball_position + camera_direction * force_multiplier * 10., ); if ball_position.z < 0.25 { entity::set_component(*player_ball, linear_velocity(), Vec3::ZERO); entity::set_component(*player_ball, angular_velocity(), Vec3::ZERO); entity::set_component( *player_ball, translation(), entity::get_component(*player, player_restore_point()).unwrap_or_default(), ); } if new.mouse_buttons.contains(&MouseButton::Left) { entity::set_component(*player, player_restore_point(), ball_position); entity::set_component( *player_ball, linear_velocity(), camera_direction * 50. * force_multiplier, ); let stroke_count = entity::get_component(*player, player_stroke_count()).unwrap_or_default() + 1; entity::set_component(*player_text, text(), stroke_count.to_string()); entity::set_component(*player, player_stroke_count(), stroke_count); } } }); EventOk } |
L'équipe propose plusieurs autres exemples de jeux
Installer AmbientSource : Ambient
Et vous ?
Que pensez-vous d'Ambient tel qu'il est présenté ?
Comprenez-vous la motivation de l'équipe derrière l'outil ?
Seriez-vous tenté de l'essayer ?
Si vous vous lancez, pourriez-vous faire un retour utilisateur (courbe d'apprentissage, facilité d'utilisation, etc.) ?
Vous avez lu gratuitement 3 799 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.