IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

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

Le , par Stéphane le calme

14PARTAGES

4  0 
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
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
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 :


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
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
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 :





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 :





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]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())[/code=rust]...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !