Stride Community Toolkit Preview - Code-Only Feature Basics in C#

Discover the Stride Community Toolkit, a powerful collection of extensions and helpers designed for the Stride game engine. In this blog post, we dive into the toolkit's code-only feature, empowering C# and .NET developers to create immersive 2D/3D games and visualizations with ease. Explore how this community-driven, open-source project can simplify your game development journey.

This blog post is part 1 of a 3-part series:

Table of Contents:

Introduction ๐ŸŒฑ ๐Ÿ”—

Welcome to the preview of the Stride Community Toolkit, a collection of extensions and helpers for the Stride C# game engine. This community-driven, open-source project is designed to assist developers in creating 2D/3D games and visualizations using Stride.

Although the toolkit is still in its early stages, it already offers several valuable features. In this post, I will focus on the code-only approach, which I found particularly useful for development.

This article assumes that you have some experience with .NET and C# programming.

The toolkit allows you to create a game using a code-only approach, meaning you can develop a game without relying on the Stride Game Studio. As a C#/.NET developer in my day job, I found this approach very helpful for getting started with the Stride engine and game development, bypassing the need to work directly in the Game Studio.

You can also access the full source code for this post on GitHub to follow along or explore the final implementation directly.

Additional details on the benefits of the code-only approach can be found here in the toolkit documentation.

We will be using a standard .NET 8 Console App to create a simple game by adding some NuGet packages to get started.

Hereโ€™s the process I found to be the easiest way to begin with the code-only approach:

  1. Run the minimal code to get the game window running
  2. Add entities/primitives to the scene
  3. Add interaction with the keyboard and mouse
  4. Add output to the console or screen
  5. Play around, be creative and have fun

What You'll Learn ๐ŸŽฏ ๐Ÿ”—

By the end of this post, you will have a solid foundation in using the Stride Community Toolkit's code-only feature to create a simple game. Youโ€™ll learn how to:

  • Set up the game window and initialize the core components
  • Add and manipulate entities within the scene
  • Implement basic interactivity using the keyboard and mouse
  • Display text output to the console or directly on the screen for debugging or player feedback

Coding enhances creativity and problem-solving, and we can take that creativity to the next level through visualization, whether it's in games, simulations, or simply experimenting with 3D models in a virtual space.

Whether youโ€™re a seasoned developer or new to game development, this post will guide you through the essential steps to get your first game up and running with Stride. Ready to dive in? Letโ€™s get started! ๐Ÿš€

Basic Terminology ๐Ÿ“š ๐Ÿ”—

Before diving into the steps, it's helpful to understand some key terms that will be used throughout this guide:

  • Stride: A C# game engine for creating 2D/3D games and visualizations.
  • Stride Community Toolkit: A collection of extensions and helpers for the Stride engine.
  • Code-Only: A feature of the toolkit that allows you to create a game without using the Game Studio.
  • Game: In the context of this post, a game refers to any interactive or visual project created using a game engine. This can range from traditional playable games to simulations, visualizations, or any real-time interactive experiences where users can interact with or observe elements within a scene.
  • Scene: The container for entities, which defines the game world or environment.
  • Entity: An object in the scene that can represent anything from a 3D model to a camera or light and aggregates multiple EntityComponents.
  • EntityComponent: A base component that defines the behavior or properties of an entity. Other components inherit from this class.
  • RigidbodyComponent: A physics component that allows an entity to respond to forces like gravity and collisions.
  • Graphics Compositor: A component that organizes how scenes are rendered in the Stride engine.
  • Camera: A camera that allows viewing the scene from different angles.
  • Camera Controller: A script that enables basic camera movement using keyboard and mouse inputs.
  • 3D Primitive: A basic 3D model, such as a capsule, cube, or sphere.
  • Collider: A component that defines the shape of an entity for physical interactions.
  • Physics Engine: A system that simulates physical interactions between entities in the scene.
  • Profiler: A tool that monitors performance metrics like frames per second (FPS) and memory usage.
  • Skybox: A textured 3D model that provides a background for the scene.
  • Game Loop: The main loop that drives the game, updating the state and rendering the scene.
  • Update Method: A callback method that is called every frame to update the game state.
  • Physics-Based Movement: Moving entities using the physics engine to simulate realistic interactions.
  • Non-Physical Movement: Moving entities by directly changing their position without physics interactions.
  • Transform: Defines an entity's position, rotation, and scale in the scene.
  • Force: A vector that represents a physical force applied to an entity.
  • Delta Time: The time elapsed between frames, used for frame-independent movement.
  • Material: A visual property that defines how an entity is rendered, including color, texture, and shading.
  • Ground Gizmo: A visual representation of the ground plane and axis directions in the scene.
  • GameTime: A structure that provides time-related information for the game loop.
  • Vector3: A 3D vector that represents a point or direction in 3D space.
  • Frame Rate: The number of frames rendered per second, measured in frames per second (FPS).

Prerequisites ๐Ÿ  ๐Ÿ”—

These prerequisites were tested on a clean Windows 11 installation.

  1. Install the Microsoft Visual C++ 2015-2022 Redistributable (25MB) and restart your system if prompted.
  2. Install the .NET 8 SDK x64 (200MB).
  3. Install the IDE of your choice. I will be using Visual Studio 2022, but you can also use Visual Studio Code, Rider, or any other IDE that supports .NET development.

How to use the code snippets ๐Ÿ“ ๐Ÿ”—

You can copy and paste the code into your Program.cs file and run the application to see the results.

You will be guided to replace some parts of the code with the new code snippets as you progress through the steps or replacing the entire Program.cs file.

Also, the code snippets contain comments which part of the code is new or updated.

Code-Only on Windows ๐ŸชŸ ๐Ÿ”—

The code-only approach is currently available only on Windows. The toolkit provides a set of NuGet packages that you can use to create a game without the need for the Game Studio.

Code-Only on Other Platforms ๐Ÿง ๐Ÿ”—

This option is not yet available but is planned for the future (Use CompilerApp cross-platform binary instead of exe). While Stride is a cross-platform engine, you can build the game on Windows and then run it on other platforms. However, one of the build tools, Stride.Core.Assets.CompilerApp.exe, which is responsible for building the assets, is currently only available on Windows.

The Story of the Brave Explorers ๐Ÿ“˜ ๐Ÿ”—

๐ŸŒ Welcome, brave explorers of the digital wilderness! Today, we embark on an exciting journey into the heart of the Stride game engine. Our guide? None other than the Stride Community Toolkit.

In the vast expanse of the coding universe, we'll create a new world from nothing but a .NET 8 Console App. Prepare to witness the birth of a game window, a black void of nothingness that will soon teem with life ๐ŸซŽ.

As we venture further, we'll bring light into our world, transforming the empty void into a vibrant blue expanse. But what's a world without inhabitants? We'll conjure a 3D capsule, our first digital lifeform, into existence.

Beware, fellow adventurers! Our capsule is a wild creature, prone to falling into the void. But fear not, for we'll harness the power of a 3D camera ๐ŸŽฅ controller to keep a watchful eye ๐Ÿ‘๏ธ on our creation.

As we continue, we'll introduce motion, allowing our entities to interact with their surroundings and respond to player input. From keyboard controls to mouse interactions, our world will come to life with movement and action. ๐Ÿ•น๏ธ

Finally, weโ€™ll display messages and feedback with both console output and on-screen text, letting our creation communicate with us through the game window. ๐Ÿ“

