Camera Shake

Aug 30, 2011 at 9:56 PM

Since i just implemented this for my game i thought i'd share - small modification to the base Camera3D class to incorporate the camera shake functionality from the MS sample

using Indiefreaks.Xna.Core;
using Indiefreaks.Xna.Input;
using Microsoft.Xna.Framework;
using SynapseGaming.LightingSystem.Core;
using System;

namespace Indiefreaks.Xna.Rendering.Camera
{
    /// <summary>
    ///   Abstract 3d camera
    /// </summary>
    /// <remarks>
    ///   Override UpdateInput and UpdateViewMatrix to create your own custom camera
    /// </remarks>
    public abstract class Camera3D : ICamera
    {
        private float _aspectRatio;
        private float _farPlaneDistance;
        private float _fieldOfView;
        private bool _isProjectionDirty;
        private float _nearPlaneDistance;

        private bool _shaking;
        private float _shakeMagnitude;
        private float _shakeDuration;
        private float _shakeTimer;
        private Vector3 _shakeOffset;
        private Random _random;

        /// <summary>
        ///   Creates a new Camera instance
        /// </summary>
        /// <param name = "aspectRatio">The viewport aspect ratio</param>
        /// <param name = "fieldOfView">The _instances of view expressed in radians</param>
        /// <param name = "nearPlaneDistance">The nearest point in projected space of the camera</param>
        /// <param name = "farPlaneDistance">The farest point in projected space of the camera</param>
        protected Camera3D(float aspectRatio, float fieldOfView, float nearPlaneDistance,
                         float farPlaneDistance)
        {
            _aspectRatio = aspectRatio;
            _fieldOfView = fieldOfView;
            _nearPlaneDistance = nearPlaneDistance;
            _farPlaneDistance = farPlaneDistance;

            _isProjectionDirty = true;

            SceneState = new SceneState();
            SceneEnvironment = new SceneEnvironment();

            _random = new Random();
        }

        /// <summary>
        ///   Returns the SunBurn SceneState associated with this camera
        /// </summary>
        public SceneState SceneState { get; private set; }

        /// <summary>
        /// Returns the SunBurn SceneEnvironment associated with this camera
        /// </summary>
        public SceneEnvironment SceneEnvironment { get; set; }

        /// <summary>
        ///   Gets or sets the Vector3 serving as the target position
        /// </summary>
        public Vector3 TargetPosition { get; set; }

        /// <summary>
        ///   Returns the View matrix of the camera
        /// </summary>
        public Matrix ViewMatrix { get; private set; }

        /// <summary>
        ///   Returns the Projection matrix of the camera
        /// </summary>
        public Matrix ProjectionMatrix { get; private set; }

        /// <summary>
        ///   Gets or sets the _instances of view used to calculate the Projection matrix
        /// </summary>
        public float FieldOfView
        {
            get { return _fieldOfView; }
            set
            {
                if (_fieldOfView != value)
                {
                    _fieldOfView = value;
                    _isProjectionDirty = true;
                }
            }
        }

        /// <summary>
        ///   Gets or sets the aspect ratio used to calculate the Projection matrix
        /// </summary>
        public float AspectRatio
        {
            get { return _aspectRatio; }
            set
            {
                if (_aspectRatio != value)
                {
                    _aspectRatio = value;
                    _isProjectionDirty = true;
                }
            }
        }

        /// <summary>
        ///   Gets or sets the near plane distance used to calculate the Projection matrix
        /// </summary>
        public float NearPlaneDistance
        {
            get { return _nearPlaneDistance; }
            set
            {
                if (_nearPlaneDistance != value)
                {
                    _nearPlaneDistance = value;
                    _isProjectionDirty = true;
                }
            }
        }

        /// <summary>
        ///   Gets or sets the far plane distance used to calculate the Projection matrix
        /// </summary>
        public float FarPlaneDistance
        {
            get { return _farPlaneDistance; }
            set
            {
                if (_farPlaneDistance != value)
                {
                    _farPlaneDistance = value;
                    _isProjectionDirty = true;
                }
            }
        }

        /// <summary>
        ///   Gets or sets the position of the camera
        /// </summary>
        public Vector3 Position { get; set; }

        private float NextFloat()
        {
            return (float)_random.NextDouble() * 2f - 1f;
        }

