This tutorial was written on Windows 10 using WSL with an O3DE version that
was built from source, on commit e2c4c0e5e3
In the previous article we created and built our start menu. In this article we are going to write some scripts to allow us to dismiss the start menu and start our game.
To do this we will be looking into how to receive player input by way of the
Input Component
and Input Bindings
. We will then write out first Lua script
that will help us react to these input events and control our menu.
Input Routing
An Input Component
is used to bind raw inputs to events in our game. Just like
how we added the UI Canvas Asset Ref
component in the previous article, let's
go ahead and add an Input
component onto our Start Menu
entity.
The Input
component takes a set of Input Bindings
which are a separate asset,
and will generate events for them. For our start menu, we are going to want
to create an input binding that captures when the player presses the Enter
key
so we know when to close the menu and start the game. To create Input Bindings
we use the Asset Editor
. This can be located either in the start menu of the
editor under Tools | Asset Editor
or can be opened directly from the component
by pressing the Open in Asset Editor
button. You can also open it from the
main toolbar at the top right of your main editor window.
With the asset browser open create new input bindings by using the File | New | Input Bindings
option within the start menu.
Now we have an empty Input Binding
asset. Let's save it to our project right away
but going to File | Save As..
. I saved this as StartMenu.inputbindings
in our
UI
folder, but you can put it anywhere you like.
Use the +
button to add a new binding.
Then set the event name to EnterToStart
While the name itself isn't important, we will be using this EXACT string elswhere to identify this event. Make sure you check spelling and capitalization to your liking!
Next we need to specify what triggers this event. Press the +
button next to
Event Generators
to open up a Class to create
dialog. In this dialog select
InputEventMap
which gives us access to our keyboard events. Press Okay
.
This adds a new entry to the event generator and we want to tell it to react to the keyboard enter key being pressed. Change:
Input Device Type
to be keyboardInput Name
to bekeyboard_key_edit_enter
Then leave the other fields default.
Save this asset using File | Save
.
We now have a complete input binding file we can reference in the Input
component
on our Start Menu
. Reselect your Start Menu
entity, and using the asset
selector button, open the Asset Picker
window and locate our input bindings file.
Or you can simply drag from your Asset Browser
directly into the field of your
Input
component.
Lua Script
Now we are ready to add a lua script to our start menu. Just like adding the
Input
component, we will add a Lua
component using the Add Component
button.
We can press the Open Lua Editor
button to open the Lua Editor
where we will
create our script. We can also open the Lua Editor from the Tools | Lua Editor
option if you prefer.
We will create a new lua script by using File | New
. We can save it immediately,
I like to use a folder in my project called Scripts
and I named my script
StartMenu.lua
All O3DE scripts have to follow just a few small conventions.
They all return a Table, which is an associative array and constitutes one of the primary data structures in the Lua language. It's like a dictionary in python or kinda like an object in javascript.
This table has a few features so O3DE knows how to work with it.
- It can declare
Properties
which will expose variables we can modify in the editor. - It can declare an
OnActivate
function which is called automatically when the component starts up. - It can declare an
OnDeactivate
function which is called automatically when the component shuts down.
We can use OnActivate
and OnDeactivate
to interact with the Event Bus
or EBus
which is the primary method by which this component will interact with other
entities and components, as well as respond to events, or even subscribe to an
OnTick
event if a component requires per-frame updating.
This has many advantages and some challenges, and is certainly a paradigm shift if you are coming from other models. For more information about the EBus concept and implementation you can read more here:
Here is the skeleton code for our StartMenu.lua
file. For more information
about this skeleton file see O3DE Learn: Basic Lua Script
StartMenu = {
Properties = {
-- Property definitions
}
}
function StartMenu:OnActivate()
-- called when the component is activated
end
function StartMenu:OnDeactivate()
-- called when the component is deactivated
end
What we want to do is OnActivate
connect to the InputEventNotificationBus
.
This bus is responsible for carrying all the input events. When we connect,
we specify a handler, (self
meaning this component) and we need to pass the
id for the event we want to respond to. We use a helper function InputEventNotificationId
to convert the string name EnterToStart
into a valid event id.
StartingPointInput
gem, specificaly located in the
Gems/StartingPointInput
source folder in the engine repo. The file
InputEventNotificationBus.h
declares the InputEventNotifications
that are triggered (OnPressed
OnHeld
and OnReleased
functions)
and their signatures.The class which generates these events is the
InputEventMap
which inherits from
InputChannelEventListener
. The InputNotificationBus
is apparently an abstraction
layer over the InputChannelEvent
system used internally by O3DE. I'd like to know
more but I've digressed enough for this article and haven't done a deep dive into
the input system yet.
1 2 -- 3 -- Properties ################################################################# 4 -- 5 StartMenu = { 6 Properties = { 7 -- Property definitions 8 } 9 } 10 11 -- 12 -- Input Handler ############################################################## 13 -- 14 15 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased 16 -- they each come with a value that may represent the state of the input (for example 17 -- the tilt of a thumbstick). The value is configured in the input binding. 18 function StartMenu:OnPressed (value) 19 Debug.Log("Enter Key Pressed"); 20 end 21 22 -- 23 -- Lifecycle ################################################################## 24 -- 25 26 function StartMenu:OnActivate() 27 -- connect to InputEventNotificationBus and set ourselves as the handler 28 self.InputNotificationBus = InputEventNotificationBus.Connect( 29 self, InputEventNotificationId("EnterToStart") 30 ); 31 end 32 33 -- 34 -- ---------------------------------------------------------------------------- 35 -- 36 37 function StartMenu:OnDeactivate() 38 self.InputNotificationBus:Disconnect(); 39 end 40 41 -- 42 -- ############################################################################ 43 -- 44 45 return StartMenu;
With this script we can run the game and see that when we press Enter
we
trigger the OnPressed
function, which prints Enter Key Pressed
to the console.
Closing the Start Menu
Now that we can receive the enter event we need to trigger closing, or hiding, the UI Canvas that is displaying. To do this, we simply need to send a message on the appropriate Event Bus to notify our canvas to hide itself. What event? And how do we send events?
Take a look at this:
11 -- 12 -- Input Handler ############################################################## 13 -- 14 15 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased 16 -- they each come with a value that may represent the state of the input (for example 17 -- the tilt of a thumbstick). The value is configured in the input binding. 18 function StartMenu:OnPressed (value) 19 20 -- get the canvas id from the UI Canvas Ref component on our entity 21 local canvasId = UiCanvasRefBus.Event.GetCanvas(self.entityId); 22 23 -- send a message to the Ui Canvas with the id we got from the Ui Canvas ref 24 -- and tell it to disable itself 25 UiCanvasBus.Event.SetEnabled(canvasId, false); 26 end
Remember that we don't actually have a Canvas
, we havea UiCanvasRef
component.
This component takes a canvas asset file, and (optionally) creates a canvas when
it activates. Since it created the actual canvas, we need to ask it for the canvas
id. Since we only communicate on message buses, we fire an event on the
UiCanvasRefBus
asking for the canvasId
. We don't just fire it to EVERY
UiCanvasRefBus
in our level though. We specifically request it from ones
only that have our entityId
- in other words, ones that are on the same entity
as us.
Once we have this canvasId
we are free to send it messages directly through
the EBus as well. Specifically we fire an event telling it to set its enabled
state to false.
Bus
dedicated to buses it exposes for the UI. A quick
look through there helped me find what I needed, as well as the LyShine lua
examples. Hopefully I can get the code inspector working in O3DE though, that
would be awesome! Or find some more thorough documentation.By convention, it appears most components have a bus named after them that allows you to both send
Event
s, and receive notifications (like we did for
OnPressed
Now with this bit of code, when we press enter, the canvas no longer renders.
However, we're not quite done yet. Although we won't be using the Fly Camera component for much longer, lets apply what we've learned to locate the proper bus to message to re-enable camera controls when the menu closes. For that we will need a reference to the camera.
To get it we add make a Property
that takes the entityId of the camera, and
we can use that to send a message on a FlyCameraInputBus
(did you guess that name?)
One tricky bit, however, is that if you look at the FlyCameraInputBus.h
file you
will see that the event name is SetIsEnabled
as opposed to canvas which is
SetEnabled
. Good thing we know how to look up the interface!
1 /* 2 * Copyright (c) Contributors to the Open 3D Engine Project. 3 * For complete copyright and license terms please see the LICENSE at the root of this distribution. 4 * 5 * SPDX-License-Identifier: Apache-2.0 OR MIT 6 * 7 */ 8 #pragma once 9 #include <AzCore/Component/ComponentBus.h> 10 11 namespace AZ 12 { 13 namespace AtomBridge 14 { 15 /// This bus is used to enable and disable the FlyCamera so that input can be used for UI etc. 16 class FlyCameraInputInterface 17 : public AZ::ComponentBus 18 { 19 public: 20 virtual void SetIsEnabled(bool isEnabled) = 0; 21 virtual bool GetIsEnabled() = 0; 22 23 public: // static member data 24 25 //! Only one component on a entity can implement the events 26 static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; 27 }; 28 29 typedef AZ::EBus<FlyCameraInputInterface> FlyCameraInputBus; 30 } 31 }
Now that we figured it out, though, we can update our StartMenu.lua
script to propertly turn it back on.
1 2 -- 3 -- Properties ################################################################# 4 -- 5 StartMenu = { 6 Properties = { 7 cameraId = EntityId() 8 } 9 } 10 11 -- 12 -- Input Handler ############################################################## 13 -- 14 15 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased 16 -- they each come with a value that may represent the state of the input (for example 17 -- the tilt of a thumbstick). This is configured in the input binding. 18 function StartMenu:OnPressed (value) 19 20 -- get the canvas id from the UI Canvas Ref component on our entity 21 local canvasId = UiCanvasRefBus.Event.GetCanvas(self.entityId); 22 23 -- send a message to the Ui Canvas with the id we got from the Ui Canvas ref 24 -- and tell it to disable itself 25 UiCanvasBus.Event.SetEnabled(canvasId, false); 26 27 -- tell the fly camera input component on the camera to enable. 28 FlyCameraInputBus.Event.SetIsEnabled(self.Properties.cameraId, true); 29 end 30 31 -- 32 -- Lifecycle ################################################################## 33 -- 34 35 function StartMenu:OnActivate() 36 -- connect to InputEventNotificationBus and set ourselves as the handler 37 self.InputNotificationBus = InputEventNotificationBus.Connect( 38 self, InputEventNotificationId("EnterToStart") 39 ); 40 end 41 42 -- 43 -- ---------------------------------------------------------------------------- 44 -- 45 46 function StartMenu:OnDeactivate() 47 self.InputNotificationBus:Disconnect(); 48 end 49 50 -- 51 -- ############################################################################ 52 -- 53 54 return StartMenu;
Run the game and enjoy flying the camera around this default space!
Conclusion
Make sure to save your work with Ctrl+S
or File | Save
In this guide we covered a lot of new topics, including:
- How to connect to an EBus to receive notifications.
- How to send events on an EBus to other components.
- How to use
Properties
in a lua script. - How to look up EBus API by reading the source code.
In the next article we are going to learn how to create the level assets using the `White Box` gem.