So, refresh your mouse agility skills ๐Ÿ–ฑ๏ธ, and join us on this exhilarating expedition. Let's dive into the code! ๐Ÿ’ป

Step 1: Create a New C# .NET 8 Console App - Nothingness โšซ ๐Ÿ”—

  1. Create a new C# .NET 8 Console App in your IDE.
  2. Add the following NuGet package: ๐Ÿ“ฆ
    dotnet add package Stride.CommunityToolkit.Windows --prerelease
  3. Paste the following code into your Program.cs file: ๐Ÿ’ป
    using Stride.Engine;
    
    // Create an instance of the game
    using var game = new Game();
    
    // Start the game loop
    // This method initializes the game, begins running the game loop,
    // and starts processing events.
    game.Run();
  4. Build the project from the command line or use your IDE to build it:
    dotnet build
  5. Run the application.
  6. Behold the black void of nothingness ๐Ÿ™€.
The NuGet package Stride.CommunityToolkit.Windows is used specifically for code-only projects. You should use the Stride.CommunityToolkit NuGet package when referencing from a regular Stride project generated from the Game Studio.
Youโ€™ve learned how to set up a Stride game window using the Stride Community Toolkit. Even though it's just a black screen, the game window is running, marking the first step in your journey.

Step 2: Let There Be Light - Or at Least Blue ๐ŸŒŒ ๐Ÿ”—

Once upon a time in a galaxy far, far away, you should see a window with a black background. This is the Stride game window. ๐Ÿ–ฅ๏ธ As a black screen is not very exciting, let's add some mystery code to make it more interesting. This time, we use the Stride.CommunityToolkit.Engine namespace so we can reference some of the toolkit helper methods. ๐Ÿ”งโœจ

Update the Program.cs file to look like this, or simply replace the entire file:

using Stride.CommunityToolkit.Engine; // This was added: Import the toolkit's helper methods
using Stride.Engine;

// Create an instance of the game
using var game = new Game();

// Start the game loop and provide the Start method as a callback
// This method initializes the game, begins running the game loop,
// and starts processing events.
game.Run(start: Start);  // This was updated

// This was added
// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera to the scene to allow viewing from different angles
    game.Add3DCamera();
}

The code above does the following:

  • Start() method is a callback that is invoked when the game starts. It takes a Scene object as a parameter, representing the root scene of the game.
  • AddGraphicsCompositor() organizes how scenes are rendered in the Stride engine, enabling extensive customization of the rendering pipeline.
  • Add3DCamera() adds a 3D camera to the scene, allowing you to view it from various angles.

Run the application again. Now, instead of a black screen, you should see a blue screen. ๐ŸŒŒ While not overly exciting, itโ€™s a step in the right direction. Weโ€™re looking through the camera, but thereโ€™s nothing to see yet. ๐Ÿ‘€

Youโ€™ve learned that Graphics Compositor is used to handle rendering and 3D Camera is used to view the scene from different angles. There's still nothing to see yet because we havenโ€™t added any objects to the scene. ๐Ÿคทโ€โ™‚๏ธ

Step 3: Add Some Shapes - Capsule Time! ๐ŸŽจ ๐Ÿ”—

Let's add something to the scene. ๐ŸŽจ This time, we will be utilizing the Stride.CommunityToolkit.Rendering.ProceduralModels namespace, which provides helper methods for generating procedural models like capsules, cubes, and spheres. We will add a capsule to the scene.

Update the Program.cs file to look like this, or simply replace the entire file:

using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;  // This was added: Import procedural model helpers
using Stride.Engine;

// Create an instance of the game
using var game = new Game();

// Start the game loop and provide the Start method as a callback
// This method initializes the game, begins running the game loop,
// and starts processing events.
game.Run(start: Start);

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera to the scene to allow viewing from different angles
    game.Add3DCamera();

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule); // This was added
}

In this step, we added a new line that creates a 3D primitive capsule. ๐ŸงŠ The Create3DPrimitive() method takes a PrimitiveModelType enum as a parameter and returns an Entity object, with a collider included by default. The PrimitiveModelType enum defines the types of primitive 3D models that can be generated, such as capsules, cubes, and spheres.

Run the application again. Surprise, nothing happened! ๐Ÿ˜ฒ๐Ÿคฌ We created an entity, but we didn't add it to the scene. This is a typical beginner's mistake ๐Ÿคฆโ€โ™‚๏ธ. To fix it, update the Start() method to look like this.

You can replace the entire Start() method with the code below:

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera to the scene to allow viewing from different angles
    game.Add3DCamera();

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene; // This was added
}

Now, run the application again. You should see a capsule in the middle of the screen if you're lucky because it's falling down. Fast! ๐Ÿš€

Stride Game Engine - Code Only Basic - Capsule in blue game window
Youโ€™ve learned how to create and add a 3D capsule to the scene using procedural models. By adding the entity to the root scene, it becomes part of the scene graph. The capsule is falling because it lacks a collider to interact with the ground, which we'll address later.

Step 4: Control the Camera - Look Around! ๐Ÿ–ฑ๏ธ ๐Ÿ”—

Maybe we should at least look around the scene and view the capsule from different angles as it falls into the void ๐Ÿค”?

Let's add a 3D camera controller using the Add3DCameraController() method. ๐ŸŽฎ This extension adds basic camera functionality, allowing you to interact with the camera through keyboard and mouse inputs. Specifically, it attaches a regular SyncScript to the camera with custom logic to handle camera movement.

In most games, you would implement your own custom camera logic to fit the specific needs of your game. However, for this example, we'll use the default camera controller to get basic camera movement up and running quickly.

Time to refresh those mouse agility skills!

// This was updated: Add a camera controller for basic camera movement
game.Add3DCamera().Add3DCameraController();

Run the application again and use right-click and hold to rotate the camera towards the capsule, or follow the instructions displayed on the screen. ๐ŸŽฅ Feeling a bit more satisfied now? Letโ€™s make the experience even more interesting! ๐ŸŽจโœจ

Stride Game Engine - Code Only Basic - Game controller instructions
Youโ€™ve learned how to add a 3D camera controller to the scene, enabling basic camera movement. Now, you can rotate the camera using the mouse and keyboard, giving you more control over how you view the scene.

Step 5: Reposition the Capsule - More Excitement! ๐Ÿ“ ๐Ÿ”—

Let's reposition the capsule to add a bit more excitement and give us a few extra seconds to admire it before it falls. Update the Start() method as shown below, and don't forget to add the Stride.Core.Mathematics namespace for handling the 3D positioning.

Update the Program.cs file to look like this, or simply replace the entire file:

using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.Core.Mathematics;  // This was added: Import Vector3 and other math utilities
using Stride.Engine;

// Create an instance of the game
using var game = new Game();

// Start the game loop and provide the Start method as a callback
// This method initializes the game, begins running the game loop,
// and starts processing events.
game.Run(start: Start);

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera and a controller for basic camera movement
    game.Add3DCamera().Add3DCameraController();

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Reposition the capsule 8 units above the origin in the scene
    entity.Transform.Position = new Vector3(0, 8, 0); // This was added

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene;
}
  • entity.Transform.Position sets the position of the entity within the scene. The Vector3 object represents a 3D vector, which in this case, places the capsule at coordinates (0, 8, 0), which is 8 units above the scene's origin.
  • Stride.Core.Mathematics Stride uses its own implementation of Vector3 and other math utilities, so we need to add this namespace to access those features.

