O3DE - PongLua - Part 5

Scripting our Pong Paddles in Lua

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 assets to populate our level.

In this article we are going to be writing the scripts that control our paddles.

Setting up Input

Much like we did in Part 3 of this series where we captured input to dimiss the start menu. The first step, as before, is to create an input bindings file to capture the keyboard events we are interested in.

Since we will be adding new input bindings to the StartMenu.inputbindings file, we should rename it to be more accurate. Let's call it Keyboard.inputbindings We can't rename it in the Editor so we will have to do it in Explorer.

The editor will automatically pick up the change, however, the Input component on the Start Menu entity did not recognize the change! We are going to have to take our newly renamed Keyboard.inputbindings and drag them back onto our Input component.

Next let's add our new bindings. Right click the Keyboard.inputbindings asset in the Asset Browser and select Open in Asset Editor...

In the Asset Browser lets add a new Input Event Group and call it Paddle01_Controls. These will be the keys we use to control Paddle01.

We are going to add two Event Generators one for the key that moves the paddle up, and one that moves the paddle down.

Press the + button to add the first Event Generator to bring up the Class to create popup. From the dropdown list select InputEventMap. Do it again so you have 2 Event Generators for Paddle01_Controls

Set both Event Generators to use keyboard as their Input Device Type.

We want one to capture key presses for the W key, which will move our paddle upwards. The other will capture key presses for the S key, which will move our paddle downwards. We will be using the event value multiplier to modify the value variable that is sent to the event handler to know which key was pressed.

Set the W key to have a 1.0 modifier, and the S key to have a -1.0 modifier.

Make sure we save afterwards!

Find our Paddle_01 entity and select it in the Entity Outliner. Use the Add Component button to add an Input component to this entity. Just like before we can drag our Keyboard.inputbindings into the appropriate property on the Input component.

We can take advantage of copying and pasting components to add the same input component onto Paddle_02. Use the right-click popup menu on the Input component to select Copy Component. Then right click anywhere on the Paddle_02 Entity Inspector and select Paste Component.


Receiving Key Events For Paddle Movement

Select the Paddle_01 entity and add a Lua Script component. Then open the Lua Editor either from the component, or through the Tools | Lua Editor menu.

Create a new Lua Script from File | New in the Lua Editor. Save it in your Scripts directory and name it PaddleMovement.lua

Just like the previous StartMenu.lua script, we can create the basic skeleton needed for O3DE lua scripts.

Scripts/PaddleMovement.lua
 1 
 2 --
 3 -- Properties #################################################################
 4 --
 5 PaddleMovement = {
 6     Properties = {
 7     }
 8 }
 9 
10 --
11 -- Lifecycle ##################################################################
12 --
13 
14 function PaddleMovement:OnActivate()
15 
16 end
17 
18 --
19 -- ----------------------------------------------------------------------------
20 --
21 
22 function PaddleMovement:OnDeactivate()
15 
24 end
25 
26 --
27 -- ############################################################################
28 --
29 
30 return PaddleMovement;

Similar to how we configured StartMenu.lua we want to subscribe to the InputEventNotificationBus to be notified whenever the keys we are interested are pressed. One small change, however, is that since want to share this script with both paddles, we should expose the Input Event Group name to the component so we can change exactly which keyset we are subscribing to.

We also want to subscribe not just to the key press event, but while the key is held down as well.

