Click or drag to resize
DigitalRuneStep 6: Game object service

In this step we will add game logic to control the camera with keyboard and mouse.

Add a game object to control the camera

Currently, the camera is static. To simplify debugging, I always try to add code which moves the camera as soon as possible. Code that controls objects of the game, like the camera, is called game logic. We could add the code to MyGame or MyGameComponent, but it is better to put it into a separate class. It is good to separate game logic into separate classes to keep the code clean and to allow reusing the code in other projects. The DigitalRune Engine provides the base class DigitalRune.Game.GameObject. This is the ideal place for game logic.

First, let's remove the camera code from MyGraphicsScreen.cs:

MyGraphicsScreen.cs
public MyGraphicsScreen(IGraphicsService graphicsService)
  : base(graphicsService)
{
    var spriteFont = graphicsService.Content.Load<SpriteFont>("SpriteFont1");
    DebugRenderer = new DebugRenderer(graphicsService, spriteFont);

    // var projection = new PerspectiveProjection();                          // REMOVE
    // projection.SetFieldOfView(                                             // REMOVE
    //     ConstantsF.PiOver4,                                                // REMOVE
    //         graphicsService.GraphicsDevice.Viewport.AspectRatio,           // REMOVE
    //         0.1f,                                                          // REMOVE
    //         100);                                                          // REMOVE
    // var camera = new Camera(projection);                                   // REMOVE
    // CameraNode = new CameraNode(camera);                                   // REMOVE
    // CameraNode.PoseWorld = new Pose(new Vector3F(0, 1, 5));                // REMOVE
}
…

Let's add a new item CameraObject.cs to the project. Here is the code of the CameraObject:

CameraObject.cs
using System;
using DigitalRune.Game;
using DigitalRune.Game.Input;
using DigitalRune.Geometry;
using DigitalRune.Graphics;
using DigitalRune.Graphics.SceneGraph;
using DigitalRune.Mathematics;
using DigitalRune.Mathematics.Algebra;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MathHelper = DigitalRune.Mathematics.MathHelper;

namespace MyGame
{
    public class CameraObject : GameObject
    {
        private Game _game;
        private IInputService _inputService;
        private float _currentYaw;
        private float _currentPitch;

        public CameraNode CameraNode { get; private set; }

        public CameraObject()
        {
            _game = ServiceLocator.Current.GetInstance<Game>();
            _inputService = ServiceLocator.Current.GetInstance<IInputService>();

            var graphicsService = ServiceLocator.Current.GetInstance<IGraphicsService>();
            var projection = new PerspectiveProjection();
            projection.SetFieldOfView(
                ConstantsF.PiOver4,
                    graphicsService.GraphicsDevice.Viewport.AspectRatio,
                    0.1f,
                    100);
            var camera = new Camera(projection);
            CameraNode = new CameraNode(camera);
            CameraNode.PoseWorld = new Pose(new Vector3F(0, 1, 3));
        }

        protected override void OnLoad()
        {
        }

        protected override void OnUnload()
        {
        }

        protected override void OnUpdate(TimeSpan deltaTime)
        {
            _inputService.EnableMouseCentering = _game.IsActive;

            float deltaTimeF = (float)deltaTime.TotalSeconds;

            Vector2F mousePositionDelta = _inputService.MousePositionDelta;

            float deltaYaw = -mousePositionDelta.X;
            _currentYaw += deltaYaw * deltaTimeF * 0.1f;

            float deltaPitch = -mousePositionDelta.Y;
            _currentPitch += deltaPitch * deltaTimeF * 0.1f;

            // Limit the pitch angle to +/- 90°.
            _currentPitch = MathHelper.Clamp(_currentPitch, -ConstantsF.PiOver2, ConstantsF.PiOver2);

            // Compute new orientation of the camera.
            QuaternionF orientation = QuaternionF.CreateRotationY(_currentYaw) * QuaternionF.CreateRotationX(_currentPitch);

            Vector3F velocity = Vector3F.Zero;
            KeyboardState keyboardState = _inputService.KeyboardState;
            if (keyboardState.IsKeyDown(Keys.W))
                velocity.Z--;
            if (keyboardState.IsKeyDown(Keys.S))
                velocity.Z++;
            if (keyboardState.IsKeyDown(Keys.A))
                velocity.X--;
            if (keyboardState.IsKeyDown(Keys.D))
                velocity.X++;
            if (keyboardState.IsKeyDown(Keys.R))  // R … "raise"
                velocity.Y++;
            if (keyboardState.IsKeyDown(Keys.F))  // F … "fall"
                velocity.Y--;

            velocity = orientation.Rotate(velocity);
            Vector3F translation = velocity * deltaTimeF * 5;

            CameraNode.LastPoseWorld = CameraNode.PoseWorld;
            CameraNode.PoseWorld = new Pose(
                CameraNode.PoseWorld.Position + translation,
                orientation);
        }
    }
}