Run the application again. You should see a capsule falling down from the top of the screen. I know, the capsule is black, but don't worry, we'll fix that later. ๐Ÿ˜‰

Stride Game Engine - Code Only Basic - Reposition Capsule
Youโ€™ve learned how to reposition the capsule in the scene using the Transform component's Position property. The capsule is now positioned 8 units above the origin, giving you more time to admire it as it falls into the void.

Step 6: Add a Ground - Catch the Capsule! ๐Ÿ›‘ ๐Ÿ”—

Now let's catch the capsule by adding some ground to the scene. Update the Start() method to look like this so we can feel more grounded.

You can replace the entire Start() method with the code below:

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera and a controller for basic camera movement
    game.Add3DCamera().Add3DCameraController();

    // Add a 3D ground plane to catch the capsule
    game.Add3DGround(); // This was added

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Reposition the capsule 8 units above the origin in the scene
    entity.Transform.Position = new Vector3(0, 8, 0);

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene;
}
  • Add3DGround() is a toolkit helper method that creates a 3D ground plane in the scene, providing a surface for the capsule to land on.

Run the application again. You should see the capsule falling down and landing on the ground. ๐ŸŽ‰ Unfortunately, it's still a black capsule on a black ground ๐Ÿคฆโ€โ™‚๏ธ. But don't worry, weโ€™ll fix that later! In the meantime, you can move the camera around using the mouse and keyboard (Q or E) to lower or raise the camera and watch the capsule roll around on the ground. ๐Ÿ–ฑ๏ธโŒจ๏ธ

If you're wondering why the ground is invisible from below, it's due to back face culling. This technique prevents the engine from drawing polygons that face away from the camera, improving performance by avoiding unnecessary rendering. The same applies to the cube and other primitives: if the camera is inside the object, the walls won't be visible.
Stride Game Engine - Code Only Basic - Ground added
Stride Game Engine - Code Only Basic - Ground, different angle
Youโ€™ve learned how to add a 3D ground plane to the scene, giving the capsule a surface to land on. Now, the capsule falls and lands on the ground, making the scene feel more dynamic.

Step 7: Illuminate the Scene - Add Light! ๐Ÿ’ก ๐Ÿ”—

It's time to brighten things up! Adding some light to the scene will help us see our objects more clearly. Thankfully, there's a helper method called AddDirectionalLight() that adds a directional light to the scene. Update the Start() method to look like this:

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera and a controller for basic camera movement
    game.Add3DCamera().Add3DCameraController();

    // Add a directional light to illuminate the scene
    game.AddDirectionalLight(); // This was added

    // Add a 3D ground plane to catch the capsule
    game.Add3DGround();

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Reposition the capsule 8 units above the origin in the scene
    entity.Transform.Position = new Vector3(0, 8, 0);

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene;
}

Run the application again. ๐ŸŒŸ You should now see the capsule falling down and landing on the ground, with everything clearly illuminated by the directional light. The capsule and ground are much easier to see, thanks to the added light shining on the scene. ๐ŸŽ‰

Finally, no more black-on-black mysteries! ๐Ÿ’ก

Stride Game Engine - Code Only Basic - Light added
Youโ€™ve learned how to add a directional light to the scene, illuminating both the capsule and the ground. The scene is now well-lit, making it much easier to see and interact with the objects.

Step 8: Break 1 - Let's Reflect ๐Ÿ˜… ๐Ÿ”—

Tedious work, but you just learned the very basics of game setup behind the scenes, which is usually done in the Game Studio for you automatically.

Once the basics are set up, you need to add entities to the scene. In our example, we added:

  • A 3D Ground, a simple primitive model that provides a surface.
  • A Capsule, another primitive model, which weโ€™ve repositioned and dropped into the scene.

The toolkit added colliders for the ground and capsule, ensuring that the capsule doesn't fall through the ground but instead interacts realistically with the scene.



You can review the implementation of each Stride toolkit extension, which wraps some boilerplate code, and create your own custom implementation.

Whew! ๐Ÿ˜… Take a deep breath, and get ready for the next part of our journey. Up next: performance optimizations and more interactive elements. ๐Ÿš€

Step 9: Add Profiler - Performance! ๐Ÿ“ˆ ๐Ÿ”—

As game developers, we love seeing those sweet FPS numbers! ๐ŸŽฎ The toolkit provides a AddProfiler() method that adds a performance profiler to the game, allowing us to monitor important metrics like frames per second (FPS).

Update the Start() method to look like this:

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera and a controller for basic camera movement
    game.Add3DCamera().Add3DCameraController();

    // Add a directional light to illuminate the scene
    game.AddDirectionalLight();

    // Add a 3D ground plane to catch the capsule
    game.Add3DGround();

    // Add a performance profiler to monitor FPS and other metrics
    game.AddProfiler(); // This was added

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Reposition the capsule 8 units above the origin in the scene
    entity.Transform.Position = new Vector3(0, 8, 0);

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene;
}

Run the application again. You should see profiler text output in the top-left corner of the screen, showing the frames per second (FPS) and other performance metrics. ๐Ÿš€ Press F1 to cycle through different profiler outputs and monitor various aspects of your game's performance. ๐Ÿ“Š

Stride Game Engine - Code Only Basic - Profiler added
Youโ€™ve learned how to add a performance profiler to the scene, enabling you to monitor FPS and other important metrics. The profiler provides valuable insights into your game's performance, helping you identify areas for optimization.

Step 10: Illuminate the Scene - Add Skybox! ๐ŸŒ‡ ๐Ÿ”—

As exciting as things are looking, we can make our scene even better by adding a skybox. A skybox will enhance the overall atmosphere and give our scene a more polished, realistic look. ๐ŸŒ‡

The toolkit provides a AddSkybox() method that adds a skybox to the scene. First, we need to add another NuGet package, Stride.CommunityToolkit.Skyboxes, which includes assets required for the skybox.

dotnet add package Stride.CommunityToolkit.Skyboxes --prerelease

Then, update the code to look like this:

using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Skyboxes;  // This was added: Import skybox helpers
using Stride.Core.Mathematics;
using Stride.Engine;

// Create an instance of the game
using var game = new Game();

// Start the game loop and provide the Start method as a callback
// This method initializes the game, begins running the game loop,
// and starts processing events.
game.Run(start: Start);

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera and a controller for basic camera movement
    game.Add3DCamera().Add3DCameraController();

    // Add a directional light to illuminate the scene
    game.AddDirectionalLight();

    // Add a 3D ground plane to catch the capsule
    game.Add3DGround();

    // Add a performance profiler to monitor FPS and other metrics
    game.AddProfiler();

    // Add a skybox to enhance the scene's visuals
    game.AddSkybox(); // This was added

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Reposition the capsule 8 units above the origin in the scene
    entity.Transform.Position = new Vector3(0, 8, 0);

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene;
}

Run the application again. ๐ŸŽฎ You should now see a beautiful skybox surrounding the scene, making it look more immersive and realistic. ๐ŸŒ… The skybox is essentially a large, textured 3D model that wraps around the entire scene, providing a visually appealing background. ๐ŸŒ‡

Stride Game Engine - Code Only Basic - Skybox added
Youโ€™ve learned how to add a skybox to the scene, enhancing the visuals and creating a more immersive environment. The skybox provides a realistic backdrop, adding depth and atmosphere to your scene.

Step 11: Add Motion - I like to Move It, Move It! ๐Ÿ•บ ๐Ÿ”—

