Basic Tutorial 4
From Axiom
Beginner Tutorial 4: Frame Listeners and Windows.Forms Input
- Original version by Clay Culver
- Port to Axiom Crickhollow by patientfox
Any problems you encounter while working with this tutorial should be posted to the Crickhollow Forum.
Contents |
Prerequisites
SPECIAL NOTE: This tutorial was developed with the Crickhollow alpha3 (REV 1216) release of the engine. It uses a special, "Crickhollow-friendly" version of the ExampleApplication class, which can be found at ExampleApplicationCrickhollow.cs. This version of the class conforms to the previous version as closely as possible with naming conventions and so on. But, there are still a few differences.
This tutorial assumes you have knowledge of C# programming and are able to setup and compile an Ogre application (if you have trouble setting up your application (although it uses hobbiton, it is quite useful. Replace the Hobbiton ExampleApplication.cs with ExampleApplicationCrickhollow.cs as neccesary and you will be good-to-go), see Basic Tutorial 1 for help). This tutorial builds on the previous tutorials, so be sure that you have already worked through them.
Introduction
In this tutorial we will be introducing one of the most useful Axiom constructs: FrameStarted/Ended events (known traditionally as "FrameListeners"). By the end of this tutorial you will understand what FrameListeners events accomplish and how to use them to do things that require updates every frame. We will use FrameListeners in this tutorial to introduce another useful concept: how to use Axiom's default input handling API: InputReader.
As you go through the tutorial you should be slowly adding code to your own project and watching the results as we build it.
Getting Started
As with the previous tutorials, we will be using a pre-constructed code base as our starting point. Create a Console Application project and replace the code in Program.cs with the following:
using System;
using System.Collections.Generic;
using Axiom;
using Axiom.Core;
using Axiom.Graphics;
using Axiom.Input;
using Axiom.Math;
using ExampleApplicationCrickhollow;
namespace Tutorial04
{
static class Program
{
static void Main()
{
CustomInputApplication app = new CustomInputApplication();
app.Run();
}
}
class CustomInputApplication : ExampleApplication
{
const float TRANSLATE = 200;
const float ROTATE = 0.2f;
protected float camSpeed = 0.0f;
Vector3 _translation = Vector3.Zero;
Vector3 _lastPosition = Vector3.Zero;
Vector3 _mouseRotation = Vector3.Zero;
protected override void CreateScene()
{
SceneManager.AmbientLight = new ColorEx(0.25f, 0.25f, 0.25f);
Entity ent = SceneManager.CreateEntity("Ninja", "ninja.mesh");
SceneManager.RootSceneNode.CreateChildSceneNode().AttachObject(ent);
Light light = SceneManager.CreateLight("Light");
light.Type = LightType.Point;
light.Position = new Vector3(250, 150, 250);
light.Diffuse = ColorEx.White;
light.Specular = ColorEx.White;
Camera.Position = new Vector3(0, 200, 400);
Camera.LookAt(ent.BoundingBox.Center);
}
// this is the once-per-frame, check for key-pressing of all of
// our valid push keys.
protected override void UpdateInput(object source, FrameEventArgs e)
{
// call this once per cycle to get input
Input.Capture();
// example of dealing with key input
if (Input.IsKeyPressed(KeyCodes.A))
{
// do something
}
}
protected override void SetupFrameHandlers()
{
}
protected override void OnFrameStarted(object source, FrameEventArgs e)
{
}
protected override void OnFrameEnded(object source, FrameEventArgs e)
{
// things to be processed post-render
}
}
}
Make sure you can compile and run the application before continuing. If you are having difficulty, refer to Basic Tutorial 1 or post to the forums. Since we have overridden the UpdateInput() method and not added useful code to it yet, mouse and camera movement will be disabled for now. If everything is working right, you should be staring at the back of the ninja mesh.
In this tutorial we will start with a "default" scene (see the code snippet above). The only thing interesting to note is the Camera.LookAt() call. In this line of code we have set the camera to look at the center of the entity. This works because the node we created resides at the origin (0, 0, 0). If you want to duplicate this functionality later on you have to add the node's position to the entity's bounding box's center.
FrameListeners
Introduction
In the previous tutorials we only looked at what we could do when we create the scene. In Axiom, we can register a class to receive notification before and after a frame is rendered to the screen. The Root class provides the FrameStarted and FrameEnded events to handle these actions.
Axiom's main loop looks like this:
- The Root object fires the FrameStarted event.
- The Root object renders one frame.
- The Root object fires the FrameEnded event.
This loops until any of the event handlers throw an exception or other measures are taken to interrupt the flow of the program. The FrameEvent object contains two variables, but only the timeSinceLastFrame is useful in a FrameListener. This variable keeps 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).
One important concept to realize about Axiom's FrameListeners is that the order in which they are called is entirely up to Axiom. You cannot determine which FrameListener is called first, second, third...and so on. If you need to ensure that FrameListeners are called in a certain order, then you should register only one FrameListener and have it call all of the objects in the proper order (hence our overriding of the OnFrameStarted()/OnFrameEnded() methods).
You might also notice that the main loop really only does three things, and since nothing happens in between the FrameEnded and FrameStarted methods being called, you can use them almost interchangably. Where you decide to put all of 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.
Frame Listeners vs Timers
Frame listeners are one of the most useful Axiom constructs, as they allow us to incrementally update objects in the scene. You may have also noticed that a Timer (Axiom.Core.Timer) could do the same thing, and they allow you to control how often they are fired (as opposed to the frame listeners which fire as fast as you are rendering). In practice, these two things are virtually interchangable, but I have found that these usage guidelines work well for me:
- If you are updating objects which are being rendered you should use a frame listener to update it every frame. For example, if you are moving an object incrementally across the screen, you should use frame listeners.
- If you are performing an action which should happen often, but the result of which is not being directly rendered to the screen, you should use a Timer. For example, let's say your program is running at 600 FPS. You do not need to poll the keyboard, joystick(s), and network interfaces every frame (which would be 600 times per second) when polling it 10 times per second wouldn't be a noticable difference to the user.
You should mix and match frame listeners and timers based on your needs for the program. For the purposes of this tutorial, we will discuss implementing user input with FrameListeners.
Registering a FrameListener
Currently the code we have will run, but since we have overridden the SetupFrameHandlers() method of ExampleApplication, keyboard and mouse input is no longer working. In this tutorial we will be adding mouse and keyboard input back into the program.
Since the Root class is what will render frames, it also is in charge of keeping track of FrameListeners. The first thing we need to do is register a FrameStarted event handler in our "CustomInputApplication" class. Find the SetupFrameHandlers() method in CustomInputApplication and add the following code to it:
Root.FrameStarted += OnFrameStarted;
Then create a new function to handle this event called "FrameStarted":
protected override void OnFrameStarted(object source, FrameEventArgs e)
{
UpdateDebugOverlay(source, e);
UpdateInput(source, e);
}
Be sure you can compile and run the application before continuing. Notice that the Debug Overlay is now being updated. But why isn't input working? We'll fix that in the next section.
Handling User Input
Introduction
Now that we have a basic application set up, we will be writing code to move the camera based on mouse and keyboard input. Since moving the camera changes what we render every frame, we will use a frame listener for this instead of a timer.
Our strategy for keyboard input is to keep track of camera movement with a single Vector3 variable. When the user presses and releases specific keys we will add and subtract from this vector and move the camera by this amount every frame. Our strategy for handling mouse input is a bit more tricky. We want to rotate the camera only when the right mouse button is held down. To accomplish this we will need a boolean variable to keep track of the state of the right mouse button. If you look in the CustomInputApplication class you will see these two variables.
Key Input
The first thing we will do is translate the camera based on key input. Find the UpdateInput() method in CustomInputApplication and add the following code:
// this is the once-per-frame, check for key-pressing of all of
// our valid push keys.
protected override void UpdateInput(object source, FrameEventArgs e)
{
// call this once per cycle to get input
Input.Capture();
float _camScale = TRANSLATE * e.TimeSinceLastFrame;
_translation = Vector3.Zero;
if (Input.IsKeyPressed(KeyCodes.W)||Input.IsKeyPressed(KeyCodes.Up)) _translation.z -= TRANSLATE;
if (Input.IsKeyPressed(KeyCodes.S) || Input.IsKeyPressed(KeyCodes.Down)) _translation.z += TRANSLATE;
if (Input.IsKeyPressed(KeyCodes.A) || Input.IsKeyPressed(KeyCodes.Left)) _translation.x -= TRANSLATE;
if (Input.IsKeyPressed(KeyCodes.D) || Input.IsKeyPressed(KeyCodes.Right)) _translation.x += TRANSLATE;
if (Input.IsKeyPressed(KeyCodes.Q) || Input.IsKeyPressed(KeyCodes.PageUp)) _translation.y += TRANSLATE;
if (Input.IsKeyPressed(KeyCodes.E) || Input.IsKeyPressed(KeyCodes.PageDown)) _translation.y -= TRANSLATE;
}
Next we will add code for actually move the camera, based off of our input. For this tutorial, we won't be implementing anything too snazzy. Add this code after the previous chunk:
_camVelocity += _translation;
// move the camera based on the accumulated movement vector
Camera.MoveRelative(_camVelocity * e.TimeSinceLastFrame * 200);
_camVelocity = Vector3.Zero;
This moves the camera's position every frame. We multiply the translation by the camera's orientation so that when we translate, we are moving in the correct direction. That means that even when we hold down the W button we move "forward" no matter what direction the camera faces. If we did not do this, our camera would rotate independantly of its movement. (Which is currently not what we are going for.) We multiply the translation by the time since the last frame to keep the movement smooth, and independent of framerate. Also at the end we are multiplying it by a "magic number" fixed value of 200 so that it isn't moving terribly slow, since this is a rather primitive movement algorithm. But as you can see, so far, the camera just starts and stops when we move it (and there may be some skipping artifacts depending on your system). How about a bit of "smoothing" around that?
Replace the previous block of code with the following:
_camVelocity += _translation;
// move the camera based on the accumulated movement vector
Camera.MoveRelative(_camVelocity * e.TimeSinceLastFrame);
// damping .. if the user stops pressing a direction.. we slow down gradually
if (_translation == Vector3.Zero)
{
_camVelocity *= (1 - (6 * e.TimeSinceLastFrame));
}
Compile the code and run it. Much nicer, don't you think? Since we are no longer zeroing out the _camVelocity Vector3 after every frame, we no longer have the need to add in that "magic number" mentioned earlier. Also, when the user lets go a direction key, the camera gradually slows back down and comes to a stop. A bit more visually pleasing than the previous solution. It does have some peculiarities, though (like if you press two directions at once (ie left and up) and then let go of one key but keep pressing the other, you will keep drifting in both directions, since the camera won't begin slowing down until all keys are released).
And while we're on the topic of key input, let's very quickly add in a key to exit the application:
if (Input.IsKeyPressed(KeyCodes.Escape)) Root.Instance.QueueEndRendering();
Well, now we can move the camera about in several directions, but what about being able to actually look around?
Mouse Input
Now we need to add mouse-look to the program. We are going to take the fairly straightforward approach of gathering mouse movement information in a Vector3 which we re-create every frame and multiplying that information by our fixed "rotation rate" value. Add the following block of code to the UpdateInput() method:
// here be mouse-look, in all it's splendor
_mouseRotation = Vector3.Zero;
_mouseRotation.x += Input.RelativeMouseX * ROTATE;
_mouseRotation.y += Input.RelativeMouseY * ROTATE;
And now that we've gathered that information in an acceptable format, let's apply it to the camera the old fashioned way (you'll find out, as you get more experienced that there are "better ways" to solve this problem and problems like it):
Camera.Yaw(-_mouseRotation.x);
Camera.Pitch(-_mouseRotation.y);
That's it, run your program. You can now move and rotate the mouse based on user input.
Final Note
This tutorial shows you the basics of implementing an input system using Axiom.Input and FrameListeners. This namespace is undergoing some change as of the porting of this tutorial (February 2008) and the Input namespace's functionality is incomplete compared to what was previously available in Hobbiton (for example, the KeyEventArgs class doesn't expose information on which key(s) was pressed, thus making KeyDown/KeyUp event handlers worthless for their primary use case. This leaves FrameListener-based input as the only viable option, currently). That being said, the basic functionality required to accomplish typical input tasks is available. An alternative approach would be to tie the UpdateInput() method to a Timer object, if you were using Axiom for an application that didn't have very high demands for user feedback.
Source Code
Here is the full source code for this tutorial:
using System;
using System.Collections.Generic;
using Axiom;
using Axiom.Core;
using Axiom.Graphics;
using Axiom.Input;
using Axiom.Math;
using ExampleApplicationCrickhollow;
namespace Tutorial04
{
static class Program
{
static void Main()
{
CustomInputApplication app = new CustomInputApplication();
app.Run();
}
}
class CustomInputApplication : ExampleApplication
{
const float TRANSLATE = 200;
const float ROTATE = 0.15f;
Vector3 _translation = Vector3.Zero;
Vector3 _camVelocity = Vector3.Zero;
Vector3 _mouseRotation = Vector3.Zero;
protected override void CreateScene()
{
SceneManager.AmbientLight = new ColorEx(0.25f, 0.25f, 0.25f);
Entity ent = SceneManager.CreateEntity("Ninja", "ninja.mesh");
SceneManager.RootSceneNode.CreateChildSceneNode().AttachObject(ent);
Light light = SceneManager.CreateLight("Light");
light.Type = LightType.Point;
light.Position = new Vector3(250, 150, 250);
light.Diffuse = ColorEx.White;
light.Specular = ColorEx.White;
Camera.Position = new Vector3(0, 200, 400);
Camera.LookAt(ent.BoundingBox.Center);
}
// this is the once-per-frame, check for key-pressing of all of
// our valid push keys.
protected override void UpdateInput(object source, FrameEventArgs e)
{
// call this once per cycle to get input
Input.Capture();
float _camScale = TRANSLATE * e.TimeSinceLastFrame;
_translation = Vector3.Zero;
if (Input.IsKeyPressed(KeyCodes.W)||Input.IsKeyPressed(KeyCodes.Up)) _translation.z -= _camScale;
if (Input.IsKeyPressed(KeyCodes.S) || Input.IsKeyPressed(KeyCodes.Down)) _translation.z += _camScale;
if (Input.IsKeyPressed(KeyCodes.A) || Input.IsKeyPressed(KeyCodes.Left)) _translation.x -= _camScale;
if (Input.IsKeyPressed(KeyCodes.D) || Input.IsKeyPressed(KeyCodes.Right)) _translation.x += _camScale;
if (Input.IsKeyPressed(KeyCodes.Q) || Input.IsKeyPressed(KeyCodes.PageUp)) _translation.y += _camScale;
if (Input.IsKeyPressed(KeyCodes.E) || Input.IsKeyPressed(KeyCodes.PageDown)) _translation.y -= _camScale;
_camVelocity += _translation;
// move the camera based on the accumulated movement vector
Camera.MoveRelative(_camVelocity * e.TimeSinceLastFrame);
// damping .. if the user stops pressing a direction.. we slow down gradually
if (_translation == Vector3.Zero)
{
_camVelocity *= (1 - (6 * e.TimeSinceLastFrame));
}
if (Input.IsKeyPressed(KeyCodes.Escape)) Root.Instance.QueueEndRendering();
// here be mouse-look, in all it's splendor
_mouseRotation = Vector3.Zero;
_mouseRotation.x += Input.RelativeMouseX * ROTATE;
_mouseRotation.y += Input.RelativeMouseY * ROTATE;
Camera.Yaw(-_mouseRotation.x);
Camera.Pitch(-_mouseRotation.y);
}
protected override void SetupFrameHandlers()
{
Root.FrameStarted += OnFrameStarted;
}
protected override void OnFrameStarted(object source, FrameEventArgs e)
{
UpdateDebugOverlay(source, e);
UpdateInput(source, e);
}
protected override void OnFrameEnded(object source, FrameEventArgs e)
{
// things to be processed post-render
}
}
}