Scripts/PaddleMovement.lua
 1 
 2 --
 3 -- Properties #################################################################
 4 --
 5 local PaddleMovement = {
 6     Properties = {
 7         InputEventName = "Paddle01_Controls",
 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 PaddleMovement:OnPressed (value)
19     Debug.Log(tostring(value))
20 end
21 
22 --  
23 -- ----------------------------------------------------------------------------
24 --
25 
26 function PaddleMovement:OnHeld (value)
27     Debug.Log(tostring(value))
28 end
29 
30 
31 --
32 -- Lifecycle ##################################################################
33 --
34 
35 function PaddleMovement:OnActivate()
36     -- connect to InputEventNotificationBus and set ourselves as the handler
37     self.InputNotificationBus = InputEventNotificationBus.Connect(
38         self, InputEventNotificationId(self.Properties.InputEventName)
39     );
40 
41 end
42 
43 --
44 -- ----------------------------------------------------------------------------
45 --
46 
47 function PaddleMovement:OnDeactivate()
48     self.InputNotificationBus:Disconnect();
49 end
50 
51 --
52 -- ############################################################################
53 --
54 
55 return PaddleMovement;

We print out the value that is being sent with us and with this script when you run the game, you should get 1.0 printing when you press W and -1.0 printing when you press S. Now all we need to do is use these values to translate our paddle.

Moving our Paddle

We can do this by sending an event on the TransformBus with our entityId (so we communicate with our own Transform component). The event we want to send is called MoveEntity. We could just pass it the value we received from the input system, but we could also add a Property called PaddleSpeed and use that as a multiplier so we have some control over how fast it moves. Putting all that together we get a script that looks like this:

Scripts/PaddleMovement.lua
 1 
 2 --
 3 -- Properties #################################################################
 4 --
 5 local PaddleMovement = {
 6     Properties = {
 7         InputEventName = "Paddle01_Controls",
 8         PaddleSpeed = 1.0,
 9     }
10 }
11 
12 --
13 -- Input Handler ##############################################################
14 --
15 
16 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased
17 -- they each come with a value that may represent the state of the input (for example
18 -- the tilt of a thumbstick).  This is configured in the input binding.
19 function PaddleMovement:OnPressed (value)
20     self:_Move(value);
21 end
22 
23 --  
24 -- ----------------------------------------------------------------------------
25 --
26 
27 function PaddleMovement:OnHeld (value)
28     self:_Move(value);
29 end
30 
31 
32 --
33 -- Lifecycle ##################################################################
34 --
35 
36 function PaddleMovement:OnActivate()
37     -- connect to InputEventNotificationBus and set ourselves as the handler
38     self.InputNotificationBus = InputEventNotificationBus.Connect(
39         self, InputEventNotificationId(self.Properties.InputEventName)
40     );
41 
42 end
43 
44 --
45 -- ----------------------------------------------------------------------------
46 --
47 
48 function PaddleMovement:OnDeactivate()
49     self.InputNotificationBus:Disconnect();
50 end
51 
52 --
53 -- internal methods ###########################################################
54 --
55 
56 function PaddleMovement:_Move(deltaMove)
57     TransformBus.Event.MoveEntity(self.entityId, Vector3(0, deltaMove * self.Properties.PaddleSpeed, 0));
58 end
59 
60 --
61 -- ############################################################################
62 --
63 
64 return PaddleMovement;

When we run this, we see this is better but there are three problems.

  • Problem 1: Our start menu continues to close when we press Enter but we no longer are blocking input until it is dismissed, so the paddles can move before we dismiss the Start Menu.

  • Problem 2: Or paddles can travel outside of the arena!

  • Problem 3: This is harder to see right now, but it's the fact that we are moving our paddles based on the speed at which we are receiving inputs from the InputNotificationBus. The problem with this is that the speed of our paddles is now tied to how often we receive notifications. If the framerate is running quickly, and the bus is sending us a notification every frame, then the paddles will move quicker. If the framerate is slow, and the bus is sending us notifications slowly, our paddles will slow down.

Traditionally we would solve Problem 3 by updating the paddle motion once per frame and scale it based on the amount of time that has passed since the last update (deltaTime). This way rather than tying the speed of the paddles to the rate at which we receive input events, we would instead scale their movement based on how much time has elapsed. This way we will always get consistent speeds regardless of framerate hitches or spikes.

To do this, we need to get a reliable update message from the engine, as well as information about how much time has elapsed since the last update so we can scale our motion. In O3DE we do this by subscribing to the TickBus.

However, since we also need to solve Problem 2 (colliding with the walls) and in the near future we need to collide with the ball, we will instead be transitioning to a movement system that leverages the PhysX physics system. By moving to PhysX we automatically get the benefits of scaling our motion based on frame time (since the physics simulation will be taking over our motion controls)

As an exercise for the curious I've posted the code sample for solving Problem 3 using the TickBus as an appendix at the end of this article. I did this effort just for you, and not at all because I did it before realizing I would have to remove all that code once I switched over to using PhysX. Honest.

Fixing Problem 1 - Paddles Moving during Start Menu

Here the solution is solved in two steps. First, we need to create and send an event that notifies all entities that the game has started (triggered by the player pressing the enter key). Second, we want to prevent the paddles from moving until we receive this message.

This mechanism is also future proof, since when we start adding additonal paddle and ball movement, we can leverage the same system.

To do this, we are going to use the GameplayNotificationBus, a general purpose bus designed to carry these kinds of messages.

Why not Script Events? The documentation seems to recommend them over GameplayNotificationBus. The answer is that there is no useful documentation for how to get it to work.

Creating the Script Event Asset using the Asset Editor does make the event show up in Script Canvas and is usable there, but there is no corresponding global bus that is connectable in Lua. The only examples available are really really sketchy

The example basically creates a local script event and subscribes to it in a single file, but it doesn't show how Script Event Assets are made globally accessible in lua (or how to fetch them).

I tried looking through the code, and through other lua scripts, and read through every file in the Script Events gem and I could not find a helpful API. I even tried mimicing the sketchy script and instead of relying on the asset, and tried creating a Script Event in code, but the example seems to imply that after calling Register on the definition the variable becomes available globally, but this was not the case for me.

The GameplayNotificationBus is super easy to work with. You can send 3 different kinds of notifications:

  • OnEventBegin
  • OnEventUpdating
  • OnEventEnd

When you send an event, you need to build a unique GameplayNotificationId that identifies exactly which gameplay notification you are sending. For example, lets say we wanted to send an event called MyEventName and along with it send a string, and we want anybody to be able to subscribe to it.

GameplayNotificationBus Example - Broadcasting an Event
1 local eventId = GameplayNotificationId(EntityId(), "MyEventName", typeid(string));
2 GameplayNotificationBus.Event.OnEventBegin(eventId, "Data Sent With Message")
3 

Similarly to connect to this event would look like this:

GameplayNotificationBus Example - Connecting
1 local eventId = GameplayNotificationId(EntityId(), "MyEventName", typeid(string));
2 GameplayNotificationBus.Connect(self, eventId);
3 
4 -- not real syntax but you get the idea
5 function self:OnEventBegin(data)
6     -- if we received the previous event, this would print "Data Sent With Message"
7     Debug.Log(data);
8 end

This is pretty easy, the problem is that there's a lot of hardcoded magic values make it work. To solve this, we put all of this functionality into a single Lua file. Inside your Scripts directory, create a file called GameStateEvents.lua

Inside, populate it with this:

Scripts/GameStateEvents.lua
 1 
 2 --
 3 -- Game Start Event ===========================================================
 4 --
 5 
 6 local GAME_START_EVENT_NAME="GameStartEvent";
 7 
 8 local GameStartEvent = {
 9     Name = GAME_START_EVENT_NAME,
10     NotificationId = GameplayNotificationId(EntityId(),GAME_START_EVENT_NAME, typeid(string)),
11 };
12 
13 --
14 -- ----------------------------------------------------------------------------
15 --
16 
17 function GameStartEvent.Broadcast()
18     GameplayNotificationBus.Event.OnEventBegin(GameStartEvent.NotificationId, GameStartEvent.Name);
19 end
20 
21 --
22 -- ----------------------------------------------------------------------------
23 --
24 
25 function GameStartEvent.Connect(handler)
26     return GameplayNotificationBus.Connect(handler, GameStartEvent.NotificationId);
27 end
28 
29 --
30 -- ============================================================================
31 --
32 
33 return  {
34     GameStart = GameStartEvent,
35 };
36 

Broadcasting

Now lets broadcast the message. Simply require the new lua file in StartMenu.lua

GameStateEvents = require('scripts.gamestateevents');

Then when we have the StartMenu:OnPressed event, we need to broadcast the event:

GameStateEvents.GameStart.Broadcast();

This works... sort of. The problem is it may trigger multiple times, if the player hits the enter key repeatedly, or multiple OnPressed events are generated. So we should probably also write a small guard that only sends the event if we haven't sent it yet. (The game should only start once). If we did need to reset the game, we will need to remember to reset this flag as well.

Scripts/GameStateEvents.lua
 1 
 2 --
 3 -- Properties #################################################################
 4 --
 5 StartMenu = {
 6     Properties = {
 7     }
 8 }
 9 
10 GameStateEvents = require('scripts.gamestateevents');
11 
12 --
13 -- Input Handler ##############################################################
14 --
15 
16 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased
17 -- they each come with a value that may represent the state of the input (for example
18 -- the tilt of a thumbstick).  This is configured in the input binding.
19 function StartMenu:OnPressed (value)
20 
21     if not self._gameStarted then
22         -- get the canvas id from the UI Canvas Ref component on our entity
23         local canvasId = UiCanvasRefBus.Event.GetCanvas(self.entityId);
24 
25         -- send a message to the Ui Canvas with the id we got from the Ui Canvas ref
26         -- and tell it to disable itself
27         UiCanvasBus.Event.SetEnabled(canvasId, false);
28 
29         -- tell everyone the game has started
31         GameStateEvents.GameStart.Broadcast();
32 
33         -- dont send repeats
34         self._gameStarted = true;
35     end
36 end
37 
38 --
39 -- Lifecycle ##################################################################
40 --
41 
42 function StartMenu:OnActivate()
43 
44     -- a flag to ensure we dont send multiple notifications
45     self._gameStarted = false;
46 
47     -- connect to InputEventNotificationBus and set ourselves as the handler
48     self.InputNotificationBus = InputEventNotificationBus.Connect(
49         self, InputEventNotificationId("EnterToStart")
50     );
51 end
52 
53 --
54 -- ----------------------------------------------------------------------------
55 --
56 
57 function StartMenu:OnDeactivate()
58     self.InputNotificationBus:Disconnect();
59 end
60 
61 --
62 -- ############################################################################
63 --
64 
65 return StartMenu;

Connecting

Connecting will seem mostly familiar. We will again need to import GameStateEvents.lua and make sure we Connect to it using the GameStateEvents.GameStart.Connect function.

By convention, we are sending the name of the event as the payload. So on in our handler function PaddleMovement:OnEventBegin we take a parameter e which will be GameStateEvents.GameStart.Name If we receive a GameStartEvent with that as the payload, we know we are starting!

We will also need a flag to track whether or not we received the message, and we should not allow the paddle to move until we do.

Lastly, don't forget to disconnect during OnDeactivate.

Scripts/PaddleMovement.lua
 1 
 2 --
 3 -- Properties #################################################################
 4 --
 5 local PaddleMovement = {
 6     Properties = {
 7         InputEventName = "Paddle01_Controls",
 8         PaddleSpeed = 1.0,
 9     }
10 }
11 
12 GameStateEvents = require('scripts.gamestateevents');
13 
14 --
15 -- Input Handler ##############################################################
16 --
17 
18 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased
19 -- they each come with a value that may represent the state of the input (for example
20 -- the tilt of a thumbstick).  This is configured in the input binding.
21 function PaddleMovement:OnPressed(value)
22     self:_Move(value);
23 end
24 
25 --  
26 -- ----------------------------------------------------------------------------
27 --
28 
29 function PaddleMovement:OnHeld(value)
30     self:_Move(value);
31 end
32 
33 --
34 -- Internal Methods ###########################################################
35 --
36 
37 function PaddleMovement:_Move(moveDir)
38     if self._gameStarted then
39         -- scale the current moveDir by both the paddle speed and the frame delta time
40         local deltaMove = Vector3(0, moveDir, 0) * self.Properties.PaddleSpeed;
41         TransformBus.Event.MoveEntity(self.entityId, deltaMove);
42     end
43 end
44 
45 --
46 -- Lifecycle ##################################################################
47 --
48 
49 function PaddleMovement:OnActivate()
50 
51     self._moveDir = 0;
52     self._gameStarted = false;
53 
54     -- connect to InputEventNotificationBus and set ourselves as the handler
55     self.InputNotificationBus = InputEventNotificationBus.Connect(
56         self, InputEventNotificationId(self.Properties.InputEventName)
57     );
58 
59     -- connect to the script event `GameStateEvents` to receive the game start event
60     self.GameplayNotificationBus = GameStateEvents.GameStart.Connect(self);
61 
62 end
63 
64 --
65 -- ----------------------------------------------------------------------------
66 --
67 
68 function PaddleMovement:OnEventBegin(e)
69     if e == GameStateEvents.GameStart.Name then
70         self._gameStarted = true;
71     end
72 end
73 
74 --
75 -- ----------------------------------------------------------------------------
76 --
77 
78 function PaddleMovement:OnDeactivate()
79     -- disconnect from busses we connected to.
80     self.InputNotificationBus:Disconnect();
81     self.GameplayNotificationBus:Disconnect();
82 end
83 
84 --
85 -- ############################################################################
86 --
87 
88 return PaddleMovement;

Fixing Problem #2 - Paddles Leaving the Arena

There are several ways we could handle this. A cheap and easy way might be just checking the x position of the paddle. If it's not within a hardcoded range we prevent it from moving further. This would work great, and be performant, the problem is we are going to want to leverage the physics system to handle the movement of the balls, which means we need the walls and paddles to be physics objects as well.

So to solve this problem, we're going to hook up physics now.

Physics in O3DE is handled across several components working together in tandem. Depending on the effect you are trying to achieve, you can use a different combination of components. Broadly speaking, though, I often find most of my objects fall into 1 of 5 categories.

1 - RigidBodies. These are objects which are fully simulated by the physics system. Things like props that can be pushed around, grenades, or for games that are physics based (space simulators for example) are all examples of places where I would use a rigidbody. The ball in this Pong game is another example, and we'll look at setting it up in the next article.

2 - Static Colliders. These are objects like level geometry that doesn't move, but still interact with the physics simulation. Walls, barricades, and other non-moving objects that physics objects can still bump into are Static colliders. The walls of our arena are a good example of these types.

3 - Characters. These are special objects that don't behave like normal physics objects. They can move through the simulation but have custom collision handling code. Rather rather than bouncing and being controlled through impulses like rigidbodies, characters will often be handled as a special case. For example they must not be able to pass through walls, and have other considerations - like the ability to climb steps, or walk up slopes - but they probably won't have a physics material describing how much friction they have, or how bouncy they are.
Our paddles are good examples, as well as - well, most characters in games.

4 - Triggers. These are objects that have colliders but they do not change the simulation. If a rigidbody or character runs into a trigger they do not bump or bounce off. Instead, triggers are used to generate events when physics objects enter, stay in, or leave specific areas. We can use these to start cutscenes, create damage zones, play sound effects, and more. Triggers are giga useful. We could use triggers to determine if a player scored a point, for example, by creating a trigger collider near the goal point and detecting when the ball entered it.

5 - Kinematic Rigidbodies. I don't use these very often, but they are objects that, like characters, interact with the physics scene, but unlike characters, they are unaffected by it. In a way, Characters could be considered a special form of kinematic rigidbodies, just with custom collision handling that prevents them from moving through colliders, but I usually reserve this designation for objects that push and affect other things, but are not pushed by them. The canonical example I use for these are things like moving platforms. We won't have any in this project, (and I don't often use these).

Static Colliders on Walls

With that out of the way, lets first configure our walls so that they are Static Colliders

This is super easy. Add a PhysX Collider Component to each of our walls. Configure the component so that the Simulated toggle is enabled. Additionally, make sure that the collision shape is set to Box since our walls are boxes. We will also need to describe to the physics engine exactly the dimensions of our object, so use the Box | Dimensions field to edit the collision box to match our render geometry.

The mesh we use for rendering is rarely the one we use for simulating physics. The more complicated the mesh, the more expensive it is to simulate. Spheres are the best, and boxes, or compound primitives like capsules, are good too. Generally we only simulate low fidelity proxies of our meshes in order to keep things running fast and smooth.

Then make sure that the Static toggle is enabled on your Transform component.

Do this to all 4 walls.

Character Control on the Paddles

Add a PhysX Character Controller to your paddle. There are a few settings that are important to set, and there are many others that might be fun to play around with.

  • Maximum Slope: 0.0 prevents our paddle from traveling up any ramps. Although our arena is perfectly flat, this can help prevent some kinds of glitching due to colliding with the walls and the ball.
  • Step Height: 0.0 same idea as slope. We don't have stairs, and I've definitely had physics simulations do things at high speeds like attempt to have our paddle climb ontop of our ball. This could help with that kind of thing, (and it doesn't matter since we don't want our paddle climbing stairs anyway)
  • Apply Move OnTick does the fix we spoke about for problem #3 automatically for us. It collects all the movement changes for the frame, adds them together, then scales the motion based on the deltaTime. This way we get silky smooth and consistent paddle speeds.
  • Slope Behavior: Prevent Climbing See a pattern here? We want to give as much info to the physics simulation that we don't want anything other than smooth motion along the x axis. (If there are motion constraints exposed somewhere, I'd definitely use those!)
  • Scale, Shape, and Dimensions should all be set to match your paddle as closely as possible. One thing I did do, however, is extend the paddle in the z axis way beyond the visual mesh. This will help prevent problems like a high speed ball getting jammed under our paddle. Hopefully anyway, we'll find out in the next lesson when we get the ball physics working.

Now that we have the character controller setup, all we need to do is modify the paddle movement to request motion through the CharacterController rather than modifying the Transform directly.

CharacterControllerRequestBus.Event.AddVelocity should do the trick, and it's a drop in replacement for TransformBus.Event.MoveEntity.

Here's the full source list anyway, just for copy and pasting.

Scripts/PaddleMovement.lua
 1 
 2 --
 3 -- Properties #################################################################
 4 --
 5 local PaddleMovement = {
 6     Properties = {
 7         InputEventName = "Paddle01_Controls",
 8         PaddleSpeed = 1.0,
 9     }
10 }
11 
12 GameStateEvents = require('scripts.gamestateevents');
13 
14 --
15 -- Input Handler ##############################################################
16 --
17 
18 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased
19 -- they each come with a value that may represent the state of the input (for example
20 -- the tilt of a thumbstick).  This is configured in the input binding.
21 function PaddleMovement:OnPressed(value)
22     self:_Move(value);
23 end
24 
25 --  
26 -- ----------------------------------------------------------------------------
27 --
28 
29 function PaddleMovement:OnHeld(value)
30     self:_Move(value);
31 end
32 
33 --
34 -- Internal Methods ###########################################################
35 --
36 
37 function PaddleMovement:_Move(moveDir)
38     if self._gameStarted then
39         -- scale the current moveDir by both the paddle speed and the frame delta time
40         local deltaMove = Vector3(0, moveDir, 0) * self.Properties.PaddleSpeed;
41         CharacterControllerRequestBus.Event.AddVelocity(self.entityId, deltaMove);
42     end
43 end
44 
45 --
46 -- Lifecycle ##################################################################
47 --
48 
49 function PaddleMovement:OnActivate()
50 
51     self._moveDir = 0;
52     self._gameStarted = false;
53 
54     -- connect to InputEventNotificationBus and set ourselves as the handler
55     self.InputNotificationBus = InputEventNotificationBus.Connect(
56         self, InputEventNotificationId(self.Properties.InputEventName)
57     );
58 
59     -- connect to the script event `GameStateEvents` to receive the game start event
60     self.GameplayNotificationBus = GameStateEvents.GameStart.Connect(self);
61 
62 end
63 
64 --
65 -- ----------------------------------------------------------------------------
66 --
67 
68 function PaddleMovement:OnEventBegin(e)
69     if e == GameStateEvents.GameStart.Name then
70         self._gameStarted = true;
71     end
72 end
73 
74 --
75 -- ----------------------------------------------------------------------------
76 --
77 
78 function PaddleMovement:OnDeactivate()
79     -- disconnect from busses we connected to.
80     self.InputNotificationBus:Disconnect();
81     self.GameplayNotificationBus:Disconnect();
82 end
83 
84 --
85 -- ############################################################################
86 --
87 
88 return PaddleMovement;

Bonus: Fixing Problem #3 Using TickBus

Remember, rather than tying the speed of the paddles to the rate at which we receive input events, we should instead scale their movement based on how much time has elapsed. This way we will always get consistent speeds regardless of framerate hitches or spikes.

One way to do this, we need to get a reliable update message from the engine, as well as information about how much time has elapsed since the last update so we can scale our motion. In O3DE we do this by subscribing to the TickBus.

Scripts/PaddleMovement.lua
 1 
 2 --
 3 -- Properties #################################################################
 4 --
 5 local PaddleMovement = {
 6     Properties = {
 7         InputEventName = "Paddle01_Controls",
 8         PaddleSpeed = 1.0,
 9     }
10 }
11 
12 --
13 -- Input Handler ##############################################################
14 --
15 
16 -- there are 3 handlers we can receive for each input: OnPressed, OnHeld, and OnReleased
17 -- they each come with a value that may represent the state of the input (for example
18 -- the tilt of a thumbstick).  This is configured in the input binding.
19 function PaddleMovement:OnPressed(value)
20     self._moveDir = value;
21 end
22 
23 --  
24 -- ----------------------------------------------------------------------------
25 --
26 
27 function PaddleMovement:OnHeld(value)
28     self._moveDir = value;
29 end
30 
31 --
32 -- ----------------------------------------------------------------------------
33 --
34 
35 function PaddleMovement:OnReleased(value)
36     -- when a movement key is released, set the movement direction to 0
37     self._moveDir = 0;
38 end
39 
40 
41 --
42 -- Lifecycle ##################################################################
43 --
44 
45 function PaddleMovement:OnActivate()
46     self._moveDir = 0;
47 
48     -- connect to InputEventNotificationBus and set ourselves as the handler
49     self.InputNotificationBus = InputEventNotificationBus.Connect(
50         self, InputEventNotificationId(self.Properties.InputEventName)
51     );
52 
53     -- connect to the tick bus so we can have a per frame update with frame times
54     self.TickNotificationBus = TickBus.Connect(self);
55 
56 end
57 
58 --
59 -- ----------------------------------------------------------------------------
60 --
61 
62 -- This is the function that is called whenever we receive a tick event from
63 -- the TickBus.  It gives us both a deltaTime (how much time has elapsed since
64 -- the last update) as well as a currentTime (which we don't use in this example)
65 -- We could rename `currentTime` to `_` to be more idomatic.
66 function PaddleMovement:OnTick(deltaTime, currentTime)
67     -- scale the current moveDir by both the paddle speed and the frame delta time
68     local deltaMove = Vector3(0, self._moveDir, 0) * self.Properties.PaddleSpeed * deltaTime;
69     TransformBus.Event.MoveEntity(self.entityId, deltaMove);
70 end
71 
72 --
73 -- ----------------------------------------------------------------------------
74 --
75 
76 function PaddleMovement:OnDeactivate()
77     -- disconnect from busses we connected to.
78     self.InputNotificationBus:Disconnect();
79     self.TickNotificationBus:Diconnect();
80 end
81 
82 --
83 -- ############################################################################
84 --
85 
86 return PaddleMovement;

Now if we run, the paddle barely moves at all! That's because we changed the paddleSpeed to 0.1 before. Paddle speed now has a specific scale: its in units per second. A value of 0.1 means it moves 0.1 units every second. That's very slow. Try playing around with the value until you like the speed at which it moves (for my level, I like 10.0)

Just note: this version does not collide with walls, and is unecessary if you are going through a CharacterController. But, might be useful down the road~

Conclusion

Make sure to save your work with Ctrl+S or File | Save

In this guide we covered a lot of new topics, including:

  • Adding multiple Event Generators to an input binding.
  • Using the TransformBus to move Entities.
  • Use the GameplayNotificationBus even if the O3DE makers don't want us to.
  • Import library lua scripts from elsewhere in our project.
  • Create static colliders
  • Create character controlled entities
  • As a bonus, how to subscribe to the TickBus and get an update event with deltaTime.

In the next article we are going to learn how to script the movement of the ball.