Before we dive into coding, let's take a moment to explore the different ways we can move entities in our scene. In Stride, we can move objects using two main approaches: non-physical movement and physics-based movement. Each method has its strengths and use cases, depending on the type of game or simulation you're creating.

Moving Entities by Changing Their Position Directly (Without Colliders) ๐Ÿ”—

This method involves directly modifying an entity's position in the scene by changing its Transform.Position. It's a straightforward approach that allows you to move objects freely without considering the physics of the environment. Here are some key points to keep in mind:

  • Simple Movement: This method is ideal for scenarios where you want to move objects in a controlled manner without requiring interaction with other entities.
  • Non-Physical: The entity doesn't respond to gravity, collisions, or any external forces. This makes it perfect for UI elements, floating objects, or objects that need to follow a predefined path.
  • No Interaction: By default, the entity won't collide with or interact with other objects in the scene. This can simplify certain game mechanics but also limits realism.

Use cases for non-physical movement include:

  • Camera Movement: Moving a camera smoothly around the scene without it being affected by the environment.
  • UI Elements: Moving user interface elements, like menus or health bars, without considering collisions.
  • Cutscenes or Animations: Predefined animations where objects follow scripted paths.
You can still add custom code or use Stride's CollisionHelper for basic interactions. For example, you can check for conditions like BoxIntersectsBox or calculate distances between objects with DistanceBoxBox.

Moving Entities Using Physics (With Colliders) ๐Ÿ”—

This approach leverages Stride's physics engine to handle movement. By applying forces and impulses to an entity's RigidbodyComponent, we can create realistic interactions that respond to gravity, collisions, and other physical phenomena. Key aspects of this approach include:

  • Realistic Movement: Entities move according to the laws of physics, making this method suitable for objects that need to interact with the environment.
  • Collisions: The entity will collide with other objects, allowing for dynamic interactions, such as objects bouncing off surfaces or pushing each other.
  • Forces: Movement can be influenced by various forces, including gravity, wind, or explosions. This allows for more complex and dynamic gameplay mechanics.
  • Physics-Driven Gameplay: This method is ideal for games that rely on physics-based mechanics, such as puzzle games, platformers, or simulations.

Use cases for physics-based movement include:

  • Dynamic Objects: Moving objects that need to interact with other entities, like bouncing balls or rolling barrels.
  • Character Movement: Characters or vehicles that need to respond to the environment realistically.
  • Environmental Interaction: Objects that react to player actions, such as crates that can be pushed or destroyed.
Youโ€™ve learned about the two main approaches to moving entities in Stride: non-physical movement and physics-based movement. Each method offers unique advantages, depending on whether you need simple control or realistic physics-driven interactions in your game or simulation.

Step 12: Add Motion without Physics - Move the Cube without Colliders ๐Ÿ“ฆ ๐Ÿ”—

Let's add a a cube ๐Ÿ“ฆ to the scene! ๐ŸŽ‰

We'll start with the first option: moving the cube by directly changing its position. Update the code to look like this or replace the entire file:

using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;  // This was added

float movementSpeed = 1f; // This was added
Entity? cube1 = null; // This was added

// Create an instance of the game
using var game = new Game();

// Start the game loop and provide the Start and Update methods as callbacks
// This method initializes the game, begins running the game loop,
// and starts processing events.
game.Run(start: Start, update: Update); // This was updated

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera and a controller for basic camera movement
    game.Add3DCamera().Add3DCameraController();

    // Add a directional light to illuminate the scene
    game.AddDirectionalLight();

    // Add a 3D ground plane to catch the capsule
    game.Add3DGround();

    // Add a performance profiler to monitor FPS and other metrics
    game.AddProfiler();

    // Add a skybox to enhance the scene's visuals
    game.AddSkybox();

    // Add a ground gizmo to visualize axis directions
    game.AddGroundGizmo(position: new Vector3(-5, 0.1f, -5), showAxisName: true);

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Reposition the capsule 8 units above the origin in the scene
    entity.Transform.Position = new Vector3(0, 8, 0);

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene;

    // This was added
    // Create a cube with material, disable its collider, and add it to the scene
    // The cube is hanging in the default position Vector(0,0,0) in the air,
    // well intersecting the ground plane as it is not aware of the ground
    cube1 = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Gold),
        IncludeCollider = false // No collider for non-physical movement
    });
    cube1.Scene = scene;
}

// This was added
// Define the Update method, called every frame to update the game state
void Update(Scene scene, GameTime time)
{
    // Calculate the time elapsed since the last frame for consistent movement
    // This is crucial for frame-independent movement, ensuring consistent
    // behaviour regardless of frame rate.
    var deltaTime = (float)time.Elapsed.TotalSeconds;

    if (cube1 != null)
    {
         // Move the cube along the negative X-axis with frame-independent motion
        cube1.Transform.Position -= new Vector3(movementSpeed * deltaTime, 0, 0);
    }
}
  • movementSpeed determines how fast the cube moves.
  • cube1 is an Entity object representing the cube in the scene.
  • game.Run(start: Start, update: Update) now includes the Update method as a callback for updating the game state every frame.
  • AddGroundGizmo() adds a visual representation of the ground plane and axis directions. Just a visual aid, it doesn't affect the physics of the scene.
  • CreateMaterial() allows you to color the cube (and even the capsule if you want ๐Ÿ˜‰).
  • Update() is a callback method that is called every frame to update the game state.

Run the application. ๐Ÿƒ You should see a box (cube) moving along the X-axis, without interacting with other entities.

Youโ€™ve learned how to move a cube without using colliders, applying non-physical movement. The cube moves smoothly along the X-axis, showcasing basic object manipulation and animation.

Step 13: Add Motion with Physics - Move the Cube with Colliders! ๐ŸงŠ ๐Ÿ”—

Now that we've moved the cube without colliders, let's dive into the more realistic option: moving the cube using physics. With this approach, the cube will interact with the environment, responding to forces like gravity and colliding with other entities.

Update the code to include physics-based movement or replace the entire file:

using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Physics; // This was added

float movementSpeed = 1f;
float force = 3f; // This was added
Entity? cube1 = null;
Entity? cube2 = null; // This was added

// Create an instance of the game
using var game = new Game();

// Start the game loop and provide the Start and Update methods as callbacks
// This method initializes the game, begins running the game loop,
// and starts processing events.
game.Run(start: Start, update: Update);

// Define the Start method to set up the scene
void Start(Scene scene)
{
    // Add the default graphics compositor to handle rendering
    game.AddGraphicsCompositor();

    // Add a 3D camera and a controller for basic camera movement
    game.Add3DCamera().Add3DCameraController();

    // Add a directional light to illuminate the scene
    game.AddDirectionalLight();

    // Add a 3D ground plane to catch the capsule
    game.Add3DGround();

    // Add a performance profiler to monitor FPS and other metrics
    game.AddProfiler();

    // Add a skybox to enhance the scene's visuals
    game.AddSkybox();

    // Add a ground gizmo to visualize axis directions
    game.AddGroundGizmo(position: new Vector3(-5, 0.1f, -5), showAxisName: true);

    // Create a 3D primitive capsule and store it in an entity
    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);

    // Reposition the capsule 8 units above the origin in the scene
    entity.Transform.Position = new Vector3(0, 8, 0);

    // Add the entity to the root scene so it becomes part of the scene graph
    entity.Scene = scene;

    // Create a cube with material, disable its collider, and add it to the scene
    // The cube is hanging in the default position Vector(0,0,0) in the air,
    // well intersecting the ground plane as it is not aware of the ground
    cube1 = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Gold),
        IncludeCollider = false // No collider for simple movement
    });
    cube1.Scene = scene;

    // This was added
    // Create a second cube with a collider for physics-based interaction
    cube2 = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Orange)
    });
    cube2.Transform.Position = new Vector3(-3, 5, 0);  // Reposition the cube above the ground
    cube2.Scene = scene;
}

