When I finished the pursuit camera service, I was unhappy with some aspects of it. Fixing them requires implementing the camera as an entity. Most importantly this allows me to update the position every frame and requires the camera to not be the main camera. As a nice consequence implementing this as an entity allows the camera to play well with the record and playback functionality.
Implementing a new camera entity is easy. I just create a new C# file in my existing Pursuit Camera project and add a new class which derives from CameraEntity.
namespace Robotics.PursuitCamera { [DataContract] public class PursuitCameraEntity : CameraEntity {
To configure the entity it needs properties that define its behavior
string _targetName; [DataMember, Browsable(true), Description("Name of the entity to track")] public string TargetName { get { return _targetName; } set { if (value != _targetName) { _targetName = value; FindTarget(); } } } [DataMember, Browsable(true), Description("Minimum distance to keep the camera from the entity")] public float MinDistance { get; set; } [DataMember, Browsable(true), Description("Maximum distance to keep the camera from the entity")] public float MaxDistance { get; set; } [DataMember, Browsable(true), Description("Height above ground plane to keep the camera")] public float Altitude { get; set; }
These properties clearly define the essential minimum information that the entity needs to track a target. The name of the target is, as before, the entity name to target, the other properties should be fairly obvious.
Note: the setter for TargetName calls a method FindTarget(), I'll come back to that when I discuss the initialization code.
FindTarget()
Simulation entities typically get constructed in one of two scenarios.
Knowing this, I implement two constructors, and a method to populate all the configuration with sensible default values.
public PursuitCameraEntity() { SetDefaults(); } public PursuitCameraEntity(string target) : base() { _targetName = target; SetDefaults(); } private void SetDefaults() { MinDistance = 4; MaxDistance = 6; Altitude = 2; OcclusionThreshold = 0.5f; }
Regardless of how the entity is created, it will be initialized before it is used. I use this as the opportunity to find the target entity, and to validate the camera type.
public override void Initialize(xna.Graphics.GraphicsDevice device, PhysicsEngine physicsEngine) { base.Initialize(device, physicsEngine); if (CameraModel != CameraModelType.FirstPerson) { CameraModel = CameraModelType.FirstPerson; } FindTarget(); } private void FindTarget() { if (HasBeenInitialized) { var query = new VisualEntity(); query.State.Name = _targetName; Activate( Arbiter.Choice( SimulationEngine.GlobalInstancePort.Query(query), success => Target = success.Entity, CcrServiceBase.EmptyHandler ) ); } } public VisualEntity Target; void Activate<T>(params T[] tasks) where T : ITask { SimulationEngine.GlobalInstance.Activate(tasks); }
So, after calling the base Initialize() method, I check that the camera is a first person camera type (which simplifies managing the viewing parameters of the camera) and call FindTarget(), which allows me to post a query to the SimulationEngine that looks for an entity with the name _targetName. If an entity is found then the Target field is set appropriately
Initialize()
_targetName
Target
Note: querying the simulation engine is an asynchronous process, so I will need to be careful to check that it has completed successfully before I rely on any result.
The only thing that remains is to implement the tracking logic on each frame. The Update() method of each VisualEntity is called once per frame. So all I need to do is override that method, and implement the same logic as I did in the service implementation.
public override void Update(FrameUpdate update) { if (Target != null) { // vector to the camera, used for computing the new position var entityToCamera = Location - Target.Position; var distance = entityToCamera.Length(); if (distance > 0.1) { float scale = 1; // scale the vector if the distance is outside the bounds if (distance > MaxDistance) { scale = MaxDistance / distance; } else if (distance < MinDistance) { scale = MinDistance / distance; } var scaled = xna.Vector3.Multiply(entityToCamera, scale); // set the new camera position, although the altitude will be off. var newCamera = Target.Position + scaled; // constrain the altitude. newCamera.Y = Altitude; base.SetViewParameters(newCamera, Target.Position); } } base.Update(update); }
That's all the coding needed to implement this as a simulation entity. Now all that remains is to add this entity to a scene. To do this:
samples\Config\MobileRobots.P3DX.Simulation.manifest.xml
P3DXMotorBase
Only two things really remain at this point