        void ICamera.Update(GameTime gameTime)
        {
            UpdateInput(Application.Input);

            if (_shaking)
            {
                _shakeTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
                if (_shakeTimer >= _shakeDuration)
                {
                    _shaking = false;
                    _shakeTimer = _shakeDuration;
                }
                float progress = _shakeTimer / _shakeDuration;
                float magnitude = _shakeMagnitude * (1f - (progress * progress));
                _shakeOffset = new Vector3(NextFloat(), NextFloat(), NextFloat()) * magnitude;

                Position += _shakeOffset;
                TargetPosition += _shakeOffset;
            }

            if (SceneEnvironment.VisibleDistance != _farPlaneDistance && !_isProjectionDirty)
                FarPlaneDistance = SceneEnvironment.VisibleDistance;
            else if (SceneEnvironment.VisibleDistance != _farPlaneDistance && _isProjectionDirty)
                SceneEnvironment.VisibleDistance = _farPlaneDistance;
            
            // if one of the projection matrix properties has changed, we update the Projection matrix
            if (_isProjectionDirty)
            {
                ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(_fieldOfView, _aspectRatio, _nearPlaneDistance,
                                                                       _farPlaneDistance);
                _isProjectionDirty = false;
            }


            ViewMatrix = UpdateViewMatrix(gameTime);
        }

        public void Shake(float magnitude, float duration)
        {
            _shaking = true;
            _shakeMagnitude = magnitude;
            _shakeDuration = duration;
            _shakeTimer = 0f;
        }

        /// <summary>
        ///   Override this method to catch input events and act on the camera
        /// </summary>
        /// <param name = "input">The current input instance</param>
        protected abstract void UpdateInput(InputManager input);

        /// <summary>
        ///   Override this method to update the ViewMatrix property
        /// </summary>
        /// <param name = "gameTime" />
        protected abstract Matrix UpdateViewMatrix(GameTime gameTime);

        /// <summary>
        /// Initializes the Camera and SunBurn SceneState to be used for rendering
        /// </summary>
        /// <param name="gameTime"/>
        /// <param name="frameBuffers"/>
        public void BeginFrameRendering(GameTime gameTime, FrameBuffers frameBuffers)
        {
            SceneState.BeginFrameRendering(ViewMatrix, ProjectionMatrix, gameTime, SceneEnvironment, frameBuffers, true);
        }

        /// <summary>
        /// Finalizes Camera and SunBurn SceneState for this frame
        /// </summary>
        public void EndFrameRendering()
        {
            SceneState.EndFrameRendering();
        }
    }
}

Coordinator
Sep 5, 2011 at 4:10 PM

Cool stuff!

I'll be working soon on making IGF cameras inheriting from SceneEntity which will bring the cool SunBurn Component system to it which will make this shaking feature a good candidate for a Camera component ;)

Regards,

Philippe

Sep 18, 2011 at 8:41 PM

Hello bamyazi,

Just wanted to say thank you for sharing!  I've ported over the key parts of code to work with the Farseer 2d camera and it works great, big time saver.

Thank you,

Nick