// Define the Update method, called every frame to update the game state
void Update(Scene scene, GameTime time)
{
    // Calculate the time elapsed since the last frame for consistent movement
    var deltaTime = (float)time.Elapsed.TotalSeconds;

    if (cube1 != null)
    {
        // Move the first cube along the negative X-axis (non-physical movement)
        cube1.Transform.Position -= new Vector3(movementSpeed * deltaTime, 0, 0);
    }

    // This was added
    // Handle physics-based movement for cube2
    if (cube2 != null)
    {
        // Retrieve the RigidbodyComponent, which handles physics interactions
        var rigidBody = cube2.Get<RigidbodyComponent>();

        // Check if cube2 is stationary by verifying if its linear velocity is effectively zero.
        if (Math.Round(rigidBody.LinearVelocity.Length()) == 0)
        {
            // Apply an impulse to cube2 along the X-axis, initiating movement.
            rigidBody.ApplyImpulse(new Vector3(force, 0, 0));

            // Reverse the direction of the impulse for the next impulse,
            // allowing cube2 to move back and forth along the X-axis.
            force *= -1;
        }
    }
}
  • Stride.Physics provides access to physics-related classes and components, including the RigidbodyComponent.
  • force determines the strength of the impulse applied to the cube.
  • cube2 is an Entity object representing the cube that will move using physics-based interactions.
  • RigidbodyComponent handles physics interactions, allowing the entity to respond to forces, gravity, and collisions.
  • LinearVelocity represents the velocity of the cube. We check if the velocity is near zero (indicating the cube is stationary) before applying the impulse.
  • ApplyImpulse() applies a force to the entity, causing it to move in the direction of the applied force. In this case, weโ€™re applying an impulse to the cube, making it move along the X-axis.

Run the application. ๐Ÿƒโ€โ™‚๏ธ You should now see two cubes in the scene:

  • Cube 1 moves along the X-axis using non-physical movement, just like in the previous step.
  • Cube 2 interacts with the environment using physics, responding to forces and collisions.

This step introduces a new level of realism by making the cube react to physical forces, adding depth and complexity to your game. ๐ŸŽฎ

The main difference between the two cubes is that Cube 1 moves without interacting with the environment. We directly modify the entity's Transform.Position to move it, resulting in simple, non-physical movement. In contrast, Cube 2 responds to physics, collisions, and forces. Instead of manually changing its position, we control its movement through the RigidbodyComponent, which handles all the physics-based interactions, including gravity, impulses, and collisions with other objects in the scene. This makes Cube 2's movement more realistic and reactive to its surroundings.

Youโ€™ve learned how to move a cube using physics-based movement, allowing it to interact with the environment. The cube responds to forces, gravity, and collisions, creating a more dynamic and realistic scene. You now understand the key differences between non-physical and physics-based movement, giving you greater control over how objects interact in your game world.

Step 14: Add Keyboard Interaction - Move the Cube! โŒจ๏ธ ๐Ÿ”—

Now it's time to add some interactivity! ๐ŸŽฎ We will update the Update() method to allow the player to move the cubes around using the keyboard. We'll make sure both Cube 1 (non-physical movement) and Cube 2 (physics-based movement) respond to key presses.

Ensure that the using Stride.Input; namespace is included to handle input.

You can replace the existing Update method with the following code:

// Define the Update method, called every frame to update the game state
void Update(Scene scene, GameTime time)
{
    // Calculate the time elapsed since the last frame for consistent movement
    var deltaTime = (float)time.Elapsed.TotalSeconds;

    // This was updated
    // Handle non-physical movement for cube1
    if (cube1 != null)
    {
        // Move the first cube along the negative X-axis when the Z key is held down
        if (game.Input.IsKeyDown(Keys.Z))
        {
            cube1.Transform.Position -= new Vector3(movementSpeed * deltaTime, 0, 0);
        }
        // Move the first cube along the positive X-axis when the X key is held down
        else if (game.Input.IsKeyDown(Keys.X))
        {
            cube1.Transform.Position += new Vector3(movementSpeed * deltaTime, 0, 0);
        }
    }

    // This was updated
    // Handle physics-based movement for cube2
    if (cube2 != null)
    {
        // Retrieve the RigidbodyComponent, which handles physics interactions
        var rigidBody = cube2.Get<RigidbodyComponent>();

        // We use KeyPressed instead of KeyDown to apply impulses only once per key press.
        // This means the player needs to press and release the key to apply an impulse,
        // preventing multiple impulses from being applied while the key is held down.

        // Apply an impulse to the left when the C key is pressed (and released)
        if (game.Input.IsKeyPressed(Keys.C))
        {
            rigidBody.ApplyImpulse(new Vector3(-force, 0, 0));
        }
        // Apply an impulse to the right when the V key is pressed (and released)
        else if (game.Input.IsKeyPressed(Keys.V))
        {
            rigidBody.ApplyImpulse(new Vector3(force, 0, 0));
        }
    }
}
  • game.Input.IsKeyDown() checks if a key is currently held down. This is useful for continuous actions, like moving an object as long as the key is pressed.
  • game.Input.IsKeyPressed() checks if a key was pressed and released once. This is ideal for triggering actions that should only occur once per key press, such as applying an impulse to a physics object, to avoid multiple impulses being applied while the key is held down.

Additional Points:

  • The non-physical movement for Cube 1 allows smooth, continuous movement along the X-axis when holding the Z or X keys.
  • The physics-based movement for Cube 2 applies an impulse to the cube when the C or V keys are pressed, allowing it to interact with the environment.
  • The capsule does not collide with Cube 1 because we disabled the collider for that cube but interacts with Cube 2, which has a collider and responds to physics.

Run the application. You should now be able to control Cube 1's position with the Z and X keys, moving it left and right, and apply impulses to Cube 2 with the C and V keys, pushing it back and forth and colliding with the capsule. ๐ŸŽฎ

This step introduces basic keyboard controls, adding interactivity to your scene and allowing the player to manipulate objects in real-time. Ready to add even more interaction? Let's move on to mouse controls next! ๐Ÿ–ฑ๏ธ

You learned how to add keyboard interaction to the scene, allowing players to move the cubes using the Z, X, C, and V keys. Cube 1 responds to continuous key presses for smooth, non-physical movement, while Cube 2 reacts to single key presses, applying impulses for physics-based movement. These controls enable dynamic interaction and real-time object manipulation within the scene.

Step 15: Add Mouse Interaction - Do something! ๐Ÿ–ฑ๏ธ ๐Ÿ”—

Let's add mouse interaction to the scene! ๐Ÿญ In this step, we'll update the Update() method to allow players to interact with the cubes and the capsule using the mouse ๐Ÿ–ฑ๏ธ. We'll make sure both Cube 1 (non-physical movement), Cube 2, and the capsule (physics-based movement) respond to mouse input.

The previous comments have been streamlined to keep the code clean and focused๐Ÿงน. Also, make sure you can see your console output to see the results of the mouse interactions.

You can replace the entire code with the following, or refer to the comments labelled // This was added to see the specific changes.

using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Input;
using Stride.Physics;

