Views
Basic Tutorial 4 (Frame Listeners, and Input Handling)
From Axiom
Contents |
Prerequisites
- This tutorial assumes you have basic knowledge of C#, VB.NET, or Python, or other .NET language, though only the mentioned languages will have sample code.
- This tutorial builds on the previous tutorials, and it assumes you have already worked through them.
Getting Started
As with the previous tutorials, we will be using the TechDemo class as our starting point. We will be adding code to the CreateScene method, so go ahead and clear out the contents of that method now.
Frame Listeners
In Ogre's C++, we would register a class to receive notification before and after a frame is rendered to the screen. Such a class is known as a Frame Listener. In Axiom this behavior has been translated to .NET events. This means that using frame listeners is silghtly different in Axiom compared to Ogre. In Ogre it is necessary to register a whole class as a frame listener (observer pattern) and implement several methods for all the events described below. In Axiom on the other hand we just use events and we register single methods only to the events we are interested in.
Frame Events
There are three frame events in Axiom:
| FrameStarted | Called just before a frame is rendered. |
| FrameRenderingQueued | Called after the viewport has rendered, but before the render window has flipped buffers. |
| FrameEnded | Called just after a frame has been rendered. |
When we register a Frame Event Handler, A FrameEventArgs object is passed to it. The FrameEventArgs object contains three variables, two of which are useful to note: TimeSinceLastFrame, whichkeeps track of how long it's been since the FrameStarted or FrameEnded last fired. Note that in the FrameStarted method, FrameEvent.timeSinceLastFrame will contain how long it has been since the last FrameStarted event was last fired (not the last time a FrameEnded event was fired). Next, StopRendering, which is a boolean variable. When set to true, this tells Axiom that it's time to start Shutting down.
One thing to remember, is that if you register more than one Event Handler, it can be easy to loose track of which order they get called in. For this reason, it's typically good practice to register only one FrameListener, and call the needed methods in the proper order from there.
So, which one of the three frame listener events should you choose?
Since nothing happens in between the FrameEnded and FrameStarted methods being called, you can use them almost interchangably. Where you decide to put your code is entirely up to you. You can put it all in one big FrameStarted or FrameEnded handler, or you could divide it up between the two.
However, if you want to update your stuff once per frame, you should do that in the FrameRenderingQueued event, because that one is called just before the GPU is made busy by flipping the render buffer. So you want to keep your CPU busy while the GPU works.
Or, to quote Ogre's API docs:
The usefulness of this event comes from the fact that rendering
commands are queued for the GPU to process. These can take a little
while to finish, and so while that is happening the CPU can be doing
useful things. Once the request to 'flip buffers' happens, the thread
requesting it will block until the GPU is ready, which can waste CPU
cycles. Therefore, it is often a good idea to use this callback to
perform per-frame processing. Of course because the frame's rendering
commands have already been issued, any changes you make will only
take effect from the next frame, but in most cases that's not noticeable.
In other words as a rule of thumb you should use FrameRenderingQueued to perform your per-frame updates for performance.
Registeriing a Frame Event Handler
Given all of the above, registering a frame listener in Axiom is very easy. Add the following code to your tutorial class:
void Instance_FrameStarted(object sender, FrameEventArgs e)
{
}
public override void CreateScene()
{
Root.Instance.FrameStarted += new EventHandler<FrameEventArgs>(Instance_FrameStarted);
}
Private Sub Instance_FrameStarted(sender As Object, e As FrameEventArgs)
End Sub Public Overrides Sub CreateScene() Root.Instance.FrameStarted += New EventHandler(Of FrameEventArgs)(AddressOf Instance_FrameStarted) End Sub
def Instance_FrameStarted(sender, e)
pass
def CreateScene(self):
Root.Instance.FrameStarted += Instance_FrameStarted;
There, now anything that is put inside of Instance_FrameStarted will be called every Frame, So let's doing something with that, in this next part we will be Handling Input.
Input Handling
In this tutorial we will add more mouse and keyboard processing to modify the scene. We will be moving a ninja around and turning a light on and off. . Add the following code to your class:
void HandleKeyboardInput(FrameEventArgs e)
{
}
void Instance_FrameStarted(object sender, FrameEventArgs e)
{
HandleKeyboardInput(e);
}
public override void CreateScene()
{
Root.Instance.FrameStarted += new EventHandler<FrameEventArgs>(Instance_FrameStarted);
}
Private Sub HandleKeyboardInput(e As FrameEventArgs)
End Sub Private Sub Instance_FrameStarted(sender As Object, e As FrameEventArgs) HandleKeyboardInput(e) End Sub Public Overrides Sub CreateScene() Root.Instance.FrameStarted += New EventHandler(Of FrameEventArgs)(AddressOf Instance_FrameStarted) End Sub
def HandleKeyboardInput(e):
pass
def Instance_FrameStarted(sender, e):
HandleKeyboardInput(e)
def CreateScene():
Root.Instance.FrameStarted +=Instance_FrameStarted;
Before we start handling input, it's worth mentioning how we will be managing input. Axiom provides a class for us called InputReader. InputReader is actually a wrapper around your Rendering Systems input management. DirectInput for DirectX9 and OpenTK's input for OpenGL. While this works for now, it's somewhat bad practice to have "a wrapper for a wrapper" and because of this it becomes necessary to have the proper Platform Manager for the rendering system you're using: Axiom.PlatformManagers.Win32 for DirectX and Axiom.Platforms.OpenTK for OpenGL. This can be somewhat of a hassle if you're trying to swap Render Systems or Platforms. In any case, TechDemo has initialized an InputReader object for us called 'input' and we will be using this.
In your HandleKeyboardInput method, place the following line:
input.Capture();
input.Capture()
self.input.Capture()
Technically TechDemo already does this for us, but it's useful in letting us see how to Capture both Keyboard and Mouse states and makes sure that we have the most up to date States for the next part.
Add the following code to your Tutorial class:
Entity ninjaEntity;
SceneNode ninjaNode;
Light light;
float lightToggleTimeout = 0;
void HandleKeyboardInput(FrameEventArgs e)
{
input.Capture();
}
void Instance_FrameStarted(object sender, FrameEventArgs e)
{
HandleKeyboardInput(e);
}
public override void CreateScene()
{
Root.Instance.FrameStarted += new EventHandler<FrameEventArgs>(Instance_FrameStarted);
scene.AmbientLight = new ColorEx(0.25f, 0.25f, 0.25f);
ninjaEntity = scene.CreateEntity("Ninja", "ninja.mesh");
ninjaNode = scene.RootSceneNode.CreateChildSceneNode("NinjaNode");
ninjaNode.AttachObject(ninjaEntity);
light = scene.CreateLight("pointLight");
light.Type = LightType.Point;
light.Position = new Vector3(250, 150, 250);
light.Diffuse = ColorEx.White;
light.Specular = ColorEx.White;
}
Private ninjaEntity As Entity Private ninjaNode As SceneNode Private light As Light Private lightToggleTimeout As Single = 0
Private Sub HandleKeyboardInput(e As FrameEventArgs) input.Capture() End Sub Private Sub Instance_FrameStarted(sender As Object, e As FrameEventArgs) HandleKeyboardInput(e) End Sub Public Overrides Sub CreateScene() Root.Instance.FrameStarted += New EventHandler(Of FrameEventArgs)(AddressOf Instance_FrameStarted)
scene.AmbientLight = New ColorEx(0.25F, 0.25F, 0.25F)
ninjaEntity = scene.CreateEntity("Ninja", "ninja.mesh") ninjaNode = scene.RootSceneNode.CreateChildSceneNode("NinjaNode") ninjaNode.AttachObject(ninjaEntity)
light = scene.CreateLight("pointLight") light.Type = LightType.Point light.Position = New Vector3(250, 150, 250) light.Diffuse = ColorEx.White light.Specular = ColorEx.White
End Sub
ninjaEntity = None
ninjaNode = None
light = None
lightToggleTimeout = 0
def HandleKeyboardInput(e):
input.Capture()
def Instance_FrameStarted( sender, e):
HandleKeyboardInput(e);
def CreateScene(self):
Root.Instance.FrameStarted += Instance_FrameStarted
self.scene.AmbientLight = ColorEx(0.25f, 0.25f, 0.25f)
ninjaEntity = self.scene.CreateEntity("Ninja", "ninja.mesh")
ninjaNode = self.scene.RootSceneNode.CreateChildSceneNode("NinjaNode")
ninjaNode.AttachObject(ninjaEntity)
light = self.scene.CreateLight("pointLight")
light.Type = LightType.Point
light.Position = Vector3(250, 150, 250)
light.Diffuse = ColorEx.White
light.Specular = ColorEx.White
}
Keyboard
We want to move the ninja around using the I,J,K,L keys. So let's do so. Add the following code to your ProcessUnbufferedInput method, underneath input.Capture():
Vector3 translateVector = Vector3.Zero;
if (input.IsKeyPressed(Input.KeyCodes.I))
{
translateVector.z -= 1;
}
if (input.IsKeyPressed(Input.KeyCodes.K))
{
translateVector.z += 1;
}
if (input.IsKeyPressed(Input.KeyCodes.J))
{
translateVector.x -= 1;
}
if (input.IsKeyPressed(Input.KeyCodes.L))
{
translateVector.x += 1;
}
ninjaNode.Translate(translateVector * e.TimeSinceLastFrame, TransformSpace.Local);
Dim translateVector As Vector3 = Vector3.Zero
If input.IsKeyPressed(Input.KeyCodes.I) Then translateVector.z -= 1 End If If input.IsKeyPressed(Input.KeyCodes.K) Then translateVector.z += 1 End If If input.IsKeyPressed(Input.KeyCodes.J) Then translateVector.x -= 1 End If If input.IsKeyPressed(Input.KeyCodes.L) Then translateVector.x += 1 End If
ninjaNode.Translate(translateVector * e.TimeSinceLastFrame, TransformSpace.Local)
translateVector = Vector3.Zero;
if (input.IsKeyPressed(Input.KeyCodes.I)):
translateVector.z -= 1
if (input.IsKeyPressed(Input.KeyCodes.K)):
translateVector.z += 1
if (input.IsKeyPressed(Input.KeyCodes.J)):
translateVector.x -= 1
if (input.IsKeyPressed(Input.KeyCodes.L)):
translateVector.x += 1
ninjaNode.Translate(translateVector * e.TimeSinceLastFrame, TransformSpace.Local)
In this code we are moving the ninja every frame a distance relative to the length of the frame. This is measured by the time passed since the last frame, which is a float value. If we are running at 50 FPS, the value of timeSinceLastFrame will be 0.02f. We multiply that by our vector, which is always of the same constant length - 200 and we get a 200pt/sec movement vector, regardless of FPS. The TransformSpace.Local parameter means we move the Ninja relative to his own rotation, which will help us later when we rotate him.
You will use this technique a lot so make sure you understand it now - almost anything that happens repeatedly in a frame listener must use sizes relative to the frame's time.
Next we'll turn the spacebar into a light switch - every time the spacebar is pressed, the point light we created will turn on and off. Add the following code to your ProcessKeyboardInput method:
if (input.IsKeyPressed(Input.KeyCodes.Space))
light.IsVisible = !light.IsVisible;
If input.IsKeyPressed(Input.KeyCodes.Space) Then light.IsVisible = Not light.IsVisible End If
if (input.IsKeyPressed(Input.KeyCodes.Space)):
light.IsVisible = not light.IsVisible;
Compile and run the code. What happens when we press the spacebar button? Instead of switching on and off, the light flickers. What gives? Even the most seasoned gamer presses a button for one or two hundred miliseconds. That's enough time for a few frames. In each one of these frames the keyboard object would say that the spacebar is down. Our code then switches the light on or off. And the result is that the light flickers at the rate of the FPS. This is clearly not what we want. But how can we overcome this?
There are a few ways to overcome this problem, but we'll demonstrate a simple one here. Remember the lightToggleTimeout variable we mentioned before? Well now we'll use it. Replace the above code with the following one:
if (lightToggleTimeout > 0)
{
lightToggleTimeout -= e.TimeSinceLastFrame;
}
else
{
if (input.IsKeyPressed(Input.KeyCodes.Space))
{
light.IsVisible = !light.IsVisible;
lightToggleTimeout = 0.5f;
}
}
If lightToggleTimeout > 0 Then lightToggleTimeout -= e.TimeSinceLastFrame Else If input.IsKeyPressed(Input.KeyCodes.Space) Then light.IsVisible = Not light.IsVisible lightToggleTimeout = 0.5F End If End If
if (lightToggleTimeout > 0):
lightToggleTimeout -= e.TimeSinceLastFrame
else:
if (input.IsKeyPressed(Input.KeyCodes.Space)):
light.IsVisible = not light.IsVisible
lightToggleTimeout = 0.5
Compile and run the code. The spacebar light switch works as expected now. Why?
What we've done is simple: each time we flick the light switch on or off, we set lightToggleTimeout to 0.5. We then ignore the spacebar button for as long as lightToggleTimeout is bigger than zero, and we reduce it by the length of the frame. In other words lightToggleTimeout is a timer of half a second that we set each time we switch the light on or off.
This is also a common technique that you should get used to - creating simple timers by setting a variable to the length of time we want to wait first, then subtracting from it the length of the frame in each frame until it reaches zero or below.
This technique works but it isn't a perfect system in our case. First of if we leave the spacebar pressed the light will still flicker at a rate of twice a second. If we press the spacebar very fast on purpose, it won't respond as fast since it's limited by our timer. In other words we're still not detecting a button press properly, we are just using a simple delay mechanism delay to stop the light from flickering. Still this is good enough for some things and it's enough to demonstrate the basic usage of inputReader. A more advanced system could be to use a boolean variable to remember the previous state of the spacebar, and so detect a key press rather than rely on IsKeyPressed.
Now that we have our ninja moving. Let's use the mouse to get him turning.
Mouse
The MouseState object has X, Y and Z properties that tell us where the mouse cursor is in terms of integer screen coordinates (The Z represents the position on the Scroll Wheel) There are two properties for each Axis, Relative and Absolute. Absolute gives the position of the cursor exactly where it is on the render window. Relative gives how much the mouse has moved since the last time the Mouse State was polled.
So let's rotate the ninja based on mouse movement. The TechDemo uses the mouse movement to rotate the camera and we want to disable this behavior while the left mouse button is down and do something else with the mouse movement. This is easy to do. If you haven't already override the OnFrameStarted method from TechDemo:
protected override void OnFrameStarted(object source, FrameEventArgs evt)
{
base.OnFrameStarted(source, evt);
}
Protected Overrides Sub OnFrameStarted(source As Object, evt As FrameEventArgs) MyBase.OnFrameStarted(source, evt) End Sub
def OnFrameStarted(self, object source, FrameEventArgs evt):
TechDemo.OnFrameStarted(self, source, evt)
Comment out the base.OnFrameStarted(), MyBase.OnFrameStarted, or replace TechDemo.OnFrameStarted() with pass, for C#, VB.Net, and Python respectively. This will stop the default behavior of TechDemo.
Now then add the Method HandleMouseInput and call it from the FrameStarted event handler:
void HandleMouseInput(FrameEventArgs e)
{
}
void Instance_FrameStarted(object sender, FrameEventArgs e)
{
HandleKeyboardInput(e);
HandleMouseInput(e);
}
Private Sub HandleMouseInput()
End Sub Private Sub Instance_FrameStarted(sender As Object, e As FrameEventArgs) HandleKeyboardInput(e) HandleMouseInput(e) End Sub
def HandleMouseInput(FrameEventArgs e):
pass
def Instance_FrameStarted(sender, e):
HandleKeyboardInput(e)
HandleMouseInput(e)
Now then, add the following line to your HandleMouseInput:
ninjaNode.Yaw(-input.RelativeMouseX * e.TimeSinceLastFrame);
ninjaNode.Yaw(-input.RelativeMouseX * e.TimeSinceLastFrame)
ninjaNode.Yaw(-input.RelativeMouseX * e.TimeSinceLastFrame)
This should translate the relative mouse movement to rotation of the ninja left and right using Yaw which you've seen before. We are multiplying this value by the time length of the frame, as we should, right?
Compile and run this program you might need to move the mouse a lot to notice any movement at all. What's happening?
The problem here is that inputReader already give us a relative value between captures. It doesn't matter how much time passed since the last frame, the relative mouse movement since the last Capture call is still the same. Remember that it's measured in integer values. We then took this relative integer value, and multiplied it by the frame length. The result is skewed because it's relative twice.
This flashes out another rule with frame listeners - when the values you are dealing with are already relative you should use them as they are and not multiply them by the frame length. This is something to look out for which might not always be obvious. Your best guide is playing with different frame rates and making sure that the program behaves the same way under different FPSs.
Fixing this problem is simple, simply change the Yaw call above and remove the multiplication by the frame length:
ninjaNode.Yaw(-input.RelativeMouseX);
ninjaNode.Yaw(-input.RelativeMouseX)
ninjaNode.Yaw(-input.RelativeMouseX)
This concludes the usage of keyboard and mouse movement with InputReader. Of course, with .Net there are other options available for input: [The old Basic Tutorial 4 http://axiom3d.net/wiki/index.php/Basic_Tutorial_4] talks about how to handle input using Windows.Forms.Input.



