O3DE Book Final 2111
O3DE Book Final 2111
iii
Game Programming
with Open 3D Engine
iv
List of Figures
2.1. Editor: new level ........................................................................................................ 11
3.1. An Entity with Transform and Camera components ........................................................... 16
3.2. Editor: Entity Outliner ................................................................................................. 17
3.3. Editor: A sphere Entity in "Simple" level ........................................................................ 18
3.4. Editor: Create (child) entity .......................................................................................... 18
3.5. Editor: child entity ...................................................................................................... 19
3.6. Entity with Child Transform ......................................................................................... 19
3.7. Entity with Mesh component ........................................................................................ 20
3.8. Selecting Mesh Asset via Interactive Search .................................................................... 21
3.9. Editor: _Sphere_1x1 entity ........................................................................................... 21
3.10. Editor: Composite Object ............................................................................................ 22
3.11. Editor: Creating an Entity ........................................................................................... 22
3.12. Editor: Creating a child Entity ..................................................................................... 22
3.13. Cylinder 1 Entity ....................................................................................................... 26
3.14. Cylinder 2 Entity ....................................................................................................... 27
4.1. MyProject solution in Visual Studio ............................................................................... 30
4.2. MyProject.Static library ................................................................................................ 31
4.3. MyProjectModule.cpp in Visual Studio ........................................................................... 36
4.4. Visual Studio solution with MyComponent ...................................................................... 38
6.1. Accessing Level entity and its components ...................................................................... 50
6.2. Level entity in Entity Inspector ..................................................................................... 51
6.3. Using AZ::Interface ............................................................................................. 52
7.1. Setting up an AZ::Event ............................................................................................... 56
7.2. TransformComponent notifies using AZ::Event ................................................................ 58
8.1. Calling through an interface behind an EBus ................................................................... 64
8.2. TransformBus and TransformComponent ........................................................................ 66
10.1. Basic Serialization ..................................................................................................... 82
10.2. SerializeContext and EditContext ................................................................................. 85
11.1. How to bring up Entity Outliner .................................................................................. 89
11.2. How to bring up Asset Browser ................................................................................... 89
12.1. Build structure of a gem ........................................................................................... 105
12.2. Gem Code Structure ................................................................................................. 106
12.3. mygem_files.cmake ........................................................................................... 109
13.1. IConsole snippet .................................................................................................. 111
13.2. Example of a console command: SetWindowXY .......................................................... 113
14.1. Configure Gems ...................................................................................................... 116
14.2. Chicken from NvCloth gem ....................................................................................... 117
14.3. Instantiate Prefab ..................................................................................................... 117
14.4. Pick Chicken_Actor.prefab .............................................................................. 118
14.5. Entities in the Level ................................................................................................. 119
14.6. Chicken in O3DE .................................................................................................... 119
15.1. Location of unit tests in a gem ................................................................................... 123
15.2. Location of unit tests in a gem ................................................................................... 124
17.1. Picking third person movement input bindings .............................................................. 143
17.2. Input component with third person movement bindings .................................................. 143
17.3. Asset Viewer .......................................................................................................... 143
17.4. "Move forward" action ............................................................................................. 144
17.5. Design of Capturing Input ......................................................................................... 144
17.6. Input Binding for "move forward" in Asset Editor ......................................................... 145
17.7. Chicken Actor with Chicken Controller component ........................................................ 146
18.1. Design of Moving the Chicken .................................................................................. 153
v
Game Programming
with Open 3D Engine
vi
Game Programming
with Open 3D Engine
vii
List of Examples
2.1. C:\Users\<user>\.o3de\Registry\bootstrap.setreg ................................................................. 9
2.2. The Asset Processor finished processing game assets ......................................................... 10
4.1. MyProject\Code\myproject_files.cmake ........................................................ 31
4.2. A default module file for a new project, MyProjectModule.cpp .................................... 32
4.3. The simplest component ............................................................................................... 33
4.4. The simplest MyComponent.cpp ................................................................................ 34
4.5. Building the project from command line ......................................................................... 36
4.6. MyComponent.cpp with Editor reflection .................................................................... 38
5.1. FindComponent in AzCore\Component\Entity.h ................................................ 43
5.2. An Example of Calling GetWorldTM on Transform Component ........................................ 43
5.3. An example of FindComponent ..................................................................................... 44
5.4. Definition of MyProject.Static in CMake ......................................................................... 44
5.5. AzToolsFramework\ToolsComponents\TransformComponent.cpp ........................................ 45
5.6. C:\git\o3de\Code\Framework\AzFramework\CMakeLists.txt ............................................... 46
5.7. MyProject.Static linking against AZ::AzFramework library ................................................ 46
5.8. MyFindComponent.h .............................................................................................. 48
5.9. MyFindComponent.cpp .......................................................................................... 48
6.1. Level entity with My Level Component .......................................................................... 51
6.2. C:\git\book\MyProject\Code\Include\MyProject\MyInterface.h ............................................ 52
6.3. MyLevelComponent with AZ::Interface .......................................................................... 52
6.4. Using AZ::Interface in another component ...................................................................... 53
6.5. MyLevelComponent.h ............................................................................................ 53
6.6. MyLevelComponent.cpp ........................................................................................ 54
6.7. MyInterfaceComponent.h .................................................................................... 54
6.8. MyInterfaceComponent.cpp ................................................................................ 55
7.1. MyEventComponent.h ............................................................................................ 59
7.2. MyEventComponent.cpp ........................................................................................ 60
8.1. TransformBus definition .......................................................................................... 65
8.2. TransformInterface snippet ................................................................................. 65
8.3. Example of an EBus call .............................................................................................. 66
9.1. OscillatorComponent.h snippet ........................................................................... 71
9.2. Example of handling AZ::TickBus::Handler::OnTick ........................................... 74
9.3. OscillatorComponent.h full listing ....................................................................... 76
9.4. OscillatorComponent.cpp full listing ................................................................... 77
10.1. AzCore\Serialization\SerializeContext.h ................................................. 83
10.2. AzCore\Serialization\EditContext.h .......................................................... 84
10.3. Reflect() with Editor Configuration ........................................................................ 85
10.4. AzCore\Serialization\EditContextConstants.inl ..................................... 86
11.1. Complex_Object.prefab snippet with Root Entity entity ................................................... 92
11.2. Complex_Object.prefab snippet with OscillatorComponent ............................................... 92
11.3. Method SpawnAllEntities ................................................................................... 94
11.4. MySpawnerComponent .......................................................................................... 94
11.5. Spawn entities at a location ......................................................................................... 95
11.6. MySpawnerComponent with Complex_Object.prefab ....................................................... 96
11.7. MySpawnerComponent.h ....................................................................................... 96
11.8. MySpawnerComponent.cpp ................................................................................... 97
12.1. enabled_gems.cmake ......................................................................................... 104
12.2. MyGemBus.h .......................................................................................................... 108
12.3. MyGemModuleInterface ........................................................................................... 109
13.1. AZ_CONSOLEFUNC usage ........................................................................................ 111
13.2. Use of AZ_CONSOLEFREEFUNC ............................................................................... 111
viii
Game Programming
with Open 3D Engine
ix
Game Programming
with Open 3D Engine
x
Introduction
Copyright
To illustrate how to use Open 3D Engine, screen shots, images, models, textures and short snippets of
source code belonging to and copyrighted by Open 3D Foundation and the Linux Foundation are repro-
duced in this book. Open 3D Engine (O3DE) is an Apache 2.0-licensed multi-platform 3D engine.
Tip
You should refer to Open 3D Engine website and the Linux Foundation websites for details on their
licenses:
• https://o3de.org/
• https://github.com/o3de/o3de/blob/development/LICENSE.txt
• https://o3d.foundation/
While writing this book, I was careful to always take several steps back from my own knowledge and
consider what I had to know to become comfortable with a task or a subsystem of O3DE. Whenever I
thought a certain idea was essential to comprehension, I would take another step back and see if there were
other important preceding concepts and details that I had come to take for granted, or implicitly assumed
to be there in my mind. And then I would carefully present each idea in the proper order.
I chose to error on the side of moving slowly and providing too much information rather than omit a detail
that would stop someone from learning O3DE from scratch.
Intended Audience
If you are new to O3DE and wish to learn the basics of the engine, then this is the book for you.
It will help a lot if you are a C++ developer already. O3DE is written primarily in C++. It does support
scripting languages, such as Lua and a visual language Script Canvas. However, one gets the most benefit
from O3DE knowing its fundamental layers in C++.
No one book can make a person an expert of O3DE. If I were to enumerate all the details and knowledge
necessary to achieve that level, one would get bored reading all the material. A human mind does not work
that way. Instead, this book goes over the essential sub-systems of the engine in moderate depth and shows
how to use them. What is a component? What is Behavior Context? How does one add a sound effect?
How do you build a server-authoritative multiplayer game in O3DE?
The end goal of this book is to provide a wide overview of the engine. After that you will be able to learn
O3DE on your own beyond the scope of this book.
xi
Introduction
Some programming books present code examples where a big portion of the code is hidden behind their
custom helper frameworks. You will find no such helper functions in this book. I believe it is better to show
the entire code and avoid hiding details behind methods whose only purpose is to make examples smaller.
If the source code printed in this book is not enough, the accompanying source code and assets in their
entirety can be found on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111
A unique Git branch is assigned to each chapter of this book. The main branch is empty except for an
introductory file by design. Each following branch builds on the previous one. Each chapter includes a
link to the correct GitHub branch from the above repository.
• https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch15_unittests
• https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch16_unittests_mock
Correspond to Chapter 15, Writing Unit Tests for Components and Chapter 16, Unit Tests with Mock Com-
ponents.
1. Go to the branch associated with the chapter you are reading. For example:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch04_creating_components
xii
Introduction
5. That will create a pull request configuration that will show all the differences between the two branch-
es.
xiii
Introduction
Note
If you are curious what this command does, as you should, here is the official reference for it:
https:/go.microsoft.com/fwlink/?LinkID=135170
If you are at all unsure of what this does to your system, then do not run it. You will not need it to
understand the content of this book. You just will not be able to run some helpful script from various
chapters, but you can always make your own in your favorite scripting language. You can find more
documentation on PowerShell here:
https://docs.microsoft.com/en-us/powershell/
Tip
You can always revert the changes to execution policy by executing:
xiv
Introduction
notepad $PROFILE
$PROFILE is your personal configuration file for PowerShell that will execute commands of your choice
every time a new PowerShell console opens. Therefore, we can modify this script to help us with O3DE
development by adding helpful console shortcuts.
Set-Location "C:\git\book"
function Run-AP() {
& "C:\O3DE\21.11.2\bin\Windows\profile\Default\AssetProcessor.exe"
}
function Run-Editor() {
& "C:\O3DE\21.11.2\bin\Windows\profile\Default\Editor.exe"
}
The next time you open a PowerShell console, you will start in your work folder and load up any custom
scripts that you write in $profile.
C:\git\book> Run-Editor
And now onto learning Open 3D Engine! The game of Chicken Ball awaits!
xv
Part I. Getting Started
Table of Contents
1. Installing Open 3D Engine on Windows ............................................................................. 3
Introduction ............................................................................................................... 3
Preparing Visual Studio ............................................................................................... 3
Acquiring O3DE ......................................................................................................... 4
Running the Installer ................................................................................................... 4
2. Creating a New Project .................................................................................................... 6
Introduction ............................................................................................................... 6
O3DE Command Line Interface .................................................................................... 6
Linking Projects and O3DE .......................................................................................... 7
Building with CMake and Visual Studio ......................................................................... 7
Asset Processor .......................................................................................................... 8
Create a Level .......................................................................................................... 11
Summary ................................................................................................................. 11
2
Chapter 1. Installing Open 3D Engine
on Windows
Introduction
This chapter will cover the following topics:
• Getting O3DE.
• Installing O3DE. (At the time of writing this book, O3DE 21.11.2 release was available.)
https://www.o3de.org/docs/welcome-guide/setup/
O3DE is a C++ game engine, so it requires a few native Software Development Kits and various tools
from Visual Studio (if you are building on Windows OS). By default, Visual Studio might not have some
of these requirements, so I will go over the components that you do need to install.
Note
Make sure to refer to the official reference on this topic:
https://www.o3de.org/docs/welcome-guide/requirements/
Important
The supported version of Visual Studio by O3DE as of 21.11 is 2019, however, I have not found any
issues with using Visual Studio 2022 and O3DE 21.11 together. The installation steps for either Visual
Studio versions are the same.
2. Modify its installation to add Desktop development with C++ and Game development with C++.
3. Install CMake 3.20.5 or later from https://cmake.org/download/. During installation, select one of the
options that adds CMake to the system PATH.
3
Installing Open 3D
Engine on Windows
Acquiring O3DE
O3DE is available in two different ways. One source is a direct stand-alone installer and the other is O3DE
GitHub repository. I will start by using the installer. You can download the installer from here:
https://www.o3de.org/download/#windows
Note
I present the steps for building your own installations of O3DE from its GitHub repository in Chap-
ter 28, Wwise for O3DE and Chapter 31, Setting Up Multiplayer.
Once you download the installer and launch it, you will see its starting window.
4
Installing Open 3D
Engine on Windows
You can find where the engine will be installed by looking at Options.
Important
Prior to launching the installer, you have to install CMake first as the installer needs to perform a
number of initial steps that uses CMake, such as downloading various dependencies.
Once the installation has completed, confirm that the installed location contains the engine. By default,
the location will be C:\O3DE\21.11.2. For the remainder of the book, I will reference this path as the
engine path. If you choose to install it elsewhere, adjust the instructions accordingly.
If the engine installed successfully, you will be able to launch O3DE Project Manager from the installer
by clicking Launch button or directly from C:\O3DE\21.11.2\bin\Windows\profile\De-
fault\o3de.exe.
5
Chapter 2. Creating a New Project
Introduction
Note
The source and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch02_new_project
Tip
You can find the official getting started guide at:
https://www.o3de.org/docs/welcome-guide/create/creating-projects-using-project-manager/
PS C:\git\book> C:\O3DE\21.11.2\scripts\o3de.bat -h
usage: o3de.py [-h]
Note
The parameter -pp stands for project path. The script will deduce the project name to be MyProject.
There are more options and commands available for o3de.bat but this is enough for us to get started.
Confirm that C:\git\book\MyProject was created and contains various project files.
Tip
I advise you to put your project under Git source control right away. As a reminder, I am creating
the project under c:\git\book folder. If you are following along the book, you can clone the
accompanying source code with the following commands:
6
Creating a New Project
mkdir c:\git\
cd c:\git
git clone https://github.com/AMZN-Olex/O3DEBookCode2111 book
cd book
git checkout origin/ch02_new_project
There are two important files involved. The first one is project.json that is found in your project at
C:\git\book\MyProject\project.json. The second one is C:\Users\<user>\.o3de
\o3de_manifest.json.
project.json specifies the name of O3DE engine installation to use with engine property.
{
"project_name": "MyProject",
...
"engine": "o3de-sdk"
}
o3de_manifest.json specifies all your engine installations and their corresponding names.
"engines_path": {
"o3de-sdk": "C:/O3DE/21.11.2"
}
If you ever move the engine installation or wish to try out a new engine, you will need to update these
entries accordingly.
Tip
The location of engines and projects is entirely up to you, so long as the two files above agree.
I have chosen C:\git\book\build for the remainder of the book. Once inside the folder, execute the
following CMake instruction to configure the project:
Parameter -S specifies the source location of our project, while -B specifies the build folder location. The
build folder is set to be the current folder which is C:\git\book\build.
7
Creating a New Project
Tip
One other parameter I often use is -DLY_UNITY_BUILD=OFF. That turns off CMake unity files
that compiles several source files together as one. While that can speed up your first initial compilation,
I find that on incremental small changes inside a project, it is faster to work with unity builds disabled.
This step can take a while depending on your Internet download speed. A lot of third party packages will
be downloaded and installed into C:/Users/<user>/.o3de/3rdParty.
Once the configuration has completed, Visual Studio solution will be generated at C:\git\book
\build\MyProject.sln. From this point on, you can either build your project from Visual Studio
or from the command line, such as:
Asset Processor
Before we start looking into launching our game project, we need to understand the main four executables
involved in designing and running a game in O3DE: the Editor, the Asset Processor, game and server
launchers.
Whenever you launch the Editor, the Asset Processor will be launched in the background if it is not running
yet. I recommend running the Asset Processor on its own at first to confirm that the project setup is good
and all the assets are processing.
C:\O3DE\21.11.2\bin\Windows\profile\Default\AssetProcessor.exe
Before you launch the Asset Processor you need to configure the default project for O3DE. Otherwise,
you will see the following error.
The Asset Processor tried to read the default project but did not find one specified in C:\Users\<user>
\.o3de\o3de_manifest.json. You should either pass the default project to the Asset Processor
8
Creating a New Project
with --project-path C:\git\book\MyProject or by setting the default project using O3DE Command Line
Interface:
C:\O3DE\21.11.2\scripts\o3de.bat set-global-project
-pp C:\git\book\MyProject
Tip
You can confirm that the above operation succeeded by looking at C:\Users\<user>\.o3de
\Registry\bootstrap.setreg. It should have project_path set to C:\git\book\MyPro-
ject. You can either modify this file directly or use o3de.bat.
{
"Amazon": {
"AzCore": {
"Bootstrap": {
"project_path": "C:/git/book/MyProject"
}
}
}
}
With this configuration, the Asset Processor can be launched from the command line without specifying
the project path.
Important
Only one Asset Processor can be launched for your project. If you attempt to launch another one it
will throw an error "Another instance of the Asset Processor may already be running on port 45643".
If another instance is already running, then there is no need to launch another one. The Editor and
game launchers will connect to the Asset Processor if one is already running.
Tip
The Asset Processor can be sneaky. If it is launched by the Editor or a game launcher, it will start in
the background, and you will have to find it in the Windows system tray. However, if you launch it
directly from the command line, it will open in the foreground.
Once Asset Processor processes all the assets in the top left corner of its window it should show the
following:
• Status: Idle... - otherwise it is still processing assets and you should wait until all the assets are done.
• Root: C:/O3DE/21.11.2 - confirm that the Asset Processor is using the expected engine.
9
Creating a New Project
C:\O3DE\21.11.2\bin\Windows\profile\Default\Editor.exe
Or from Visual Studio solution by building and running the Editor project that is in O3DE_SDK solution
folder.
Important
Do not close Asset Processor while working with O3DE. It is needed for many O3DE tools, including
the Editor. Instead, minimize the Asset Processor and let it idle in the Windows system tray.
Tip
One of most common pitfalls is getting build errors due to the Asset Processor holding a lock on your
game binaries. The way to solve that is to stop the Asset Processor every time before re-compiling
your project. However, that gets tiresome. When you close the Editor, the Asset Processor is still
running in the background. One way to simplify your life is to tell the Asset Processor to shutdown
itself whenever the Editor or the last game launcher quits. You can do so by adding this switch to C:
\git\book\MyProject\game.cfg.
10
Creating a New Project
ap_tether_lifetime=1
Create a Level
The new project we created comes without a level, but we can create a new one from an existing O3DE
template. Once the Editor launches, you will see a Welcome to O3DE dialog. Create a new level using
Create new... button.
Summary
Note
The source and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch02_new_project
11
Creating a New Project
Our next topic will be Entities. You can see them in Entity Outliner. If not visible, you can bring it up
from Editor's menu: Tools→ Entity Outliner.
Now we are ready to start looking into the most fundamental aspects of O3DE: entities and their compo-
nents. That will be the focus of Chapter 3, Introduction to Entities and Components.
12
Part II. Entities and Components
Table of Contents
3. Introduction to Entities and Components ........................................................................... 15
Introduction .............................................................................................................. 15
O3DE Components .................................................................................................... 17
Composing Objects with Entities ................................................................................. 21
Summary ................................................................................................................. 27
4. Writing Your Own Components ....................................................................................... 29
Introduction to Game Project Files ............................................................................... 29
Creating Your Own Component ................................................................................... 33
14
Chapter 3. Introduction to Entities and
Components
To exist is to be something, as distinguished from the nothing of nonexistence, it is to
be an entity of a specific nature made of specific attributes.
—Ayn Rand
Introduction
Note
We are picking up where we left off in Chapter 2, Creating a New Project where we created "MyLevel"
in the Editor. The assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch03_intro_to_entities
• What is an Entity?
• What is a Component?
• Transform component.
• Mesh component.
In O3DE the game world is constructed out of Entities (AZ::Entity in C++) that may contain a number
of Components (AZ::Component). An entity is an independent group of components. Conceptually, a
component is a particular attribute of an entity. For example, for an entity to be possess a location in space,
it must have a transform component, TransformComponent. Similarly, if you wish to create an entity
that is a movable camera, then it must have a transform and a camera component.
An Entity by itself is an empty container of components. It has little to itself aside from the attached
components. You can find Entity source header at: C:\O3DE\21.11.2\Code\Framework\Az-
Core\AzCore\Component\Entity.h.
An important aspect of entities is how light they are. All they really have is their identifier, AZ::Enti-
tyId. The rest of their interface has to do with interacting and managing the components that an entity
has. All the behavior of an entity comes from the components it has. The components describe behavior
and attributes of an entity. You are free to compose entities out of any combination of components.
15
Introduction to Enti-
ties and Components
Here, we are viewing an entity in Entity Inspector that has Transform and Camera components attached.
Note
You can find the official reference on entities and components at:
https://www.o3de.org/docs/welcome-guide/key-concepts/#the-component-entity-system
/**
* Base class for all components.
*/
class Component
Before we start digging into C++ code and interface of AZ::Component, let us go through the workflow
and the common way of using entities and components to build game objects in the Editor.
16
Introduction to Enti-
ties and Components
O3DE Components
A great way to get an idea about components is by taking a look at components that O3DE comes with. The
first few components you are bound to run into are TransformComponent and MeshComponent.
Note
A great starting guide to O3DE Editor on how to create entities and components can be found at
https://www.o3de.org/docs/learning-guide/tutorials/first-project/.
You should go through the tutorial and tinker on your own in the Editor to get a sense for the interface.
The rest of the chapter will go through the workflow of adding and modifying entities.
Transform Component
All entities created inside the Editor get a Transform component automatically added to it.
Why does an entity get a Transform component by default? If you were to create an Entity at runtime
in game code, then the entity would be entirely empty. It will not even have a Transform component unless
you add it yourself at runtime. However, when you create an entity in the Editor that implicitly places
the entity somewhere in the level. Somewhere implies a location, therefore it must have a Transform
component to be somewhere on the level.
Tip
One exception to that rule is a special Level entity and its Level components. We take a look at them
in Chapter 6, What is AZ::Interface?
"MyLevel" contains a few entities already. Here is one, Shader Ball, viewed in Entity Inspector.
17
Introduction to Enti-
ties and Components
Note
The official reference for Transform component can be found at: https://www.o3de.org/docs/user-
guide/components/reference/transform/ alongside with many other components.
What is "Parent entity" field? Components can introduce their own concepts and relationships. This
field associates one entity to another entity in terms of their spatial relationship in the world. A child
parent's position, orientation and scale will depend on the values of its parent entity.
"Child" Entities
You can create a "child" Entity in Entity Outliner by right clicking on an existing entity and choosing
Create entity. This is how it would look like in the Editor.
After the entity is created, select it in Entity Outliner, and rename to "Child of Shader Ball" in Entity
Inspector.
18
Introduction to Enti-
ties and Components
The entity name is optional but it reminds us here that this entity is the child of Shader Ball entity, as
expressed in the property Parent entity of Transform component.
Parent entity points to Shader Ball. That means the values of Translate, Rotate and Uniform Scale
become local values that depend on the parent's values, which means Shader Ball's Transform component
values.
19
Introduction to Enti-
ties and Components
Mesh Component
A Mesh component allows you to add a graphical object to an entity. You can add it to an entity using
Add Component menu in Entity Inspector.
Tip
I have collapsed Transform component to keep it out of the way but it is still there. You can toggle
the collapsed state using the little triangle next to the name of the component.
In this case, Mesh asset has already been set, but you can always change it using a dialog that open when
you click on folder button to the right of Mesh Asset property.
Another way to specify mesh asset is by typing directly in Mesh Asset text field. As you type a drop down
list will appear the matches your text entry.
20
Introduction to Enti-
ties and Components
Add Material Component button allows you to assign a custom material for the mesh. In this case, the
entity does not have a material component, so a default material of the mesh is used.
Notice that Mesh component does not have any offset or position properties. Mesh component does
not provide any properties to specify how the mesh is to be positioned. Implicitly, the mesh is placed at the
center of the entity, the local position of (0, 0, 0). Imagine for a moment that the mesh asset was centered
at some non-zero local point. Then we would need to do some additional work to center it properly. We
will tackle that in the next section.
21
Introduction to Enti-
ties and Components
We will start with an empty "root" entity that is going to be the parent for all other entities we will create
to represent the object. This is often useful, so that we can move the entire group together in the Editor
by moving the "root" entity. It is still an entity like any other but Transform component parent entity field
will keep them all together.
An entity can be created in the Editor by right clicking in the empty space in the main viewport and
selecting Create entity.
The Editor will assign some auto-generated name for it, such as Entity11, but we can rename it in Entity
Inspector. I'll choose Root Entity.
Create a child entity of Root Entity for the ball in the middle of the composite object.
Create two cylinder meshes by creating two more child entities. This time the parent will be Ball Entity.
Name child entities Cylinder 1 Entity and Cylinder 2 Entity. The outline will show their structure.
22
Introduction to Enti-
ties and Components
Now we can assign various mesh components to ball and cylinder entities. Here is how you can add a mesh
component to Ball Entity. Select Ball Entity. In Entity Inspector click on Add Component.
That will list all available components given your current project configuration.
Tip
I find it easier to enter the name of the component in the Search... field to quickly find the component
I am looking to add.
23
Introduction to Enti-
ties and Components
Atom/Mesh is the component we want. Clicking on it will add it to the entity. And then we can specify
Mesh asset to be _Sphere_1x1 just like the other entities we have looked at earlier.
Note
Not all components can be added more than once to the same entity. You can specify that restriction
yourself when you write your components with static C++ method GetIncompatibleServices,
which is covered in Chapter 9, Using AZ::TickBus. Here is how Add Component menu would look
like if you were considering adding a second Mesh component to the same entity.
The Editor automatically adds (1) to the name of the component. That indicates that there is already
a Mesh component on your entity. If a component was configured to limit itself to a single instance
per entity, then you would no longer be able to find it in Add Component menu.
24
Introduction to Enti-
ties and Components
Cylinder entities follow the same structure but with a different Mesh asset and custom values in Transform
component in order to move them to the side and change their scale in X and Y dimensions. Specifically,
Ball Entity has a Translate value of (0, 0, 0) which is the center of the object. But cylinders' Translate
values are (0.5, 0, 0) and (-0.5, 0, 0).
Note
Non-uniform Scale is a special component that allows you to configure a non-uniform scale. Other-
wise the entity is scaled uniformly in all directions.
25
Introduction to Enti-
ties and Components
26
Introduction to Enti-
ties and Components
Summary
Note
The assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch03_intro_to_entities
You can move the object by its Root Entity into the view of the camera entity. Once you are done with
the changes, you need to save the level. The shortcut for that is CTRL+S or from the Editor's main menu
File → Save.
You can then press CTRL+G to test the level in the editor. The composite object we have made in this
chapter would look like this:
27
Introduction to Enti-
ties and Components
This chapter looked at the general idea of entities and components in O3DE. The real power lies in creating
your components. In order to create custom components, one has to write them in C++ code. The next
chapter will start by looking at the overall structure of a O3DE C++ project and how brand new components
can be written from scratch in C++.
28
Chapter 4. Writing Your Own
Components
Official reference how to create O3DE game projects:
https://www.o3de.org/docs/user-guide/project-config/project-manager/
Note
In later chapters, we will re-visit the structure of a game project in O3DE once we go over more
concepts, such as EBus, gems, prefabs, etc. For now, I am going to cover just the basics to get us
started writing new components.
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch04_creating_components
There are a lot of projects and sub-folders in here but we are going to focus on the essentials.
• ZERO_CHECK is a special CMake target that rebuilds Visual Studio from any changes to CMake
build files. When we add a new component we will need to build this project and re-load the solution
with the updates.
• Editor is the target that runs O3DE Editor with our project. It does not actually build any of the Editor
code. The Editor binary is provided by the O3DE installation in C:\O3DE\21.11.2.
29
Writing Your Own Components
• MyProject is the dynamic library that links MyProject.Static and provides the component informa-
tion to the Editor and game launcher. In other words, MyProject.Static defines the components and
MyProject registers them for your project.
At the moment the only component we have in the project is the default system component.
• C:\git\book\MyProject\Code\Source\MyProjectSystemComponent.h
• C:\git\book\MyProject\Code\Source\MyProjectSystemComponent.cpp
• Include/MyProject contains various public interfaces. I will cover various options available in
O3DE in later chapters when we tackle communication between components, starting with Chapter 5,
What is FindComponent?
• Source contains source and header files of the components and any other classes and objects you
might write.
• enabled_gems.cmake deals with the gem system that we will tackle in Chapter 12, What is a Gem?
• myproject_files.cmake defines the list of source and other files to be included in the build and
in Visual Studio solution.
30
Writing Your Own Components
Generally, any new component you would write would be placed under MyProject\Code\Source,
either directly in Source folder or under a sub-folder of your choice.
myproject_files.cmake
*_files.cmake are O3DE project files that list the files to be included and compiled for a project.
set(FILES
Include/MyProject/MyProjectBus.h
Source/MyProjectSystemComponent.cpp
Source/MyProjectSystemComponent.h
enabled_gems.cmake
)
Tip
You can also include files other than the source code, such as various configuration files, shaders files,
if you wish to have easier access them inside Visual Studio. The build system will exclude them from
compilation.
You can see references to source and header files of MyProjectSystemComponent. File paths must
be local to the project's Code folder: C:\git\book\MyProject\Code.
Important
In order to add new files to the build, you must reference them in this file.
31
Writing Your Own Components
Note
O3DE uses CMake as its build system. This chapter will only cover the essentials of CMake in order
to add a new component. Each following chapter will introduce just enough of CMake knowledge to
accomplish each task. As you progress through the chapters you will acquire all the necessary basics
to feel comfortable with O3DE's use of CMake.
MyProjectModule.cpp
And the other reference to MyProjectSystemComponent is in the module file. A module source file
is the root file of a project that declares the project and, most importantly for this chapter, registers all
the components.
MyProjectModule()
: AZ::Module()
{
// Push results of [MyComponent]::CreateDescriptor()
// into m_descriptors here.
m_descriptors.insert(m_descriptors.end(), {
MyProjectSystemComponent::CreateDescriptor(),
});
}
Any components added to m_descriptors in the code snippet above would be available for use in your
project and, if properly configured, in the Editor. Here is the entire module file for reference.
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Module/Module.h>
#include "MyProjectSystemComponent.h"
namespace MyProject
{
class MyProjectModule
: public AZ::Module
{
public:
AZ_RTTI(MyProjectModule,
"{4b67609b-b22c-4a71-9afe-3f9d10bcf5ac}", AZ::Module);
AZ_CLASS_ALLOCATOR(MyProjectModule, AZ::SystemAllocator, 0);
MyProjectModule()
: AZ::Module()
{
m_descriptors.insert(m_descriptors.end(), {
MyProjectSystemComponent::CreateDescriptor(),
});
}
/**
* Add required SystemComponents to the SystemEntity.
32
Writing Your Own Components
*/
AZ::ComponentTypeList
GetRequiredSystemComponents() const override
{
return AZ::ComponentTypeList{
azrtti_typeid<MyProjectSystemComponent>(),
};
}
};
}// namespace MyProject
AZ_DECLARE_MODULE_CLASS(Gem_MyProject, MyProject::MyProjectModule)
Note
Note that GetRequiredSystemComponents() is a special method to register components that
are marked as system components. System components are placed on a special system entity that is
activated as soon as the engine starts and persists outside of game levels until shutdown.
https://www.o3de.org/docs/user-guide/programming/components/create-component/
You cannot create your own custom AZ::Entity in O3DE but you can create your own custom com-
ponent derived from AZ::Component. This chapter will show how to create a simple component that
shows up in the Editor. We will create a dummy component called, MyComponent. In general, whenever
you add a new component to a project, you have to do the following steps:
MyComponent.h
The most basic and simplest component that does nothing aside from existing is as follows:
#pragma once
#include <AzCore/Component/Component.h>
33
Writing Your Own Components
namespace MyProject
{
// An example of the simplest O3DE component
class MyComponent : public AZ::Component
{
public:
AZ_COMPONENT(MyComponent,
"{4b589f6b-79f3-47b6-b730-aad0871d5f8f}");
// AZ::Component overrides
void Activate() override {}
void Deactivate() override {}
MyComponent must derive from AZ::Component. All O3DE game components must be derived
from AZ::Component either directly or through another class that derives from it.
Macro AZ_COMPONENT. This macro marks the component type with a unique guid string. The first
parameter is the class name. The second parameter is a unique identifier for this class in a guid form. You
are responsible for creating a unique guid with curly brackets around it.
Note
Visual Studio IDE has a built-in guid generator in the menu: Tools → Create GUID. You can also
generate a guid using Visual Code plugins or even a PowerShell command:
PS C:\work\book\MyProject> (New-Guid).ToString("B")
{dcbbe9e2-5630-4e4b-ad7a-308c7abe5abf}
Reflect() method: provides a runtime serialization of this component to the O3DE engine. In the
next section, I will show the minimum work involved in reflecting a component to the Editor.
MyComponent.cpp
This is where the description and the behavior of the component will be. However, for now we are going
to only provide a simple stub.
#include "MyComponent.h"
#include <AzCore/Serialization/EditContext.h>
34
Writing Your Own Components
Tip
AZ_UNUSED is macro that does nothing but pretends to use a parameter in order to silence some
compiler warnings. In modern C++ you can also use [[maybe_unused]] on the parameter dec-
laration.
This is enough to compile the project. Once we have everything in place, we will return to Reflect
method. It will be useful to try different implementation of Reflect and see the consequences. So let us
finish the rest of the C++ code changes that we need.
myproject_files.cmake
The file list for the project is located at C:\git\book\MyProject\Code\myprojec-
t_files.cmake. Since we only added one component, the following changes are enough:
set(FILES
Include/MyProject/MyProjectBus.h
Source/MyProjectSystemComponent.cpp
Source/MyProjectSystemComponent.h
enabled_gems.cmake
Source/MyComponent.cpp # new
Source/MyComponent.h # new
)
MyProjectModule.cpp
And lastly, the project needs to register the new component by adding its descriptor to the project module.
...
#include "MyComponent.h"
...
MyProjectModule()
: AZ::Module()
{
m_descriptors.insert(m_descriptors.end(), {
...
MyComponent::CreateDescriptor(),
});
}
You can find this module file under MyProject build target in Visual Studio.
35
Writing Your Own Components
CreateDescriptor is a method of AZ::Component. For simple tasks, you do not need to worry
about what it does at this point. Just be aware that it provides a description of the component to the engine.
Tip
Close the Editor and the Asset Processor if they are running. Otherwise, the Asset Processor or the
Editor might hold a lock on one of your project binaries. In such a case you will get a build error:
We have two ways of compiling the project on Windows: either Visual Studio solution or through the
CMake command line interface.
Note
"--build" is a switch that tells CMake where the binary folder is for our project. Early on we created
it at C:\git\book\build. --config profile tells CMake to build profile flavor of our project.
Tip
If you ever need to debug your components, instead of compiling the project in debug build, you can
add the following lines instead and still use profile binaries.
36
Writing Your Own Components
https://docs.microsoft.com/en-us/cpp/preprocessor/optimize?view=msvc-170
Build started...
1>------ Build started: Project: ZERO_CHECK
1>Checking Build System
1>CMake is re-running because ...
1> the file 'C:/git/book/MyProject/Code/myproject_files.cmake'
1> is newer than ...
Tip
CMake detects changes based on file modification time. ZERO_CHECK should only run when one
of the build files have changed in your project. If you are seeing it run on every build, then inspect the
build log and see which file CMake thinks has changed since the last build.
Once the build completes, Visual Studio will ask you if you want to reload the solution from disk, select
Reload All.
Reload VS solution
Here is how the project now looks like in Visual Studio with MyComponent files.
37
Writing Your Own Components
Summary
If you were to re-launch the Editor, you would not find the component in Add Component menu in Entity
Inspector. This is because MyComponent::Reflect() was left empty, therefore it did not tell the Editor
about MyComponent. Let us change that to finish up this chapter.
Note
Reflecting a component to the Editor is a design choice. Not all components need to be visible in the
Editor. We will see various reasons for both sides in later chapters.
sc->Class<MyComponent, Component>()
->Version(1);
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
38
Writing Your Own Components
Reflection in O3DE is broken down into several parts. For now, it is enough to know that the above code
describes the following:
• MyComponent is an empty component with no properties and its version is one (1).
sc->Class<MyComponent, Component>()
->Version(1);
• MyComponent is a component that is available for use in game (and in the Editor) with name "My
Component" under the category "My Project".
Now Add Component menu in the Editor will list this component.
And you can add it to any entity, for example to Root Entity of the object we built in Chapter 3, Intro-
duction to Entities and Components.
39
Writing Your Own Components
Note
The source code for this project can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch04_creating_components
40
Part III. Introduction to
Component Communication
Now that we know how to create components, we need to learn how to build logic out of multiple components. The
next milestone that will allow you to do that is some form of inter-component communication. You need various
components talking to other components.
42
Chapter 5. What is FindComponent?
Introduction
The easiest and most direct form of communication between components is to get the pointer to the other
component and then directly invoke its methods. With a raw pointer to another component, you get the
fastest form of communication. There are some drawbacks to this approach but very often it is worth it.
It is also the easiest one to understand, which is why I am starting the discussion with the use of Find-
Component().
Note
You can find the code and project changes for this chapter on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch05_find_component
An Example
Any component that derives from AZ::Component can invoke the following:
GetEntity()->FindComponent<T>()
That enumerates over the components of an entity until a component of a given type T is found. You can
find it in the engine code here:
In previous chapters I covered Transform component. In this chapter, I will show you how to get its pointer
and get world translation (position) of the entity using TransformComponent's public interface.
• We are referencing a class from AzFramework library, so we will need to update our build script to
include AzFramework for MyProject.Static library.
43
What is FindComponent?
• We are adding a linking cost to the project, as building MyProject.Static from now on has to include
the entire AzFramework static library. Over time that can grow to a large amount of time linking for
sizable game projects.
Note
AzFramework library is included just about everywhere, so it will not be an issue but the general
idea applies. Keep an eye on the linking time of your project.
• We have added a dependence on another component. It means we have to consider what to do if the
component is not found. If we place this code at component activation, such as inside MyFindCom-
ponent::Activate(), what happens if TransformComponent activates after MyFindCom-
ponent? The component may not be ready for us to communicate.
Important
Activation of Transform component must occur before activation of MyFindComponent for our logic
to work.
44
What is FindComponent?
The list of included libraries for MyProject.Static is under BUILD_DEPENDENCIES. It has two sections:
private and public. Private portion tells the build system which libraries to include for this build target
only. Public portion tells the build system to include the libraries for the build target and for any targets
that include MyProject.Static as well.
At the moment, we are not interested in this difference and PRIVATE section will work.
BUILD_DEPENDENCIES
PRIVATE
AZ::AzFramework
How do you know which library to include to get access to TransformComponent? For this, we
have to do some spelunking. We will go on a journey of going through source and build files to find out
where TransformComponent lives. In the beginning we might only know about a component through
the Editor.
The component is called Transform in the Entity Inspector. As I showed in the previous chapter, a com-
ponent declares its name by specifying in its Reflect method. If you search long enough you can find
the following declaration:
Note
Here I am looking at the engine code directly. You should clone a copy of the engine if for nothing
else then for reference.
45
What is FindComponent?
cd c:\git
git clone https://github.com/o3de/o3de
namespace AzToolsFramework
{
namespace Components
{
class TransformComponent
: public EditorComponentBase
For now, just know that if a component you are investigating is inheriting from EditorComponent-
Base then it is an editor component that is temporary in nature and only exists at design time. When a
level is spawned at game time, these editor components are converted to game components.
Tip
In fact, you can see which game component is created by looking at BuildGameEntity method
of Transform Component.
Now that we know which component we will interact with at runtime we can find the library that in-
cludes it. The reference to TransformComponent.h is found in C:\git\o3de\Code\Frame-
work\AzFramework\AzFramework\azframework_files.cmake. Well, what is using this list
of files? The build script for AzFramework static library does.
Now we can link against this static library by using its name and namespace - AZ::AzFramework.
46
What is FindComponent?
NAMESPACE Gem
...
BUILD_DEPENDENCIES
PRIVATE
AZ::AzFramework
...
)
TransformComponent* tc = e->FindComponent<TransformComponent>();
We can ensure that we find the component we are looking for declaring a game design dependency of our
component on TransformComponent. A component can declare services it provides and services it
requires. For example, the editor version of Transform component declares the following service:
void TransformComponent::GetProvidedServices(
AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("TransformService", 0x8ee22c50));
}
Our example component, MyFindComponent, will then declare a dependency in static GetRe-
quiredServices method:
Note
Both AZ_CRC and AZ_CRC_CE macros serve the same function. They convert a string into a hashed
integer. The only difference is that AZ_CRC_CE computes the value at compile time.
When you are building entities in the Editor, the components will be saved in such a way that Trans-
formComponent will always come before MyFindComponent.
Important
There is one gotcha with these dependencies and the Editor, when you add or change a dependency
rule for a component that is already on the entity, you will have to modify the entity's components
for the rule to be re-applied.
Summary
Note
You can find the code and project changes for this chapter on GitHub:
47
What is FindComponent?
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch05_find_component
namespace MyProject
{
// An example of the simplest O3DE component
class MyFindComponent : public AZ::Component
{
public:
AZ_COMPONENT(MyFindComponent,
"{FE4F2E82-8E03-48EC-A967-559705597040}");
// AZ::Component overrides
void Activate() override;
void Deactivate() override {}
void MyFindComponent::Activate()
{
AZ::Entity* e = GetEntity();
48
What is FindComponent?
sc->Class<MyFindComponent, Component>()
->Version(1);
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
49
Chapter 6. What is AZ::Interface?
Introduction
The majority of component communication in O3DE occurs using AZ::EBus. It is the most powerful,
the most generalized and the most feature rich inter-component communication system in the engine. I
could even say that once you understand AZ::EBus you will understand how to work with the engine.
However, I will start by showing you the simpler models first - AZ::Interface in this chapter and then
AZ::Event in the next chapter. By studying these two first you will be ready to understand AZ::EBus.
Note
You can find the code and project changes for this chapter on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch06_az_interface
AZ::Interaface in O3DE is a fancy singleton that works across module boundaries. It allows you
declare an interface and then implement it in one of your objects. However, since AZ::Interface is,
essentially, a singleton, a component that acts as an interface handler has to be the only component that
handles that interface.
Note
You can find the documentation on AZ::Inteface online at
https://www.o3de.org/docs/user-guide/programming/az-interface/
Level Components
That brings us to a component type that is naturally unique and would serve as a great candidate to handle
an AZ::Interface: Level components. We have already seen regular game components that can be
added to entities in the Editor. There is another type of entity hidden in plain sight in Entity Outliner.
If you click on Level title, Entity Inspector will show you the Level entity, which by default has no com-
ponents on it.
50
What is AZ::Interface?
Only a component marked as a Level component in their Reflect method can be added to Level entity.
Tip
Whenever you find the Editor or the Asset Processor failing to start after you just added a new com-
ponent, check that your new components have unique guids. It is easy to copy and paste the new com-
ponent files and forget to update the guids. The debug log in Visual Studio will show the following
error in such a scenario:
Defining an AZ::Interface
In order to define and use your own AZ::Interface, there are three steps involved:
• Define a pure virtual class as the interface in a public header, such as MyInterface.
51
What is AZ::Interface?
#pragma once
#include <AzCore/RTTI/RTTI.h>
namespace MyProject
{
class MyInterface
{
public:
AZ_RTTI(MyInterface, "{D477F807-6795-4A1B-A475-AFBCF0584A08}");
virtual ~MyInterface() = default;
Here is an example of a component that acts as a handler for any requests placed on the interface.
52
What is AZ::Interface?
AZ::Interface<MyInterface>::Registrar will register the class as the handler for the inter-
face at construction time of MyLevelComponent and unregister at destruction time.
Tip
You can also manually register and unregister from the interface using Register and Unregister meth-
ods.
AZ::Interface<MyInterface>::Register(this);
// ...
AZ::Interface<MyInterface>::Unregister(this);
Summary
In this chapter I covered the use of AZ::Interface as a singleton that works across module boundaries.
There were two components created. MyLevelComponent implemented MyInterface and was as-
signed to the level entity. MyInterfaceComponent accessed MyInterface. We had two compo-
nents communicate with each despite being assigned on an entirely different kind of entities.
namespace MyProject
{
// An example of a level component with AZ::Interface
class MyLevelComponent
: public AZ::Component
, public AZ::Interface<MyInterface>::Registrar
{
public:
AZ_COMPONENT(MyLevelComponent,
"{69C64F9C-4C35-4612-9B3B-85FEBCC06FDB}");
// AZ::Component overrides
void Activate() override {}
void Deactivate() override {}
53
What is AZ::Interface?
// MyInterface
int GetMyInteger() override { return 42; }
};
}
sc->Class<MyLevelComponent, Component>()
->Version(1);
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
namespace MyProject
{
// An example of using AZ::Interface
class MyInterfaceComponent : public AZ::Component
{
public:
AZ_COMPONENT(MyInterfaceComponent,
"{F73AB7B7-4F19-42FD-9651-13167FD222A6}");
// AZ::Component overrides
void Activate() override;
54
What is AZ::Interface?
void MyInterfaceComponent::Activate()
{
if (MyInterface* myInterface = AZ::Interface<MyInterface>::Get())
{
AZ_Printf("Example", "%d", myInterface->GetMyInteger());
}
}
sc->Class<MyInterfaceComponent, Component>()
->Version(1);
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
55
Chapter 7. What is an AZ::Event?
Introduction
Note
The official reference for AZ::Event can be found here:
https://www.o3de.org/docs/user-guide/programming/az-event/
AZ::Event is a great tool for implementing notification systems between components, where one com-
ponent acts a publisher of events and others act as subscribers. There can be many subscribers on the same
publisher. AZ::Event is not suited as a getter interface but is excellent for notification calls, such as a
Transform component notifying any listening components that a change in world transform has occurred.
AZ::Event<int> myEvent;
AZ::Event<int>::Handler myHandler(
[](int value)
56
What is an AZ::Event?
{
/*process*/
});
myHandler.Connect(myEvent);
myEvent.Signal(42);
Tip
You can define none or multiple parameters in the template arguments of AZ::Event<T>. The
handler must match those types. Signal must use the same type of parameters as well.
For example, TransformComponent sends notifications out whenever its entity moves or changes its
rotation or scale. It does so by defining the following event in C:\git\o3de\Code\Framework\Az-
Core\AzCore\Component\TransformBus.h:
There has to be a way to connect handlers to this event, which is done by calling TransformCompo-
nent::BindTransformChangedEventHandler:
void TransformComponent::BindTransformChangedEventHandler(
AZ::TransformChangedEvent::Handler& handler)
{
handler.Connect(m_transformChangedEvent);
}
If you look over the code of TransformComponent.cpp you will see that it signals the event at the
appropriate time with:
m_transformChangedEvent.Signal(m_localTM, m_worldTM);
Example
With the introductory details out of the way, we can build a new component that will print a debug message
whenever an entity moves. There are four steps here.
1. Declare the right type of handler and optionally a callback method for the notification.
AZ::TransformChangedEvent::Handler m_movementHandler;
void OnWorldTransformChanged(const AZ::Transform& world);
2. In the constructor of the component define the lambda invoking our callback method OnWorld-
TransformChanged.
MyEventComponent::MyEventComponent()
: m_movementHandler(
[this](
const AZ::Transform& /*local*/,
57
What is an AZ::Event?
void MyEventComponent::OnWorldTransformChanged(
const AZ::Transform& world)
{
AZ_Printf("MyEvent", "now at %f %f %f",
world.GetTranslation().GetX(),
world.GetTranslation().GetY(),
world.GetTranslation().GetZ());
}
Note
One could also just do all the work inside the lambda.
58
What is an AZ::Event?
void MyEventComponent::Activate()
{
AZ::Entity* e = GetEntity();
TransformComponent* tc = e->FindComponent<TransformComponent>();
// track the movement of the entity
tc->BindTransformChangedEventHandler(m_movementHandler);
}
Tip
Different components may provide different ways to connect your handlers to their events but often
the method name is of a similar name schema: BindSomethingEventHandler.
Summary
Note
You can find the code and project changes for this chapter on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch06_az_interface
namespace MyProject
{
// An example of listening to movement events
// of TransformComponent using an AZ::Event
class MyEventComponent : public AZ::Component
{
public:
AZ_COMPONENT(MyEventComponent,
"{934BF061-204B-4695-944A-23A1BA7433CB}");
MyEventComponent();
// AZ::Component overrides
void Activate() override;
void Deactivate() override {}
59
What is an AZ::Event?
private:
AZ::TransformChangedEvent::Handler m_movementHandler;
void OnWorldTransformChanged(const AZ::Transform& world);
};
}
MyEventComponent::MyEventComponent()
: m_movementHandler(
[this](
const AZ::Transform& /*local*/,
const AZ::Transform& world)
{
OnWorldTransformChanged(world);
})
{
}
void MyEventComponent::Activate()
{
AZ::Entity* e = GetEntity();
sc->Class<MyEventComponent, Component>()
->Version(1);
60
What is an AZ::Event?
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
61
Chapter 8. What is an AZ::EBus?
Introduction
A great way to explain something complex is by contrasting it to something similar but slightly different.
What are common ways for objects or components in a game engine to communicate with each
other? Most often the method is by a direct call. That is you get a pointer or a reference to an instance
of a component, and then invoke a method on it. Sometimes it might be done through a direct type pointer
and sometimes it is done through a base class pointer.
// pseudo-code ahead
{
MyComponent* a;
a->DoSomething();
// OR
MyComponent* a;
MyBase* b = a;
b->DoSomething();
}
In programming, whatever seems simple on small scale is often completely broken and terrible on
large scale. The above design certainly works. However, in large systems it often leads to everything
becoming intertwined and turning into spaghetti code. Here is my theory on what is causing that in the
context of a large scale product code with direct component interaction. Decoupling and abstraction is
often only thought about member variables and base class pure interfaces. But there is more to decoupling
than just decoupling a component's interface.
Abstracting away the invocation of a component is just as important as abstracting away the details
of the implementation of a component. The fundamental principles of Object-Oriented Programming
are well-known. One of such principles is abstraction. One abstracts away the details of a class, so that
one can provide various runtime behaviors without binding the caller to a specific interface. This way A
can call either B or C the same way, so long as both inherit from the same base class. However, there is
another element here that can be abstracted away.
Consider that whenever you invoke a method on any instance, you had to get a pointer or a reference to
that instance. That is a form of coupling between the caller and the callee. After all, however you got that
pointer or a reference is how you will have to invoke the object.
Now imagine that you abstracted away the details of how you call an object. Now you decouple
both what method you call on an object and how you call the method. That means you do not need worry
about the receiving side at all. Maybe it is there, maybe it is not. Since you no longer hold on to a pointer,
you do not have to commit yourself to a specific instance that you are invoking. One moment you might
be talking to one instance, and the next moment you may be talking to a different instance. Or you might
be calling a method on different number of objects at different times.
That is the real power of a design behind EBuses. Let us now take a look of what this theory means in
practice.
62
What is an AZ::EBus?
// pseudo-code ahead
Caller A;
Callee B;
A::Do()
{
bb->DoSomething();
}
Now consider what you would need to do replace what bb is pointing to. For example, let us say that
you were writing a unit test and wished to test the behavior of A towards B. If A had saved the pointer
bb internally, which is common, you would have to make sure there is a way to modify that pointer and
be sure that it is safe to change it.
Have you ever seen the pattern of DEBUG_SetVariable() in a public class before? Perhaps, it was
even defined out in release builds, using preprocessor techniques with #ifdef? It is often created just for
this purpose. Somebody had to write custom code to poke into an object to make it talk to a test object
(sometimes known as a mock object).
// pseudo-code ahead
class A{
public:
void DoSomething();
#ifndef _RELEASE
void DEBUG_ChangePointerToB(BaseCallee*); // YUCK!
#endif
};
Or perhaps, this was solved by passing the object at construction. However, that couples the two objects at
construction time, which has its own issues, because then you have to pass the right object at construction
time. Long story short, while all such approaches work to one degree or another, they all are caused by
not abstracting away the communication between objects.
Now, imagine a pseudo-code that does not couple the method of invocation:
// pseudo-code ahead
Caller A;
Callee B;
B.connect_on(42);
A::CallAnother()
{
call_on(42, &DoSomething); // where DoSomething is a method
}
63
What is an AZ::EBus?
42 is only meant to be an arbitrary identifier. Its purpose is to figure out who will get DoSomething().
Taking this logic one step further, you can imagine B connecting and disconnecting on 42 whenever it
pleases.
// pseudo-code ahead
B.connect_on(42);
// somebody calls
call_on(42, &DoSomething);
D.connect_on(42);
// now this method will be called on two objects
call_on(42, &DoSomething);
// or both disconnect
B.disconnect(42);
D.disconnect(42);
Now, you could re-route DoSomething from the Caller to another destination without changing the
Caller at all. Instead, it would be up to MyComponent or other objects that act like it, such as MyMock-
Component.
Important
Incidentally, this makes unit testing O3DE components a breeze. We will take a look at how to write
unit tests for components in Chapter 15, Writing Unit Tests for Components.
64
What is an AZ::EBus?
/**
* The EBus for requests to position and parent an entity.
* The events are defined in the AZ::TransformInterface class.
*/
typedef AZ::EBus<TransformInterface> TransformBus;
This is a declaration of an EBus. It declares a new type of AZ::EBus, TransformBus, with an interface
provided by a pure virtual class TransformInterface. You can find it in the same file:
class TransformComponent
: public AZ::Component
, public AZ::TransformBus::Handler
...
As you can see it implements Handler sub-class of the EBus. In other words, that declares that Trans-
formComponent will handle calls for TransformBus. And lastly, TransformComponent has to
connect to TransformBus. You can see that in the corresponding source file, C:\git\o3de\Code
\Framework\AzFramework\AzFramework\Components\TransformComponent.cpp:
void TransformComponent::Activate()
{
AZ::TransformBus::Handler::BusConnect(m_entity->GetId());
...
65
What is an AZ::EBus?
Note
C:\git\o3de is my location where I cloned GitHub repository of O3DE:
https://github.com/o3de/o3de
We will come back to the meaning of Activate() and m_entity->GetId() in a moment. For now,
let us glance at the whole picture.
AZ::TransformBus::Event(
// where is it going?
entity.GetId(),
// what method will be invoked?
&AZ::TransformBus::Events::SetWorldTranslation,
// input parameter(s)?
position);
This means that the caller is calling somebody through TransformBus whose identifier is enti-
ty.GetId() on the method SetWorldTranslation with a single input parameter, position.
Who will receive this call? Whoever connected to TransformBus with the same entity id.
AZ::TransformBus::Handler::BusConnect(m_entity->GetId());
66
What is an AZ::EBus?
Note
This is not the only way to call an EBus. There are many different ways to suite your needs. Later
chapters will go over common cases.
What is an AZ::EntityId?
In a world of Entities and Components attached to Entities, what would you expect to be the common
way of addressing a specific component in the world? The most common communication in O3DE
is of a form: "Hey, you, the component of type X attached to entity Y, do this!" For example, if you wish
to move an entity, then you would need to tell its attached Transform component to move. Thus, we have
to know how to uniquely identify entities.
Note
In the Editor, one can set Entity names, however, those names are not unique and are only meant to
be used for debugging purposes.
Entities are uniquely identified by an AZ::EntityId. Its definition can be found at C:\O3DE
\21.11.2\Code\Framework\AzCore\AzCore\Component\EntityId.h
/**
* Entity ID type.
* Entity IDs are used to uniquely identify entities. Each
* component that is attached to an entity is tagged with
* the entity's ID, and component buses are typically
* addressed by entity ID.
*/
class EntityId
{
...
/**
* Entity ID.
*/
u64 m_id;
Under the hood, EntityId is a 64-bit unsigned integer, which is an AZ::u64 type in O3DE.
67
What is an AZ::EBus?
class Component
{
...
/**
* Returns the entity ID if the component is attached to
* an entity. If the component is not attached to any
* entity, this function asserts. As a safeguard, make
* sure that GetEntity()!=nullptr.
* @return The ID of the entity that contains the component.
*/
EntityId GetEntityId() const;
So, we can rewrite the earlier example of calling SetWorldTranslation if it was called within the
same entity:
AZ::TransformBus::Event(
// to the same entity
GetEntityId(),
&AZ::TransformBus::Events::SetWorldTranslation,
position);
AZ::EntityId parentId;
// Get parent entity id of the current entity
AZ::TransformBus::EventResult(parentId, GetEntityId(),
&AZ::TransformBus::Events::GetParentId);
class TransformInterface {
//! Returns the entityId of the entity's parent.
//! @return The entityId of the parent. The entityId is
//! invalid if the entity does not have a parent.
virtual EntityId GetParentId() { return EntityId(); }
AZ::TransformBus::Event(
// to the parent entity
parentId,
&AZ::TransformBus::Events::SetWorldTranslation,
position);
class TransformInterface {
//! Returns the entityIds of the entity's immediate children.
68
What is an AZ::EBus?
AZStd::vector<AZ::EntityId> children;
AZ::TransformBus::EventResult(children, GetEntityId(),
&AZ::TransformBus::Events::GetChildren);
Note
BusId in the above examples were AZ::EntityId but it could be any other type as well.
69
Chapter 9. Using AZ::TickBus
Tick: You're not going crazy. You're going sane in a crazy world!
—The Tick (TV Series 1994-1997)
Introduction to AZ::TickBus
Note
You can find the code for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch09_oscillator
Aside from calling an EBus on another component, such as invoking TransformBus, we also need to
know how to sign up for an EBus. A common EBus to sign up for is AZ::TickBus. It allows one to
perform an action on every game frame, otherwise known as a game tick in O3DE.
A great way to learn how to use an EBus is to implement a component that uses one. This chapter will
write a new C++ component: Oscillator component. It will oscillate the position of the entity it is attached
to using TransformBus and TickBus.
/**
* The EBus for tick notification events.
* The events are defined in the AZ::TickEvents class.
*/
typedef AZ::EBus<TickEvents> TickBus;
class TickEvents
: public AZ::EBusTraits
{
...
/**
* Signals that the application has issued a tick.
* @param deltaTime The delta (in seconds) from
* the previous tick and the current time.
* @param time The current time.
*/
virtual void OnTick(float deltaTime, ScriptTimePoint time) = 0;
70
Using AZ::TickBus
OnTick is the callback we override to get tick events. Let us take a look how one would create a component
that handles it.
OscillatorComponent
We are going to create a new component, OscillatorComponent. Here is a portion of the header
file to get started.
Note
See previous chapters how to create a new component.
protected:
// AZ::Component overrides
void Activate() override;
void Deactivate() override;
// AZ::TickBus overrides
void OnTick(float dt, AZ::ScriptTimePoint) override;
Note
This is why I like EBuses - you can make all methods protected and nobody can talk to your component
unless they go through an approved interface, its EBus. It is like having a lawyer!
AZ::Component Overrides
This is a component, thus it has to inherit from AZ::Component. Activate and Deactivate are
pure virtual methods and have to be overridden and implemented. We had skimmed over these methods
before, so let us spend a moment to take a deeper look at them.
71
Using AZ::TickBus
Note
AZ::Component is defined at C:\O3DE\21.11.2\Code\Framework\AzCore\Az-
Core\Component\Component.h
Source code comment for Activate() states: “[Activate()] puts the component into an active state.
The system calls this function once during activation of each entity that owns the component. You must
override this function. The system calls a component's Activate() function only if all services and compo-
nents that the component depends on are present and active. Use GetProvidedServices and Get-
DependentServices to specify these dependencies.”
And for Deactivate() method: “Deactivates the component. The system calls this function when the
owning entity is being deactivated. You must override this function. As the best practice, ensure that this
function returns the component to a minimal footprint. The order of deactivation is the reverse of activa-
tion, so your component is deactivated before the components it depends on. The system always calls the
component's Deactivate() function before destroying the component. However, deactivation is not always
followed by the destruction of the component. An entity and its components can be deactivated and reac-
tivated without being destroyed. Ensure that your Deactivate() implementation can handle this scenario.”
In other words, Activate is called by O3DE when your component comes to life and is expected to
do its work. Whereas Deactivate tells your component that it must stop its work. It may be deleted
afterwards or remain unused for some time and be activated later again.
Dependency Declaration
GetRequiredServices is a special static method out of four (4) static methods that any component
may declare to define its relation to other components on the same entity. In this case, Oscillator-
Component will express that it requires TransformComponent to be present on the entity. Its im-
plementation will be as follows:
void OscillatorComponent::GetRequiredServices(
AZ::ComponentDescriptor::DependencyArrayType& req)
{
// OscillatorComponent requires TransformComponent
req.push_back(AZ_CRC_CE("TransformService"));
}
How we do know to use "TransformService" value? The only way to know that is to look at Trans-
formComponent::GetProvidedServices:
void TransformComponent::GetProvidedServices(
AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC_CE("TransformService"));
}
In general, a component has four different ways to express various relationships with other components by
providing: GetRequiredServices, GetProvidedServices, GetDependentServices and
GetIncompatibleServices. The comments in O3DE code are great at explaining what these meth-
ods do, so here they are:
72
Using AZ::TickBus
• GetRequiredServices
/**
* Specifies the services that the component requires.
* The system activates the required services before it activates
* this component. It also deactivates the required services
* after it deactivates this component. If a required service is
* missing before this component is activated, the system returns
* an error and does not activate this component.
*/
• GetProvidedServices
/**
* Specifies the services that the component provides.
* The system uses this information to determine when to
* create the component.
*/
• GetDependentServices
/**
* Specifies the services that the component depends on, but does
* not require. The system activates the dependent services before
* it activates this component. It also deactivates the dependent
* services after it deactivates this component. If a dependent
* service is missing before this component is activated,
* the system does not return an error and still activates this
* component.
*/
• GetIncompatibleServices
/**
* Specifies the services that the component cannot operate with.
* For example, if two components provide a similar service and
* the system cannot use the services simultaneously, each of
* those components would specify the other component as an
* incompatible service.
*/
Note
All 4 of these static methods have the same input parameter:
AZ::ComponentDescriptor::DependencyArrayType&.
73
Using AZ::TickBus
trigonometric function, we would have to save the origin and force the position of the entity on every tick.
That can work for many cases.
However, imagine you wanted to be able to affect the entity's position while it was oscillating. For example,
you wrote another component, ShoveComponent, which moves the entity sidewise. Would it not be
cool if both hypothetical ShoveComponent and OscillatorComponent could work together? I
think so, therefore the design of OscillatorComponent calculates the change in position needed for
each tick and does not save the starting position.
Activation of OscillatorComponent
Compared to an earlier example component, MyComponent, the main new feature in this component is
a custom implementation of Activate and Deactivate methods.
void OscillatorComponent::Activate()
{
// We must connect, otherwise OnTick() will never be called.
// Forgetting this call is a common error in O3DE!
AZ::TickBus::Handler::BusConnect();
}
void OscillatorComponent::Deactivate()
{
// good practice on cleanup to disconnect
AZ::TickBus::Handler::BusDisconnect();
}
MyComponent was an empty component that did nothing, so it had nothing to do when it was activated.
OscillatorComponent needs to sign up to AZ::TickBus. Inheriting will not connect to the bus
by default. You have to do so manually when your component is ready. The soonest that can be is in
Activate or later.
Important
The most frequent mistake for a beginner O3DE developer is forgetting to connect to the bus! If
something does not work, check if you have connected to the buses you are handling events for.
Note
OnTick will not be called until you call BusConnect. Once you call BusDisconnect, OnTick
will no longer be called.
74
Using AZ::TickBus
AZ::TransformBus::EventResult(position, GetEntityId(),
&AZ::TransformBus::Events::GetWorldTranslation);
Summary
Note
You can find the code for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch09_oscillator
This chapter showed an example how to connect to an EBus, AZ::TickBus, and use it in conjunction
with TransformBus in order to move an entity.
75
Using AZ::TickBus
namespace MyProject
{
// An example of singing up to an Ebus, TickBus in this case
class OscillatorComponent
: public AZ::Component
, public AZ::TickBus::Handler // for ticking events
{
public:
// be sure this guid is unique, avoid copy-paste errors!
AZ_COMPONENT(OscillatorComponent,
"{302AE5A0-F7C4-4319-8023-B1ADF53E1E72}");
protected:
// AZ::Component overrides
void Activate() override;
void Deactivate() override;
// AZ::TickBus overrides
void OnTick(float dt, AZ::ScriptTimePoint) override;
private:
float m_period = 3.f;
76
Using AZ::TickBus
void OscillatorComponent::Activate()
{
// We must connect, otherwise OnTick() will never be called.
// Forgetting this call is the common error in O3DE!
AZ::TickBus::Handler::BusConnect();
}
void OscillatorComponent::Deactivate()
{
// good practice on cleanup to disconnect
AZ::TickBus::Handler::BusDisconnect();
}
77
Using AZ::TickBus
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
using namespace AZ::Edit::Attributes;
// reflection of this component for O3DE Editor
ec->Class<OscillatorComponent>("Oscillator Component",
"[oscillates the entity]")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AppearsInAddComponentMenu, AZ_CRC_CE("Game"))
->Attribute(Category, "My Project");
}
void OscillatorComponent::GetRequiredServices(
AZ::ComponentDescriptor::DependencyArrayType& req)
{
// OscillatorComponent requires TransformComponent
req.push_back(AZ_CRC_CE("TransformService"));
}
78
Part IV. Introduction to Component
Reflection and Prefabs
Table of Contents
10. Configuring Components in the Editor ............................................................................. 81
Introduction .............................................................................................................. 81
Component Properties in the Editor .............................................................................. 81
Layers of Reflection .................................................................................................. 82
Summary ................................................................................................................. 87
11. Introduction to Prefabs .................................................................................................. 88
Introduction .............................................................................................................. 88
What are Prefabs? ..................................................................................................... 88
Creating a Prefab ...................................................................................................... 89
Editing Prefabs ......................................................................................................... 91
Prefab is also a File ................................................................................................... 92
Spawning Prefabs ...................................................................................................... 92
Summary ................................................................................................................. 96
80
Chapter 10. Configuring Components
in the Editor
Note
The source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch10_reflection
Introduction
This chapter will cover the following topics:
That is far too much effort and time to change one variable. Specifically, OscillatorComponent has
two variables that would make sense to expose to the Editor: period and amplitude.
class OscillatorComponent
{
...
float m_period = 3.f;
float m_amplitude = 10.f;
};
We can modify its reflection, so that we can change some of its behavior in the Editor. By exposing period
and amplitude values to the Editor, Entity Inspector will show the component with two modifiable fields.
81
Configuring Components in the Editor
This way you would be able to enter a new value and test it immediately without re-compiling and restarting
the Editor. This is possible to accomplish with O3DE reflection in Reflect method of a component.
Layers of Reflection
The magic lies in OscillatorComponent::Reflect. As of last chapter, it provided no extra config-
uration for the Editor beyond the basic registration.
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
using namespace AZ::Edit::Attributes;
// reflection of this component for O3DE Editor
ec->Class<OscillatorComponent>("Oscillator Component",
"[oscillates the entity]")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AppearsInAddComponentMenu, AZ_CRC("Game"))
->Attribute(Category, "My Project");
}
Reflection in O3DE is described in layers, called contexts. The first and main layer is SerializeCon-
text. There are two layers built on top of it: EditContext (for the Editor) and BehaviorContext
(for scripting).
At the moment, the reflection data of OscillatorComponent can be represented like this:
82
Configuring Components in the Editor
SerializeContext
Example 10.1. AzCore\Serialization\SerializeContext.h
/**
* Serialize context is a class that manages information
* about all reflected data structures. You will use it
* for all related information when you declare your data
* for serialization.
* In addition it will handle data version control.
*/
class SerializeContext
sc->Class<OscillatorComponent, Component>()
Class follows a programming method, called Builder pattern, where you can continuously invoke meth-
ods on their return values, because their return values are always ClassBuilder.
/**
* Internal structure to maintain class information while
* we are describing a class. User should call variety of
* functions to describe class features and data.
*/
class ClassBuilder
ClassBuilder has a lot of methods but we will focus on the ones that we used in OscillatorCom-
ponent. Those methods are also the most common.
ClassBuilder: Version.
sc->Class<OscillatorComponent>()
->Version(1);
This specifies the version of your component in the context of reflection. You can use it as a form of version
control. Whenever you make significant changes to a component during development, it is advisable to
increase the version.
Note
Here is Version() declaration:
EditContext
83
Configuring Components in the Editor
Important
Before you can describe a class in EditContext you must describe it in SerializeContext.
A similar class builder pattern is used in EditContext as well.
ec->Class<OscillatorComponent>("Oscillator Component",
"[oscillates the entity]")
Class is a templated method that declares a component for the Editor. The first parameter is the name
as it will appear in Add Component menu. It does not need to match the exact class name and it may
include spaces. The second is the description that will show up as a tooltip when you hover over its UI
element in the Editor.
Note
You can find declaration for Class<T>() in EditContext.h:
template<class T>
EditContext::ClassBuilder
EditContext::Class(const char* displayName,
const char* description)
Next up are various Editor specific attributes of a class. But before you can specify Attribute, you
have to declare its EditorData:
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
That tells the serialization system that we will describe editor specific data.
Important
Without the following Attribute the component will NOT show up the Editor!
->Attribute(AppearsInAddComponentMenu, AZ_CRC("Game"))
The value "Game" is a bit unfortunate as it really means that this component will appear in the Edi-
tor's Add Component menu. If you wish to hide your component from the Editor, you can omit this
attribute completely.
Note
You may wonder if there are any other Add Component menus beside the Editor. Yep, there are
a few more types, such as System components ("System"), Level components ("Level") and User
Interface components ("UI") and Canvas User Interface components ("CanvasUI"). For example, a
system component can be specified with:
84
Configuring Components in the Editor
->Attribute(AppearsInAddComponentMenu, AZ_CRC("System"))
Another useful attribute is Category. It allows you to group your components together in Add Com-
ponent menu.
This is the attribute that makes OscillatorComponent show up under My Project in the Editor.
Important
Before you can declare a configurable field in the Editor, you have to define it in SerializeCon-
text. Also notice that the names have to be the same. In this case, different contexts provide addi-
tional descriptions of the same field. In a sense, SerializeContext declares and creates a field
and EditContext expands that definition with Editor configuration.
85
Configuring Components in the Editor
if (!sc) return;
sc->Class<OscillatorComponent, Component>()
// serialize m_period
->Field("Period", &OscillatorComponent::m_period)
// serialize m_amplitude
->Field("Amplitude", &OscillatorComponent::m_amplitude)
->Version(2);
AZ::EditContext* ec = sc->GetEditContext();
if (!ec) return;
using namespace AZ::Edit::Attributes;
// reflection of this component for O3DE Editor
ec->Class<OscillatorComponent>("Oscillator Component",
"[oscillates the entity]")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AppearsInAddComponentMenu, AZ_CRC("Game"))
->Attribute(Category, "My Project")
// expose the setting to the editor
->DataElement(nullptr, &OscillatorComponent::m_period,
"Period", "[the period of oscillation]")
// expose the setting to the editor
->DataElement(nullptr, &OscillatorComponent::m_amplitude,
"Amplitude", "[the height of oscillation]");
}
Compared to the previous Reflect implementation, there are two new changes: Field and DataEle-
ment.
Field
Recall that OscillatorComponent has a member variable: m_period. Given that, it can be declared
to be a serialized field with:
->Field("Period", &OscillatorComponent::m_period)
The first parameter for Field is a const char* and the second is a point to the member variable.
DataElement
The counterpart for the Editor (and Project Configurator) is DataElement:
->DataElement(Default, &OscillatorComponent::m_period,
"Period", "[the period of oscillation]")
86
Configuring Components in the Editor
The second parameter to DataElement is a pointer to a member variable of the component. The third
parameter is the name that was assigned to the member variable with Field earlier. The last parameter
is the description.
Summary
With the changes in this chapter, the component is now configurable in the Editor.
Note
The source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch10_reflection
You can quickly test the changes by modifying the values and using the shortcut CTRL+G to test the
behavior right away in the Editor. (ESC exits the play in the Editor.)
87
Chapter 11. Introduction to Prefabs
Introduction
Note
You can find the code and assets for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch11_prefabs
• Spawning prefabs.
A group of entities
It would be tedious and error prone to manually re-create copies of it, if we wanted to have many of such
objects. But if we turn this group of entities into a prefab, then we can easily duplicate it over and over
again.
88
Introduction to Prefabs
Creating a Prefab
This section will cover how to create a prefab in the Editor. Before we get started we need two Editor
panels that are useful when working with prefabs: Entity Outliner and Asset Browser. You can launch
both from Tools menu of the Editor.
Previous chapter already created a complex object with Root Entity as its parent entity.
Current scene
89
Introduction to Prefabs
That will bring up a Save As... dialog where you can choose the file name of the prefab.
Save as Complex_Object.prefab
A prefab instance
90
Introduction to Prefabs
Note
A spawnable is a derivative product of a prefab that can be spawned at runtime, whereas a prefab is
a design time product. We will look at spawnables later in this chapter.
Editing Prefabs
Notice that the entity structure within a prefab is now hidden. You must enter the edit mode of the prefab
to see its entities. You can do that by double clicking on it or clicking the icon to the left of the prefab.
Editing a prefab
When you are editing a prefab, the Editor viewport turns to focus mode, where only changes to prefab
entities are allowed until you exit prefab editing mode.
Once you make a change to prefab, its name will be marked with * in Entity Outliner.
To save the prefab, right click on it in Entity Outliner and choose Save Prefab to file.
91
Introduction to Prefabs
Tip
You can also save the entire level and all the prefabs in it with CTRL+S.
You can exit the prefab edit mode with ESC or click on the icon to the left of the prefab title.
A prefab is saved to disk as a JSON file. If you were to open it up you could search its contents for one of
the components we created and attached to Root Entity, OscillatorComponent.
Spawning Prefabs
In the Editor, you can add more prefab instances by duplicating existing prefabs in the level.
92
Introduction to Prefabs
Or by instantiating one in the level with right click and then "Instantiate Prefab..."
Instantiate Prefab
And then selecting the prefab of your choice.
Select a prefab
AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities(...);
93
Introduction to Prefabs
What is a spawnable? A spawnable is a runtime product of a prefab. A spawnable is the product that
you can spawn in your game at runtime. You can see that each prefab has a corresponding spawnable in
Asset Browser.
MySpawnerComponent
Note
The source code and the level for this section can be found at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch11_prefabs
Here is the complete example of how to spawn a prefab from a component. We are going to build a
component that will have a field that can be populated in the Editor and will spawn one instance of it when
the entity is activated.
94
Introduction to Prefabs
With that in mind, here are the steps to spawn entities from a spawnable:
AZ::Data::Asset<AzFramework::Spawnable> m_spawnableAsset;
AzFramework::EntitySpawnTicket m_ticket;
m_ticket = AzFramework::EntitySpawnTicket(m_spawnableAsset);
3. Now if we pass this ticket into SpawnAllEntities it will spawn the entities from that spawn-
able. But there is one gotcha, where will those entities be spawned at? Unless, we specify the loca-
tion, they will be spawned relative to the origin. SpawnAllEntities does not specify a way to
specify the location, so how do we do that? The answer is in AzFramework::SpawnAllEnti-
tiesOptionalArgs. It provides a way to modify the entities before they are activated via the mem-
ber m_preInsertionCallback. Here is how to place the root entity where you want it.
SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_preInsertionCallback = AZStd::move(preSpawnCallback);
AzFramework::SpawnableEntitiesInterface::Get()->
SpawnAllEntities(m_ticket, AZStd::move(optionalArgs));
Note
The entities may not spawn immediately, so be sure that your code is ready to received the callback
and the entities at the next or later game tick.
Tip
If your prefab is built with an entity that contains all other entities as we did in Chapter 3, Introduction
to Entities and Components, then you only need to assign the location of the root entity. Other child
entities will position themselves relative to their parent's transform.
95
Introduction to Prefabs
Summary
Note
You can find the code and assets for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch11_prefabs
namespace MyProject
{
// An example of spawning prefab from C++.
class MySpawnerComponent : public AZ::Component
{
public:
AZ_COMPONENT(MySpawnerComponent,
"{F75160EB-CB82-4A41-8AB0-68AD43B9625B}");
// AZ::Component overrides
void Activate() override;
void Deactivate() override {}
96
Introduction to Prefabs
private:
AZ::Data::Asset<AzFramework::Spawnable> m_spawnableAsset;
AzFramework::EntitySpawnTicket m_ticket;
};
}
void MySpawnerComponent::Activate()
{
using namespace AzFramework;
AZ::Transform world = GetEntity()->GetTransform()->GetWorldTM();
m_ticket = EntitySpawnTicket(m_spawnableAsset);
auto cb = [world](
EntitySpawnTicket::Id /*ticketId*/,
SpawnableEntityContainerView view)
{
const AZ::Entity* e = *view.begin();
if (auto* tc = e->FindComponent<TransformComponent>())
{
tc->SetWorldTM(world);
}
};
if (m_ticket.IsValid())
{
SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_preInsertionCallback = AZStd::move(cb);
SpawnableEntitiesInterface::Get()->SpawnAllEntities(
m_ticket, AZStd::move(optionalArgs));
}
}
sc->Class<MySpawnerComponent, Component>()
->Field("Prefab", &MySpawnerComponent::m_spawnableAsset)
->Version(1);
AZ::EditContext* ec = sc->GetEditContext();
97
Introduction to Prefabs
if (!ec) return;
98
Part V. Modularity in O3DE
Table of Contents
12. What is a Gem? ......................................................................................................... 101
Introduction ............................................................................................................ 101
Creating a New Gem ............................................................................................... 101
Adding Gem to the Project ....................................................................................... 102
Where Does a Project List the Gems It Uses? ............................................................... 104
File Structure of a Gem ............................................................................................ 104
Summary ................................................................................................................ 109
13. Gem: Set Window Position .......................................................................................... 110
Introduction and Motivation ...................................................................................... 110
Create WindowPosition Gem ..................................................................................... 110
Console Subsystem .................................................................................................. 110
Console Command Callback ...................................................................................... 111
Conclusion and Source Code ..................................................................................... 113
14. Enabling NvCloth Gem ............................................................................................... 115
Introduction ............................................................................................................ 115
Adding NvCloth Gem .............................................................................................. 115
Using NvCloth Gem ................................................................................................ 117
Adding Your Assets to Gems .................................................................................... 118
Summary ................................................................................................................ 119
100
Chapter 12. What is a Gem?
Introduction
This chapter covers the following topics:
• What is a Gem?
• Gem's structure.
The gems system was developed to make it easy to share code between projects. Gems
are reusable packages of module code and/or assets which can be easily added to or
removed from an O3DE project.
O3DE engine was built to be modular. That is achieved by using gems. A gem is a collection of code and
assets (or only assets without any native C++ code). The intention of a gem system is that a developer
would have a collection of gems to pick from, as well as be able to write their own gems that they may use
in their projects. A gem is a stand-alone piece of code with assets and can be re-used in multiple projects.
For example, if one were to write a gem that positions the game window at launch time, then you could
re-use this functionality in different projects without having to copy paste the code. In fact, Chapter 13,
Gem: Set Window Position will write such a gem!
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch12_new_gem
O3DE comes with a lot of gems. They are all located under your O3DE install path or a GitHub clone. Here
is my install location: C:\O3DE\21.11.2\Gems. For example, there are EMotionFX, Multiplayer,
NvCloth gems and many others. These are core O3DE gems. If you were to write your own gem, I suggest
placing it alongside with your project. So far we have created a project at C:\git\book\MyProject.
A good option is to place my gems under C:\git\book\Gems. For example, in this chapter I will
create a gem called MyGem and place it at C:\git\book\Gems\MyGem. The specific location of gems
is entirely up to you. I will show you how to configure your project to see a gem from anywhere on your
system.
Note
The official O3DE documentation for creating a new gem can be found here:
https://www.o3de.org/docs/user-guide/programming/gems/
Let us create a brand new gem and see what we get out of the box and its overall structure. To create a new
gem, I will use the command line interface of scripts\o3de.bat:
101
What is a Gem?
Tip
The script is smart enough to figure out that the name of the game should be MyGem. However, you
can also specify the name yourself. Run the command with -h switch to view all available options.
C:\O3DE\21.11.2\scripts\o3de.bat create-gem -h
https://www.o3de.org/docs/user-guide/project-config/add-remove-gems/
However, it is useful to understand what is going on behind the scenes. There are two configuration files
that need to be modified to add a new gem to a project.
{
"project_name": "MyProject",
"origin": "...",
"license": "...",
"display_name": "MyProject",
"summary": "A short description of MyProject.",
"canonical_tags": [
"Project"
],
"user_tags": [
"MyProject"
],
"icon_path": "preview.png",
"engine": "o3de-sdk",
"external_subdirectories": [
]
}
Property array external_subdirectories is responsible for making a gem visible to the project. The fol-
lowing example adds the path to MyGem to the project.
"external_subdirectories": [
"C:/git/book/Gems/MyGem"
]
Now if you build ZERO_TARGET in Visual Studio, this gem will appear in the solution.
102
What is a Gem?
Important
This does not enable the gem just yet! The gem is now visible by the project and will be built but
one more step remains.
2. The second file enables the gem by its name in the project file: C:\git\book\MyProject\Code
\enabled_gems.cmake.
set(ENABLED_GEMS
MyProject
...
MyGem # new
)
Where does the name MyGem come from? It comes from the gem's json configuration file "gem_name"
property in C:\git\book\Gems\MyGem\gem.json.
{
"gem_name": "MyGem",
...
}
With these changes the new gem will be compiled as part of the project and will be enabled at runtime.
103
What is a Gem?
Tip
You can confirm that the gem is enabled by placing a breakpoint in C:\git\book\Gems\MyGem
\Code\Source\MyGemSystemComponent.cpp at MyGemSystemComponent::Acti-
vate. If you launch the Editor and the breakpoint gets hit then the gem has been successfully enabled.
MyGem # new
)
These are the default gems that are enabled when you create a project with default settings. MyProject
gem is the project gem, which includes the project code at C:\git\book\MyProject\Code. Then
there is a list of various core O3DE gems. In this chapter we just added MyGem in the last line.
• Assets folder
• A gem may provide assets for your project. If you had any, you would place them here. That could
be scripts, prefabs and other types of assets. Since this is a new gem, this folder is empty.
• Code folder
104
What is a Gem?
• This is where C++ source code for your gem will reside. We will go over the details in just a moment.
• This is a default icon for your gem. This icon is the icon that O3DE Project Manager shows next to
your gem. You may change it and customize it to your taste.
• CMakeLists.txt is the entry build file for the gem. We do not need to worry about it. The important
build file for the gem is under Code folder, at C:\git\book\Gems\MyGem\Code\CMakeList-
s.txt.
• gem.json configuration file. The only importance of this file for us the name of the gem under
gem_name property.
Important
This is the only required build target for a gem. You could actually merge the static library into
the gem module but it is useful to put the component into a static library as it will be used in a
few other build targets as well.
3. (Optional) MyGem.Tests module links against MyGem.Static and include any unit tests you might
add.
105
What is a Gem?
Let us dive deeper into the code structure of a gem that is created by default for us by scripts/
o3de.bat at C:\git\book\Gems\MyGem\Code.
• Include\MyGem is the place for public C++ API of a gem, which should primarily be composed of
header files, such as EBus headers and simple flat data structures.
• Platform folder contains various build and source files for specific platforms, such as Windows,
Linux and so on.
• Source folder is the location for internal gem code, such as custom components.
• Tests folder contains basic unit tests for you to start with to test gem's code.
106
What is a Gem?
• CMakeLists.txt is the main build file for the gem. It defines all the build targets, such as gem's
static library, gem's main module, unit tests build targets, etc.
Note
Here is a quick summary on Editor specific targets. O3DE allows you to separate your Editor spe-
cific logic versus game runtime specific code. For example, you might want to create some design
tools in the Editor that save their output to be used in the game. In such case, you might decide to
separate that design code from your game code. Then the design components will go into Editor
build targets, while final game components will go into regular components. This is optional, how-
ever. It is important to first understand how the regular non-Editor build targets work.
set(FILES
Include/MyGem/MyGemBus.h
Source/MyGemModuleInterface.h
Source/MyGemSystemComponent.cpp
Source/MyGemSystemComponent.h
)
Note
This lists both header and source files. In Visual Studio it will automatically put them into different
filters.
set(FILES
Source/MyGemModule.cpp
)
As expected, it only contains the module source file. All other component classes will come from the
static library MyGem.Static.
set(FILES
Tests/MyGemTest.cpp
)
Tip
If you are only adding new components, you only need to worry about updating mygem_files.c-
make to add your new component files. The content of other files will rarely change.
107
What is a Gem?
Note
AZ::Interface was covered in Chapter 6, What is AZ::Interface? EBus is a unique system within
O3DE, for details see Chapter 8, What is an AZ::EBus?
You can have any number of EBuses in a gem. You may rename MyGemBus.h or delete it altogether.
#include <AzCore/EBus/EBus.h>
#include <AzCore/Interface/Interface.h>
namespace MyGem
{
class MyGemRequests
{
public:
AZ_RTTI(MyGemRequests,
"{45ec313a-31a7-41ca-9e19-0f3f9351e328}");
virtual ~MyGemRequests() = default;
// Put your public methods here
};
class MyGemBusTraits
: public AZ::EBusTraits
{
public:
// EBusTraits overrides
static constexpr AZ::EBusHandlerPolicy HandlerPolicy =
AZ::EBusHandlerPolicy::Single;
static constexpr AZ::EBusAddressPolicy AddressPolicy =
AZ::EBusAddressPolicy::Single;
};
} // namespace MyGem
PS C:\git\book\Gems\MyGem\Code\Source> ls -Name
MyGemEditorModule.cpp
MyGemEditorSystemComponent.cpp
MyGemEditorSystemComponent.h
MyGemModule.cpp
MyGemModuleInterface.h
MyGemSystemComponent.cpp
108
What is a Gem?
MyGemSystemComponent.h
MyGemModule.cpp is the main class that declares the gem. At the moment it only registers a system
component.
MyGemModuleInterface.h is the common base for both Editor and Game flavor of our gem. It lists the
components that are currently registered with the engine.
MyGemSystemComponent is an empty system component that was generated for you. It is not necessary
for a gem to have a system component. You may delete if you wish.
Note
A system component is a special component that is activated when the engine is initialized. It does
not depend on any level and persists until the game launcher or the Editor quits. It is attached to the
unique system entity. System components are great for handling system wide services that are not
level dependent.
If you rename any of these files or add new files, be sure to update mygem_files.cmake.
Summary
We have gone over the basics of a gem system and what we get when O3DE Project Manager script creates
a new gem for us. In later chapters we will take a look at some examples of gems. That will allow us to
understand how gems work and how they can be used to help you structure your game code.
Note
You can find the code for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch12_new_gem
109
Chapter 13. Gem: Set Window Position
Note
You can find the code for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch13_gem_window_position
With this gem, you will be able to set the window position by passing the commands to the game launcher
starting parameters, such as:
SetWindowXY will expect two parameters: x and y distance from the top left corner of the screen to place
the top left corner of the O3DE application.
C:\O3DE\21.11.2\scripts\o3de.bat create-gem
-gp C:\git\book\Gems\WindowPosition
"external_subdirectories": [
"C:/git/book/Gems/MyGem",
"C:/git/book/Gems/WindowPosition"
]
set(ENABLED_GEMS
...
MyGem
WindowPosition # new
)
Console Subsystem
O3DE allows you to add console commands using AZ::Console macros from AzCore/Con-
sole/IConsole.h.
110
Gem: Set Window Position
There is a number of useful macros that register new console commands and variables.
• AZ_CONSOLEFREEFUNC is the one I will use in this chapter to register a free function as a callback
when an AZ::Console command is invoked.
• There is also AZ_CONSOLEFUNC that implements the callback as a class member function.
AZ_CONSOLEFUNC(ConsoleTests, TestClassFunc,
AZ::ConsoleFunctorFlags::Null, "");
Which can be used in the same source file by its name cloth_DebugDraw.
if (cloth_DebugDraw == 1) { /*...*/ }
In source files outside of AZ_CVAR definition, you have to first refer to it by AZ_CVAR_EXTERNED.
AZ_CVAR_EXTERNED(int32_t, cloth_DebugDraw);
SetWindowXY is both the callback method for the console command and is the name to call in the when
passing the command to the game launcher or invoking from an in-game console.
111
Gem: Set Window Position
AZ::ConsoleCommandContainer is a fixed vector of strings. You can extract the parameters one at
a time by converting them to the type you expect them to be.
The rest of the implementation is Windows specific. It acquires a window handle and sets the position. It
is outside of scope of this book to discuss Windows specific API, so we are going to just breeze through
it by showing the final result.
Note
If you are curious where one can read up the documentation of GetActiveWindow, GetWin-
dowInfo and SetWindowPos, you should refer to public MSDN at:
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos
112
Gem: Set Window Position
if (handle)
{
SetWindowPos(handle, nullptr,
x, y,
0, 0, SWP_NOOWNERZORDER | SWP_NOSIZE);
}
}
Note
You can find the code for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch13_gem_window_position
WindowConsoleCommand.cpp
Example 13.4. WindowConsoleCommand.cpp
#include <AzCore/PlatformIncl.h>
#include <AzCore/Console/IConsole.h>
namespace WindowPosition
{
void SetWindowXY(const AZ::ConsoleCommandContainer& args)
{
if (args.size() != 2)
{
return;
}
113
Gem: Set Window Position
if (handle)
{
SetWindowPos(handle, nullptr,
x, y,
0, 0, SWP_NOOWNERZORDER | SWP_NOSIZE);
}
}
AZ_CONSOLEFREEFUNC(SetWindowXY, AZ::ConsoleFunctorFlags::Null,
"Moves the window to x,y coordinates");
} // namespace WindowPosition
Notice that this time this file is not part of any component. It is a stand-alone source file but it still needs
to be added to windowposition_files.cmake.
Source/WindowConsoleCommand.cpp # new
)
114
Chapter 14. Enabling NvCloth Gem
Introduction
O3DE engine has a lot of gems that are ready to be used. This chapter will go over the steps involved in
adding one of core gems to our project.
Note
The changes in this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch14_enable_nvcloth
Tip
If a project does not appear there, then needs to be added to C:\Users\<user>\.o3de
\o3de_manifest.json. This can be done either using the script:
C:\O3DE\21.11.2\scripts\o3de.bat register
115
Enabling NvCloth Gem
--project-path C:\git\book\MyProject
Or modifying o3de_manifest.json's projects property directly and adding the full path to the
project.
{
"o3de_manifest_name": "<user>",
"origin": "C:/Users/<user>/.o3de",
...
"projects": [
"C:/git/book/MyProject"
],
You can add, remove and explore gems by choosing Configure Gem option from the drop down list next
the project.
You can enable the gem here and save the changes. Verify that the gem was added in C:\git\book
\MyProject\Code\enabled_gems.cmake.
set(ENABLED_GEMS
...
116
Enabling NvCloth Gem
MyGem
WindowPosition
NvCloth # new
)
Spawn the chicken by right clicking in the viewport and choosing Instantiate Prefab.
117
Enabling NvCloth Gem
118
Enabling NvCloth Gem
Summary
Note
The changes in this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch14_enable_nvcloth
In this chapter, we have enabled NvCloth gem and added a new FBX asset under MyGem. Here is the
look of the level with these changes.
119
Part VI. Unit Tests in O3DE
Table of Contents
15. Writing Unit Tests for Components ............................................................................... 122
Introduction ............................................................................................................ 122
Unit Test Build Target .............................................................................................. 122
Unit Tests ............................................................................................................... 124
16. Unit Tests with Mock Components ................................................................................ 131
Testing with Mocks ................................................................................................. 131
Mocking TransformComponent ........................................................................... 131
Summary ................................................................................................................ 137
121
Chapter 15. Writing Unit Tests for
Components
The system should first be made to run, even though it does nothing useful except call
the proper set of dummy subprograms. Then, bit-by-bit it is fleshed out, with the sub-
programs in turn being developed into actions or calls to empty stubs in the level below.
—Frederick P. Brooks Jr. "No Silver Bullet - Essence and Accident in Software Engi-
neering", 1986
Introduction
Note
The source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch15_unittests
An often overlooked development process in game development is unit testing. O3DE engine comes with
a powerful Google Test and Google Mock frameworks included.
Tip
The presentation of Google Test and Google Mock framework is outside of the scope of this book,
but you can find great tutorials for them online:
• https://github.com/google/googletest
• https://github.com/google/googletest/blob/main/docs/primer.md
• https://github.com/google/googletest/blob/main/docs/gmock_for_dummies.md
• https://github.com/google/googletest/blob/main/docs/gmock_cook_book.md
This chapter will show you how to write a simple unit test for OscillatorComponent that we imple-
mented in Chapter 9, Using AZ::TickBus.
However, when we generated MyProject it did not receive a unit test build target. We need MyProjec-
t.Tests build target in order to test OscillatorComponent, since the component is inside MyProject
and not inside a gem. While it is not at all necessary to place components inside a project, it will serve as
a good example of what it takes to add unit tests from scratch.
122
Writing Unit Tests for Components
Since we are adding a new build target to the project, we have to modify C:\git\book\MyPro-
ject\Code\CMakeLists.txt. Here is the change to add to the end of CMakeLists.txt.
123
Writing Unit Tests for Components
ly_add_googletest(
NAME Gem::MyProject.Tests
)
This way you can launch this target as a Google Unittest from Visual Studio.
• Target MyProject.Tests links against MyProject.Static, which has the object code with Oscilla-
torComponent.
BUILD_DEPENDENCIES
PRIVATE
Gem::MyProject.Static
Unit Tests
MyProjectTest.cpp will serve as the unit test setup.
124
Writing Unit Tests for Components
Tip
If you take a peak at the property pages for MyProject.Tests you can find the steps needed to run
the unit tests from the command line:
C:/O3DE/21.11.2/bin/Windows/profile/Default/AzTestRunner.exe
C:/git/book/build/bin/profile/MyProject.Tests.dll AzRunUnitTests
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch15_unittests
The majority of functionality of a game in O3DE is done in components. So it only makes to write unit
tests for components. A common approach is as follows:
1. Create an AZ::Entity.
3. Add another component that is involved in the interaction you are testing.
For example, here is the structure of a unit test we will write for OscillatorComponent (from Chap-
ter 9, Using AZ::TickBus):
125
Writing Unit Tests for Components
1. Create an AZ::Entity as mentioned. Nothing special is required for that. You may create one on
the stack or on a heap.
3. Add AzFramework::TransformComponent.
I will go through each part of the unit test source code file and present the entire code at the end.
#include <AzCore/UnitTest/TestTypes.h>
...
class OscillatorTest
: public ::UnitTest::AllocatorsFixture
{
...
void SetUp() override
{
::UnitTest::AllocatorsFixture::SetUp();
...
With that, O3DE memory allocators will be correctly set up and you will not need to deal with errors,
such as:
If you ever attempted to write your own O3DE unit test, you have likely seen such runtime crashes. Al-
locatorsFixture solves that problem.
Registering Components
A unit test runs in a slim environment. There are no gems loaded, even the project's components are not
automatically registered.
Tip
If you attempt to create a component in a unit test without first registering it with O3DE, you would
hit a runtime assert, such as:
o3de\Code\Framework\AzCore\AzCore/UnitTest/UnitTest.h(155):
error: Entity '6688814788' [6688814788] cannot be activated.
No descriptor registered for Component class 'OscillatorComponent'.
That is solved by creating an instance of AZ::SerializeContext and reflecting the components you
wish to use.
126
Writing Unit Tests for Components
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_td;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_od;
...
// register components involved in testing
m_sc = AZStd::make_unique<AZ::SerializeContext>();
m_td.reset(TransformComponent::CreateDescriptor());
m_td->Reflect(m_sc.get());
m_od.reset(OscillatorComponent::CreateDescriptor());
m_od->Reflect(m_sc.get());
Entity e;
...
// helper method
void PopulateEntity(Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// And how it interacts with
e.CreateComponent<AzFramework::TransformComponent>();
...
Test Body
As described earlier, this test will trigger one tick event and test the results on TransformComponent.
TEST_F(OscillatorTest, EntityMovingUp)
{
Entity e;
PopulateEntity(e);
// tick once
127
Writing Unit Tests for Components
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
Note
OscillatorTest is the unit test fixture that this test body inherits from. That is why we can use
PopulateEntity directly. EntityMovingUp is the name of the test.
#include <AzTest/AzTest.h>
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
Output
If all went well, you should see the following output in the command line upon running the unit tests.
Source Code
Note
The source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch15_unittests
128
Writing Unit Tests for Components
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Components/TransformComponent.h>
#include <AzTest/AzTest.h>
namespace MyProject
{
class OscillatorTest
: public ::UnitTest::AllocatorsFixture
{
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_td;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_od;
protected:
void SetUp() override
{
::UnitTest::AllocatorsFixture::SetUp();
::UnitTest::AllocatorsFixture::TearDown();
}
// helper method
void PopulateEntity(AZ::Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// And how it interacts with
e.CreateComponent<AzFramework::TransformComponent>();
TEST_F(OscillatorTest, EntityMovingUp)
{
using namespace AZ;
Entity e;
PopulateEntity(e);
129
Writing Unit Tests for Components
// tick once
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
130
Chapter 16. Unit Tests with Mock
Components
Note
The source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch16_unittests_mock
The purpose of such a test is to check that OscillatorComponent invokes a particular EBus call. This
is different from checking if some variable has the expected value. Instead, this type of unit test checks if a
particular interface was invoked. So let us write a unit test that will check that OscillatorComponent
calls SetWorldTranslation on a Transform-like component.
A "mock" component is a fake test component. It is created to test the interaction between the mock and
some real component. It is a powerful testing tool. O3DE ships with Google Mock which provides a
powerful C++ mock framework.
Note
You can familiarize yourself with Google Mock on its extensive documentation online:
• https://github.com/google/googletest/blob/master/googlemock/README.md
• https://github.com/google/googletest/blob/main/docs/gmock_for_dummies.md
• https://github.com/google/googletest/blob/main/docs/gmock_cook_book.md
Mocking TransformComponent
Conceptually, a component in O3DE is identified by EBus handlers it implements. You could choose to
have a mock object inherit all of such handlers that TransformComponent implements. However, in
the case of OscillatorComponent interacting with TransformComponent, we know that only
TransformBus is involved, thus it is sufficient to only handle TransformBus::Handler.
#include <AzCore/Component/TransformBus.h>
...
/**
* \brief Pretend to be a TransformComponent
*/
class MockTransformComponent
: public AZ::Component
, public AZ::TransformBus::Handler
131
Unit Tests with Mock Components
Of course, it still has to be a O3DE component, so it has to inherit from AZ::Component and implement
the most basic component interface.
...
// be sure this guid is unique, avoid copy-paste errors!
AZ_COMPONENT(MockTransformComponent,
"{7E8087BD-46DA-4708-ADB0-08D7812CA49F}");
class MockTransformComponent
...
// OscillatorComponent will be calling these methods
MOCK_METHOD1(SetWorldTranslation, void (const AZ::Vector3&));
MOCK_METHOD0(GetWorldTranslation, AZ::Vector3 ());
...
class TransformInterface {
...
virtual void SetWorldTranslation(const AZ::Vector3&);
virtual AZ::Vector3 GetWorldTranslation()
MOCK_METHOD0 , MOCK_METHOD1 and so on provide stub implementations of these methods with many
additional features. In this chapter, SetWorldTranslation and GetWorldTranslation will be
used. A few other pure virtual methods from TransformBus will not be. Those still need to be mocked
out, so that we can instantiate the object:
132
Unit Tests with Mock Components
133
Unit Tests with Mock Components
void(ChildChangedEvent::Handler&));
MOCK_METHOD2(NotifyChildChangedEvent,
void(ChildChangeType, EntityId));
};
class OscillatorMockTest
: public ::UnitTest::AllocatorsFixture
{
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_md;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_od;
protected:
void SetUp() override
{
::UnitTest::AllocatorsFixture::SetUp();
The difference will be in how we attach components to the entity. Specifically, we will need to keep a
pointer to MockTransformComponent, so that we can do some Google Mock magic on it in the test
body.
...
// helper method
void PopulateEntity(Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// We can mock out Transform and test the interaction
mock = new MockTransformComponent();
e.AddComponent(mock);
Now, we have a helper method to create and activate the entity while keeping a pointer to mock.
134
Unit Tests with Mock Components
Note
e.AddComponent(mock) passes the memory ownership of the component instance to the entity,
so do not try to delete mock pointer in the test body. Entity destructor will take care of that.
TEST_F(OscillatorMockTest, Calls_SetWorldTranslation)
{
Entity e;
PopulateEntity(e);
The one feature we are going to use in this chapter is setting expectation that a particular method will be
called. For example, we will test that SetWorldTranslation is called once.
Note
The underscore in SetWorldTranslation(_) is a special Google Mock object ::test-
ing::_ that matches any argument. So this statement is: "expect SetWorldTranslation to be
called once with any parameter."
So it will not return any value by default. If your test were to run and call this method without any extra
preparation, it would error out at runtime with an error:
[ RUN ] OscillatorMockTest.Calls_SetWorldTranslation
unknown file: error: C++ exception with description
"Uninteresting mock function call - returning default value.
Function call: GetWorldTranslation()
The mock function has no default action set, and its return
type has no default value set." thrown in the test body.
Google Mock provides a way to customize the behavior of GetWorldTranslation without having to
modify MockTransformComponent. Here is how we can set it to return vector of (0,0,0):
TEST_F(OscillatorMockTest, Calls_SetWorldTranslation)
135
Unit Tests with Mock Components
{
Entity e;
PopulateEntity(e);
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
}
4. Ticks once.
5. At the end of the scope, Google Mock will do the work for us to ensure the expectation was met.
Tip
If you were to run such a test you see a warning by Google Mock:
GMOCK WARNING:
Uninteresting mock function call - taking default action specified
MyProject\Gem\Code\Tests\MyProjectTest.cpp(105):
Function call: GetWorldTranslation()
Returns: 16-byte object <00-00 ... 00-00>
NOTE: You can safely ignore the above warning unless this call
should not happen. Do not suppress it by blindly adding
an EXPECT_CALL() if you don't mean to enforce the call.
Google Mock is telling us in this warning that GetWorldTranslation was called as we would
expect OscillatorComponent to do but that the test body did not specify any expectation re-
garding that. A good rule of thumb as the warning suggests is to only test for what really matters. In
this case, SetWorldTranslation is the call that matters and the expectation for it was set.
Once you are sure that the test good, you can get rid of this warning by changing the type of mock
object on creation from:
To:
136
Unit Tests with Mock Components
e.AddComponent(mock);
NiceMock is a Google Mock class that silently ignores unexpected calls into a mock object.
Summary
Note
The source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch16_unittests_mock
If all goes well, you should see the following output for this chapter's unit test:
/**
* \brief Pretend to be a TransformComponent
*/
class MockTransformComponent
: public AZ::Component
, public AZ::TransformBus::Handler
{
public:
// be sure this guid is unique, avoid copy-paste errors!
AZ_COMPONENT(MockTransformComponent,
"{7E8087BD-46DA-4708-ADB0-08D7812CA49F}");
137
Unit Tests with Mock Components
AZ::ComponentDescriptor::DependencyArrayType& req)
{
req.push_back(AZ_CRC("TransformService"));
}
class OscillatorMockTest
: public ::UnitTest::AllocatorsFixture
{
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_md;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_od;
protected:
void SetUp() override
{
::UnitTest::AllocatorsFixture::SetUp();
138
Unit Tests with Mock Components
::UnitTest::AllocatorsFixture::TearDown();
}
// helper method
void PopulateEntity(Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// We can mock out Transform and test the interaction
mock = new NiceMock<MockTransformComponent>();
e.AddComponent(mock);
TEST_F(OscillatorMockTest, Calls_SetWorldTranslation)
{
Entity e;
PopulateEntity(e);
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
}
139
Part VII. Character Controller
Table of Contents
17. Player Input .............................................................................................................. 142
Introduction ............................................................................................................ 142
Starting Point Input Gem .......................................................................................... 142
Adding Input Mapping ............................................................................................. 142
Linking Against Starting Point Input Gem ................................................................... 144
Receiving Inputs in a C++ Component ........................................................................ 145
Source Code ........................................................................................................... 147
18. Character Movement ................................................................................................... 150
Introduction ............................................................................................................ 150
PhysX Character Controller ....................................................................................... 150
Moving the Character ............................................................................................... 152
Source Code ........................................................................................................... 155
19. Turning using Mouse Input .......................................................................................... 159
Introduction ............................................................................................................ 159
Customizing Input Event Groups ................................................................................ 159
Mouse Input ........................................................................................................... 160
Source Code ........................................................................................................... 162
141
Chapter 17. Player Input
Introduction
Note
The accompanying source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch17_input
One of the common elements in a game is a player controlled character. This part of the book will build
a player controlled chicken that we added in Chapter 14, Enabling NvCloth Gem. But before we get to
moving our chicken, I will show you how to capture player's input in O3DE.
set(ENABLED_GEMS
...
StartingPointInput
...
)
Note
The official document on Input component can be found here:
https://www.o3de.org/docs/user-guide/interactivity/input/using-player-input/
2. Add Input component. Local player index is the value to point to which local player will receive the
inputs. For now, keeping it at value "-1" means that the input will be sent to all local player controllers.
142
Player Input
3. At the moment, there is no input bindings assigned. Assign one for Input to event bind-
ings property. We can start with a template from Starting Point Input gem. It is locat-
ed at C:\O3DE\21.11.2\Gems\StartingPointInput\Assets\thirdpersonmove-
ment.inputbindings.
5. At this point, you can inspect the bindings using the right most icon next to Input to event bindings,
which will open Asset Viewer.
143
Player Input
7. We have arrived at the definition of move forward action, which is assigned keyboard key W.
Now that we have a clear path of how an action is triggered from a given input, let us take a look at how
to capture it in a C++ component.
144
Player Input
header file that contains the ebus, we will need to link against the static library StartingPointIn-
put.Static.
ly_add_target(
NAME MyGem.Static STATIC
...
BUILD_DEPENDENCIES
PUBLIC
Gem::StartingPointInput.Static
...
)
class ChickenControllerComponent
: public AZ::Component
, public StartingPointInput::InputEventNotificationBus
::MultiHandler
// AZ::InputEventNotificationBus interface
void OnPressed(float value) override;
void OnReleased(float value) override;
const StartingPointInput::InputEventNotificationId
MoveFwdEventId("move forward");
Important
The value of "move forward" has to match the value of the appropriate Event Name in Input
component binding.
145
Player Input
if (*inputId == MoveFwdEventId)
{
AZ_Printf("Chicken", "forward axis %f", value);
}
}
With these changes, you can press W and S to trigger movement input action.
Note
Negative values are coming from pressing S key, which is mapped to the backward motion by sending
move forward with a negative value.
146
Player Input
Source Code
Note
The accompanying source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch17_input
namespace MyGem
{
const StartingPointInput::InputEventNotificationId
MoveFwdEventId("move forward");
class ChickenControllerComponent
: public AZ::Component
, public StartingPointInput::
InputEventNotificationBus::MultiHandler
{
public:
AZ_COMPONENT(ChickenControllerComponent,
"{fe639d60-75c0-4e16-aa1a-0d44dbe6d339}");
// AZ::InputEventNotificationBus interface
void OnPressed(float value) override;
void OnReleased(float value) override;
};
} // namespace MyGem
namespace MyGem
{
using namespace StartingPointInput;
147
Player Input
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<ChickenControllerComponent, AZ::Component>()
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<ChickenControllerComponent>(
"Chicken Controller",
"[Player controlled chicken]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC("Game"));
}
}
}
void ChickenControllerComponent::Activate()
{
InputEventNotificationBus::
MultiHandler::BusConnect(MoveFwdEventId);
}
void ChickenControllerComponent::Deactivate()
{
InputEventNotificationBus::MultiHandler::BusDisconnect();
}
if (*inputId == MoveFwdEventId)
{
AZ_Printf("Chicken", "forward axis %f", value);
}
}
148
Player Input
if (*inputId == MoveFwdEventId)
{
AZ_Printf("Chicken", "forward axis %f", value);
}
}
} // namespace MyGem
149
Chapter 18. Character Movement
Introduction
Note
The accompanying source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch18_character_movement
Previous chapter captured the input. This chapter will move the chicken based on that input by updating
the code of Chicken Controller component. Pressing W or S will move the chicken forward and back,
while pressing A or D will move it sideways left and right.
set(ENABLED_GEMS
...
PhysX
)
1. Detach chicken entities from the prefab. We are going to build a new chicken prefab.
2. This puts the entities out of the prefab and onto the level directly.
Note
When you detach a prefab, the Editor will create a new entity with the name of the prefab and
place all prefab entities under it. The prefab instance is then deleted, leaving the entities behind.
150
Character Movement
4. You have a few options for the shape of the controller on Shape property. I went with a capsule and
modified the capsule shape to match the chicken, using Height and Radius properties from Capsule
section.
151
Character Movement
6. Create an entity with a camera under Chicken_Action entity. This way the camera will follow the
chicken as it moves around the level.
7. Move the child entity Camera to some distance away from the chicken to create a comfortable third-
person view.
Now that the chicken character is setup with entities, we can improve ChickenControllerCompo-
nent to respond to player's input.
152
Character Movement
That adds some amount of velocity to the next frame to be simulated by PhysX, thus moving the character
in the physical world.
Tip
You can call AddVelocity multiple times per frame. The character will accumulate them together,
such as adding player movement and gravity separately.
class ChickenControllerComponent
: public AZ::Component
, public AZ::TickBus::Handler
void ChickenControllerComponent::OnTick(
float, AZ::ScriptTimePoint)
{
const ChickenInput input = CreateInput();
153
Character Movement
ProcessInput(input);
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
}
ChickenInput ChickenControllerComponent::CreateInput()
{
ChickenInput input;
input.m_forwardAxis = m_forward;
return input;
}
5. ChickenInput is processed and applied to the character using PhysX Character Controller com-
ponent API.
void ChickenControllerComponent::ProcessInput(
const ChickenInput& input)
{
UpdateVelocity(input);
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
m_velocity);
}
6. UpdateVelocity calculates the direction of the character's movement and saves the result into
m_velocity of type AZ::Vector3.
void ChickenControllerComponent::UpdateVelocity(
const ChickenInput& input)
{
float currentHeading = GetEntity()->GetTransform()->
GetWorldRotationQuaternion().GetEulerRadians().GetZ();
AZ::Vector3 fwd = AZ::Vector3::CreateAxisY(
input.m_forwardAxis);
m_velocity = AZ::Quaternion::CreateRotationZ(currentHeading).
TransformVector(fwd) * m_speed;
154
Character Movement
Note
The math here gets the direction the character is facing and creates a vector in that direction using
player input value and speed of the chicken.
With these changes, the chicken will move forward when you press W and backward when you press S.
The camera will follow along, since the camera component was attached to a child entity of the chicken.
We will add gravity to the chicken in Chapter 20, Adding Physics to the World.
Source Code
Note
The accompanying source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch18_character_movement
Here is the updated code for ChickenControllerComponent. You can add strafing and other move-
ment using the same approach by enhancing CreateInput and ProcessInput.
namespace MyGem
{
const StartingPointInput::InputEventNotificationId
MoveFwdEventId("move forward");
class ChickenInput
{
public:
155
Character Movement
float m_forwardAxis = 0;
};
class ChickenControllerComponent
: public AZ::Component
, public AZ::TickBus::Handler
, public StartingPointInput::
InputEventNotificationBus::MultiHandler
{
public:
AZ_COMPONENT(ChickenControllerComponent,
"{fe639d60-75c0-4e16-aa1a-0d44dbe6d339}");
// AZ::InputEventNotificationBus interface
void OnPressed(float value) override;
void OnReleased(float value) override;
// TickBus interface
void OnTick(float deltaTime, AZ::ScriptTimePoint) override;
private:
ChickenInput CreateInput();
void ProcessInput(const ChickenInput& input);
} // namespace MyGem
namespace MyGem
{
using namespace StartingPointInput;
156
Character Movement
{
sc->Class<ChickenControllerComponent, AZ::Component>()
->Field("Speed", &ChickenControllerComponent::m_speed)
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<ChickenControllerComponent>(
"Chicken Controller",
"[Player controlled chicken]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"))
->DataElement(nullptr,
&ChickenControllerComponent::m_speed,
"Speed", "Chicken's speed");
}
}
}
void ChickenControllerComponent::Activate()
{
InputEventNotificationBus::MultiHandler::BusConnect(
MoveFwdEventId);
AZ::TickBus::Handler::BusConnect();
}
void ChickenControllerComponent::Deactivate()
{
AZ::TickBus::Handler::BusDisconnect();
InputEventNotificationBus::MultiHandler::BusDisconnect();
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
}
void ChickenControllerComponent::OnReleased(float)
{
const InputEventNotificationId* inputId =
InputEventNotificationBus::GetCurrentBusId();
157
Character Movement
if (inputId == nullptr)
{
return;
}
if (*inputId == MoveFwdEventId)
{
m_forward = 0.f;
}
}
void ChickenControllerComponent::OnTick(float,
AZ::ScriptTimePoint)
{
const ChickenInput input = CreateInput();
ProcessInput(input);
}
ChickenInput ChickenControllerComponent::CreateInput()
{
ChickenInput input;
input.m_forwardAxis = m_forward;
return input;
}
void ChickenControllerComponent::UpdateVelocity(
const ChickenInput& input)
{
const float currentHeading = GetEntity()->GetTransform()->
GetWorldRotationQuaternion().GetEulerRadians().GetZ();
const AZ::Vector3 fwd = AZ::Vector3::CreateAxisY(
input.m_forwardAxis);
m_velocity = AZ::Quaternion::CreateRotationZ(currentHeading).
TransformVector(fwd) * m_speed;
}
void ChickenControllerComponent::ProcessInput(
const ChickenInput& input)
{
UpdateVelocity(input);
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
m_velocity);
}
} // namespace MyGem
158
Chapter 19. Turning using Mouse Input
Introduction
In Chapter 18, Character Movement we implemented character movement using W and S for moving
forward and backward. In this chapter, I will cover using mouse motion to turn the character entity, which
will turn the camera as well.
Note
The accompanying source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch19_character_turning
4. Remove all bindings except for move forward, move right and rotate yaw.
159
Turning using Mouse Input
Mouse Input
Input Event Groups allows you to specify the device and device input for each event. Here we have mouse
device and mouse_delta_x input assigned to rotate yaw event. That means sideways motion of the mouse
will be sent to the assigned event. Here is how to capture this mouse input and apply it.
class ChickenControllerComponent
{
...
float m_yaw = 0.f;
};
const StartingPointInput::InputEventNotificationId
RotateYawEventId("rotate yaw");
void ChickenControllerComponent::Activate()
{
...
InputEventNotificationBus::MultiHandler::BusConnect(
RotateYawEventId);
160
Turning using Mouse Input
Important
This step is critical. Otherwise you will not receive the mouse events at all for "rotate yaw".
if (*inputId == RotateYawEventId)
{
m_yaw = value;
}
}
ChickenInput ChickenControllerComponent::CreateInput()
{
ChickenInput input;
...
input.m_viewYaw = m_yaw;
return input;
}
void ChickenControllerComponent::UpdateRotation(
const ChickenInput& input)
{
AZ::TransformInterface* t = GetEntity()->GetTransform();
t->SetWorldRotationQuaternion(q);
}
With these changes, the chicken will turn based on mouse moving side to side. The camera will always
stay behind the chicken, since it is locked to that orientation.
161
Turning using Mouse Input
Source Code
Note
The accompanying source code for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch19_character_turning
As a bonus, this source code also implements strafing. It is very similar in its implementation to moving
forward and backward.
#include <AzCore/Component/Component.h>
#include <AzCore/Component/TickBus.h>
#include <AzCore/Math/Vector3.h>
#include <StartingPointInput/InputEventNotificationBus.h>
namespace MyGem
{
const StartingPointInput::InputEventNotificationId
MoveFwdEventId("move forward");
const StartingPointInput::InputEventNotificationId
MoveRightEventId("move right");
const StartingPointInput::InputEventNotificationId
RotateYawEventId("rotate yaw");
class ChickenInput
{
public:
float m_forwardAxis = 0;
float m_strafeAxis = 0;
float m_viewYaw = 0;
};
class ChickenControllerComponent
: public AZ::Component
, public AZ::TickBus::Handler
, public StartingPointInput::
InputEventNotificationBus::MultiHandler
{
public:
AZ_COMPONENT(ChickenControllerComponent,
"{fe639d60-75c0-4e16-aa1a-0d44dbe6d339}");
162
Turning using Mouse Input
// AZ::InputEventNotificationBus interface
void OnPressed(float value) override;
void OnReleased(float value) override;
void OnHeld(float value) override;
// TickBus interface
void OnTick(float deltaTime, AZ::ScriptTimePoint) override;
private:
ChickenInput CreateInput();
void ProcessInput(const ChickenInput& input);
} // namespace MyGem
namespace MyGem
{
using namespace StartingPointInput;
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<ChickenControllerComponent>(
"Chicken Controller",
"[Player controlled chicken]")
163
Turning using Mouse Input
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"))
->DataElement(nullptr,
&ChickenControllerComponent::m_turnSpeed,
"Turn Speed", "Chicken's turning speed")
->DataElement(nullptr,
&ChickenControllerComponent::m_speed,
"Speed", "Chicken's speed");
}
}
}
void ChickenControllerComponent::Activate()
{
InputEventNotificationBus::MultiHandler::BusConnect(
MoveFwdEventId);
InputEventNotificationBus::MultiHandler::BusConnect(
MoveRightEventId);
InputEventNotificationBus::MultiHandler::BusConnect(
RotateYawEventId);
AZ::TickBus::Handler::BusConnect();
}
void ChickenControllerComponent::Deactivate()
{
AZ::TickBus::Handler::BusDisconnect();
InputEventNotificationBus::MultiHandler::BusDisconnect();
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
else if (*inputId == MoveRightEventId)
{
m_strafe = value;
}
}
164
Turning using Mouse Input
if (inputId == nullptr)
{
return;
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
else if (*inputId == MoveRightEventId)
{
m_strafe = value;
}
}
if (*inputId == RotateYawEventId)
{
m_yaw = value;
}
}
void ChickenControllerComponent::OnTick(float,
AZ::ScriptTimePoint)
{
const ChickenInput input = CreateInput();
ProcessInput(input);
}
ChickenInput ChickenControllerComponent::CreateInput()
{
ChickenInput input;
input.m_forwardAxis = m_forward;
input.m_strafeAxis = m_strafe;
input.m_viewYaw = m_yaw;
return input;
}
void ChickenControllerComponent::UpdateRotation(
const ChickenInput& input)
{
AZ::TransformInterface* t = GetEntity()->GetTransform();
165
Turning using Mouse Input
t->SetWorldRotationQuaternion(q);
}
void ChickenControllerComponent::UpdateVelocity(
const ChickenInput& input)
{
const float currentHeading = GetEntity()->GetTransform()->
GetWorldRotationQuaternion().GetEulerRadians().GetZ();
const AZ::Vector3 fwd = AZ::Vector3::CreateAxisY(
input.m_forwardAxis);
const AZ::Vector3 strafe = AZ::Vector3::CreateAxisX(
input.m_strafeAxis);
const AZ::Vector3 combined = (fwd + strafe).GetNormalized();
m_velocity = AZ::Quaternion::CreateRotationZ(currentHeading).
TransformVector(combined) * m_speed;
}
void ChickenControllerComponent::ProcessInput(
const ChickenInput& input)
{
UpdateRotation(input);
UpdateVelocity(input);
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
m_velocity);
}
} // namespace MyGem
166
Part VIII. Building Environment
Table of Contents
20. Adding Physics to the World ........................................................................................ 169
Introduction ............................................................................................................ 169
Create a Soccer Field ............................................................................................... 169
Add a Static Mesh Collider ....................................................................................... 172
Add a Soccer Ball ................................................................................................... 174
Summary ................................................................................................................ 175
21. Introduction to Materials and Lights .............................................................................. 176
Introduction ............................................................................................................ 176
Materials ................................................................................................................ 176
Spot Lights ............................................................................................................. 178
Disk Lights and Emissive Materials ............................................................................ 179
Changing Base Color of a Material ............................................................................ 181
Summary ................................................................................................................ 182
168
Chapter 20. Adding Physics to the
World
Introduction
Previous chapters have created a chicken controller. Our chicken can be made to run around and turn but
there. There is one glaring defect. The chicken does not collide with any objects and is not affected by
gravity. While we could easily add gravity using AddVelocity with the following example, it wouldn't
be enough.
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity, m_gravity);
Our level does not have any physical object in it, so there is nothing for the chicken to bump into. It would
just fall through the floor into oblivion. This chapter will address that by doing the following:
Note
The accompanying assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch20_physical_world
We can build a soccer field out of boxes by resizing them and adding static physical colliders on them.
Here is the entity outline of the soccer field.
169
Adding Physics to the World
Side and Ground entities have the same structure but with a different scale.
170
Adding Physics to the World
• Mesh component with a box model that is being resized by Non-uniform Scale component.
• PhysX Collider component to add a static immovable physical shape to match the scaled mesh.
Note
There is no need to scale the value of the PhysX Collider, as they will be scaled by Non-uniform
Scale component.
As I was building the soccer field, I noticed that there was a pattern of entities of building the goal line on
each side. So in order to save myself the effort, I created a prefab out of one side, duplicated and moved
it to the opposite end of the soccer field.
Inside them there are more entities of a similar design as Side and Ground entities.
There are three scaled boxes to create the goal line and edges of the field.
171
Adding Physics to the World
I created this model in Blender and exported it as .FBX file to the project but that does not automatically
provide a physical shape for us. We can generate one using the following tools.
172
Adding Physics to the World
6. Enable the mesh node, Text_1. Click Select to save the selection.
173
Adding Physics to the World
With these changes, you should see triangles form over the 3D text. That is the outline of the physical
mesh over the text. The text has become physical.
Default values on PhysX Rigid Body component will work as is. Set PhysX Collider to the shape of
Sphere with a radius of 0.5.
174
Adding Physics to the World
That will match size of _spheres_1x1 mesh that comes with the engine.
If you place this Ball entity on top of the soccer field, you will see it fall, collide with static shapes and
roll around.
Note
You might notice that running the chicken into the ball does not move the ball. The chicken is moved
using PhysX character controller. We will need to do some extra work with physical triggers to detect
collisions and apply impulse to the soccer ball. That will be done in Chapter 24, Kicking the Ball.
Summary
We created a physical world for the chicken. Now we can apply gravity to the chicken by updating
ProcessInput method with an additional call to AddVelocity.
void ChickenControllerComponent::ProcessInput(
const ChickenInput& input)
{
...
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
AZ::Vector3::CreateAxisZ(m_gravity));
}
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch20_physical_world
175
Chapter 21. Introduction to Materials
and Lights
Introduction
It is time to spice up our level. In previous chapter we built the soccer field using boxes with a default white
material. We can add a lot of color and lights to the level using only the basic engine materials and lights.
Note
The accompanying assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch21_materials_lights
Materials
Note
You will find a reference for O3DE materials online at:
https://www.o3de.org/docs/atom-guide/look-dev/materials/
We will start by modifying the material of the ground mesh that came with Atom Default Environment
entity.
1. Select Atom Default Environment, then select Ground entity under it.
Figure 21.1. Entity Atom Default Environment with Ground child entity
3. Change Default Material to "16_yellow". (This material comes with O3DE inside Atom renderer
gems.)
176
Introduction to Materials and Lights
4. Click on the sphere icon next to Default Material and then select Edit Material Instance.
5. This will open Material Property Inspector where you can change various properties of this par-
ticular material on this specific entity. This way you are modifying the material only for this entity
without affecting other entities that may use the same source material.
Tip
O3DE comes with Atom renderer that supports PBR textures (Physically Based Rendering). You
can find or make your own PBR textures and assign their component textures, such as Base Color,
Metallic, Roughness textures and so on. In this chapter, I am working with simple color textures only.
In the similar fashion as above, I have assigned O3DE mesh 13_blue PBR material, 09_moderate_red to
the sides of the soccer field and 14_green_tex to the soccer ground mesh.
177
Introduction to Materials and Lights
Important
Adding a Mesh component does not add Material component by default. You have to add Material
component yourself to an entity, otherwise the default material for the mesh is used. The easiest way
is to use the button Add Material Component on Mesh component.
Spot Lights
With some basic material assigned, we can turn towards adding a few fancy lights. Our level already came
with a skybox, skylight and a direction light. You can find them under Atom Default Environment entity.
For example, Sun child entity has a Directional Light component.
178
Introduction to Materials and Lights
5. Use transform tools to point the spot light at the goal line.
179
Introduction to Materials and Lights
Tip
You can selectively enable shadow support for each Light source to control the performance and
look of your game. Use Enable shadow on Light component.
2. Create a mesh with a sphere that is squished along one axis, so it looks like a lamp source.
5. Add a back mesh of your choice and style to create a flood lamp for the soccer field.
180
Introduction to Materials and Lights
I am not interested in a grey ball, though. So I will use Base Color section to change Color to yellow.
181
Introduction to Materials and Lights
Summary
Note
The accompanying assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch21_materials_lights
Our very brief introduction to materials and lights is over. There is a lot more to explore in these topics
but this is enough to get us started. Here is how the level looks like a few tweaks to materials and addition
of a few lights.
Note
The online documentation on materials can be found here:
https://www.o3de.org/docs/atom-guide/look-dev/materials/
https://www.o3de.org/docs/user-guide/components/reference/atom/light/
182
Part IX. Building Game Play
Table of Contents
22. Introduction to User Interface ....................................................................................... 185
Introduction ............................................................................................................ 185
Loading UI ............................................................................................................. 186
Creating a Canvas in UI Editor .................................................................................. 186
Drawing UI Canvas ................................................................................................. 190
Summary ................................................................................................................ 191
23. Interacting with UI in C++ .......................................................................................... 192
Introduction ............................................................................................................ 192
Physical Triggers ..................................................................................................... 192
Sign up for Trigger Events ........................................................................................ 194
Receiving Updates in UI ........................................................................................... 197
Moving a Rigid Body .............................................................................................. 198
Summary ................................................................................................................ 199
24. Kicking the Ball ........................................................................................................ 204
Introduction ............................................................................................................ 204
The Root Cause of the Issue ..................................................................................... 204
Trigger Shape on the Chicken ................................................................................... 204
Kicking Component ................................................................................................. 206
Source Code ........................................................................................................... 209
25. Introduction to Animation ............................................................................................ 212
Introduction to EMotionFX ....................................................................................... 212
Simple Motion Component ....................................................................................... 212
Anim Graph Component ........................................................................................... 213
Building an Animation Graph .................................................................................... 213
Animation Blending ................................................................................................. 218
Assigning the Anim Graph ........................................................................................ 221
Driving Speed Parameter .......................................................................................... 221
Source Code ........................................................................................................... 222
26. Animation State Machine ............................................................................................ 225
Introduction ............................................................................................................ 225
Adding Flapping Motion ........................................................................................... 225
Adding Celebration Blend Tree .................................................................................. 225
Summary ................................................................................................................ 230
27. Script Canvas ............................................................................................................ 231
Introduction ............................................................................................................ 231
Behavior Context ..................................................................................................... 231
Building a Canvas ................................................................................................... 233
Summary ................................................................................................................ 235
184
Chapter 22. Introduction to User
Interface
Note
The level and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch22_basic_ui
Introduction
O3DE comes with its own User Interface subsystem, LyShine. The core of the system is implemented in
LyShine gem (which is already enabled in MyProject) but it is also useful to enable two more supporting
gems for LyShine.
Note
Official reference for LyShine can be found at
https://www.o3de.org/docs/user-guide/interactivity/user-interface/
LyShineExamples and UiBasics gems provide additional canvas examples and UI elements. Their as-
sets are inside O3DE installation at Gems\UiBasics\Assets\UI\Slices\Library and Gems
\LyShineExamples\Assets\UI\Canvases\LyShineExamples.
Important
After you enable these gems, re-build the project in Visual Studio or with CMake, close the Asset
Process and open it again. It should have a few hundred new assets to process. Otherwise, you will
not see new UI elements.
In this chapter, we will create an overlay to display two numbers. This overlay will be used to keep score
of the soccer match.
185
Introduction to User Interface
Loading UI
User Interface work begins by using UI Canvas Asset Ref component to load a UI asset.
Tip
You can open UI editor from this component by clicking on the right most icon button next to
Canvas pathname property.
UiCanvasAssetRefBus::Event(entityId,
&UiCanvasAssetRefBus::Events::LoadCanvas);
That will opens UI Editor with an empty canvas. A canvas is a container for UI elements that are drawn
together. Before we get started, let us save the new empty canvas to our project location. We will save it
to C:\git\book\MyProject\Assets\UI\hud.uicanvas.
Note
The location is optional within a project and its gems. The Asset Processor will find the asset regardless
of where you put it under your project or under one of your gems but it is a good practice to follow
some standard.
Hierarchy panel on the left lists the hierarchy of UI entities. The underlying system uses O3DE entities
and components. Hierarchy panel is essentially another version of Entity Outliner. Let us create a canvas
with elements placed at that top middle portion of the canvas.
186
Introduction to User Interface
1. Right click on empty space in Hierarchy, then choose New and Empty element.
2. Name this element Root by double clicking on it. It will serve as the parent of all other UI elements.
3. Right click on Root element, then New and Element from Slice Browser.
5. Right click on Panel element, select "New", "Element from Slice Library." From group "UiBasics\As-
sets\UI\Slices\Library" select Text.
Tip
The order of the elements determines which one is drawn first. The root element is drawn first with
leaf elements drawn last.
We have the outline of the elements we need but the panel is too big and the text is in the center. The way
to move and modify elements is by using their Transform2D component and through Mode toolbar.
3. At the bottom of Transform 2D component, find Scale to device property, set it to "Scale to fit Y
(uniformly)". That will fit the contents to the size of the viewport screen. That means you do not
have to worry about setting pixel values and can think of any elements underneath as having relative
proportional values to the actual viewport screen size.
4. Then choose top middle anchor. It is a small icon under Anchors section with a white box inside a
larger gray box.
187
Introduction to User Interface
6. Select the anchor that fills the entire available space. This anchor icon looks like a full white square.
7. With "Panel" element selected, in Mode toolbar, select scaling tool and reduce the vertical size of the
element by dragging the green tool line with a square at the top.
188
Introduction to User Interface
8. With "Panel" element selected, in Image component, lower Alpha value to less then 1.0. (0.77 was
my choice.) This will make the panel semi-transparent.
9. In Hierarchy panel select "Score Text" element, then apply the anchor that fills all available space.
(It is the bottom right anchor icon.)
189
Introduction to User Interface
10. With "Score Text" element selected, go to its Text component and modify Text value to "0 : 0". You
can change font and font color as well.
11. Save the canvas. It should look similar to this image where the panel with text appears at the top
middle portion of the canvas.
Drawing UI Canvas
In order to draw this new UI canvas over the entire screen, set Canvas pathname to the canvas we created.
190
Introduction to User Interface
Set hud.uicanvas
Since we have no programmatic interaction with this canvas yet, load it automatically by enabling Load
automatically property.
Summary
Note
The level and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch22_basic_ui
We are now done! The canvas we created will not show up in Editor viewport until you enter game mode
with CTRL+G. When you do, you should see score UI at the top of your screen.
191
Chapter 23. Interacting with UI in C++
Note
The code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch23_ui_gameplay
Introduction
In this chapter we will build game logic to update UI
scores when the ball entity hits one of the goals.
Physical Triggers
Figure 23.1. PhysX Trigger Shape
3. Enable Trigger switch. That turns the collider from a static collider into a trigger volume.
192
Interacting with UI in C++
5. In order to count goals correctly our triggers should only detect the ball and no other physical objects.
We will venture into collision filtering to achieve that. Click the icon to the right of Colliders With
or from the main menu Tools → PhysX Configuration and then select Collision Filtering tab.
7. Under Groups, create Only Ball group with only "Ball" layer enabled.
8. Now we can use these groups and layers. Go to Ball entity, select its PhysX Collider component
and set "Collision Layer" to "Ball", and "Collides With" to "All". This way the ball will collide with
all objects but will be marked by a different collision layer that will give us ability to detect the ball
versus other objects in the level.
193
Interacting with UI in C++
9. For Goal 1 and Goal 2 entities, their PhysX Collider components should have "Collision Layer" set
to "Default" and "Collides With" set to "Only Ball".
AzPhysics::SceneEvents::OnSceneTriggersEvent::Handler m_trigger;
Tip
We are using AZ::Event to sign up for notifications.
194
Interacting with UI in C++
GoalDetectorComponent::GoalDetectorComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
})
{
}
void GoalDetectorComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
4. Now we can process the trigger events in OnTriggerEvents method. It needs to do the following:
• Check that the event type was Enter, otherwise will also count Exit event as well and miscount
the score.
• If everything checks out, then place the ball to the starting point and update the UI score.
void GoalDetectorComponent::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == GetEntityId())
{
if (te.m_type == TriggerEvent::Type::Enter)
{
AZ::Vector3 v = GetRespawnPoint();
RespawnBall(v);
UpdateUi();
}
5. Respawn point is set by placing an entity at the respawn location and then passing it to GoalDe-
tectorComponent. This way we can get the respawn location by querying the entity's location
instead of messing around with world coordinates. In the level that we can easily adjust the respawn
entity by moving it around and seeing visually in the level where the respawn point is.
195
Interacting with UI in C++
6. Ball entity id is saved in the Editor on GoalDetectorComponent as well, so that Goal Detector
component can move the ball back to the respawn location.
7. Team property will be used in updating UI score for the appropriate team. Goal 1 entity has Team set
to zero, while Goal 2 entity has Team set to 1. It is just an internal identifier.
8. UpdateUI method sends a notification event using a new EBus that we will create: UiScoreNo-
tificationBus.
196
Interacting with UI in C++
void GoalDetectorComponent::UpdateUi()
{
UiScoreNotificationBus::Broadcast(
&UiScoreNotificationBus::Events::OnTeamScored, m_team);
}
Receiving Updates in UI
In order to receive notifications on UiScoreNotificationBus, we are going to create UiScore-
Component that will receive them.
class UiScoreComponent
: public AZ::Component
, public UiScoreNotificationBus::Handler
2. Implement the notification callback that updates the text value in UI.
3. Notice that it invokes UiTextBus on the same entity. Add UiScoreComponent in UI Editor to
the entity Score Text, so that UiScoreComponent can communicate with Text component.
197
Interacting with UI in C++
Important
A component can be added to a UI entity if it is marked as "UI" in its Reflect method.
Attribute(AppearsInAddComponentMenu, AZ_CRC_CE("UI"))
One easy way to correct this is to disable physics of the ball, move it and then enable physics back on.
AZ::TransformBus::Event(m_ball,
&AZ::TransformBus::Events::SetWorldTranslation, v);
Physics::RigidBodyRequestBus::Event(m_ball,
&Physics::RigidBodyRequestBus::Events::EnablePhysics);
}
198
Interacting with UI in C++
Summary
Note
The code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch23_ui_gameplay
At this point, the game updates score UI when the ball hits the goal trigger on each side of the soccer field.
namespace MyGem
{
class GoalDetectorComponent
: public AZ::Component
{
public:
AZ_COMPONENT(GoalDetectorComponent,
"{eaf6ae0a-7444-47fb-a759-8d7b8a6f3356}");
GoalDetectorComponent();
private:
AzPhysics::SceneEvents::
OnSceneTriggersEvent::Handler m_trigger;
void OnTriggerEvents(
const AzPhysics::TriggerEventList& tel);
AZ::EntityId m_ball;
AZ::EntityId m_reset;
int m_team = 0;
199
Interacting with UI in C++
#include <AzCore/Interface/Interface.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Physics/PhysicsScene.h>
#include <AzFramework/Physics/RigidBodyBus.h>
#include <MyGem/UiScoreBus.h>
namespace MyGem
{
void GoalDetectorComponent::Reflect(AZ::ReflectContext* rc)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<GoalDetectorComponent, AZ::Component>()
->Field("Team", &GoalDetectorComponent::m_team)
->Field("Ball", &GoalDetectorComponent::m_ball)
->Field("Respawn", &GoalDetectorComponent::m_reset)
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<GoalDetectorComponent>(
"Goal Detector",
"[Detects when a goal is scored]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"))
->DataElement(0, &GoalDetectorComponent::m_team,
"Team", "Which team is this goal line for?")
->DataElement(0, &GoalDetectorComponent::m_ball,
"Ball", "Ball entity")
->DataElement(0, &GoalDetectorComponent::m_reset,
"Respawn", "where to put the ball after")
;
}
}
}
GoalDetectorComponent::GoalDetectorComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
})
{
}
void GoalDetectorComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
200
Interacting with UI in C++
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
void GoalDetectorComponent::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == GetEntityId())
{
if (te.m_type == TriggerEvent::Type::Enter)
{
AZ::Vector3 respawnLocation = GetRespawnPoint();
RespawnBall(respawnLocation);
UpdateUi();
}
}
}
}
void GoalDetectorComponent::UpdateUi()
{
UiScoreNotificationBus::Broadcast(
&UiScoreNotificationBus::Events::OnTeamScored, m_team);
}
} // namespace MyGem
201
Interacting with UI in C++
#include <MyGem/UiScoreBus.h>
namespace MyGem
{
class UiScoreComponent
: public AZ::Component
, public UiScoreNotificationBus::Handler
{
public:
AZ_COMPONENT(UiScoreComponent,
"{49b2e5e8-e028-48b1-bc69-82c73b32422b}");
// UiScoreNotificationBus interface
void OnTeamScored(int team) override;
private:
int m_teams[2] = { 0, 0 };
};
} // namespace MyGem
namespace MyGem
{
void UiScoreComponent::Reflect(AZ::ReflectContext* rc)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<UiScoreComponent, AZ::Component>()
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<UiScoreComponent>(
"Ui Score Component",
"[Updates score text]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("UI"));
}
}
}
202
Interacting with UI in C++
void UiScoreComponent::Activate()
{
UiScoreNotificationBus::Handler::BusConnect(GetEntityId());
}
void UiScoreComponent::Deactivate()
{
UiScoreNotificationBus::Handler::BusDisconnect();
}
203
Chapter 24. Kicking the Ball
Introduction
The next glaring gameplay issue for me is that the
chicken does not push the ball all that well. Some-
times it does and sometimes the ball ignores the
chicken entirely.
Note
The code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch24_kicking_ball
It is tempting to let the physics engine push dynamic objects by applying forces at contact
points. However it is often not a very convincing solution. The bounding volumes around
characters are artificial (boxes, capsules, etc) and invisible, so the forces computed by
the physics engine between a bounding volume and its surrounding objects will not
be realistic anyway. They will not properly model the interaction between an actual
character and these objects.
—PhysX 4.1 SDK Guide, Character Controllers
Long story short, it is up to us implement the interaction between the chicken character and the ball. That
is not a problem. We can use the knowledge from our previous chapters to solve it.
204
Kicking the Ball
4. We will also create a new component, KickingComponent, and add it to this entity.
5. In Chapter 23, Interacting with UI in C++, we created a new collision layer and a group. We can
filter collisions with the kicking trigger volume to the ball only. Set the Collision Layer to "Default".
8. Set the shape of the collider to a sphere. You may need to modify size and offset of the sphere to
match the chicken body.
205
Kicking the Ball
The steps above should create the following trigger shape on the chicken.
Kicking Component
The trigger shape will detect when a ball is right next to the chicken. We need to process trigger events
and apply impulse to the ball.
206
Kicking the Ball
Kicking component will have a configurable amount of impulse to apply to the ball. The logic of signing up
for collision notifications is very similar to GoalDetectorComponent from Chapter 23, Interacting
with UI in C++.
AzPhysics::SceneEvents::OnSceneTriggersEvent::Handler m_trigger;
KickingComponent::KickingComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
})
{
}
void KickingComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
4. When the trigger event occurs, check that the ball entered the trigger volume and if so, kick it.
void KickingComponent::OnTriggerEvents(
207
Kicking the Ball
5. Kicking the ball is done by calculating the direction from the chicken to the ball, creating a vector
along that direction, and using ApplyLinearImpulse on the ball's rigid body component.
void KickingComponent::KickBall(AZ::EntityId b)
{
AZ::Vector3 impulse = GetBallPosition(b) - GetSelfPosition();
impulse.Normalize();
impulse *= m_kickForce;
AddImpulseToBall(impulse, b);
}
void KickingComponent::AddImpulseToBall(
AZ::Vector3 v, AZ::EntityId ball)
{
Physics::RigidBodyRequestBus::Event(ball,
&Physics::RigidBodyRequestBus::Events::ApplyLinearImpulse,
v);
}
The mass of the ball can be modified on Physx Rigid Body component by turning off automatic mass
calculation by disabling Compute Mass and specifying Mass value manually.
208
Kicking the Ball
I found the ball mass of two hundred (200) and impulse value of a thousand (1000) works well for our game.
Source Code
Note
The code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch24_kicking_ball
In this chapter, we added one new component, KickingComponent, to have the chicken kick the ball
in a consistent way that is suitable for the gameplay.
namespace MyGem
{
class KickingComponent
: public AZ::Component
{
public:
AZ_COMPONENT(KickingComponent,
"{73A60188-9BFB-4168-A733-8A06BC500ECB}");
KickingComponent();
private:
AzPhysics::SceneEvents::
OnSceneTriggersEvent::Handler m_trigger;
void OnTriggerEvents(
const AzPhysics::TriggerEventList& tel);
209
Kicking the Ball
AZ::Vector3 GetSelfPosition();
void AddImpulseToBall(AZ::Vector3 v, AZ::EntityId ball);
};
} // namespace MyGem
namespace MyGem
{
void KickingComponent::Reflect(AZ::ReflectContext* rc)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<KickingComponent, AZ::Component>()
->Field("Kick force", &KickingComponent::m_kickForce)
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<KickingComponent>(
"Kicking",
"[Kicking the ball when it comes close]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"))
->DataElement(0, &KickingComponent::m_kickForce,
"Kick force", "impulse strength");
}
}
}
KickingComponent::KickingComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
}) {}
void KickingComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
210
Kicking the Ball
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
void KickingComponent::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == GetEntityId())
{
if (te.m_type == TriggerEvent::Type::Enter)
{
KickBall(te.m_otherBody->GetEntityId());
}
}
}
}
void KickingComponent::KickBall(AZ::EntityId b)
{
AZ::Vector3 impulse = GetBallPosition(b) - GetSelfPosition();
impulse.Normalize();
impulse *= m_kickForce;
AddImpulseToBall(impulse, b);
}
AZ::Vector3 KickingComponent::GetSelfPosition()
{
return GetEntity()->GetTransform()->
GetWorldTM().GetTranslation();
}
void KickingComponent::AddImpulseToBall(
AZ::Vector3 v, AZ::EntityId ball)
{
Physics::RigidBodyRequestBus::Event(ball,
&Physics::RigidBodyRequestBus::Events::ApplyLinearImpulse,
v);
}
} // namespace MyGem
211
Chapter 25. Introduction to Animation
Introduction to EMotionFX
At this point in our game development, the chicken is stuck in idle animation. The next improvement we
are going to make is to play walk animation when the chicken is moving and play idle animation when
it is not moving.
O3DE comes with an animation gem, EMotionFX. In this chapter, I am going to show you how to build
a state machine using EMotionFX to blend between idle and walk animations.
Note
The level and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch25_animation
This component has various configuration options but we are going to remove it from Chicken_Actor
entity and replace it with Anim Graph component.
Important
Both Simple Motion and Animation Graph components require an Actor component. Chicken_Ac-
tor entity already has one assigned from C:\O3DE\21.11.2\Gems\NvCloth\Assets\Ob-
jects\cloth\Chicken\Actor\chicken.fbx. Actor components provide actor mesh and
bone information for animations.
212
Introduction to Animation
When you first add this component, its fields are empty. First, we have to create an Anim graph asset.
Open Animation Editor either through the main menu Tools → Animation Editor or by clicking on the
right most icon next to Anim graph property on the component.
An animation graph is a collection of various nodes that define the logic and behavior of animations
and their interactions, such as blending multiple animations together. At a high level, we are going to
build a state machine that blends between idle motion from chickenidle.fbx and walking motion
from chickenwalking.fbx. Both files come with NvCloth gem in O3DE installation at C:\O3DE
\21.11.2\Gems\NvCloth\Assets\Objects\cloth\Chicken\Motions.
1. Go to Actor Manager tab. (If it is not open, use the main menu: View → Actor Manager.)
213
Introduction to Animation
4. Choose chicken.actor from NvCloth gem's location. That will load the chicken actor into
OpenGL Render Window. It will serve as a testing ground for the animation graph.
Test Motions
Before we spend time building out the graph, test that the motion works with chicken.
That will play the animations in OpenGL Render Window to let you see how the animation look and if
they match the actor.
214
Introduction to Animation
3. In Motion Set section, there is folder icon, click it and select chickenidle.motion
Tip
You can double click the motion chickenidle to test it in the render window.
5. Save the motion set using floppy disk icon. For example, I saved it to C:\git\book\Gems
\MyGem\Assets\Animation\chicken.motionset.
Anim Graph
We are ready to start building an animation graph that will blend walking and idle motions.
215
Introduction to Animation
Tip
You can also drag and drop BlendTree node
from Anim Graph Palette's Sources group.
Tip
You can rename node names in Attributes
panel.
5. Add Blend Two node to the left of FinalNode0 with right click → Create Node → Blending →
BlendTwo. This node will blend two motions together and assign the result into FinalNode0.
6. To the left of BlendTwo0, create two Motion nodes, with Create Node → Sources → Motion.
8. Go to Attributes panel.
10. Motion Selection Window will open, select chickenidle motion from MotionSet0.
11. For Motion1 node, select chickenwalking motion in the same manner.
216
Introduction to Animation
12. On Motion nodes, there is Output Pose output. Drag a line from the square next to Output Pose to
Pose 1 input on BlendTwo0 node.
13. Connect Outpost Pose of BlendTwo0 node with FinalNode0's Input Pose. The animation graph should
start to play. If not, there is a white arrow play button in Anim Graph's toolbar.
217
Introduction to Animation
Animation Blending
Figure 25.1. First Blending of Two Animations
Let us take a look at what is going on. We created a blend node. Inside the node, we declared two motion
sources that are blended together with Blend Two node, which outputs the result into final node. At the
moment, the only motion that is actually playing is the idle motion. This is because Weight input of
BlendTwo0 node determines the amount of blending between the two motions.
Since we are blending between idle and walking motions, it would make sense to blend it based on the
speed of the chicken. If the speed is zero, it should play idle motion. As the chicken starts to move the
anim graph should blend between the two motions until playing only the walking motion.
Parameters
To control the blending, we will add a parameter that we can specify from outside of the animation graph.
There is a C++ API to do so from a component but before we get to that, we will create a parameter and
test it inside the Animation Editor.
5. For Maximum, enter six (6.0), since the maximum speed of the chicken was set to 6 in Chapter 18,
Character Movement.
218
Introduction to Animation
Speed parameter
8. With Parameters0 node selected, go to Attributes panel and click on Select parameters.
Note
Our speed parameter range was set to be between zero and six. But the Weight input value of
BlendTwo0 takes a range from zero to one. We have to remap the range to match Weight require-
ments using Range Remapper node.
Tip
Smoothing node is optional but it lets us smoothly raise the value of a parameter and thus blend
between the motions over time without doing this work in a C++ component. However, if you
need precise control over the animation output, you will have to drive the Speed parameter your-
self.
12. With RangeRemapper0 node selected, go to Attributes panel and change Input Max to six (6) to match
the range of Speed parameter.
219
Introduction to Animation
14. Now you can test the animation graph by changing Speed parameter value.
Motion Adjustments
We have a walking animation for the chicken but it is too slow for our gameplay. We can fix that up
speeding up the play speed of the walking animation.
220
Introduction to Animation
3. "Action motion set" property will update to MotinoSet0 on its own after you set the motion set.
Note
Anim Graph component reads the animation graph we assigned, finds Speed parameter and reflects
it to the Editor. You can test the animation again by manually entering the value on the component
and playing the level with CTRL+G.
In order to separate concerns from other components we have written so far, I have created a new compo-
nent, ChickenAnimationComponent that receives speed notifications via ChickenNotifica-
tionBus from ChickenControllerComponent.
221
Introduction to Animation
void ChickenControllerComponent::UpdateVelocity(
const ChickenInput& input)
{
...
ChickenNotificationBus::Event(GetEntityId(),
&ChickenNotificationBus::Events::OnChickenSpeedChanged,
m_velocity.GetLength());
}
OnChickenSpeedChanged will assign the Speed parameter and finish the work for this chapter.
void ChickenAnimationComponent::OnChickenSpeedChanged(float s)
{
using namespace EMotionFX::Integration;
AnimGraphComponentRequestBus::Event(m_actor,
&AnimGraphComponentRequests::SetNamedParameterFloat,
"Speed", s);
}
Source Code
Note
The level and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch25_animation
222
Introduction to Animation
namespace MyGem
{
class ChickenNotifications
: public AZ::ComponentBus
{
public:
virtual void OnChickenSpeedChanged(float speed) = 0;
};
namespace MyGem
{
class ChickenAnimationComponent
: public AZ::Component
, public ChickenNotificationBus::Handler
{
public:
AZ_COMPONENT(ChickenAnimationComponent,
"{ED8B6A79-AA47-44B2-91B6-64A78439B037}");
// ChickenNotificationBus interface
void OnChickenSpeedChanged(float s) override;
private:
AZ::EntityId m_actor;
};
} // namespace MyGem
namespace MyGem
223
Introduction to Animation
{
void ChickenAnimationComponent::Reflect(AZ::ReflectContext* rc)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<ChickenAnimationComponent, AZ::Component>()
->Field("Actor", &ChickenAnimationComponent::m_actor)
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<ChickenAnimationComponent>(
"Chicken Animation",
"[Player controlled chicken]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"))
->DataElement(nullptr,
&ChickenAnimationComponent::m_actor,
"Actor", "Entity with chicken actor.");
}
}
}
void ChickenAnimationComponent::Activate()
{
ChickenNotificationBus::Handler::BusConnect(GetEntityId());
}
void ChickenAnimationComponent::Deactivate()
{
ChickenNotificationBus::Handler::BusDisconnect();
}
void ChickenAnimationComponent::OnChickenSpeedChanged(float s)
{
using namespace EMotionFX::Integration;
AnimGraphComponentRequestBus::Event(m_actor,
&AnimGraphComponentRequests::SetNamedParameterFloat,
"Speed", s);
}
} // namespace MyGem
224
Chapter 26. Animation State Machine
Introduction
Note
The level and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch26_animation_statemachine
Building on Chapter 25, Introduction to Animation, we are going to add a chicken animation to celebrate
scoring a goal using chicken flapping motion. This will involve building a state machine inside Animation
Editor, defining transitions and parameter actions.
2. Go to Motions Sets.
225
Animation State Machine
3. There is only one blend tree in the animation graph. Rename it to Playing BlendTree, so that we can
distinguish it from the second new blend tree node that we are about to add.
Note
When we were blending idle and walking motion we used a Blend Two node. However, for
celebration motion we do not need to do any blending. We just want the chicken to play the
flapping motion.
9. With the motion node selected, in Attributes panel, under Motions, assign chickenflapping
motion from C:\O3DE\21.11.2\Gems\NvCloth\Assets\Objects\cloth\Chick-
en\Motions.
10. Connect Output Pose of Celebration Motion with Input Pose of FinalNode1.
226
Animation State Machine
12. Left click and drag an arrow from Playing Blend Tree to Celebration Blend Tree.
Tip
In order to draw an arrow, left click closer to the edge of a node away from its name. If you hover
your mouse over the node and slowly move it up, you will see the mouse pointer change its look
that will indicate when the location will select the node or start creating an arrow.
Note
We are now going to add a condition when one blend tree will transition to the other one.
14. With the arrow selected, in Attributes panel, choose Add condition and then Parameter Condition.
227
Animation State Machine
Note
A boolean value of "true" is treated as the value of one (1.0) by Animation Editor in parameter
conditions.
18. That creates a condition to transition from "Playing Blend Tree" to "Celebration Blend Tree" when
Celebrating parameter is set to true.
Note
Next step is to define when we exit the celebration state and reset Celebrating parameter back
to false.
19. Left click and drag an arrow from Celebration Blend Tree to Playing Blend Tree.
21. In Attributes panel, select Add condition, choose State Condition. This will add State Condition to
the Attributes panel.
22. Under State, user Select node button to choose Celebration Motion node.
Note
The units for Play Time is seconds. If you take a look at "chicken flapping" motion under Motions
tab, you will find that it is 2.47 seconds long. However, it has some idle time at the end of it, so
I chose to transition away before the end of the motion.
25. Now choose Add action in the same Attributes panel with the arrow still selected.
228
Animation State Machine
29. For Trigger Value, keep 0.0. This creates an action of setting Celebration parameter value to zero
(false for boolean parameters) when the selected transition occurs.
With these changes, we now have a state machine that switches between celebrating motion and blending
between idle and walking motion.
You can test this logic by setting Celebrating parameter to enabled and then watch chicken celebration
motion play for two seconds and then switch back to Player Blend Tree while setting Celebrating value
back to false.
Once you save the changes in the animation graph, Anim Graph component will update the list of para-
meters to reveal Celebrating boolean parameter.
229
Animation State Machine
Summary
In this chapter, we enhanced the animation graph to add celebration motion and tested it in the Animation
Editor. We know it works but there is no game logic to trigger it. We can do it by adding logic to a C++
component as we did in the previous chapter. However, we have already done that before, so in the next
chapter I will show you how to do it using Script Canvas, the visual scripting in O3DE.
Note
The assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch26_animation_statemachine
230
Chapter 27. Script Canvas
Introduction
Note
The assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch27_scriptcanvas
Our goal for this chapter is to have the chicken celebrate each goal with a flapping motion. We created
the animation graph to support that animation. We need to set 'Celebrating' animation parameter to true
whenever a goal is scored. We could do it with C++ but this is a good opportunity to show you how to
do the same work using visual scripting.
Script Canvas is a visual scripting tool in O3DE. For example, here is a visual node that is equivalent to
the C++ method of SetNamedParameterBool, a similar method to what we used in earlier chapters.
void GoalDetectorComponent::UpdateUi()
{
UiScoreNotificationBus::Broadcast(
&UiScoreNotificationBus::Events::OnTeamScored, m_team);
}
We need to sign up to receive this event and set Celebration parameter on Anim Graph component to true.
Behavior Context
In order for Script Canvas to provide you with a node for a new custom notification EBus, we have to
reflect that EBus to Behavior Context. Here are the steps to reflect a notification bus.
231
Script Canvas
class UiScoreNotifications
: public AZ::ComponentBus
{
public:
virtual void OnTeamScored(int team) = 0;
};
class ScoreNotificationHandler
3. Inherit from the EBus you are looking to reflect to scripting and from AZ::BehaviorEBusHan-
dler.
class ScoreNotificationHandler
: public UiScoreNotificationBus::Handler
, public AZ::BehaviorEBusHandler
4. List all the methods you are looking to reflect with AZ_EBUS_BEHAVIOR_BINDER macro. In this
case, there is only OnTeamScored.
AZ_EBUS_BEHAVIOR_BINDER(ScoreNotificationHandler,
"{33B5BC25-622B-4DF0-92CF-987CC6108C31}",
AZ::SystemAllocator,
OnTeamScored);
Tip
If there were more methods to reflect you would list them after one at a time at the end of the
macro.
AZ_EBUS_BEHAVIOR_BINDER(ScoreNotificationHandler,
"{33B5BC25-622B-4DF0-92CF-987CC6108C31}",
AZ::SystemAllocator,
Method1,
Method2,
Method3);
5. Override each method to call FN_<method name> methods that are generated by AZ_EBUS_BE-
HAVIOR_BINDER. For example, there is FN_OnTeamScored for OnTeamScored .
6. Register this handler with BehaviorContext. It can be done in any Reflect method. For ex-
ample, in GoalDetectorComponent. It is a good spot, since it invokes an event on it. Another
good option is to put it inside a system component of your gem or project.
232
Script Canvas
//...
if (auto bc = azrtti_cast<AZ::BehaviorContext*>(rc))
{
bc->EBus<UiScoreNotificationBus>("ScoreNotificationBus")
->Handler<ScoreNotificationHandler>();
}
}
Tip
If you are reflecting a method to call from Script Canvas into C++, it takes just a few lines of code
in C++. For example, here is the reflection for SetNamedParameterBool that we saw in the
beginning of this chapter.
behaviorContext->EBus<AnimGraphComponentRequestBus>(
"AnimGraphComponentRequestBus")
>Event("SetNamedParameterBool",
&AnimGraphComponentRequestBus::Events::SetNamedParameterBool)
Building a Canvas
Once the project is compiled, you can re-open the Editor and create a new script canvas. Here are the steps
to set Celebration parameter from Script Canvas.
3. Open Script Canvas editor using Tools → Script Canvas or from the icon on Script Canvas component.
4. Once inside Script Canvas editor, start a new script with File → New Script.
233
Script Canvas
7. Search for "Set Named Parameter Bool" in Node Palette. Drag and drop it into the canvas as well.
8. Connect the execution line from OnTeamScored to the In execution connector on "Set Named
Parameter Bool" node. The notification node will start the execution of the script when an event is
invoked. The execution will be passed to "Set Named Parameter Bool" node. Afterwards the Out
connector will pass the execution to the next node if one is connected.
11. Save the canvas under the project or one of the gems, for example at C:\git\book\MyPro-
ject\scriptcanvas\celebrate.scriptcanvas.
13. On Script Canvas component, assign the new canvas to Script Canvas Source File property.
234
Script Canvas
With this component, anytime a goal is scored the chicken will play celebration flapping motion for two
seconds.
Summary
Note
The source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch27_scriptcanvas
namespace MyGem
{
class UiScoreNotifications
: public AZ::ComponentBus
{
public:
virtual void OnTeamScored(int team) = 0;
};
235
Script Canvas
/// NEW
class ScoreNotificationHandler
: public UiScoreNotificationBus::Handler
, public AZ::BehaviorEBusHandler
{
public:
AZ_EBUS_BEHAVIOR_BINDER(ScoreNotificationHandler,
"{33B5BC25-622B-4DF0-92CF-987CC6108C31}",
AZ::SystemAllocator, OnTeamScored);
Note
You can find documentation on how to reflect various classes, structures and interfaces to Be-
havior Context at: https://docs.o3de.org/docs/user-guide/programming/components/reflection/behav-
ior-context/
236
Part X. Audio Effects (Wwise)
Table of Contents
28. Wwise for O3DE ....................................................................................................... 239
Introduction ............................................................................................................ 239
Installing Wwise ..................................................................................................... 239
Building O3DE Installer with Wwise .......................................................................... 240
Setting up a Wwise Project ....................................................................................... 241
Generating a Sound Bank ......................................................................................... 245
Summary ................................................................................................................ 246
29. Importing Wwise Project ............................................................................................. 247
Import to O3DE ...................................................................................................... 247
Summary ................................................................................................................ 248
30. Introduction to Audio Components ................................................................................ 249
Introduction ............................................................................................................ 249
Audio Trigger Component ........................................................................................ 249
Audio Proxy Component .......................................................................................... 252
Audio Listener Component ....................................................................................... 252
Summary ................................................................................................................ 253
238
Chapter 28. Wwise for O3DE
You'd better wise up, man!
—The Outsiders, 1983
Introduction
Note
Source code for this chapter can be found on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch28_wwise_setup
This chapter will take a look at adding sound effects to our project. O3DE comes with an integration of
Audiokinetic Wwise. However, the installer does not include it, since Wwise is not open-source.
Important
Wwise is not a product of O3DE. It is stand-alone product. See its website for full conditions, licensing
and details.
http://www.audiokinetic.com/pricing
In this chapter we will built a new installation of O3DE with Wwise integration enabled.
Installing Wwise
1. Navigate to https://www.audiokinetic.com/download/
4. Install Wwise 2021 version with "Authoring" and "SDK (C++)" packages.
Note
At the time of writing this book, Wwise 2021.1.7.7796 was available and used.
Important
If you can see environment variable WWISEROOT on the console, then the installation was
successful. You may need to re-open a command console (and maybe even reboot your system)
to see it after the installation.
During CMake configure step, you should see a message such as:
239
Wwise for O3DE
Audiokinetic\Wwise 2021.1.7.7796
You can also test it by adding the following to your gem or project to confirm that CMake sees
Wwise installation.
BUILD_DEPENDENCIES
PUBLIC
3rdParty::Wwise
We are going to build a new local O3DE installer with Wwise gem enabled.
1. Clone the repository of O3DE with tag 2111.2 to match the installer we have used so far in the book.
4. From the build folder, configure CMake. This should pick up Wwise installation.
C:\git\o3de\build> cmake -S .. -B .
Note
This step will take a while, since it builds the entire engine, all the gems and creates a local
installation.
9. Open C:\git\o3de\install\engine.json.
C:\git\o3de\install> .\python\get_python.bat
240
Wwise for O3DE
14. Verify that this engine was registered. You should see the following lines.
{
...
"engines": [
"C:/O3DE/21.11.2",
"C:/git/o3de/install",
...
],
"engines_path": {
"o3de-sdk": "C:/O3DE/21.11.2",
"my-o3de-2111": "C:/git/o3de/install",
...
}
"engine": "my-o3de-2111",
17. Re-run Asset Processor. (It is going to re-process all the assets.)
Tip
Avoid starting the Editor before all the assets are processed. Crashes may occur before all the
critical assets are ready.
Initially, you would not have any Wwise projects, so you will need to create a new one.
241
Wwise for O3DE
• Location: C:\git\book\MyProject\Sounds\
Important
There are hidden requirements that O3DE places on Wwise projects.
Once you create the project you will greeted with Wwise Project Explorer.
Wwise application
Important
Wwise's job is to generate sound banks for O3DE. However, O3DE expects the banks to be at a
specific location, which is different from the default sound bank path set by Wwise. So before we get
to adding audio files to the project, configure an important project setting that is vital to get audio
working with O3DE. From the main menu, select Project→Project Settings...
242
Wwise for O3DE
Create a new audio element under Actor-Mixer Hierarchy. Right click on Default Work Unit→ New
Child→ Sound SFX.
Sound SFX is the simplest way to get started with adding audio to a Wwise project. It represents a single
audio effect. Rename new sound sfx to Cheer. The next step is to assign it an audio asset by right clicking
on Cheer→Import Audio Files.
The importer dialog will show up. Select Add Files... and navigate to applause.wav at C:\git\book
\MyProject\Assets\Source Audio\. Click on Import in Audio File Importer dialog.
243
Wwise for O3DE
That will assign the wave file to Cheer. You should see that in Contents Editor.
Tip
You can press SPACE while Cheer is selected to test if the sound is the one you wanted and that it
actually plays, at least inside Wwise application.
Personally, I found that the applause sound effect was too loud for my taste. So I modified the volume of
this sound element to -8 in Sound Property Editor.
244
Wwise for O3DE
Each audio object in Wwise should two events: play and stop. O3DE will manipulate the object through
these events. You have to explicitly create them first in Wwise by right clicking on Cheer→ New Event→
Play.
"Stop" event requires a similar procedure: Cheer→ New Event→ Stop→ Stop. Now, under Event Viewer
with Cheer selected, you should see new events.
2. Under SoundBankManager, click New... This will create a new sound bank for us.
4. From Event Viewer, drag and drop Play_Cheer and Stop_Cheer events into Main sound bank in
SoundBank Manager.
5. SoundBank Editor window should now list under the events under Hierarchy Inclusion.
245
Wwise for O3DE
Summary
One last step is to enable AudioSystem and AudioEngineWwise gems in C:\git\book\MyPro-
ject\Code\enabled_gems.cmake
set(ENABLED_GEMS
...
AudioSystem
AudioEngineWwise
)
Important
You should see Main.bnk inside C:\git\book\MyProject\Sounds\wwise folder. It
should have been generated by Wwise project.
Note
Source code for this chapter can be found on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch28_wwise_setup
The next step is to import these assets into O3DE. The next chapter will show you how to do that.
246
Chapter 29. Importing Wwise Project
Import to O3DE
Note
You can find the assets for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch29_wwise_import
The sound banks generated by Wwise in the previous chapter are not immediately usable in O3DE. One
has to create sound controls for them. They have to be manually created in via Audio Controls Editor from
the main menu: Tools→ Other→ Audio Controls Editor.
247
Importing Wwise Project
Summary
We went over how to add sound effects to Wwise project and import them to O3DE projects. We are ready
to start controlling and playing them in the game.
Note
You can find the assets for this chapter on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch29_wwise_import
248
Chapter 30. Introduction to Audio
Components
Introduction
So far we created a Wwise project, added a cheer sound effect and imported sound events that control it
into the project. This chapter will take a look at playing sounds. We will need to do the following:
• Add audio components: an Audio Trigger, an Audio Proxy and, optionally, an Audio Listener.
• Play "cheer" sound effect using Script Canvas when a goal is scored.
Note
You can find the code and the assets for this chapter on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch30_audio_components
249
Introduction to Audio Components
Default 'play' and 'stop' triggers are the control events. For our cheering sound effect they need to be
Play_Cheer and Stop_Cheer events that we added in Audio Controls Editor.
Once both triggers are assigned, we are done with configuring the Audio Trigger component.
Audio Trigger components can be controlled via its EBus with AudioTriggerComponentRequest-
Bus. In Script Canvas you can invoke the trigger by connecting "On Team Scored" node with Audio Play
node, in a similar way we used it in Chapter 27, Script Canvas.
250
Introduction to Audio Components
Now every time a goal is scored, the cheer sound effect will play.
AudioTriggerComponentRequestBus
We can also play a sound effect from C++ using EBus AudioTriggerComponentRequestBus.
You can find it at LmbrCentral\Audio\AudioTriggerComponentBus.h. Here is the interest-
ing part for this chapter.
class AudioTriggerComponentRequests
: public AZ::ComponentBus
{
public:
//! Executes the play trigger if set.
virtual void Play() = 0;
251
Introduction to Audio Components
So if you had a C++ component on the entity with an Audio Trigger component, then you could call it
this way:
AudioTriggerComponentRequestBus::Event(GetEntityId(),
&AudioTriggerComponentRequestBus::Events::Play);
While Audio Trigger component controls playing a sound effect, Audio Proxy component is the sound
effect holder. It determines the sound location among other things. The sound effect location follows
the entity's position by default. Audio Proxy's EBus is AudioProxyComponentRequestBus from
"LmbrCentral" gem at LmbrCentral\Audio\AudioProxyComponentBus.h. For example, you
can set the position manually or change movement following behavior.
class AudioProxyComponentRequests
: public AZ::ComponentBus
{
public:
//...
If you do not specify "Rotation Entity" and "Position Entity" on Audio Listener component, the entity's
position will be used as the location of the audio listener. Or you can specify Chicken entity.
252
Introduction to Audio Components
Summary
There was no C++ code in this chapter, since we used Script Canvas to connect "On Team Scored" noti-
fication event with playing a sound effect.
Note
You can find the code and the assets for this chapter on GitHub:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch30_audio_components
253
Part XI. Multiplayer
You have made it! Welcome to multiplayer section of the book! In preceding chapters we created a single player game
where a single chicken can kick the ball around and score goals on either side of the field. In this section, we will
convert the game to multiplayer where each player can control a chicken from a different team.
O3DE Multiplayer is a server-authoritative system with support for local prediction of player's input. We will
write our game logic in such a way that the server determines what goes in the game, while keeping player controls
responsive. There is a lot to go through but I will break up the material into small consumable chapters.
Table of Contents
31. Setting Up Multiplayer ................................................................................................ 257
Introduction ............................................................................................................ 257
Enabling Multiplayer Code Generation ........................................................................ 257
Building Installer from Development Branch of O3DE ................................................... 260
Summary ................................................................................................................ 262
32. Auto Components ....................................................................................................... 263
Introduction ............................................................................................................ 263
Code Generation ..................................................................................................... 263
Components and Controllers ...................................................................................... 264
Summary ................................................................................................................ 270
33. Multiplayer in the Editor ............................................................................................. 271
Introduction ............................................................................................................ 271
Editor Server .......................................................................................................... 271
Multiplayer Components in the Level ......................................................................... 272
Summary ................................................................................................................ 273
34. Simple Player Spawner ............................................................................................... 274
Introduction ............................................................................................................ 274
Design ................................................................................................................... 274
Summary ................................................................................................................ 279
35. Multiplayer Input Controls ........................................................................................... 282
Introduction ............................................................................................................ 282
Chicken Movement Component ................................................................................. 282
Create Input ............................................................................................................ 284
Process Input .......................................................................................................... 287
Summary ................................................................................................................ 290
36. Multiplayer Physics .................................................................................................... 296
Introduction ............................................................................................................ 296
Network Ball .......................................................................................................... 296
Network Property .................................................................................................... 296
Multiplayer Goal Detector ......................................................................................... 297
Summary ................................................................................................................ 300
37. Removal and Spawning ............................................................................................... 304
Introduction ............................................................................................................ 304
Ball Component ...................................................................................................... 305
Ball Spawner Component ......................................................................................... 305
Entity Changes ........................................................................................................ 308
Summary ................................................................................................................ 308
38. Multiplayer Animation ................................................................................................ 313
Introduction ............................................................................................................ 313
Changes to Chicken Movement Component ................................................................. 313
Chicken Animation Component ................................................................................. 314
Entity Changes ........................................................................................................ 315
Summary ................................................................................................................ 316
39. Team Spawner ........................................................................................................... 319
Introduction ............................................................................................................ 319
Adding Team Value ................................................................................................. 320
Adding More Chickens ............................................................................................. 320
Changes to Chicken Component ................................................................................ 322
Changes to Chicken Spawn Component ...................................................................... 323
Summary ................................................................................................................ 323
40. Multiplayer Camera .................................................................................................... 327
255
Multiplayer
256
Chapter 31. Setting Up Multiplayer
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch31_enable_multiplayer_gem
Our project has Multiplayer gem enabled, however, more setup is required before we can start writing
multiplayer components.
In O3DE writing multiplayer components involves code generation, so we have to add code generation
support to gems and projects that will be generating and compiling multiplayer components.
Important
For any gems that include multiplayer components, the same steps will be required as well.
set(FILES
${AUTOPATH}/AutoComponent_Common.jinja
${AUTOPATH}/AutoComponent_Header.jinja
${AUTOPATH}/AutoComponent_Source.jinja
${AUTOPATH}/AutoComponentTypes_Header.jinja
${AUTOPATH}/AutoComponentTypes_Source.jinja
)
ly_add_target(
NAME MyGem.Static STATIC
FILES_CMAKE
257
Setting Up Multiplayer
mygem_autogen_files.cmake
...
ly_add_target(
NAME MyGem.Static STATIC
...
AUTOGEN_RULES
*.AutoComponent.xml,AutoComponent_Header.jinja,
$path/$fileprefix.AutoComponent.h
*.AutoComponent.xml,AutoComponent_Source.jinja,
$path/$fileprefix.AutoComponent.cpp
*.AutoComponent.xml,AutoComponentTypes_Header.jinja,
$path/AutoComponentTypes.h
*.AutoComponent.xml,AutoComponentTypes_Source.jinja,
$path/AutoComponentTypes.cpp
)
Important
Each line of AUTOGEN_RULES should have no line breaks in it. The book format forces line
breaks but this portion should actually look like this in your CMakeLists.txt:
AUTOGEN_RULES
*.AutoComponent.xml,Auto...ileprefix.AutoComponent.h
*.AutoComponent.xml,Auto...ileprefix.AutoComponent.cpp
*.AutoComponent.xml,Auto...ComponentTypes.h
*.AutoComponent.xml,Auto...ComponentTypes.cpp
ly_add_target(
NAME MyGem.Static STATIC
BUILD_DEPENDENCIES
PUBLIC
# For autogen components
Gem::Multiplayer.Static
...
5. At this point, CMake build target for MyGem.static should look as follows.
258
Setting Up Multiplayer
PRIVATE
Source
BUILD_DEPENDENCIES
PUBLIC
AZ::AzCore
AZ::AzFramework
Gem::StartingPointInput.Static
Gem::LyShine.Static
Gem::PhysX.Static
Gem::EMotionFXStaticLib
# For autogen components
Gem::Multiplayer.Static
AUTOGEN_RULES
*.AutoComponent.xml,Au...refix.AutoComponent.h
*.AutoComponent.xml,Au...refix.AutoComponent.cpp
*.AutoComponent.xml,Au...ComponentTypes.h
*.AutoComponent.xml,Au...ComponentTypes.cpp
)
6. Add multiplayer component descriptors to the gem. You need one such call for all multiplayer com-
ponents in a gem.
#include <Source/AutoGen/AutoComponentTypes.h>
MyGemModuleInterface()
{
m_descriptors.insert(m_descriptors.end(), {
...
});
...
//< Register multiplayer components
CreateComponentDescriptors(m_descriptors);
}
Note
Some gems do not have a ModuleInterface and instead register component descriptors inside
their Module constructors. Either way, you should add CreateComponentDescriptors to
where you add component descriptors to AZ::Module's m_descriptors.
#include <Source/AutoGen/AutoComponentTypes.h>
void MyGemSystemComponent::Activate()
{
// Register multiplayer components
RegisterMultiplayerComponents();
}
259
Setting Up Multiplayer
<?xml version="1.0"?>
<Component
Name="MyFirstNetworkComponent"
Namespace="MyGem"
OverrideComponent="false"
OverrideController="false"
OverrideInclude=""
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</Component>
set(FILES
...
Source/AutoGen/MyFirstNetworkComponent.AutoComponent.xml # new
)
10. Re-compile the gem and the project. If you see a CMake log line similar to the following snippet, you
have successfully configured the gem to support multiplayer components.
2>Generating C:\git\book\build\External\MyGem-409da4e6\
Code\Azcg\Generated\Source\AutoGen\
MyFirstNetworkComponent.AutoComponent.h using
template C:/git/o3de/install/Gems/Multiplayer/
Code/Include/Multiplayer/AutoGen/AutoComponent_Header.jinja
and inputs C:\git\book\Gems\MyGem\Code\Source\
AutoGen\MyFirstNetworkComponent.AutoComponent.xml
In order to get the best experience with multiplayer features of O3DE, we have to get features that were
implemented since the release of O3DE Stable 21.11.2. Here are the steps to create a new installer from
scratch from https://github.com/o3de/o3de/tree/development.
Tip
You can get the latest nightly pre-built installer online at: https://o3debinaries.org/download/win-
dows.html, look for "Nightly Development Build."
260
Setting Up Multiplayer
Note
At the time of writing this book, I tested the rest of the book with development branch at commit
b4b7e5a from March 26th, 2022. You can reset to that point with the following git command.
C:\git\o3de\build> cmake -S .. -B .
Note
This step will take a while, since it builds the entire engine and all the gems.
8. Open C:\git\o3de\install\engine.json.
C:\git\o3de\install> .\python\get_python.bat
13. Verify that this engine was registered. You should see the following lines.
{
...
"engines": [
"C:/O3DE/21.11.2",
"C:/git/o3de/install",
...
],
"engines_path": {
"o3de-sdk": "C:/O3DE/21.11.2",
"o3de-install": "C:/git/o3de/install",
...
}
261
Setting Up Multiplayer
"engine": "o3de-install"
C:\git\o3de\install\bin\Windows\profile\Default\AssetProcessor.exe
--project-path C:\git\book\MyProject\
Now we can use the latest and greatest features of multiplayer in O3DE!
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch31_enable_multiplayer_gem
We have enabled code generation of multiplayer components for MyGem. Now we are free to start looking
at what is the format of *.AutoComponent.xml files, what they generate and what you can do with
them in the next chapters.
262
Chapter 32. Auto Components
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch32_auto_components
In O3DE multiplayer components are code generated from their XML definitions, such as MyFirst-
NetworkComponent.AutoComponent.xml in the previous chapter. An auto-component is a code
generated multiplayer component. This chapter will cover enough theory of auto-components to get us
through the rest of multiplayer chapters.
• What is a Controller?
Note
In this section of the book, multiplayer and network terms are used interchangeably.
Code Generation
Example 32.1. An auto-component
<?xml version="1.0"?>
<Component
Name="MyFirstNetworkComponent"
Namespace="MyGem"
OverrideComponent="false"
OverrideController="false"
OverrideInclude=""
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</Component>
Given the above XML definition the following files will be produced for you:
• MyFirstNetworkComponent.AutoComponent.h
• MyFirstNetworkComponent.AutoComponent.cpp
These files are placed inside the build code-generation section, such as C:\git\book\build\Ex-
ternal\MyGem-409da4e6\ Code\Azcg\Generated\Source\AutoGen\MyFirstNet-
workComponent.AutoComponent.h.
263
Auto Components
Tip
Your specific build path might be different but if you search within your build folder for these com-
ponents you will find them.
In general, you should not pay too much attention to these files but you should know that they provide the
backbone of multiplayer functionality of your components as well as various Editor and script reflection.
Code generated headers (*.AutoComponent.h) declare two important classes: a Component and a
Controller.
Note
There is one exception for autonomous entities, such as player entities, that need to be able to locally
predict themselves. In such a case, there is a controller on a client as well but only for the player entity
that the client controls. I cover this unique case in Chapter 40, Multiplayer Camera.
Where do these controllers and components come from? They are created by the code generator we
have configured in previous chapter. An XML definition is the root source that defines the data and basic
structure of a multiplayer component.
264
Auto Components
OverrideController="true"
If you wish to provide client-specific logic, then override the component with:
OverrideComponent="true"
Here is example of a component that overrides both the component and the controller.
<?xml version="1.0"?>
<Component
Name="MyFirstNetworkComponent"
Namespace="MyGem"
OverrideComponent="true"
OverrideController="true"
OverrideInclude="Source/MyFirstNetworkComponent.h"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</Component>
265
Auto Components
Here are the steps on writing network component code once you have an XML definition.
1. Create an XML definition that overrides either the component or the controller or both.
OverrideController="true"
2. Specify OverrideInclude with the path to the header file where your own game logic will be written.
This path is relative to the gem's location at C:\git\book\Gems\MyGem\Code\Source.
OverrideInclude="Multiplayer\MyFirstNetworkComponent.h"
Note
Adding Multiplayer folder is optional. You can create your own folder structure or no folder
at all and place your source code directly under Source, in which case OverrideInclude would
be:
OverrideInclude="MyFirstNetworkComponent.h"
Note
If neither a component nor a controller was overridden then you can skip the rest of the steps. The
component will appear in the Editor but will not be particularly useful, since it will not possess
any game logic.
6. You will get a lot of build errors but look for the errors that mention MyFirstNetworkCompo-
nent.AutoComponent.h. In Visual Studio you can double click on the error and jump to the
generated header. Or navigate there by looking up the path in the build log.
7. Open MyFirstNetworkComponent.AutoComponent.h.
8. Look for the first big comment block at the top of the header file. The comment block with start with
"You may use the classes below as a basis for your new derived classes." The rest of the comment
block will provide you with stubs for the header and source file.
266
Auto Components
9. Copy the code from "/// Place in your .h" to "/// Place in your .cpp" into
MyFirstNetworkComponent.h.
namespace MyGem
{
class MyFirstNetworkComponentController
: public MyFirstNetworkComponentControllerBase
{
public:
MyFirstNetworkComponentController(
MyFirstNetworkComponent& parent);
protected:
};
}
namespace MyGem
{
MyFirstNetworkComponentController::
MyFirstNetworkComponentController(MyFirstNetworkComponent& p)
: MyFirstNetworkComponentControllerBase(p)
{
}
void MyFirstNetworkComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
}
void MyFirstNetworkComponentController::OnDeactivate(
267
Auto Components
Multiplayer::EntityIsMigrating)
{
}
}
Note
There is a reference here to MyFirstNetworkComponent. This component was generated for
us by Multiplayer gem code generator. This component is already reflected and registered with the
engine.
Even though the code above does not look like much, it comes with a lot power underneath by deriving
from MyFirstNetworkComponentControllerBase. For example, the controller can at any time
access its component by GetParent() or get the entity pointer the controller is on with GetEntity().
We will explore multiplayer functionality of these classes when we start writing multiplayer game logic
in the next chapters.
Overriding Components
Using the same steps as with controllers, you can also override the component to perform various logic
that does not require write access to the component's data.
Tip
An example of such logic is receiving a notification when a goal was scored on the server in order to
update client's UI we created in Chapter 23, Interacting with UI in C++.
2. Build MyGem.Static.
3. There will be build errors, since we did not provide the component in MyFirstNetworkCompo-
nent but by visiting MyFirstNetworkComponent.AutoComponent.h in the build folder,
you will find that the stub has been updated to include both the controller and the component for
you to start with.
268
Auto Components
#include <Source/AutoGen/MyFirstNetworkComponent.AutoComponent.h>
namespace MyGem
{
class MyFirstNetworkComponent
: public MyFirstNetworkComponentBase
{
public:
AZ_MULTIPLAYER_COMPONENT(MyGem::MyFirstNetworkComponent,
s_myFirstNetworkComponentConcreteUuid,
MyGem::MyFirstNetworkComponentBase);
protected:
};
...
}
namespace MyGem
{
void MyFirstNetworkComponent::Reflect(AZ::ReflectContext* rc)
{
auto sc = azrtti_cast<AZ::SerializeContext*>(rc);
if (sc)
{
sc->Class<MyFirstNetworkComponent,
MyFirstNetworkComponentBase>()
->Version(1);
}
MyFirstNetworkComponentBase::Reflect(rc);
}
void MyFirstNetworkComponent::OnInit()
{
}
void MyFirstNetworkComponent::OnActivate(
Multiplayer::EntityIsMigrating)
{
269
Auto Components
void MyFirstNetworkComponent::OnDeactivate(
Multiplayer::EntityIsMigrating)
{
}
...
}
Reflect is a much simpler method in auto-components because all the reflection is done by code gen-
erated base classes, which can handle reflecting properties to the Editor and to scripting context.
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch32_auto_components
We have gone over enough aspects of network components to get started with writing our first multiplayer
game logic. The important ideas to remember are the following:
270
Chapter 33. Multiplayer in the Editor
Introduction
Note
The accompanying assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch33_multiplayer_in_editor
There are two ways to iterate on multiplayer work, either by launching game launchers directly or using the
Editor. MyProject.ServerLauncher with "+host +loadlevel MyLevel" parameters starts the server.
MyProject.GameLauncher with "+connect" parameter starts and connects a client to the local server.
Using the Editor is far easier and faster. This chapter will present the way to use the Editor in the most
efficient and fastest method to iterate on a multiplayer game in O3DE.
Editor Server
By default, when you press CTRL+G in the Editor multiplayer support is disabled. To enable the multi-
player mode, you have to set a CVar variable editorsv_enabled to true. By default, it is set to false, so
in order to avoid having to enable it in the Editor console each time you launch the Editor, we can create
a console command file that is loaded automatically by the Editor on launch: C:\git\book\MyPro-
ject\editor.cfg.
editorsv_enabled enables the editor server, editorsv_hidden launches the server without a window in
the background and editorsv_rhi_override as "null" disables the rendering sub-system. (RHI stands for
rendering hardware interface.) Together these are the fastest settings for you to iterate on multiplayer.
Tip
If you need to see the server window while working with the Editor, then set editorsv_rhi_override
to either "dx12" or "vulkan", depending on your platform, or leave it blank and have it be picked by
the Atom renderer.
5. In a few seconds the Editor will connect as a client to the server in the background.
271
Multiplayer in the Editor
Important
All multiplayer components require Net Binding component.
Important
Multiplayer components must not be placed directly into the level. They must be wrapped with a
prefab, and then the prefab can be placed into the level.
For example, we can create our first multiplayer entity with Transform, Network Binding, My First Net-
work Component, Network Transform Component and a Mesh component.
Important
Without Network Transform Component, the entity will not appear on clients.
272
Multiplayer in the Editor
This temporarily adds a white box in the middle of our level (Multiplayer_Test.prefab) and when
I enter game mode with multiplayer, the white box immediately disappears. This happens because a level
prefab is split into two portions: regular and network portion. Regular portion can be spawned immediately
but then network portion has to wait until the Editor connects as a client to the server. Then the server will
tell the client about all the network entities on the level.
Meanwhile you can confirm that this entity does spawn in the server by looking at the Editor console where
it prints the debug lines we added in the previous chapter.
MyGem::MyFirstNetworkComponentController::OnActivate:
we are on entity Multiplayer Test
Summary
Note
The accompanying assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch33_multiplayer_in_editor
In this chapter we learned how to play a multiplayer game in the Editor and how to place multiplayer
entities in the level using prefabs. However, at the moment, none of multiplayer entities appear when
entering game mode because our client setup is missing a critical piece without which a network connection
between the server and the Editor cannot be established.
We are missing a player object. Once we tell the multiplayer system which entity is our player entity,
the connection will be properly established and the development can begin. The next chapter will tackle
creation of the simplest player spawner that I could come up with.
273
Chapter 34. Simple Player Spawner
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch34_player_spawner
Gem Multiplayer expects us to assign a player entity for each incoming connection on the server.
Multiplayer::NetworkEntityHandle OnPlayerJoin(
uint64_t userId,
const Multiplayer::MultiplayerAgentDatum& agentDatum)
NetworkEntityHandle(AZ::Entity*)
Tip
Even though the name of the interface is a multiplayer spawner there is nothing in its API that forces
our design choice of how to create our player entities. We can certainly spawn on demand, however,
in this chapter I am going to keep it simple by pre-creating the player entity as a prefab on the level
and when the Editor joins as a client, I will grab that prefab and return the entity from it.
Design
I am going to create two new components: Chicken Spawn Component and Chicken Component. Chicken
Spawn Component will be responsible for inheriting from IMultiplayerSpawner and implementing On-
274
Simple Player Spawner
PlayerJoin. Chicken Component will be added to the main chicken entity and will report itself to
Chicken Spawn Component on activation on the server. Chicken Spawn Component will then assign this
entity to the player connection.
namespace MyGem
{
class ChickenNotifications
: public AZ::ComponentBus
{
public:
...
virtual void OnChickenCreated(
[[maybe_unused]] AZ::Entity* e) {}
};
When a chicken's prefab is spawned in the level, the chicken's entity will call OnChickenCreated that
Chicken Spawner component will be expecting.
Chicken Component
This is going to be our first multiplayer controller implementation. We want the chicken component to
register itself with Chicken Spawn Component only on the server as clients are not involved in player
entity creation. We are server-authoritative after all!
275
Simple Player Spawner
The first step is to create the XML definition of an auto-component that overrides the controller.
Next we follow the steps from Chapter 32, Auto Components to create ChickenComponent.h and
ChickenComponent.cpp.
namespace MyGem
{
class ChickenComponentController
: public ChickenComponentControllerBase
{
public:
ChickenComponentController(ChickenComponent& parent);
namespace MyGem
{
ChickenComponentController::
ChickenComponentController(ChickenComponent& p)
: ChickenComponentControllerBase(p) {}
void ChickenComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
if (!IsAuthority()) return;
ChickenNotificationBus::Broadcast(
&ChickenNotificationBus::Events::OnChickenCreated,
GetEntity());
276
Simple Player Spawner
}
}
What is IsAuthority? This is a method from the base class MultiplayerController that is
only true when the controller is on the authoritative server.
If the controller has write access to the component, then it is considered to have an authority over it. As
a quick side, there are only two options for a controller. A controller can either have an authority, if it is
on the server, or it can be an autonomous controller on the client that controls the chicken and can locally
predict the movement of the chicken. Otherwise, no controller would be created for a component. We will
tackle autonomous controllers when we implement multiplayer chicken movement.
Tip
As you can see writing network components takes a lot less lines of code then a fully written
AZ::Component. A lot of boilerplate is handled by the code generator.
Chicken component goes on the root entity of the chicken structure we have in the level.
class ChickenSpawnComponent
277
Simple Player Spawner
: public AZ::Component
, public Multiplayer::IMultiplayerSpawner
, public ChickenNotificationBus::Handler
{
public:
...
// ChickenNotificationBus
void OnChickenCreated(AZ::Entity* e) override;
// IMultiplayerSpawner overrides
Multiplayer::NetworkEntityHandle OnPlayerJoin(
uint64_t userId,
const Multiplayer::MultiplayerAgentDatum&) override;
...
private:
AZ::Entity* m_chicken = nullptr;
void ChickenSpawnComponent::OnChickenCreated(AZ::Entity* e)
{
m_chicken = e;
}
NetworkEntityHandle ChickenSpawnComponent::OnPlayerJoin(
[[maybe_unused]] uint64_t userId,
const Multiplayer::MultiplayerAgentDatum&)
{
return NetworkEntityHandle{ m_chicken };
}
Chicken Spawn component is not a multiplayer component, so it can be placed directly on the level, for
example under a new entity, Chicken Spawn.
1. For each chicken entity, add Net Binding and Network Transform components.
Important
If you do not add both of these components, then those entities will not show up on clients at
all. Net Binding component marks the entity as a network entity. Network Transform component
specifies where to spawn the entity on clients and will update client position if the server moves
the entity.
2. Add Local Prediction Player Input Component to the parent entity, Chicken. In the next chapter, it
will be used to process player's input.
278
Simple Player Spawner
3. Select all the chicken entities and create a prefab out of them.
4. For better organization, I moved the prefab under Chicken Spawn entity.
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch34_player_spawner
With these changes, when you press CTRL+G, you will finally see your camera follow a chicken. Also
you will see multiplayer entities show up when the Editor connects to the server.
Note
It takes a few seconds for the server to start and the Editor to connect. If you can see your camera
switch to follow a chicken, then everything is working as intended. Server waits for a client to join
and then assigns it an entity to control that is specified by Chicken Spawn component.
279
Simple Player Spawner
Important
The implementation in this chapter is very limited. It supports only one player on the server. In Chap-
ter 39, Team Spawner, we will enhance Chicken Spawn Component to support multiple clients and
have them join on different teams.
namespace MyGem
{
class ChickenSpawnComponent
: public AZ::Component
, public Multiplayer::IMultiplayerSpawner
, public ChickenNotificationBus::Handler
{
public:
AZ_COMPONENT(ChickenSpawnComponent,
"{814BAF21-10E4-4BE9-8380-C23B0EC27205}");
// ChickenNotificationBus
void OnChickenCreated(AZ::Entity* e) override;
// IMultiplayerSpawner overrides
Multiplayer::NetworkEntityHandle OnPlayerJoin(
uint64_t userId,
const Multiplayer::MultiplayerAgentDatum&) override;
void OnPlayerLeave(
Multiplayer::ConstNetworkEntityHandle entityHandle,
const Multiplayer::ReplicationSet& replicationSet,
AzNetworking::DisconnectReason reason) override {}
private:
AZ::Entity* m_chicken = nullptr;
};
} // namespace MyGem
280
Simple Player Spawner
namespace MyGem
{
using namespace Multiplayer;
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<ChickenSpawnComponent>(
"Chicken Spawn",
"[Player controlled chickens]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"));
}
}
}
void ChickenSpawnComponent::Activate()
{
AZ::Interface<IMultiplayerSpawner>::Register(this);
ChickenNotificationBus::Handler::BusConnect(GetEntityId());
}
void ChickenSpawnComponent::Deactivate()
{
ChickenNotificationBus::Handler::BusDisconnect();
AZ::Interface<IMultiplayerSpawner>::Unregister(this);
}
void ChickenSpawnComponent::OnChickenCreated(AZ::Entity* e)
{
m_chicken = e;
}
NetworkEntityHandle ChickenSpawnComponent::OnPlayerJoin(
[[maybe_unused]] uint64_t userId,
const Multiplayer::MultiplayerAgentDatum&)
{
return NetworkEntityHandle{ m_chicken };
}
} // namespace MyGem
281
Chapter 35. Multiplayer Input Controls
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch35_multiplayer_input
It may appear that our chicken is already moving on its own. After all, it does move in the Editor. However,
if you were to launch a standalone server, you would notice that the chicken entity is only moving on the
client. We are still using single player input component that is directly controlling the position of the entity.
So far the server has no idea that you are issuing input.
Warning
Out of all multiplayer topics, this chapter will be the most complicated as it involves re-writing the
old single player input logic into a system that supports local prediction, correction and server side
roll back.
The good news is that the actual logic is very similar to Chapter 18, Character Movement. It just needs
to be placed under different methods. As I go through in this chapter, I will present single player and
multiplayer implementations side by side.
XML Definition
XML definition of ChickenMovementComponent will have a number of new elements. This section
will cover each new type:
• Component Relation
282
Multiplayer Input Controls
• Archetype Property
• Network Input
Component Relation
A multiplayer controller can declare a dependency on another component's controller on the same enti-
ty. This will code generate a getter method. For example, we will need to use the controller of Network
Character component in order to move our multiplayer chicken. The API for that is NetworkCharacter-
ComponentController::TryMoveWithVelocity. Instead of trying to get the entity, then Network Char-
acter component and then fight the API to get to controller of another entity, we will declare a Component
Relation.
That will allow us to call TryMoveWithVelocity from Chicken Movement Component Controller direct-
ly.
GetNetworkCharacterComponentController()->
TryMoveWithVelocity(m_velocity, deltaTime);
Note
This is similar to providing GetRequiredServices() method on an AZ::Component but with
an extra benefit of receiving a helpful getter method.
Archetype Property
In Chapter 18, Character Movement, ChickenControllerComponent exposed Speed, Turn Speed
and Gravity settings to the Editor. We had to reflect it ourselves.
class ChickenControllerComponent
{
float m_speed = 6.f;
float m_turnSpeed = 0.005.f;
float m_gravity = -9.8f;
//...
->DataElement(nullptr,
&ChickenControllerComponent::m_turnSpeed,
"Turn Speed", "Chicken's turning speed")
/// and so on
With Archetype Property we can save the effort and use code generator instead.
Archetype Property will do all the work of reflecting the details to the Editor for us.
283
Multiplayer Input Controls
Network Input
In Chapter 18, Character Movement, ChickenControllerComponent was responsible for process-
ing player input and was storing it in ChickenInput structure.
class ChickenInput
{
public:
float m_forwardAxis = 0;
float m_strafeAxis = 0;
float m_viewYaw = 0;
};
Note
Reset Count is a special variable that is incremented any time the entity moves in an usual way, such
as teleporting to a new location or stepping on a launch pad. Without this counter, local prediction
will have issues with sudden movement and state changes.
Important
Multiplayer components with a Network Input require Local Prediction Player Input component on
the entity.
Create Input
When you add a Network Input to a multiplayer component, code generator will add two (2) new methods
for you to implement.
284
Multiplayer Input Controls
Creation of input occurs on clients on player owned entities, otherwise known as Autonomous entities.
Their task is to collect player's input and send it to the server.
1. UpdateAutonomous gets called at a specified client input rate. You can control this rate with a
Multiplayer gem variable, cl_InputRateMs. The default is to collect input every 33 milliseconds.
2. NetworkInputArray is a container with the current input and previous seven (7) inputs. Our job is
to collect the current input values. Local Prediction Player Input component will populate older entries
on our behalf from the local input history.
Why are we sending seven (7) older inputs each time? This is because input is sent using an un-
reliable remote procedure call. Here is a snippet from XML definition of LocalPredictionPlayerInput-
Component from Multiplayer gem, where IsReliable is set to false.
285
Multiplayer Input Controls
Multiplayer replication uses UDP1 protocol, which is unreliable by design to achieve faster delivery
and reduce stalls. Multiplayer gem overcomes this by sending multiple inputs. This means we can lose
up to seven (7) inputs and still be able to recover. Given client input rate of 33 milliseconds that allows
for almost a quarter of a second recovery time (231 milliseconds) before the server fails to receive all
input. This is sufficient for majority of real world network scenarios.
3. From Network Input Array, the multiplayer system will grab the first item (at zeroth index) as Net-
workInput and pass it to all the relevant multiplayer components.
4. CreateInput's job is to collect the input for the last input time period.
5. ProcessInput is applied on the client immediately. This is the local prediction step, where the client
guesses where the input will take it. If the server disagrees, the client will be corrected later.
Important
This was a behind-the-scenes look at the input processing logic. Users only need to worry about im-
plementing CreateInput and ProcessInput methods. The rest will be done by the Multiplayer
gem.
Here is side by side comparison between single player create input and multiplayer create input methods.
return input;
}
286
Multiplayer Input Controls
chickenInput->m_forwardAxis = m_forward;
chickenInput->m_strafeAxis = m_strafe;
chickenInput->m_viewYaw = m_yaw;
chickenInput->m_resetCount =
GetNetworkTransformComponentController()->GetResetCount();
}
Overall, the only difference from the single-player input is that we have a special input reset counter to
handle special movement cases but otherwise they are same.
Process Input
Once the input arrives to the server, it will be applied using logic written in ProcessInput.
287
Multiplayer Input Controls
2. There is no creation of an input, since this is happening on the server. ProcessInput gets called for
each relevant multiplayer component that creates input on clients.
3. If a mismatch is found between client and server results, then a correction is sent back to the client using
an unreliable remote procedure from Local Prediction Player Input component.
<RemoteProcedure Name="SendClientInputCorrection"
InvokeFrom="Authority" HandleOn="Autonomous" IsPublic="true"
IsReliable="false" GenerateEventBindings="false"
Description="Autonomous proxy correction RPC">
<Param Type="Multiplayer::ClientInputId" Name="inputId" />
<Param Type="AzNetworking::PacketEncodingBuffer"
Name="correction" />
</RemoteProcedure>
Important
The above work is done for us by Multiplayer gem. As users, we implement ProcessInput and
let the multiplayer system handle local prediction and correction.
For comparison, here are the single-player and multiplayer methods that process player's input.
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
m_velocity);
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
AZ::Vector3::CreateAxisZ(m_gravity));
}
UpdateRotation(chickenInput);
288
Multiplayer Input Controls
UpdateVelocity(chickenInput);
GetNetworkCharacterComponentController()->
TryMoveWithVelocity(m_velocity, deltaTime);
}
UpdateRotation and UpdateVelocity methods did not change. We use different ways to apply
velocity at the end of each method but otherwise the logic is essentially the same.
Note
This is where Reset Count property comes into play. The idea here is as follows. If the movement
count was reset, it means we should not try to locally predict based on input with an old count.
Imagine our character stepped on a jump pad and was now sailing through the air. We do not want
our local prediction to make a mistake of not stepping on the launch pad. It is a type of mistake we
cannot afford with local prediction.
if (chickenInput->m_resetCount !=
GetNetworkTransformComponentController()->GetResetCount())
{
return;
}
This logic will skip over input that is too old and should not be re-applied.
CreateInput collects input into a structure and ProcessInput applies it. Behind the scenes, the
input is stored and sent from the client to the server, where the input is processed using the same logic
of Process Input. In this manner, both the client and the server should arrive to the same result. If there
are any differences, the server will send a correction to the client and the input will be re-applied using
ProcessInput.
It is almost magical how local prediction of player's input is handled for us by the Multiplayer gem via
the generated code.
void ChickenMovementComponentController::ProcessInput(
Multiplayer::NetworkInput& input, float)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
Behind the scenes, any network entity that has components with network inputs gets an associated Mul-
tiplayer::NetworkInput container of component input structures, one for each such multiplayer
component.
These input structures are generated from XML definition of each multiplayer component as needed.
ChickenMovementComponentNetworkInput can be found at ChickenMovementComponent.Au-
toComponent.h.
class ChickenMovementComponentNetworkInput
: public Multiplayer::IMultiplayerComponentInput
289
Multiplayer Input Controls
{
public:
bool Serialize(AzNetworking::ISerializer& serializer);
//...
};
Network Binding and Local Prediction Player Input components work together to create a network input
structure for each of multiplayer component with inputs. These structures are then provided for you in
NetworkInput that you can query with FindComponentInput.
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch35_multiplayer_input
The rest of the component does exactly the same work as ChickenControllerComponent did in
Chapter 18, Character Movement, which is to sign up for events on InputEventNotificationBus to record
key presses and mouse movements.
290
Multiplayer Input Controls
Important
You need to modify Chicken entity to remove the old Chicken Controller component and replace it
with Chicken Movement component.
You should verify that the chicken does move with the server in the same way it moves on the client. Here
are the commands to launch the server and a client.
namespace MyGem
{
const StartingPointInput::InputEventNotificationId
MoveFwdEventId("move forward");
const StartingPointInput::InputEventNotificationId
MoveRightEventId("move right");
const StartingPointInput::InputEventNotificationId
RotateYawEventId("rotate yaw");
class ChickenMovementComponentController
: public ChickenMovementComponentControllerBase
, public StartingPointInput::
InputEventNotificationBus::MultiHandler
{
public:
ChickenMovementComponentController(
ChickenMovementComponent& parent);
291
Multiplayer Input Controls
void ProcessInput(
Multiplayer::NetworkInput& input,
float deltaTime) override;
// AZ::InputEventNotificationBus interface
void OnPressed(float value) override;
void OnReleased(float value) override;
void OnHeld(float value) override;
protected:
void UpdateRotation(
const ChickenMovementComponentNetworkInput* input);
void UpdateVelocity(
const ChickenMovementComponentNetworkInput* input);
float m_forward = 0;
float m_strafe = 0;
float m_yaw = 0;
namespace MyGem
{
using namespace StartingPointInput;
ChickenMovementComponentController::
ChickenMovementComponentController(ChickenMovementComponent& p)
: ChickenMovementComponentControllerBase(p) {}
void ChickenMovementComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
InputEventNotificationBus::MultiHandler::BusConnect(
MoveFwdEventId);
InputEventNotificationBus::MultiHandler::BusConnect(
MoveRightEventId);
InputEventNotificationBus::MultiHandler::BusConnect(
RotateYawEventId);
}
void ChickenMovementComponentController::OnDeactivate(
Multiplayer::EntityIsMigrating)
{
InputEventNotificationBus::MultiHandler::BusDisconnect();
292
Multiplayer Input Controls
void ChickenMovementComponentController::CreateInput(
Multiplayer::NetworkInput& input,
[[maybe_unused]] float deltaTime)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
chickenInput->m_forwardAxis = m_forward;
chickenInput->m_strafeAxis = m_strafe;
chickenInput->m_viewYaw = m_yaw;
chickenInput->m_resetCount =
GetNetworkTransformComponentController()->GetResetCount();
}
void ChickenMovementComponentController::ProcessInput(
Multiplayer::NetworkInput& input,
[[maybe_unused]] float deltaTime)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
if (chickenInput->m_resetCount !=
GetNetworkTransformComponentController()->GetResetCount())
{
return;
}
UpdateRotation(chickenInput);
UpdateVelocity(chickenInput);
GetNetworkCharacterComponentController()->
TryMoveWithVelocity(m_velocity, deltaTime);
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
else if (*inputId == MoveRightEventId)
{
m_strafe = value;
}
else if (*inputId == RotateYawEventId)
293
Multiplayer Input Controls
{
m_yaw = value;
}
}
if (*inputId == RotateYawEventId)
{
m_yaw = value;
}
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
else if (*inputId == MoveRightEventId)
{
m_strafe = value;
}
else if (*inputId == RotateYawEventId)
{
m_yaw = value;
}
}
void ChickenMovementComponentController::UpdateRotation(
const ChickenMovementComponentNetworkInput* input)
{
AZ::TransformInterface* t = GetEntity()->GetTransform();
294
Multiplayer Input Controls
t->SetWorldRotationQuaternion(q);
}
void ChickenMovementComponentController::UpdateVelocity(
const ChickenMovementComponentNetworkInput* input)
{
const float currentHeading = GetEntity()->GetTransform()->
GetWorldRotationQuaternion().GetEulerRadians().GetZ();
295
Chapter 36. Multiplayer Physics
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch36_multiplayer_physics
With multiplayer player movement implemented, the next priority is fixing the ball. It is a rigid body that
does not respect the server. Each instance of a game or the Editor simulates independently and so does
the goal detector.
Our first step is to convert the soccer ball entity into a network rigid body and then to implement serv-
er-authoritative goal detection, while still delivering score updates to client's user interface.
Network Ball
In order to convert the ball into a network rigid body, we have to do the following steps:
3. Add a Network Rigid Body component to let the server drive the simulation while disabling physics
for this entity on clients.
4. Turn the Ball entity into a prefab. Save the prefab as Network_Ball.prefab.
Important
We have to wrap a network entity with a prefab, otherwise the level will fail to load as network entities
are not supported directly in the level.
With these changes the ball has become a server authoritative entity but the goal detector logic is now
broken, since the client is no longer running physical simulation for the ball. Physical shape trigger is no
longer triggering on clients against a ball that has physics disabled by Network Rigid Body component.
The solution is to send score notifications from the server to clients.
Note
Network Rigid Body component still runs physical simulation on the server but disables it for the
same entity on clients. This way client entities follow server simulation.
Network Property
Since we are building a server authoritative game, the server has to decide when a goal is scored. To that
end, we are going to convert our Goal Detector component into a multiplayer component. When a goal
296
Multiplayer Physics
is scored the score needs to be replicated to clients so that they are aware when a goal is scored. That
requires that we add game logic on the server and clients, which means the new Goal Detector multiplayer
component will override both its generated component and its controller.
This is an example of a network property that keeps a game score that the authoritative server can modify
and replicate to clients, as defined by Replicate From and Replicate To fields. We will see it in action in
this chapter as we build a multiplayer goal detector.
Here are the differences between single player and multiplayer versions of Goal Detector component.
->DataElement(0, &GoalDetectorComponent::m_team,
"Team", "Which team is this goal line for?")
Score
The old goal detector kept the score value in a UI component but that will not work with the server-author-
itative design as UI is not even loaded in the server launcher. So we will move this value to the controller of
new Goal Detector component. This way the value will be controlled by the server, with clients receiving
updates to the score value when a goal is scored.
297
Multiplayer Physics
<Component
Name="GoalDetectorComponent"
Namespace="MyGem"
OverrideComponent="true"
OverrideController="true"
OverrideInclude="Multiplayer/GoalDetectorComponent.h"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
This will give the controller class a ModifyScore() method. The server side logic of detecting a goal
remains the same as the single-player version of Goal Detector component.
void GoalDetectorComponentController::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
const AZ::EntityId me = GetEntity()->GetId();
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == me)
{
if (te.m_type == TriggerEvent::Type::Enter)
{
// TODO respawn the ball
ModifyScore()++;
break;
}
}
}
}
Note
We will add respawn logic to the soccer ball in the next chapter.
The sign up method's name is the name of the property with "AddEvent" suffix.
298
Multiplayer Physics
On clients, ScoreAddEvent will allow us to sign up for change notifications. Here is how.
AZ::Event<int>::Handler m_scoreChanged;
GoalDetectorComponent::GoalDetectorComponent()
: m_scoreChanged([this](int newScore)
{
OnScoreChanged(newScore);
}) {}
void GoalDetectorComponent::OnActivate(
Multiplayer::EntityIsMigrating)
{
ScoreAddEvent(m_scoreChanged);
}
Entity Changes
As a network entity, each goal entity will need a Network Binding and a Network Transform component.
299
Multiplayer Physics
Since goal entities are now network entities, we need to wrap them in a prefab. In fact, we will need two
prefabs, one for each side of the soccer field. The only difference will be in the team value assigned to
Goal Detector components. One of them will have a team value of zero (0) and the other a value of one (1).
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch36_multiplayer_physics
Detection of a goal has been successfully moved to the server. Goals are being scored again in the Editor
game mode. UI is updated. However, the soccer ball is no longer being moved to the center of the field
after a goal is scored. In the next chapter, I will show you a different approach of restarting the ball position
by deleting the old ball and spawning a brand new one.
Until then, here is the source code for multiplayer Goal Detector component.
300
Multiplayer Physics
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
namespace MyGem
{
class GoalDetectorComponent
: public GoalDetectorComponentBase
{
public:
AZ_MULTIPLAYER_COMPONENT(MyGem::GoalDetectorComponent,
s_goalDetectorComponentConcreteUuid,
MyGem::GoalDetectorComponentBase);
GoalDetectorComponent();
static void Reflect(AZ::ReflectContext* context);
private:
AZ::Event<int>::Handler m_scoreChanged;
void OnScoreChanged(int newScore);
};
class GoalDetectorComponentController
: public GoalDetectorComponentControllerBase
{
public:
GoalDetectorComponentController(GoalDetectorComponent& p);
private:
AzPhysics::SceneEvents::
OnSceneTriggersEvent::Handler m_trigger;
301
Multiplayer Physics
void OnTriggerEvents(
const AzPhysics::TriggerEventList& tel);
};
} // namespace MyGem
namespace MyGem
{
GoalDetectorComponent::GoalDetectorComponent()
: m_scoreChanged([this](int newScore)
{
OnScoreChanged(newScore);
}) {}
if (auto bc = azrtti_cast<AZ::BehaviorContext*>(rc))
{
bc->EBus<UiScoreNotificationBus>("ScoreNotificationBus")
->Handler<ScoreNotificationHandler>();
}
}
void GoalDetectorComponent::OnActivate(
Multiplayer::EntityIsMigrating)
{
ScoreAddEvent(m_scoreChanged);
}
// Controller
GoalDetectorComponentController::GoalDetectorComponentController(
302
Multiplayer Physics
GoalDetectorComponent& parent)
: GoalDetectorComponentControllerBase(parent)
, m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
}) {}
void GoalDetectorComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
void GoalDetectorComponentController::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
const AZ::EntityId me = GetEntity()->GetId();
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == me)
{
if (te.m_type == TriggerEvent::Type::Enter)
{
// TODO respawn the ball
ModifyScore()++;
break;
}
}
}
}
} // namespace MyGem
303
Chapter 37. Removal and Spawning
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch37_removal_and_spawning
Chapter 36, Multiplayer Physics, re-implemented goal detection to use a server-authoritative mechanism.
However, the soccer ball is not being placed back in the middle of the soccer field. In this chapter, I will
show you to do that by deleting the old ball and spawning a new one.
1. Previously, the soccer ball was placed on the level directly in the Editor. This time a new entity with a
new Ball Spawner component will do that on entity activation.
3. Once Network_Ball.prefab has spawned, Ball component will call back to the spawner with
OnBallSpawned. The spawner will save the entity pointer to manage its lifetime.
4. When it is necessary to respawn a ball after a goal is scored, a new EBus request will ask the spawner
to respawn the ball with RespawnBall.
5. MarkForRemoval is a new Multiplayer gem API presented in this chapter. It is capable of deleting
network entities.
304
Removal and Spawning
Important
One must not deactivate and delete network entities directly. One has to use MarkForRemoval
method from Network Entity Manager as shown later in this chapter.
Ball Component
By creating a Ball component and placing it on the Ball entity, we can send an event when a new Ball
activates in the level after spawning. Since the spawning and removal is initiated on the server, this multi-
player component needs a controller. When a new network entity is created on the server, the multiplayer
system will take care of creating the entity on clients. The same is true when a network entity is removed
on the server.
The controller of Ball component will send out a notification when its entity activates.
void BallComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
BallNotificationBus::Broadcast(
&BallNotificationBus::Events::OnBallSpawned,
GetEntity());
}
In order to cycle through the reference of the soccer ball entities, Ball Spawner component will keep their
pointers in a small ring buffer.
AZStd::ring_buffer<AZ::Entity*> m_balls;
m_balls.set_capacity(2);
Tip
A ring buffer is a circular container of a configurable constant size. New entries will overwrite the
oldest elements first.
305
Removal and Spawning
Note
A network entity is an entity that has a Network Binding component on it.
Important
Marked entity will not go away immediately, it may take another game tick for the entity and various
network resources to be cleaned up and deleted.
In order to disable any unwanted interaction with an old ball, we will deactivate physics, so that an old
ball no longer activates goal triggers.
306
Removal and Spawning
<ArchetypeProperty
Type="AZ::Data::Asset<AzFramework::Spawnable>"
Name="BallAsset" Init="" ExposeToEditor="true" />
We will be able to get this asset by its generated getter method, GetBallAsset().
Note
XML does not allow less than ("<") and greater than (">") symbols in element property values, so we
have to escape them with "<" and ">"
Additionally, for special types such as AzFramework::Spawnable you have to provide their include
files with Include element, so that the generated base classes can compile.
<Include File="AzCore/Asset/AssetSerializer.h"/>
<Include File="AzFramework/Spawnable/Spawnable.h"/>
We are going to spawn balls where the Ball Spawner component is located at. We can help ourselves by
asking the auto-component generator to provide us with a getter for the transform component.
That will give us ability to get the world transform by calling GetParent().GetTransformCom-
ponent()->GetWorldTM(). Here is the entire RespawnBall method.
SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_preInsertionCallback = AZStd::move(cb);
SpawnableEntitiesInterface::Get()->SpawnAllEntities(
m_ticket, AZStd::move(optionalArgs));
}
307
Removal and Spawning
Entity Changes
1. Modify Network_Ball.prefab by adding Ball Component to the Ball entity.
2. Since we are going to spawn balls, remove Network_Ball.prefab from the level.
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch37_removal_and_spawning
In this chapter we added a respawn logic for the soccer balls. The moment a goal is scored, the current
ball will be marked for removal, which removes it within one game frame or so, and we spawn a new ball
in the middle of the field.
Here is the full source code for Ball and Ball Spawner components.
308
Removal and Spawning
Namespace="MyGem"
OverrideComponent="false"
OverrideController="true"
OverrideInclude="Multiplayer/BallComponent.h"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</Component>
namespace MyGem
{
class BallComponentController
: public BallComponentControllerBase
{
public:
BallComponentController(BallComponent& parent);
void OnActivate(Multiplayer::EntityIsMigrating) override;
void OnDeactivate(Multiplayer::EntityIsMigrating) override{}
};
}
namespace MyGem
{
BallComponentController::BallComponentController(
BallComponent& parent)
: BallComponentControllerBase(parent) {}
void BallComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
309
Removal and Spawning
BallNotificationBus::Broadcast(
&BallNotificationBus::Events::OnBallSpawned,
GetEntity());
}
}
<Include File="AzCore/Asset/AssetSerializer.h"/>
<Include File="AzFramework/Spawnable/Spawnable.h"/>
<ArchetypeProperty
Type="AZ::Data::Asset<AzFramework::Spawnable>"
Name="BallAsset" Init="" ExposeToEditor="true" />
</Component>
namespace MyGem
{
310
Removal and Spawning
class BallSpawnerComponentController
: public BallSpawnerComponentControllerBase
, public BallSpawnerRequestBus::Handler
, public BallNotificationBus::Handler
{
public:
BallSpawnerComponentController(BallSpawnerComponent& parent);
// BallRequestBus
void RespawnBall() override;
// BallNotificationBus
void OnBallSpawned(AZ::Entity* e) override;
private:
AzFramework::EntitySpawnTicket m_ticket;
AZStd::ring_buffer<AZ::Entity*> m_balls;
void RemoveOldBall();
};
}
namespace MyGem
{
BallSpawnerComponentController::BallSpawnerComponentController(
BallSpawnerComponent& parent)
: BallSpawnerComponentControllerBase(parent){}
void BallSpawnerComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
const AZ::EntityId me = GetEntity()->GetId();
BallSpawnerRequestBus::Handler::BusConnect(me);
BallNotificationBus::Handler::BusConnect(me);
m_balls.set_capacity(2);
RespawnBall();
}
void BallSpawnerComponentController::OnDeactivate(
Multiplayer::EntityIsMigrating)
{
BallSpawnerRequestBus::Handler::BusDisconnect();
BallNotificationBus::Handler::BusDisconnect();
311
Removal and Spawning
void BallSpawnerComponentController::RespawnBall()
{
RemoveOldBall();
auto cb = [world](
EntitySpawnTicket::Id /*ticketId*/,
SpawnableEntityContainerView view)
{
const AZ::Entity* e = *view.begin();
if (auto* tc = e->FindComponent<TransformComponent>())
{
tc->SetWorldTM(world);
}
};
SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_preInsertionCallback = AZStd::move(cb);
SpawnableEntitiesInterface::Get()->SpawnAllEntities(
m_ticket, AZStd::move(optionalArgs));
}
void BallSpawnerComponentController::OnBallSpawned(AZ::Entity* e)
{
m_balls.push_back(e);
}
void BallSpawnerComponentController::RemoveOldBall()
{
if (m_balls.empty() == false)
{
AZ::Entity* previousBall = m_balls.back();
AZ::Interface<IMultiplayer>::Get()->
GetNetworkEntityManager()->MarkForRemoval(oldBall);
}
}
}
312
Chapter 38. Multiplayer Animation
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch38_multiplayer_animation
We have made great strides in converting most of the game logic to multiplayer. The next task is to syn-
chronize the animation of chickens. In single-player, a chicken played a running animation when it moved.
This was broken as we moved to multiplayer, because the client animation component no longer receives
any updates of its velocity.
A better approach would be for the server to relay back the velocity to all clients.
void ChickenMovementComponentController::ProcessInput(...)
{
...
GetNetworkCharacterComponentController()->
TryMoveWithVelocity(GetVelocity(), deltaTime);
}
void ChickenMovementComponentController::UpdateVelocity(
const ChickenMovementComponentNetworkInput* input)
{
...
SetVelocity(AZ::Quaternion::CreateRotationZ(currentHeading).
TransformVector(combined) * GetWalkSpeed() +
AZ::Vector3::CreateAxisZ(GetGravity()));
}
313
Multiplayer Animation
This will generate VelocityAddEvent method, which serves as a way to sign up for velocity changes.
1. The context starts at the server where processing of input has not changed.
3. The multiplayer system replicates velocity change over the network to clients.
4. This is the first new change in this chapter. Chicken Animation component signs up and receives a
notification that a velocity has changed.
5. The velocity value is applied to the animation graph the same way as was done in Chapter 26, Animation
State Machine.
314
Multiplayer Animation
<Component
Name="ChickenAnimationComponent" ...>
AZ::Event<AZ::Vector3>::Handler m_velocityChangedEvent;
void OnVelocityChanged(AZ::Vector3 velocity);
ChickenAnimationComponent::ChickenAnimationComponent()
: m_velocityChangedEvent([this](AZ::Vector3 velocity)
{
OnVelocityChanged(velocity);
})
{}
void ChickenAnimationComponent::OnActivate(
Multiplayer::EntityIsMigrating)
{
GetChickenMovementComponent()->VelocityAddEvent(
m_velocityChangedEvent);
}
void ChickenAnimationComponent::OnVelocityChanged(
AZ::Vector3 velocity)
{
velocity.SetZ(0);
Note
I am setting the Z component of the velocity to zero since the running animation should not depend
on the gravity portion that is pointing down the negative Z axis.
Entity Changes
In this chapter we converted Chicken Animation component from a single-player to a multiplayer variant.
Replace the component on the root entity of Chicken.prefab.
315
Multiplayer Animation
Additionally, we need to make some structural changes to Chicken.prefab, because Chicken Animation
component now expects to find Animation Graph component on the same entity.
3. Move all the components from Chicken_Actor entity to the root Chicken entity. (Except Network
Binding and Network Transform component, they are already there on Chicken entity.)
5. Delete Chicken_Rigidbody entity. These entities are now empty and serve no purpose.
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch38_multiplayer_animation
In this chapter, we replicated animation states from the server to clients using velocity as the primary
source of animation state.
316
Multiplayer Animation
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
namespace MyGem
{
class ChickenAnimationComponent
: public ChickenAnimationComponentBase
{
public:
AZ_MULTIPLAYER_COMPONENT(MyGem::ChickenAnimationComponent,
s_chickenAnimationComponentConcreteUuid,
MyGem::ChickenAnimationComponentBase);
private:
AZ::Event<AZ::Vector3>::Handler m_velocityChangedEvent;
void OnVelocityChanged(AZ::Vector3 velocity);
};
}
namespace MyGem
{
void ChickenAnimationComponent::Reflect(AZ::ReflectContext* rc)
{
auto sc = azrtti_cast<AZ::SerializeContext*>(rc);
if (sc)
{
sc->Class<ChickenAnimationComponent,
ChickenAnimationComponentBase>()->Version(1);
}
ChickenAnimationComponentBase::Reflect(rc);
}
317
Multiplayer Animation
ChickenAnimationComponent::ChickenAnimationComponent()
: m_velocityChangedEvent([this](AZ::Vector3 velocity)
{
OnVelocityChanged(velocity);
})
{}
void ChickenAnimationComponent::OnActivate(
Multiplayer::EntityIsMigrating)
{
GetChickenMovementComponent()->VelocityAddEvent(
m_velocityChangedEvent);
}
void ChickenAnimationComponent::OnVelocityChanged(
AZ::Vector3 velocity)
{
velocity.SetZ(0);
318
Chapter 39. Team Spawner
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch39_team_spawner
We have converted most of the game logic to multiplayer but we are not yet capable of supporting multiple
players. If more than one player joins the server, they end up competing for controlling the same chicken.
That cannot go well. This chapter will build on Chapter 34, Simple Player Spawner, by adding more
chickens and giving each new player a different chicken from among two different teams.
2. Each chicken will report its activation and the team it is assigned by using ChickenNotifica-
tionBus that we already have in our game.
3. When the multiplayer system asks for a player entity for an incoming client, we will pick a chicken
entity from one of the teams.
319
Team Spawner
Note
If we just add more chicken prefabs to the level, we will uncover a defect in our design. We will have
too many cameras on the level. There is a Camera component in Chicken.prefab, which means
that for each new chicken we create in the level, another Camera component will be added. Instead
of dealing with multiple cameras, this chapter will detach the camera from the chicken prefab and
move it to the level. In the next chapter we will create Chicken Camera component to deal with the
camera properly.
There will be two teams. We will set the team value in the Editor to either zero or one.
We will give up on Chicken.prefab and create two new chicken prefabs. Chicken_Team_0.prefab
for the first chicken team and Chicken_Team_1.prefab for the second team.
Tip
You can break apart a prefab with Detach Prefab command. That will remove the prefab from the
level but leave behind all of its entities.
320
Team Spawner
3. On Chicken entity, find Chicken component and set team value to zero (0).
321
Team Spawner
This will give us two chicken types that will form opposite teams.
Under Chicken Spawn entity, create some number of chickens for each team. I created four for each team.
Note
The camera no longer belongs to chicken prefabs.
We already have a Chicken Component Controller. The only change is to use GetTeam() for the newly
added Team network property.
void ChickenComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
if (!IsAuthority()) return;
ChickenNotificationBus::Broadcast(
322
Team Spawner
&ChickenNotificationBus::Events::OnChickenCreated,
GetEntity(), GetTeam());
}
2. As chicken activation notifications come in, they will be stored into appropriate team container of
entities.
void ChickenSpawnComponent::OnChickenCreated(
AZ::Entity* e, int team)
{
if (team >= 0 && team <= 1)
{
m_teams[team].push_back(e);
}
}
3. GetNextChicken() method will pick from the team container that has more chickens remaining
to keep the teams even.
AZ::Entity* ChickenSpawnComponent::GetNextChicken()
{
AZStd::vector<AZ::Entity*>& team =
m_teams[0].size() > m_teams[1].size() ?
m_teams[0] : m_teams[1];
if (team.empty()) return nullptr;
return newChicken;
}
4. OnPlayerJoin is an existing method that the multiplayer system calls and expects to receive a
network entity.
NetworkEntityHandle ChickenSpawnComponent::OnPlayerJoin(
[[maybe_unused]] uint64_t userId,
const Multiplayer::MultiplayerAgentDatum&)
{
return NetworkEntityHandle{ GetNextChicken() };
}
Summary
This chapter upgraded Chicken Spawner Component to support multiple players by placing multiple chick-
ens on the level ahead of time. When a new player joins, they will control one of the chickens in the level.
323
Team Spawner
You could spawn player entities but this approach allowed me to set up chickens on each side to give the
soccer field a more welcoming look.
There is one last defect to take care of for the multiplayer portion of our game. The camera does not follow
the right chicken. In fact, the camera does not follow any chicken at all right now. The next chapter will
address this issue.
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch39_team_spawner
namespace MyGem
{
class ChickenSpawnComponent
: public AZ::Component
, public Multiplayer::IMultiplayerSpawner
, public ChickenNotificationBus::Handler
{
public:
AZ_COMPONENT(ChickenSpawnComponent,
324
Team Spawner
"{814BAF21-10E4-4BE9-8380-C23B0EC27205}");
// ChickenNotificationBus
void OnChickenCreated(AZ::Entity* e, int team) override;
// IMultiplayerSpawner overrides
Multiplayer::NetworkEntityHandle OnPlayerJoin(
uint64_t userId,
const Multiplayer::MultiplayerAgentDatum&) override;
void OnPlayerLeave(
Multiplayer::ConstNetworkEntityHandle,
const Multiplayer::ReplicationSet&,
AzNetworking::DisconnectReason) override {}
private:
AZStd::vector<AZ::Entity*> m_teams[2] = {};
AZ::Entity* GetNextChicken();
};
} // namespace MyGem
namespace MyGem
{
using namespace Multiplayer;
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<ChickenSpawnComponent>(
"Chicken Spawn",
"[Player controlled chickens]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
325
Team Spawner
AZ_CRC_CE("Game"));
}
}
}
void ChickenSpawnComponent::Activate()
{
AZ::Interface<IMultiplayerSpawner>::Register(this);
ChickenNotificationBus::Handler::BusConnect(GetEntityId());
}
void ChickenSpawnComponent::Deactivate()
{
ChickenNotificationBus::Handler::BusDisconnect();
AZ::Interface<IMultiplayerSpawner>::Unregister(this);
}
void ChickenSpawnComponent::OnChickenCreated(
AZ::Entity* e, int team)
{
if (team >= 0 && team <= 1)
{
m_teams[team].push_back(e);
}
}
NetworkEntityHandle ChickenSpawnComponent::OnPlayerJoin(
[[maybe_unused]] uint64_t userId,
const Multiplayer::MultiplayerAgentDatum&)
{
return NetworkEntityHandle{ GetNextChicken() };
}
AZ::Entity* ChickenSpawnComponent::GetNextChicken()
{
AZStd::vector<AZ::Entity*>& team =
m_teams[0].size() > m_teams[1].size() ?
m_teams[0] : m_teams[1];
if (team.empty()) return nullptr;
return newChicken;
}
} // namespace MyGem
326
Chapter 40. Multiplayer Camera
Introduction
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch40_multiplayer_camera
Chapter 39, Team Spawner, created two teams of chickens but disconnected the camera from following a
chicken. This chapter will attach the camera to the player controlled chicken. There are eight (8) chickens
on the level, so we will need some extra information to figure out which is the right chicken for each client.
Autonomous Controllers
During the course of developing a multiplayer game, you will often have a need to execute some game
logic that should only run on the local player controlled entity and nowhere else, not on the server and not
on entities that are controlled by other clients. For example, in this chapter, we want the camera to follow
only the chicken that the local player controls. The other chickens in the level must not grab the camera.
This is where autonomous role comes in.
An autonomous role is a special role that is only granted to a client entity that was specifically assigned
by a spawner, as we did in Chapter 34, Simple Player Spawner, and Chapter 39, Team Spawner.
A regular client component does not get a controller but the autonomous entity does get a controller and
only for that autonomous entity. Only the autonomous entity is allowed to send input to the server. You
can tell if your controller is autonomous by using MultiplayerController::IsAutonomous method.
void ChickenCameraComponentController::OnActivate(...)
{
if (IsAutonomous())
{
// ...
If you a client entity has an autonomous controller, then you can safely assume that this is the entity the
client is controlling, which makes it the right anchor for the camera.
In the constructor, we will sign up for game tick events but only if the controller is autonomous.
void ChickenCameraComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
327
Multiplayer Camera
if (IsAutonomous())
{
AZ::TickBus::Handler::BusConnect();
}
}
1. On each game tick grab the active camera. CameraSystemRequestBus is an EBus that will give
you the camera.
AZ::Entity* ChickenCameraComponentController::GetActiveCamera()
{
using namespace AZ;
using namespace Camera;
EntityId activeCameraId;
CameraSystemRequestBus::BroadcastResult(
activeCameraId,
&CameraSystemRequestBus::Events::GetActiveCamera);
auto ca = Interface<ComponentApplicationRequests>::Get();
return ca->FindEntity(activeCameraId);
}
2. Query the current position and orientation of the player controlled chicken.
3. Offset the position by GetCameraOffset() and move the camera behind the chicken.
void ChickenCameraComponentController::OnTick(
float, AZ::ScriptTimePoint)
{
328
Multiplayer Camera
...
AZ::Transform chicken =
GetParent().GetTransformComponent()->GetWorldTM();
AZ::Vector3 camera = chicken.GetTranslation() +
chicken.GetRotation().TransformVector(GetCameraOffset());
chicken.SetTranslation(camera);
m_activeCameraEntity->GetTransform()->SetWorldTM(chicken);
}
Entity Changes
Place Chicken Camera component on Chicken entity in both Chicken_Team_0.prefab and Chick-
en_Team_1.prefab.
Tip
Once you add Chicken Camera component to the prefab and save it, you can modify the component
values without the Editor.
"m_template": {
"$type": "MyGem::ChickenCameraComponent",
"CameraOffset": [
0.0,
-3.0,
1.5
]
}
4. Make the change you want and save the prefab file.
Summary
Note
The accompanying source code and assets for this chapter can be found on GitHub at:
https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch40_multiplayer_camera
In this chapter we created a component that keeps the camera behind the chicken a player controls on a
client. This allows us to keep a single camera component on the level. On the server, the camera will never
329
Multiplayer Camera
move since the controllers on the server are authoritative and not autonomous. On each client, the camera
will be moved only by one of Chicken Camera components.
namespace MyGem
{
class ChickenCameraComponentController
: public ChickenCameraComponentControllerBase
, public AZ::TickBus::Handler
{
public:
ChickenCameraComponentController(ChickenCameraComponent& p);
330
Multiplayer Camera
// TickBus
void OnTick(float deltaTime, AZ::ScriptTimePoint) override;
private:
AZ::Entity* m_activeCameraEntity = nullptr;
AZ::Entity* GetActiveCamera();
};
}
namespace MyGem
{
ChickenCameraComponentController::
ChickenCameraComponentController(ChickenCameraComponent& p)
: ChickenCameraComponentControllerBase(p) {}
void ChickenCameraComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
if (IsAutonomous())
{
AZ::TickBus::Handler::BusConnect();
}
}
void ChickenCameraComponentController::OnDeactivate(
Multiplayer::EntityIsMigrating)
{
AZ::TickBus::Handler::BusDisconnect();
}
void ChickenCameraComponentController::OnTick(
float, AZ::ScriptTimePoint)
{
if (!m_activeCameraEntity)
{
m_activeCameraEntity = GetActiveCamera();
return;
}
AZ::Transform chicken =
GetParent().GetTransformComponent()->GetWorldTM();
AZ::Vector3 camera = chicken.GetTranslation() +
chicken.GetRotation().TransformVector(GetCameraOffset());
chicken.SetTranslation(camera);
m_activeCameraEntity->GetTransform()->SetWorldTM(chicken);
}
331
Multiplayer Camera
AZ::Entity* ChickenCameraComponentController::GetActiveCamera()
{
using namespace AZ;
using namespace Camera;
EntityId activeCameraId;
CameraSystemRequestBus::BroadcastResult( activeCameraId,
&CameraSystemRequestBus::Events::GetActiveCamera);
auto ca = Interface<ComponentApplicationRequests>::Get();
return ca->FindEntity(activeCameraId);
}
}
332
Index
Audio Trigger, 249
Directional Light, 178
Image, 189
Input, 142
A Local Prediction Player Input, 278
Animation Material, 21, 176, 321
Blend Tree Node, 215 Mesh, 20
Blend Two Node, 216 Network Binding, 272
Final Node, 218 Network Character, 283
Motion Node, 218 Network Transform, 272
Parameter Action, 229 PhysX Collider, 192
Parameter Condition, 227 Simple Motion, 212
Parameters, 218 Text, 190
Play Speed, 220 Transform, 17
SetNamedParameterBool, 233 Transform 2D, 187
State Condition, 228 UI Canvas Asset Ref, 186
AnimGraphComponentRequestBus, 222, 233, 315 Component Dependency, 73
AppearsInAddComponentMenu, 84 GetDependentServices, 73
ApplyLinearImpulse, 208 GetIncompatibleServices, 73
Archetype Property, 283, 297 GetProvidedServices, 73
ArchetypeProperty, 320 GetRequiredServices, 73
Asset Processor, 8, 10 ComponentRelation, 283, 315
AudioTriggerComponentRequests, 251 Console command
AZ::Event, 56 Invoking, 113
AZ::Interface, 50 ConstNetworkEntityHandle, 306
AZ::TransformBus, 313 Controllers, 264
AZ_COMPONENT, 34 CreateComponentDescriptors, 259
AZ_CONSOLEFREEFUNC, 111
AZ_CONSOLEFUNC, 111
AZ_CVAR_EXTERNED, 111
D
AZ_UNIT_TEST_HOOK, 128 DependencyArrayType, 72
AZ_UNUSED, 35 DisablePhysics, 198
azmodel, 173
azrtti_cast, 82 E
EBus, 62
B ComponentBus, 65
Behavior Context, 231 TickBus, 70
BindTransformChangedEventHandler, 58 TransformBus, 65
bootstrap.setreg, 9 EditContext, 82, 83
Editor, 8
C UI Editor, 186
CameraSystemRequestBus, 328 editorsv_enabled, 271
CharacterBus, 152 editorsv_rhi_override, 271
CharacterRequestBus, 169 EnablePhysics, 198
ClassBuilder, 83 engine_name, 261
CMake Entity, 15
enabled_gems.cmake, 103 Entity Inspector, 12
ZERO_CHECK, 37 Entity Outliner, 89
ZERO_TARGET, 102 EntityId, 67
Component, 16 EXPECT_CALL, 135
Actor, 212 external_subdirectories, 102
Anim Graph, 213
Audio Listener, 252 F
Audio Proxy, 249 Fbx Settings, 172
333
Index
334