float movementSpeed = 1f;
float force = 3f;
Entity? cube1 = null;
Entity? cube2 = null;

CameraComponent? camera = null; // This was added: Store the camera component
Simulation? simulation = null; // This was added: Store the physics simulation
ModelComponent? cube1Component = null; // This was added: Store the model component of Cube 1

using var game = new Game();

game.Run(start: Start, update: Update);

void Start(Scene scene)
{
    game.AddGraphicsCompositor();
    game.Add3DCamera().Add3DCameraController();
    game.AddDirectionalLight();
    game.Add3DGround();
    game.AddProfiler();
    game.AddSkybox();
    game.AddGroundGizmo(position: new Vector3(-5, 0.1f, -5), showAxisName: true);

    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);
    entity.Transform.Position = new Vector3(0, 8, 0);
    entity.Scene = scene;

    cube1 = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Gold),
        IncludeCollider = false // No collider for simple movement
    });
    cube1.Scene = scene;

    cube2 = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Orange)
    });
    cube2.Transform.Position = new Vector3(-3, 5, 0);
    cube2.Scene = scene;

    // These were added
    // Initialize camera, simulation, and model component for interactions
    camera = scene.GetCamera();
    simulation = game.SceneSystem.SceneInstance.GetProcessor<PhysicsProcessor>()?.Simulation;
    cube1Component = cube1.Get<ModelComponent>();
}

void Update(Scene scene, GameTime time)
{
    var deltaTime = (float)time.Elapsed.TotalSeconds;

    // Handle non-physical movement for cube1
    if (cube1 != null)
    {
        if (game.Input.IsKeyDown(Keys.Z))
        {
            cube1.Transform.Position -= new Vector3(movementSpeed * deltaTime, 0, 0);
        }
        else if (game.Input.IsKeyDown(Keys.X))
        {
            cube1.Transform.Position += new Vector3(movementSpeed * deltaTime, 0, 0);
        }
    }

    // Handle physics-based movement for cube2
    if (cube2 != null)
    {
        var rigidBody = cube2.Get<RigidbodyComponent>();

        if (game.Input.IsKeyPressed(Keys.C))
        {
            rigidBody.ApplyImpulse(new Vector3(-force, 0, 0));
        }
        else if (game.Input.IsKeyPressed(Keys.V))
        {
            rigidBody.ApplyImpulse(new Vector3(force, 0, 0));
        }
    }

    // This was added
    // Ensure camera and simulation are initialized before handling mouse input
    if (camera == null || simulation == null || !game.Input.HasMouse) return;

    // This was added
    // Handle mouse input for interactions
    if (game.Input.IsMouseButtonPressed(MouseButton.Left))
    {
        // Check for collisions with physics-based entities using raycasting
        var hitResult = camera.RaycastMouse(simulation, game.Input.MousePosition);

        if (hitResult.Succeeded)
        {
            var message = $"Hit: {hitResult.Collider.Entity.Name}";
            Console.WriteLine(message);

            var rigidBody = hitResult.Collider.Entity.Get<RigidbodyComponent>();

            if (rigidBody != null)
            {
                var direction = new Vector3(0, 3, 0); // Apply impulse upward

                rigidBody.ApplyImpulse(direction);
            }
        }
        else
        {
            Console.WriteLine("No hit detected.");
        }

        // Check for intersections with non-physical entities using ray picking
        var ray = camera.GetPickRay(game.Input.MousePosition);

        if (cube1Component?.BoundingBox.Intersects(ref ray) ?? false)
        {
            Console.WriteLine("Cube 1 hit!");
        }
    }
}
  • camera stores the camera component for raycasting and ray picking.
  • simulation stores the physics simulation for handling interactions.
  • cube1Component stores the model component of Cube 1 for detecting intersections with the mouse ray.
  • camera.RaycastMouse() detects collisions with physics-based entities using raycasting.
  • camera.GetPickRay() checks for intersections with non-physical entities using ray picking.

Now, when you click the left mouse button, the application will respond with the following actions depending on where you click:

  • Clicking Outside the Ground: If you click anywhere in the scene that doesn't intersect with an object or the ground, it will print "No hit detected." This indicates that the mouse ray didn't collide with any entities.
  • Clicking on Cube 1: Since Cube 1 doesn't have a collider, two things will happen:
    • The raycast will pass through Cube 1 and hit the ground beneath it, printing "Hit: Ground."
    • Additionally, the ray-picking method will detect that Cube 1 was hit, and it will print "Cube 1 hit!"
  • Clicking on Cube 2: Cube 2 has a collider, so the raycast will detect the collision and print "Hit: Entity." An impulse will be applied to Cube 2, causing it to move upward in response to the click.
  • Clicking on the Capsule: Similar to Cube 2, clicking on the capsule will print "Hit: Entity," and an upward impulse will be applied, making the capsule move.
  • Clicking on the Ground: If you click directly on the ground, the raycast will detect it and print "Hit: Ground." However, the ground will remain stationary because it has a StaticColliderComponent, meaning it's a fixed object in the scene.

Nice job! Youโ€™ve now implemented mouse interaction, which adds a whole new level of interactivity to the game. ๐Ÿš€ You can now click on objects in the scene to trigger different actions, like moving cubes or capsules with physics or detecting hits on non-physical entities. This opens up endless possibilities for gameplay mechanics! ๐ŸŽฎ We rock! ๐Ÿค˜

You've learned how to add mouse interaction to the scene, allowing players to interact with objects using raycasting for physics-based entities and ray picking for non-physical ones. You can now click on objects to apply forces or detect hits, adding a new layer of interactivity to the game.

Step 16: Add Output - Console or Screen! ๐Ÿ“บ ๐Ÿ”—

In this part, weโ€™ll explore basic output options, both to the console and directly on the screen. Youโ€™ve already added simple text output using Console.WriteLine() to display interactions when an entity is hit by the mouse raycast. Now, let's expand on this to better visualize interactions and provide feedback to the player.

Output Options ๐Ÿ”—

We have several options for displaying output:

  • Console Output: Useful for debugging and logging information. This is great for developers to see real-time feedback and troubleshoot the game
    • We can use traditional Console.WriteLine()
    • We can also use Stride's GlobalLogger.GetLogger(), or the Log property when used from within a game script
  • Debug Text Output: Display text directly in the game window, which is useful for quick, in-game debugging
  • UI Elements: Create UI elements like text labels or buttons to display information to the player within the game world.

Updating the Console Output ๐Ÿ”—

Letโ€™s update the if (hitResult.Succeeded) {} block by adding two lines to include additional output options:

if (hitResult.Succeeded)
{
    var message = $"Hit: {hitResult.Collider.Entity.Name}";
    Console.WriteLine(message);
    GlobalLogger.GetLogger("Program.cs").Info(message); // This was added
    game.DebugTextSystem.Print($"Entities: {scene.Entities.Count}", new Int2(50, 50)); // This was added

    var rigidBody = hitResult.Collider.Entity.Get<RigidbodyComponent>();

    if (rigidBody != null)
    {
        var direction = new Vector3(0, 3, 0); // Apply impulse upward

        rigidBody.ApplyImpulse(direction);
    }
}

Run the application. You should see additional output in the console window when you click around, highlighted in green by GlobalLogger. However, the text in the game window may flash briefly, as itโ€™s being overwritten every frame.

Moving Output to the Screen ๐Ÿ”—

To keep the text on the screen, move the game.DebugTextSystem.Print() call to the beginning of the Update() method:

void Update(Scene scene, GameTime time)
{
    // This was moved
    game.DebugTextSystem.Print($"Entities: {scene.Entities.Count}", new Int2(50, 50));

    var deltaTime = (float)time.Elapsed.TotalSeconds;

    // Handle non-physical movement for cube1
    if (cube1 != null)
    {
        if (game.Input.IsKeyDown(Keys.Z))
        {
            cube1.Transform.Position -= new Vector3(movementSpeed * deltaTime, 0, 0);
        }
        else if (game.Input.IsKeyDown(Keys.X))
        {
            cube1.Transform.Position += new Vector3(movementSpeed * deltaTime, 0, 0);
        }
    }

    // Handle physics-based movement for cube2
    if (cube2 != null)
    {
        var rigidBody = cube2.Get<RigidbodyComponent>();

        if (game.Input.IsKeyPressed(Keys.C))
        {
            rigidBody.ApplyImpulse(new Vector3(-force, 0, 0));
        }
        else if (game.Input.IsKeyPressed(Keys.V))
        {
            rigidBody.ApplyImpulse(new Vector3(force, 0, 0));
        }
    }

    if (camera == null || simulation == null || !game.Input.HasMouse) return;

    if (game.Input.IsMouseButtonPressed(MouseButton.Left))
    {
        // Check for collisions with physics-based entities using raycasting
        var hitResult = camera.RaycastMouse(simulation, game.Input.MousePosition);

        if (hitResult.Succeeded)
        {
            var message = $"Hit: {hitResult.Collider.Entity.Name}";
            Console.WriteLine(message);
            GlobalLogger.GetLogger("Program.cs").Info(message);

            var rigidBody = hitResult.Collider.Entity.Get<RigidbodyComponent>();

            if (rigidBody != null)
            {
                var direction = new Vector3(0, 3, 0); // Apply impulse upward

                rigidBody.ApplyImpulse(direction);
            }
        }
        else
        {
            Console.WriteLine("No hit detected.");
        }

        // Check for intersections with non-physical entities using ray picking
        var ray = camera.GetPickRay(game.Input.MousePosition);

        if (cube1Component?.BoundingBox.Intersects(ref ray) ?? false)
        {
            Console.WriteLine("Cube 1 hit!");
        }
    }
}

Now, when you run the application, the text "Entities:" should remain visible on the screen at position (50, 50). This gives us some basic screen output! ๐Ÿ“บ But since this output is more suited for debugging, letโ€™s explore adding more polished UI elements.

Seeing a different number of entities than expected on the screen? Debug the Update() and find out why is that ๐Ÿ˜ฏ.

Adding UI Elements ๐Ÿ”—

Letโ€™s create a simple text block on the canvas to display information to the player.

Replace your current code with this or refer to the comments labelled // This was added to see the specific changes.

using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.Compositing; // This was added
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Diagnostics;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Graphics; // This was added
using Stride.Input;
using Stride.Physics;
using Stride.Rendering; // This was added
using Stride.UI; // This was added
using Stride.UI.Controls; // This was added
using Stride.UI.Panels; // This was added

float movementSpeed = 1f;
float force = 3f;
Entity? cube1 = null;
Entity? cube2 = null;

CameraComponent? camera = null;
Simulation? simulation = null;
ModelComponent? cube1Component = null;

SpriteFont? font = null; // This was added

using var game = new Game();

game.Run(start: Start, update: Update);

void Start(Scene scene)
{
    game.AddGraphicsCompositor().AddCleanUIStage(); // This was updated
    game.Add3DCamera().Add3DCameraController();
    game.AddDirectionalLight();
    game.Add3DGround();
    game.AddProfiler();
    game.AddSkybox();
    game.AddGroundGizmo(position: new Vector3(-5, 0.1f, -5), showAxisName: true);

    var entity = game.Create3DPrimitive(PrimitiveModelType.Capsule);
    entity.Transform.Position = new Vector3(0, 8, 0);
    entity.Scene = scene;

    cube1 = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Gold),
        IncludeCollider = false // No collider for simple movement
    });
    cube1.Scene = scene;

    cube2 = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Orange)
    });
    cube2.Transform.Position = new Vector3(-3, 5, 0);
    cube2.Scene = scene;

    camera = scene.GetCamera();
    simulation = game.SceneSystem.SceneInstance.GetProcessor<PhysicsProcessor>()?.Simulation;
    cube1Component = cube1.Get<ModelComponent>();

    // This below was added: Create and display a UI text block
    font = game.Content.Load<SpriteFont>("StrideDefaultFont");
    var canvas = new Canvas
    {
        Width = 300,
        Height = 100,
        BackgroundColor = new Color(248, 177, 149, 100),
        HorizontalAlignment = HorizontalAlignment.Left,
        VerticalAlignment = VerticalAlignment.Bottom,
    };

    canvas.Children.Add(new TextBlock
    {
        Text = "Hello, Stride!",
        TextColor = Color.White,
        Font = font,
        TextSize = 24,
        Margin = new Thickness(3, 3, 3, 0),
    });

    var uiEntity = new Entity
    {
        new UIComponent
        {
            Page = new UIPage { RootElement = canvas },
            RenderGroup = RenderGroup.Group31
        }
    };

    uiEntity.Scene = scene;
}

void Update(Scene scene, GameTime time)
{
    game.DebugTextSystem.Print($"Entities: {scene.Entities.Count}", new Int2(50, 50));

    var deltaTime = (float)time.Elapsed.TotalSeconds;

    // Handle non-physical movement for cube1
    if (cube1 != null)
    {
        if (game.Input.IsKeyDown(Keys.Z))
        {
            cube1.Transform.Position -= new Vector3(movementSpeed * deltaTime, 0, 0);
        }
        else if (game.Input.IsKeyDown(Keys.X))
        {
            cube1.Transform.Position += new Vector3(movementSpeed * deltaTime, 0, 0);
        }
    }

    // Handle physics-based movement for cube2
    if (cube2 != null)
    {
        var rigidBody = cube2.Get<RigidbodyComponent>();

        if (game.Input.IsKeyPressed(Keys.C))
        {
            rigidBody.ApplyImpulse(new Vector3(-force, 0, 0));
        }
        else if (game.Input.IsKeyPressed(Keys.V))
        {
            rigidBody.ApplyImpulse(new Vector3(force, 0, 0));
        }
    }

    if (camera == null || simulation == null || !game.Input.HasMouse) return;

    if (game.Input.IsMouseButtonPressed(MouseButton.Left))
    {
        // Check for collisions with physics-based entities using raycasting
        var hitResult = camera.RaycastMouse(simulation, game.Input.MousePosition);

        if (hitResult.Succeeded)
        {
            var message = $"Hit: {hitResult.Collider.Entity.Name}";
            Console.WriteLine(message);
            GlobalLogger.GetLogger("Program.cs").Info(message);

            var rigidBody = hitResult.Collider.Entity.Get<RigidbodyComponent>();

            if (rigidBody != null)
            {
                var direction = new Vector3(0, 3, 0); // Apply impulse upward

                rigidBody.ApplyImpulse(direction);
            }
        }
        else
        {
            Console.WriteLine("No hit detected.");
        }

        // Check for intersections with non-physical entities using ray picking
        var ray = camera.GetPickRay(game.Input.MousePosition);

        if (cube1Component?.BoundingBox.Intersects(ref ray) ?? false)
        {
            Console.WriteLine("Cube 1 hit!");
        }
    }
}
  • font stores the SpriteFont used for the UI text block.
  • AddCleanUIStage() adds a clean UI stage to the graphics compositor, allowing UI elements to be displayed on top of the 3D scene.
  • game.Content.Load<SpriteFont>() loads the default font for the UI text block.
  • Canvas is a UI panel that acts as a container for UI elements.
  • TextBlock is a UI element that displays text on the screen.
  • UIComponent is a component that manages UI elements in the scene.
  • UIPage is a UI page that contains the root element of the UI hierarchy.
  • RenderGroup.Group31 specifies the rendering order of the UI element, ensuring it appears on top of other elements.