using System;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class Camera2D
{
    private const float _minZoom = 0.02f;
    private const float _maxZoom = 20f;
    private static GraphicsDevice _graphics;

    private Matrix _batchView;

    private Vector2 _currentPosition;

    private float _currentRotation;

    private float _currentZoom;
    private Vector2 _maxPosition;
    private float _maxRotation;
    private Vector2 _minPosition;
    private float _minRotation;
    private bool _positionTracking;
    private Matrix _projection;
    private bool _rotationTracking;
    private Vector2 _targetPosition;
    private float _targetRotation;
    private Body _trackingBody;
    private Vector2 _translateCenter;
    private Matrix _view;

    public BoundingFrustum Frustum;

    private bool _shaking;
    private float _shakeMagnitude;
    private float _shakeDuration;
    private float _shakeTimer;
    private Vector2 _shakeOffset;
    private Random _random = new Random();

    /// <summary>
    /// The constructor for the Camera2D class.
    /// </summary>
    /// <param name="graphics"></param>
    public Camera2D(GraphicsDevice graphics)
    {
        _graphics = graphics;
        _projection = Matrix.CreateOrthographicOffCenter(0f, ConvertUnits.ToSimUnits(_graphics.Viewport.Width),
                                                         ConvertUnits.ToSimUnits(_graphics.Viewport.Height), 0f, 0f,
                                                         1f);
        _view = Matrix.Identity;
        _batchView = Matrix.Identity;

        _translateCenter = new Vector2(ConvertUnits.ToSimUnits(_graphics.Viewport.Width / 2f),
                                       ConvertUnits.ToSimUnits(_graphics.Viewport.Height / 2f));

        Frustum = new BoundingFrustum(_view * _projection);

        ResetCamera();
    }

    public Matrix View
    {
        get { return _batchView; }
    }

    public Matrix SimView
    {
        get { return _view; }
    }

    public Matrix SimProjection
    {
        get { return _projection; }
    }

    /// <summary>
    /// The current position of the camera.
    /// </summary>
    public Vector2 Position
    {
        get { return ConvertUnits.ToDisplayUnits(_currentPosition); }
        set
        {
            _targetPosition = ConvertUnits.ToSimUnits(value);
            if (_minPosition != _maxPosition)
            {
                Vector2.Clamp(ref _targetPosition, ref _minPosition, ref _maxPosition, out _targetPosition);
            }
        }
    }

    /// <summary>
    /// The furthest up, and the furthest left the camera can go.
    /// if this value equals maxPosition, then no clamping will be 
    /// applied (unless you override that function).
    /// </summary>
    public Vector2 MinPosition
    {
        get { return ConvertUnits.ToDisplayUnits(_minPosition); }
        set { _minPosition = ConvertUnits.ToSimUnits(value); }
    }

    /// <summary>
    /// the furthest down, and the furthest right the camera will go.
    /// if this value equals minPosition, then no clamping will be 
    /// applied (unless you override that function).
    /// </summary>
    public Vector2 MaxPosition
    {
        get { return ConvertUnits.ToDisplayUnits(_maxPosition); }
        set { _maxPosition = ConvertUnits.ToSimUnits(value); }
    }

    /// <summary>
    /// The current rotation of the camera in radians.
    /// </summary>
    public float Rotation
    {
        get { return _currentRotation; }
        set
        {
            _targetRotation = value % MathHelper.TwoPi;
            if (_minRotation != _maxRotation)
            {
                _targetRotation = MathHelper.Clamp(_targetRotation, _minRotation, _maxRotation);
            }
        }
    }

    /// <summary>
    /// Gets or sets the minimum rotation in radians.
    /// </summary>
    /// <value>The min rotation.</value>
    public float MinRotation
    {
        get { return _minRotation; }
        set { _minRotation = MathHelper.Clamp(value, -MathHelper.Pi, 0f); }
    }

    /// <summary>
    /// Gets or sets the maximum rotation in radians.
    /// </summary>
    /// <value>The max rotation.</value>
    public float MaxRotation
    {
        get { return _maxRotation; }
        set { _maxRotation = MathHelper.Clamp(value, 0f, MathHelper.Pi); }
    }

    /// <summary>
    /// The current rotation of the camera in radians.
    /// </summary>
    public float Zoom
    {
        get { return _currentZoom; }
        set
        {
            _currentZoom = value;
            _currentZoom = MathHelper.Clamp(_currentZoom, _minZoom, _maxZoom);
        }
    }

    /// <summary>
    /// the body that this camera is currently tracking. 
    /// Null if not tracking any.
    /// </summary>
    public Body TrackingBody
    {
        get { return _trackingBody; }
        set
        {
            _trackingBody = value;
            if (_trackingBody != null)
            {
                _positionTracking = true;
            }
        }
    }
    public Vector2 LastPosition;
    public bool RestrictX = false;
    public bool RestrictY = false;

    public bool EnableTrackingSmoothing = true;

    public bool EnablePositionTracking
    {
        get { return _positionTracking; }
        set { _positionTracking = value; }
    }

    public bool EnableRotationTracking
    {
        get { return _rotationTracking; }
        set
        {
            if (value && _trackingBody != null)
            {
                _rotationTracking = true;
            }
            else
            {
                _rotationTracking = false;
            }
        }
    }

    public bool EnableTracking
    {
        set
        {
            EnablePositionTracking = value;
            EnableRotationTracking = value;
        }
    }

    public void MoveCamera(Vector2 amount)
    {
        _currentPosition += amount;
        if (_minPosition != _maxPosition)
        {
            Vector2.Clamp(ref _currentPosition, ref _minPosition, ref _maxPosition, out _currentPosition);
        }
        _targetPosition = _currentPosition;
        _positionTracking = false;
        _rotationTracking = false;
    }

    public void RotateCamera(float amount)
    {
        _currentRotation += amount;
        if (_minRotation != _maxRotation)
        {
            _currentRotation = MathHelper.Clamp(_currentRotation, _minRotation, _maxRotation);
        }
        _targetRotation = _currentRotation;
        _positionTracking = false;
        _rotationTracking = false;
    }

    /// <summary>
    /// Resets the camera to default values.
    /// </summary>
    public void ResetCamera()
    {
        _currentPosition = Vector2.Zero;
        _targetPosition = Vector2.Zero;
        _minPosition = Vector2.Zero;
        _maxPosition = Vector2.Zero;

        _currentRotation = 0f;
        _targetRotation = 0f;
        _minRotation = -MathHelper.Pi;
        _maxRotation = MathHelper.Pi;

        _positionTracking = false;
        _rotationTracking = false;

        _currentZoom = 1f;

        SetView();
    }

    public void Jump2Target()
    {
        _currentPosition = _targetPosition;
        _currentRotation = _targetRotation;

        SetView();
    }

    private void SetView()
    {
        Matrix matRotation = Matrix.CreateRotationZ(_currentRotation);
        Matrix matZoom = Matrix.CreateScale(_currentZoom);
        Vector3 translateCenter = new Vector3(_translateCenter, 0f);
        Vector3 translateBody = new Vector3(-_currentPosition, 0f);

        _view = Matrix.CreateTranslation(translateBody) *
                matRotation *
                matZoom *
                Matrix.CreateTranslation(translateCenter);

        translateCenter = ConvertUnits.ToDisplayUnits(translateCenter);
        translateBody = ConvertUnits.ToDisplayUnits(translateBody);

        _batchView = Matrix.CreateTranslation(translateBody) *
                     matRotation *
                     matZoom *
                     Matrix.CreateTranslation(translateCenter);

        Frustum.Matrix = _view * _projection;
    }

    public bool isVisible(Vector2 position)
    {
        return Frustum.Contains(new Vector3(position.X, position.Y, 0)) == ContainmentType.Contains;
    }

    private float NextFloat()
    {
        return (float)_random.NextDouble() * 2f - 1f;
    }

    public void Shake(float magnitude, float duration)
    {
        _shaking = true;
        _shakeMagnitude = magnitude;
        _shakeDuration = duration;
        _shakeTimer = 0f;
    }

    /// <summary>
    /// Moves the camera forward one timestep.
    /// </summary>
    public void Update(GameTime gameTime)
    {
        if (_trackingBody != null)
        {
            if (_positionTracking)
            {
                LastPosition = _targetPosition;
                _targetPosition = _trackingBody.Position;

                if (_minPosition != _maxPosition)
                {
                    Vector2.Clamp(ref _targetPosition, ref _minPosition, ref _maxPosition, out _targetPosition);
                }
            }
            if (_rotationTracking)
            {
                _targetRotation = -_trackingBody.Rotation % MathHelper.TwoPi;
                if (_minRotation != _maxRotation)
                {
                    _targetRotation = MathHelper.Clamp(_targetRotation, _minRotation, _maxRotation);
                }
            }
        }

        if (!EnableTrackingSmoothing)
        {
            if (RestrictX && RestrictY)
            {
                return;
            }
            else if (RestrictX && !RestrictY)
            {
                _currentPosition = new Vector2(_currentPosition.X, _targetPosition.Y);
            }
            else if (!RestrictX && RestrictY)
            {
                _currentPosition = new Vector2(_targetPosition.X, _currentPosition.Y);
            }
            else
            {
                _currentPosition = _targetPosition;
            }

            SetView();
            return;
        }

        Vector2 delta = _targetPosition - _currentPosition;
        float distance = delta.Length();
        if (distance > 0f)
        {
            delta /= distance;
        }
        float inertia;
        if (distance < 10f)
        {
            inertia = (float)Math.Pow(distance / 10.0, 2.0);
        }
        else
        {
            inertia = 1f;
        }

        float rotDelta = _targetRotation - _currentRotation;

        float rotInertia;
        if (Math.Abs(rotDelta) < 5f)
        {
            rotInertia = (float)Math.Pow(rotDelta / 5.0, 2.0);
        }
        else
        {
            rotInertia = 1f;
        }
        if (Math.Abs(rotDelta) > 0f)
        {
            rotDelta /= Math.Abs(rotDelta);
        }
                
        LastPosition = _currentPosition;
        _currentPosition += 100f * delta * inertia * (float)gameTime.ElapsedGameTime.TotalSeconds;
        _currentRotation += 80f * rotDelta * rotInertia * (float)gameTime.ElapsedGameTime.TotalSeconds;

        CalculateShake(gameTime);
        SetView();
    }

    /// <summary>
    /// Moves the camera forward one timestep.
    /// </summary>
    public void Update(GameTime gameTime, Vector2 trackThisPosition)
    {

        if (_positionTracking)
        {
            LastPosition = _targetPosition;
            _targetPosition = trackThisPosition;

            if (_minPosition != _maxPosition)
            {
                Vector2.Clamp(ref _targetPosition, ref _minPosition, ref _maxPosition, out _targetPosition);
            }
        }
        if (_rotationTracking)
        {
            _targetRotation = -_trackingBody.Rotation % MathHelper.TwoPi;
            if (_minRotation != _maxRotation)
            {
                _targetRotation = MathHelper.Clamp(_targetRotation, _minRotation, _maxRotation);
            }
        }

        if (!EnableTrackingSmoothing)
        {
            if (RestrictX && RestrictY)
            {
                return;
            }
            else if (RestrictX && !RestrictY)
            {
                _currentPosition = new Vector2(_currentPosition.X, _targetPosition.Y);
            }
            else if (!RestrictX && RestrictY)
            {
                _currentPosition = new Vector2(_targetPosition.X, _currentPosition.Y);
            }
            else
            {
                _currentPosition = _targetPosition;
            }

            CalculateShake(gameTime);
            SetView();
            return;
        }

        Vector2 delta = _targetPosition - _currentPosition;
        float distance = delta.Length();
        if (distance > 0f)
        {
            delta /= distance;
        }
        float inertia;
        if (distance < 10f)
        {
            inertia = (float)Math.Pow(distance / 10.0, 2.0);
        }
        else
        {
            inertia = 1f;
        }

        float rotDelta = _targetRotation - _currentRotation;

        float rotInertia;
        if (Math.Abs(rotDelta) < 5f)
        {
            rotInertia = (float)Math.Pow(rotDelta / 5.0, 2.0);
        }
        else
        {
            rotInertia = 1f;
        }
        if (Math.Abs(rotDelta) > 0f)
        {
            rotDelta /= Math.Abs(rotDelta);
        }

        LastPosition = _currentPosition;
        _currentPosition += 100f * delta * inertia * (float)gameTime.ElapsedGameTime.TotalSeconds;
        _currentRotation += 80f * rotDelta * rotInertia * (float)gameTime.ElapsedGameTime.TotalSeconds;

        CalculateShake(gameTime);
        SetView();
    }

    private void CalculateShake(GameTime gameTime)
    {
        if (_shaking)
        {
            _shakeTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (_shakeTimer >= _shakeDuration)
            {
                _shaking = false;
                _shakeTimer = _shakeDuration;
            }
            float progress = _shakeTimer / _shakeDuration;
            float magnitude = _shakeMagnitude * (1f - (progress * progress));
            _shakeOffset = new Vector2(NextFloat(), NextFloat()) * magnitude;
            _currentPosition += _shakeOffset;
        }
    }

    public Vector2 ConvertScreenToWorld(Vector2 location)
    {
        Vector3 t = new Vector3(location, 0);

        t = _graphics.Viewport.Unproject(t, _projection, _view, Matrix.Identity);

        return new Vector2(t.X, t.Y);
    }

    public Vector2 ConvertWorldToScreen(Vector2 location)
    {
        Vector3 t = new Vector3(location, 0);

        t = _graphics.Viewport.Project(t, _projection, _view, Matrix.Identity);

        return new Vector2(t.X, t.Y);
    }
}