Character movement programming

Level: Beginner

In this tutorial we will explain how to setup a playable human-like character with keyboard controls and animations that are synced to the movement. This is similar to the Starter project - Third person shooter and we will go into details of how to set these up so that you can understand what is happening and how to extend it.

The complete code for this project can be cloned from GitHub.

https://github.com/hologyengine/movement-tutorial

Prerequisites

Before continuing, you need to create a new project and familiarise yourself with the basics of the editor. Go through the article The first steps to learn how to set up a project. Then learn how to navigate in the editor using the articles in Editor basics.

While some coding skills are useful, all the code needed will be provided in this tutorial.

Set up the scene

After creating the project, you have an empty scene that you can use for this tutorial. We will just create a couple of objects in this scene to illustrate how to make a character. In order for our character to have something to walk on, we will add a landscape shape. We will also add a spawn point actor so that we can spawn the playable character in the scene from game code.

In the Asset browser, select Shapes and scroll down to find the Landscape item.

Right click the Landscape item click "Add to scene". You will be prompted with some options for creating the landscape. For the purpose of this tutorial, you can leave the defaults as they are and click "Create".

Next, we need to create a spawn point. In the Asset browser, select Actors and then right click Spawn Point and add it to the scene. Click anywhere on the landscape you created earlier to place the spawn point.

Import the character asset

We need a 3D models for our character that has animations that we can play. For this tutorial, we will use a character from an asset pack made by Kenney.

Once you have downloaded the asset pack, unzip the file, find one of the character models such as Models/GLB format/character-human.glb and drag it into the editor to import it. We will later refer to this asset from game code.

Programming a character actor

We will now create an actor class to represent the playable character. Create a file in src/actors/character.ts. Add the code below to the file.


import { Actor, AnimationState, AnimationStateMachine, AssetLoader, BaseActor, ViewController, attach, inject } from "@hology/core/gameplay";
import { CharacterAnimationComponent, CharacterMovementComponent, CharacterMovementMode, ThirdPartyCameraComponent } from "@hology/core/gameplay/actors";

@Actor()
class Character extends BaseActor {
  private animation = attach(CharacterAnimationComponent)
  public readonly movement = attach(CharacterMovementComponent, {
    maxSpeed: 1.5,
    maxSpeedSprint: 4,
    maxSpeedBackwards: 1,
    snapToGround: 0.1,
    autoStepMinWidth: 0,
    autoStepMaxHeight: 0.1,
    fallingReorientation: true,
    fallingMovementControl: 0.2,
    colliderHeight: .4,
    colliderRadius: 0.2,
    jumpVelocity: 3.5
  })
  public readonly thirdPartyCamera = attach(ThirdPartyCameraComponent, {
    height: .7,
    offsetX: -0.3,
    offsetZ: 0.2,
    minDistance: 3,
    maxDistance: 3,
    distance: 3,
  })

  private assetLoader = inject(AssetLoader)

  async onInit(): Promise<void> {
    const { scene, animations } = await this.assetLoader.getModelByAssetName('character-human')

    this.object.add(scene)

    const clips = Object.fromEntries(animations.map(clip => [clip.name, clip]))
  
    const idle = new AnimationState(clips.idle)
    const walk = new AnimationState(clips.walk)
    const sit = new AnimationState(clips.sit)
    const jump = new AnimationState(clips.jump)
    const sprint = new AnimationState(clips.sprint)

    idle.transitionsBetween(walk, () => this.movement.horizontalSpeed > 0)
    walk.transitionsBetween(sprint, () => this.movement.isSprinting)
    idle.transitionsTo(sit, elapsedTime => elapsedTime > 1)
    sit.transitionsTo(walk, () => this.movement.horizontalSpeed > 0)
  
    for (const state of [idle, walk, sit, sprint]) {
      state.transitionsBetween(jump, () => this.movement.mode === CharacterMovementMode.falling)
    }

    const sm = new AnimationStateMachine(idle)

    this.animation.setup(scene)
    this.animation.playStateMachine(sm)
  }

}

export default Character

First we attach a few components to quickly add a lot of functionality to our actor.

  • CharacterAnimationComponent allows us to play animations using a state machine. Read more about Character Animation

  • CharacterMovementComponent can take player input and move the character with common modes such as standing idle, walking, running and jumping. Read more about Character movement

  • ThirdPartyCameraComponent moves the camera to behind the character. It provides options to offset the camera in various ways.

We load an asset using the injected AssetLoader using the name of the asset as can be found in the editor. This gives us a model's scene which is the character mesh and an array of animations.

In the rest of the code we set up the animation state machine. Read the guide on Animation State Machine to get a better understanding of how these work.

