Ambient est un environnement d'exécution visant à simplifier la création de jeux multijoueurs et d'applications 3D hautes performances,
optimisé par WebAssembly, Rust et WebGPU

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.
Principes de conception

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 newOuvrez ensuite src/lib.rs et ajoutez ce qui suit à la fonction principale et laissez votre EDI s'importer automatiquement :

Code Rust : Sélectionner tout - Visualiser dans une fenêtre à part
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();
    }
});

Cela fera apparaître un cube aléatoire pour chaque joueur rejoignant la partie. L'exemple complet ici :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 runVous devriez voir quelque chose comme ceci :

Nom : un.png
Affichages : 2514
Taille : 7,6 Ko

Ouvrez maintenant une nouvelle fenêtre de terminal et entrez : ambient joinVous 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 - Visualiser dans une fenêtre à part
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 :

Code Rust : Sélectionner tout - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 :

Code Rust : Sélectionner tout - Visualiser dans une fenêtre à part
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 :

Code Rust : Sélectionner tout - Visualiser dans une fenêtre à part
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 :

Code Rust : Sélectionner tout - Visualiser dans une fenêtre à part
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 Ambient

Source : 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.) ?