Here are few important notes about the code:

  • The CameraObject derives from GameObject.
  • The CameraObject overrides a few methods of the GameObject base class: OnLoad, OnUnload, OnUpdate.
  • In OnUpdate, we enable mouse centering:
    C#
    _inputService.EnableMouseCentering = _game.IsActive;
    XNA does not have a relative mouse mode. The mouse position is always absolute and if the mouse cursor hits the screen border, it stops. This is not suitable for typical first-person shooter controls. To create a relative mouse mode, we have to reset the mouse position in each frame, so that it can never reach the screen border. This is the purpose of EnableMouseCentering. Mouse centering should be automatically disabled when the game is not active. That means, when another desktop application is active and the XNA game is in the background, the game should not interfere with the mouse movement.
  • In OnUpdate, the class reads the mouse input and computes a new pose (= orientation + position) for the CameraNode.
  • The previous pose of the CameraNode is stored in LastPoseWorld:
    C#
    CameraNode.LastPoseWorld = CameraNode.PoseWorld;
    This is necessary for some advanced effects, e.g. motion blur effects will compare LastPoseWorld and the current PoseWorld to compute how much the scene must be blurred.

Now, we need something that calls OnLoad, OnUnload and OnUpdate methods of the game object. This is the game object service.

Add the game object service

Let's add the game object service to Game1.cs:

Game1.cs
using DigitalRune.Game;                                                                 // NEWnamespace MyGame
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        …
        private GameObjectManager _gameObjectManager;                                   // NEW
        …

        protected override void Initialize()
        {
          _services = new ServiceContainer();
          ServiceLocator.SetLocatorProvider(() => _services);

          _inputManager = new InputManager(false);
          _services.Register(typeof(IInputService), null, _inputManager);

          _graphicsManager = new GraphicsManager(GraphicsDevice, Window, Content);
          _services.Register(typeof(IGraphicsService), null, _graphicsManager);

          _gameObjectManager = new GameObjectManager();                                 // NEW
          _services.Register(typeof(IGameObjectService), null, _gameObjectManager);     // NEW

          Components.Add(new MyGameComponent(this));

          base.Initialize();
        }

        protected override void Update(GameTime gameTime)
        {
          _inputManager.Update(gameTime.ElapsedGameTime);

          base.Update(gameTime);

          _gameObjectManager.Update(gameTime.ElapsedGameTime);                          // NEW
        }
        …

The service is registered in the service container as usual. It is updated in Game1.Update. You might wonder why the input manager is updated before base.Update(gameTime) and the game object manager is update last. The reason is: base.Update(gameTime) updates the XNA game components, like MyGameComponent. Since MyGameComponent and the CameraObject use the input service, the input manager should be updated first. As you will see next, we use MyGameComponent to load the CameraObject. Therefore, it makes sense to update the XNA game components before the game objects.

However, this update order is not set in stone. The DigitalRune Engine is modular and gives you full control over the update order.

To use the camera game object add the following code to MyGameComponent.cs:

MyGameComponent.cs
using DigitalRune.Game;                                                                         // NEWnamespace MyGame
{
    public class MyGameComponent : Microsoft.Xna.Framework.GameComponent
    {
        …
        public MyGameComponent(Game game)
            : base(game)
        {
            _inputService = ServiceLocator.Current.GetInstance<IInputService>();

            _graphicsService = ServiceLocator.Current.GetInstance<IGraphicsService>();
            
            _myGraphicsScreen = new MyGraphicsScreen(_graphicsService);
            _graphicsService.Screens.Add(_myGraphicsScreen);

            var gameObjectService = ServiceLocator.Current.GetInstance<IGameObjectService>();   // NEW
            var cameraObject = new CameraObject();                                              // NEW
            _myGraphicsScreen.CameraNode = cameraObject.CameraNode;                             // NEW
            gameObjectService.Objects.Add(cameraObject);                                        // NEW
            
            _myGraphicsScreen.DebugRenderer.DrawText("MyGame");
            _myGraphicsScreen.DebugRenderer.DrawAxes(Pose.Identity, 1, false);
        }

        …

This code creates the camera game object, tells the graphics screen about the camera and adds the game object to the game object service. The game object manager automatically calls GameObject.OnLoad/OnUnload when a game object is added to or removed from the game object service. Each frame, when GameObjectManager.Update is executed by the Game class, the OnUpdate method of the game objects is called, and the CameraObject will update the camera.

Run the game. Now, you can move the camera using the keys W, A, S, D, R, F and the mouse.