XNA 4 tutorial: Selecting and moving objects with a cursor

Today’s tutorial is about selecting and moving 3D models with a cursor.

Cursor

First thing to do, is implementing a cursor. Luckily there are already two implementations on AppHub:

Within these two implementations a Cursor class is derived from DrawableGameComponent. This Cursor class would be just another input component within the simple game engine. Until now each input component was derived from GameComponent. Since it is easier for a user to control the Cursor, if it is visible, the Cursor class should be a DrawableGameComponent. The good news about this: there is no impact on the game architecture. Let’s have a look at the diagram:
[singlepic id=2129]

Selecting an entity

After adding the new Cursor to the game engine, the next step is integrating the picking code.

Requirements

Both implementations from the AppHub require 3D models with a precalculated bounding sphere and access to the triangle vertex data. A Triangle Vertex Content Processor does the job. Just add the existing Content Pipeline project from the example to your solution and set the Content Processor to the Triangle Processor within the 3D model file properties.

New selection interface

For precise selection (picking) results triangle accuracy shall be used. The debug drawing from the AppHub example will be removed.
Next the ModelEntityComponent class needs some extension in order to be selectable. Please have a look at its new ISelectableEntity interface:

interface ISelectableEntity
{
    /// 
    /// Highlights the entity, if it is selected.
    /// 
    bool Selected { get; set; }

    /// 
    /// Get/Set if the entity can be selected by a cursor ray.
    /// 
    bool Selectable { get; set; }

    /// 
    /// Is a given ray intersecting this entity? First do a fast BoundingSphere check, then a precise vertex triangle check.
    /// 
    /// The ray used to test the intersection with this entity.
    /// Request which intersection must be at least between ray and entity.
    /// Result of the intersection between the ray and the entity.
    IntersectionResult Intersects(Ray ray, Intersection intersectionTarget);
}

This interface defines some important features:

  • An Entity can be selectable or not (e. g. walls, houses, statues).
  • Each Entity is responsible for highlighting itself and how it is highlighted.
  • Each Entity does the selection check with a cursor ray.

The following code shows an Entity changing its color on selection. The ISelectableEntity implementation is inherited from the ModelEntityComponent:

class SphereEntityComponent : ModelEntityComponent
{
    private PhongEffect effect;
    private Color defaultAmbientColor;

    public Color SelectedColor { get; set; }

    public SphereEntityComponent(Game game, ICamera camera)
        : base(game, camera)
    {
        Selectable = true; // This Entity is selectable (default is false).
    }

    protected override void LoadContent()
    {
        var content = Game.Content;

        Model = content.Load("models/Sphere");

        // Load effect
        effect = content.LoadDerivedEffect();
        effect.DiffuseTexture = content.Load("textures/pak/metal/metal009_diffuse");
        Effect = effect;

        defaultAmbientColor = effect.AmbientLight;

        base.LoadContent();
    }

    public override void Draw(GameTime gameTime)
    {
        if (Selected)
        {
            effect.AmbientLight = SelectedColor;
        }
        else
        {
            effect.AmbientLight = defaultAmbientColor;
        }

        base.Draw(gameTime);
    }
}

Moving a selected entity

The Cursor component updates its position on the screen and checks, if the user is selecting any entity. Once the user has selected an entity, he can move it like the cursor.

/// 
/// Update gets the current gamepad state and mouse state and uses that data to
/// calculate where the cursor's position is on the screen. On xbox, the position
/// is clamped to the viewport so that the cursor can't go off the screen. On
/// windows, doing something like that would be rude. :)
/// Also check if user has selected a entity in order to take control over it.
/// 
/// Provides a snapshot of timing values.
public override void Update(GameTime gameTime) // from CursorComponent class
{
    if (gameTime == null)
    {
        throw new ArgumentNullException("gameTime");
    }

    // Update Mouse and GamePad inputs
    UpdateInputDevices(gameTime);

    // Select entity
    if (SelectorButtonClicked)
    {
        if (IsEntitySelected) // Deselect entity
        {
            // Restore cursor position to the center of the entity
            cursorPosition = SelectedEntity.ScreenProjectedPosition();
            SetCursorPosition(cursorPosition);

            // Deselect
            SelectedEntity.Selected = false;
            SelectedEntity = null;
        }
        else // Select entity if there is any
        {
            var cursorRay = CalculateCursorRay(Camera.Projection, Camera.View);
            SelectedEntity = EntitySelector.Select(entities, cursorRay);

            if (IsEntitySelected)
            {
                SelectedEntity.Selected = true;
            }
        }
    }

    // Move selected entity
    if(IsEntitySelected) {
        // Move selected entity in relation to the cursor movement
        var cursorPositionChanged = (!previousCursorPosition.Equals(cursorPosition));
        var cameraPositionChanged = (!previousCameraPosition.Equals(Camera.Position));
        if (cursorPositionChanged || cameraPositionChanged)
        {
            var cursorDelta = (cursorPosition - previousCursorPosition) * EntityTranslationSpeed *
                                    (float)gameTime.ElapsedGameTime.TotalSeconds;
            var cameraDelta = (Camera.Position - previousCameraPosition) * CameraTranslationSpeed * 
                                    (float)gameTime.ElapsedGameTime.TotalSeconds;

            SelectedEntity.Translate(Vector3.Right, cameraDelta.X + cursorDelta.X);
            SelectedEntity.Translate(Vector3.Down, cameraDelta.Y + cursorDelta.Y);
            SelectedEntity.Translate(Vector3.Backward, cameraDelta.Z);
        }
    }

    previousCameraPosition = Camera.Position;

    base.Update(gameTime);
}

Modifying entity selection behaviour

The algorithm used to select a entity is located in a separate EntitySelector class, so it is possible to exchange it with another algorithm. Here is a implementation of finding the closest entity in front of the users view:

class ClosestVertexTriangleEntitySelector : IEntitySelector
{
    /// 
    /// Defines which entity is selected from a list of entities.
    /// 
    /// The source entity pool where one is selected from.
    /// The ray for the intersection test.
    /// Based on the implementation an entity object is returned or null if there is no selectable entity.
    public IEntity Select(IEnumerable entities, Ray ray)
    {
        IEntity result = null;

        // Keep track of the closest object we have seen so far, so we can
        // choose the closest one if there are several models under the cursor.
        float? closestIntersection = float.MaxValue;

        foreach (var entity in entities)
        {
            var intersectionResult = entity.Intersects(ray, Intersection.VertexTriangle);

            if (intersectionResult.Intersection == Intersection.VertexTriangle)
            {
                // Is it closer than any other previously intersected entity?
                if (intersectionResult.Distance < closestIntersection)
                {
                    closestIntersection = intersectionResult.Distance;
                    result = entity;
                }
            }
        }

        return result;
    }
}

Bug and change report

Since the last tutorial following bugfixing and refactoring has been made:

  • All Model Processors are now stored in a single project
  • All entities are sharing a loaded Model object, so each entity must reassign its effect to the ModelMesh before it is drawn in order to have individual effects.

Resources


Note: Obviously this solution is yet lacking of any collision detection. :-)

Download tutorial solution (source code)


Beitrag veröffentlicht

in

von