Critical Hit Creations

Prop replicas, writing, and creative hobbies

Game Development: 3.Input Handling

No comments

Input Handling

Interaction in a game is created by determining what buttons are pressed or released by the player, where the cursor is positioned, and performing some action in response to the input. MonoGame provides an interface for reading player input, being able to report the current state of the keyboard, mouse (and gamepad). In a game environment, it may be important to have knowledge of the previous input state, such as determining if a key is being held, or how far the mouse has moved since the previous frame. I created an InputState class to capture this information, setting the previous mouse and keyboard state at the start of each update, then capturing the current player input:

        
        public void Update(bool isMouseVisible)
        {
            //Only handles input if the application is currently the active application
            if (ScreenManager.GetInstance().Game.IsActive)
            {
                //Record the states of the keyboard and mouse from the previous update
                previousKeyboardState = currentKeyboardState;
                previousMouseState = currentMouseState;

                //Get the current state of the keyboard and mouse
                currentKeyboardState = Keyboard.GetState();
                currentMouseState = Mouse.GetState();

                this.isMouseVisible = isMouseVisible;
            }
        }

For menu screens, the ScreenManager passes this InputState to each screen, which uses it to determine if the cursor is over a menu element, by checking if the mouse’s position overlaps the element’s detection box, or if the menu element has been clicked by checking if the left mouse button has been pressed while overlapping the detection box, i.e.:

   //Button Clicked
if(((input.CurrentMouseState.LeftButton == ButtonState.Pressed && input.PreviousMouseState.LeftButton == ButtonState.Released)&& detectionBox.Contains(new Point(input.CurrentMouseState.X, input.CurrentMouseState.Y))))

If this proves to be true, the button’s OnClick event is fired, providing the primary interaction with the game’s menus.

Game World Control State

For interaction within the game environment itself, I have been building a GameWorldControlState class which is passed the InputState during every engine update, and maps inputs to actions in the game world. The control state defines an enumeration of all the actions the player can take in the game world, such as selecting an ability, interacting with an object, or attacking an enemy. These values are mapped to an input, with a trigger specifying if the action should happen when the input is pressed or released:

        public enum Actions
        {
            SelectAbilityOne,
            SelectAbilityTwo,
            SelectAbilityThree,
            SelectAbilityFour,
            SelectAbilityFive,
            SelectAbilitySix,
            SelectAbilitySeven,
            SelectAbilityEight,
            SelectAbilityNine,
            SelectAbilityTen,
            PrimaryHandClickAbility,
            SecondaryHandClickAbility,
            ToggleShaders
     //...
        }
        public enum Trigger
        {
            Pressed,
            Released
        }

        public struct ActionMapping
        {
            public Actions action;

            //Keyboard/mouse etc.
            public InputType inputType;
            public Trigger trigger;

            //Use if keyboard input
            public Keys keyboardKey;
            //Use if mouse input
            public MouseButton mouseButton;
        }

        protected List<ActionMapping> actionMappings = new List<ActionMapping>();

        public bool ActionMappingTriggered(ActionMapping actionMapping, InputState inputState)
        {
            bool result = false;

            if (actionMapping.trigger == Trigger.Pressed)
            {
                if (actionMapping.inputType == InputType.Keyboard)
                {
                    if (inputState.CurrentKeyboardState.IsKeyDown(actionMapping.keyboardKey) && inputState.PreviousKeyboardState.IsKeyUp(actionMapping.keyboardKey))
                    {
                        result = true;
                    }

                }
                //...
            }
            //...

            return result
        }

When the control state is passed the InputState, it checks the state of each of these action mappings, and marks the action for activation if the trigger terms are true. Once all the actions to trigger have been determined, the control state fires the corresponding event for each action. As the GameWorldControlState class is a singleton, it is globally accessible, enabling any object in the game world to obtain a reference to any of its events, tie an event handler to them, and perform some action when the event is triggered in the control state, i.e.:

//In the player class, tie the event handler for performing the primary hand’s active ability to the corresponding action event in the control state  
GameWorldControlState.GetInstance().PrimaryHandAbilityOnClickEvent += _primaryHandSlot.ActiveAbilityOnClick;   

Mouse Ray Casting

For determining if the player has clicked on an object in the game world, I have written a raycasting function that converts the mouse’s screen space coordinates to world space and builds a Ray using the active camera’s projection and view matrices:    

        public Ray? GetMouseRay(Camera activeCamera)
        {
            Ray? ray = null;
            if (activeCamera!= null)
            {
                //Get the mouse's screen position
                Vector2 mousePoint = new Vector2(CurrentMouseState.X, CurrentMouseState.Y);

                Vector3 nearSource = new Vector3((float)mousePoint.X, (float)mousePoint.Y, 0f);
                Vector3 farSource = new Vector3((float)mousePoint.X, (float)mousePoint.Y, 1f);
                //Unproject the screen position to world space using the active camera
                Vector3 nearPoint = ScreenManager.GetInstance().GraphicsDevice.Viewport.Unproject(nearSource, activeCamera.CameraProjectionMatrix, activeCamera.CameraViewMatrix, Matrix.Identity);
                Vector3 farPoint = ScreenManager.GetInstance().GraphicsDevice.Viewport.Unproject(farSource, activeCamera.CameraProjectionMatrix, activeCamera.CameraViewMatrix, Matrix.Identity);

                // Create a ray from the near clip plane to the far clip plane.
                Vector3 direction = farPoint - nearPoint;
                direction.Normalize();

                // Create a ray.
                 ray = new Ray(nearPoint, direction);
            }

            return ray;
        }

Testing the objects for collision with this ray allows us to determine which objects lie under the mouse cursor, and by comparing the collision points along the ray, we can discover which of these objects is nearest to the screen. Efficiently partitioning the objects in your scene to reduce the search space for raycast tests is a topic I won’t cover in this post, save for briefly mentioning that I have implemented a voxel-based traversal algorithm, the original source of which can be found here: (http://www.cse.yorku.ca/~amana/research/grid.pdf).

No comments :

Post a Comment