Set up player input

To be able to control the character's movement, we will create a player controller service that takes the player's input and forwards it to the character's movement component.

Create a file at src/services/player-controller.ts with the following code.

import { inject, Service } from "@hology/core/gameplay"
import {
  InputService,
  Keybind,
  Mousebind,
  Wheelbind,
} from "@hology/core/gameplay/input"
import Character from "../actors/character"

enum InputAction {
  moveForward,
  moveBackward,
  moveLeft,
  moveRight,
  jump,
  sprint,
  rotate,
  rotateCamera,
  zoomCamera
}

@Service()
class PlayerController {
  private inputService = inject(InputService)
  private character: Character

  constructor() {
    this.inputService.setKeybind(InputAction.jump, new Keybind(" "))
    this.inputService.setKeybind(InputAction.sprint, new Keybind("Shift"))
    this.inputService.setKeybind(InputAction.moveForward, new Keybind("w"))
    this.inputService.setKeybind(InputAction.moveBackward, new Keybind("s"))
    this.inputService.setKeybind(InputAction.moveLeft, new Keybind("a"))
    this.inputService.setKeybind(InputAction.moveRight, new Keybind("d"))
    this.inputService.setMousebind(
      InputAction.rotate,
      new Mousebind(0.01, true, "x")
    )
    this.inputService.setMousebind(
      InputAction.rotateCamera,
      new Mousebind(0.003, false, "y")
    )
    this.inputService.setWheelbind(
      InputAction.zoomCamera,
      new Wheelbind(0.0003, false)
    )
  }

  public setup(character: Character) {
    this.inputService.stop()
    this.character = character
    this.bindCharacterInput()
    this.inputService.start()
  }

  private bindCharacterInput() {
    const playerMove = this.character.movement.directionInput
    const playerJump = this.character.movement.jumpInput
    const playerSprint = this.character.movement.sprintInput

    this.inputService.bindToggle(InputAction.jump, playerJump.toggle)
    this.inputService.bindToggle(InputAction.sprint, playerSprint.toggle)
    this.inputService.bindToggle(InputAction.moveForward, playerMove.togglePositiveY)
    this.inputService.bindToggle(InputAction.moveBackward, playerMove.toggleNegativeY)
    this.inputService.bindToggle(InputAction.moveLeft, playerMove.toggleNegativeX)
    this.inputService.bindToggle(InputAction.moveRight, playerMove.togglePositiveX)
    this.inputService.bindDelta(
      InputAction.rotate,
      this.character.movement.rotationInput.rotateY
    )
    this.inputService.bindDelta(
      InputAction.rotateCamera,
      this.character.thirdPartyCamera.rotationInput.rotateX
    )
    this.inputService.bindDelta(
      InputAction.zoomCamera,
      this.character.thirdPartyCamera.zoomInput.increment
    )

  }
}

export default PlayerController

This code uses the InputService. To get a better understanding of how this works, read the Player input guide.

Spawning and setup

Now we just need to spawn the character and setup the player controller which we can do from the game class found in src/services/game.ts. Replace the contents of this file with the following.


import { GameInstance, Service, World, inject } from '@hology/core/gameplay';
import { SpawnPoint } from '@hology/core/gameplay/actors';
import Character from '../actors/character';
import PlayerController from './player-controller';

@Service()
class Game extends GameInstance {
  private world = inject(World)
  private playerController = inject(PlayerController)

  async onStart() {
    const spawnPoint = this.world.findActorByType(SpawnPoint)
    const character = await spawnPoint.spawnActor(Character)
    this.playerController.setup(character)
  }
}

export default Game

In this code we do a few things in the onStart function which gets called after the game has loaded and is ready to start.

  1. We find a spawn point actor in the world.

  2. Spawn the character using the spawn point and the Character actor class created earlier to get a new character that is placed in the scene with the position and rotation of the spawn point.

  3. Call the setup function on the player controller with the character to bind the input events to this character's movement component.

This is everything necessary to have a playable character with animations.

Testing the game

Run the following command in the terminal to run the game.

npm run dev

This should give you a link such as http://localhost:5173 that you can open in your browser to see the game.

You should now see the game running like in the picture at the start of this tutorial. Try pressing W,A,S,D on your keybaord. Click somewhere in the scene with your mouse first to be able to rotate the character and camera by moving your mouse.

Wrapping up

You now have a starting point of what could be a super fun game.

  • Read more about the features covered in this tutorial by following the links.

  • Design a more interesting world by importing more assets and place them in your scene

  • Create a more interesting game. For example, add some game play mechanisms such as Trigger volumes that could trigger various things as you walk into them them or place other actors that does something when you are nearby and press a button.

Last updated