Save and run the application. You should now see the text "Hello, Stride!" displayed at the bottom left corner of the screen. ๐Ÿ“บ

Congratulations! ๐ŸŽ‰ You've successfully added output to the screen, using both simple debugging text and a more polished UI element. This visual feedback enhances the player experience by providing real-time information and interactions. ๐Ÿš€

Stride Game Engine - Code Only Basic - Output added
Youโ€™ve learned how to add various types of output, from basic console logging to real-time in-game text feedback. Using debugging text and polished UI elements, you can enhance both the developer experience and the player experience, providing essential information and interactivity in your game.

Step 17: Break 2 - Let's Reflect ๐Ÿ˜… ๐Ÿ”—

Time for a quick reflection on what we've achieved in the last few steps. We've significantly expanded our game's functionality and interactivity. Here's a recap:

  • Performance Monitoring: We added a profiler to monitor key metrics like FPS, helping optimize performance for a smoother experience. You also learned how to toggle through profiler outputs to track different aspects of the game. ๐Ÿš€
  • Enhanced Visuals: By adding a skybox, we made the scene more immersive, creating a polished and professional look. ๐ŸŽจ
  • Understanding Motion: We explored the difference between non-physical and physics-based movement, learning how to decide which method to use based on game mechanics. ๐Ÿง 
  • Non-Physical Movement: You learned how to move entities using Transform.Position without interacting with other objects. This approach is great for UI elements or simple animations. ๐Ÿšถโ€โ™‚๏ธ
  • Physics-Based Movement: By applying forces to entities via RigidbodyComponent, we introduced realistic interactions with gravity and collisions, adding depth to the gameplay. โš™๏ธ
  • Keyboard Controls: We implemented basic keyboard inputs to move entities, adding interactivity and responsiveness to the game. ๐ŸŽฎ
  • Mouse Controls: We extended player interaction by integrating mouse clicks, allowing players to trigger actions like applying forces to entities. ๐Ÿ–ฑ๏ธ
  • Displaying Output: Finally, we explored output options, from console logs to UI elements, enhancing player feedback and communication. ๐Ÿ“Š
  • Entities Count: While debugging, you discovered that an entity can be added to the scene without necessarily having a visual representation. You also learned that the camera, light, game profiler, and skybox are all entities as well. ๐Ÿคฏ

Step 18: Add More Primitives - Let's go crazy! ๐Ÿคช ๐Ÿ”—

Two boxes ๐Ÿ“ฆ๐ŸงŠ and a capsule ๐Ÿ’Š? That's not enough fun! I didn't sign up for just that! Let's crank it up and add more shapes to the scene!

To do this, add the following code inside the Update() method, below the if (cube2 != null) {} block:

if (game.Input.IsKeyDown(Keys.Space))
{
    var entity = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
    {
        Material = game.CreateMaterial(Color.Green),
        Size = new Vector3(0.5f),
    });

    entity.Transform.Position = new Vector3(0, 10, 0);
    entity.Scene = scene;
}

Now, run the application, zoom out the camera to view the entire ground, and press the Space key. Watch as new cubes spawn, pushing your FPS to its limits! ๐Ÿš€ You can still use the left mouse button to apply forces to the cubes and the capsule, but thatโ€™s getting a bit old, isnโ€™t it? ๐Ÿฅฑ

Let's spice things up with more mouse interaction, this time using the middle mouse button. Firstly add this namespace using Stride.CommunityToolkit.Helpers; and then add the following code inside the Update() method, just below the line if (camera == null || simulation == null || !game.Input.HasMouse) return;:

 if (game.Input.IsMouseButtonDown(MouseButton.Middle))
 {
     var hitResult = camera.RaycastMouse(simulation, game.Input.MousePosition);

     if (hitResult.Succeeded)
     {
         var rigidBody = hitResult.Collider.Entity.Get<RigidbodyComponent>();

         if (rigidBody != null)
         {
             var direction = VectorHelper.RandomVector3([-20, 20], [0, 20], [-20, 20]);

             rigidBody.ApplyImpulse(direction);
         }
     }
 }

The final code is also available on GitHub.

Run the application, and now, whenever you click the middle mouse button on an object, it will get a random impulse in a random direction. ๐ŸŽฒ How cool is that?

More cubes = more responsibility! Keep an eye on performance. Once cubes float too far away into the void, consider removing them from the scene to maintain performance. We will cover this in Part 2 of the series.

You've now learned how to create more primitives using keyboard input and apply random forces to objects with the middle mouse button - all in just around 240 lines of code.

Wrapping Up: Your Journey Continues ๐ŸŽฏ ๐Ÿ”—

Congratulations, explorer! ๐ŸŽ‰ You've navigated through another significant chapter in your game development journey. ๐ŸŒ In this part, you delved deeper into interactivity, mastering how to implement mouse and keyboard controls to make your game more dynamic and engaging.

What You've Achieved ๐Ÿ”—

By adding mouse interactions, youโ€™ve empowered players to directly interact with objects in your scene using techniques like raycasting and ray picking. Combined with the keyboard controls you implemented earlier, your game now responds to player inputs, allowing for real-time movement, actions, and feedback. ๐Ÿ–ฑ๏ธโŒจ๏ธ

The Power of Interactivity ๐Ÿ”—

Interactivity is at the heart of game design. By giving players the ability to influence the game world, you create a more immersive and engaging experience. The skills you've developedโ€”handling inputs, moving objects, managing physics, and providing feedbackโ€”are foundational to building more complex and polished games. ๐ŸŒŸ

If you'd like to explore the final code for this project, you can access it on GitHub. Feel free to check it out and experiment! ๐Ÿ’ป

Follow-Up Articles ๐Ÿšถ ๐Ÿ”—

In the not-so-distant future, we will cover the following topics:

  • Stride Community Toolkit Preview - Code-Only Feature - Advanced: Let's get creative and explore more advanced features to take your game to the next level. ๐Ÿš€
    • Maximize the game window
    • Setting the Game Window Title
    • Hot reload
    • Removing entities from the scene
    • Entities vs Children vs Components
    • Interaction with the UI and from the UI
    • Transforming entities
    • Advanced physics interactions
    • Audio and sound effects
    • Particle effects
  • Stride Community Toolkit Preview - Code-Only Feature - Refactoring: Let's refactor the code to make it more modular, reusable, and maintainable. ๐Ÿ› ๏ธ
    • Or in other words, letโ€™s clean up the mess we made! ๐Ÿ˜…

Support Stride Engine ๐ŸŒŸ ๐Ÿ”—

Stride is an open-source project that thrives on community contributions and support. By using Stride, sharing your experiences, and contributing to the community, you help make the engine better for everyone. ๐Ÿš€

This content was reviewed and enhanced with the assistance of ChatGPT.

C#Stride3D.NETGame DevelopmentAdvanced

Recent Posts