O3DE - PongLua - Part 3

Scripting the Start Menu

O3DE is the new (ly rebranded) game engine released into open source by Amazon. You can read more about it here These articles will try not to cover the exact material already available in the documentation, but hopefully will supplement them!

This series explores translating the video series for creating Pong in O3DE but instead of using ScriptCanvas we will do it using Lua Script.

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.

Note: The size, shape, and location of your Asset Editor may be different. Feel free to size and position it to your liking!

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 keyboard
  • Input Name to be keyboard_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.

Note: this is pretty different from other Entity Component model engines you may have used before where you may be more used to getting a reference to other objects and interacting with / calling methods on them directly. O3DE instead uses the EBus system, where you communicate with eachother using messages on a bus.

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

Scripts/StartMenu.lua
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.

If you're interested in exploring deeper, this functionality is all provided by the 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.
StartMenu.lua
 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:

StartMenu.lua
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.

Where do we find all these methods? The documentation is unfortunately a little sparse on this, and for some reason my Lua Editor does not show any information about avaiable buses (although it seems to work in videos, I'll have to debug this). For me, the quickest way to discover functionality has been grepping through the code, specifically, LyShine (the UI system used by O3DE) has an entire directory called 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 Events, 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!

{O3DE_SOURCE_DIR}/Gems/AtomLyIntegration/Code/Include/AtomBridge/FlyCameraInputBus.h
 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.

StartMenu.lua
 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.