Move the player

The player object

  1. In the Hierarchy, select + and choose 3D Object ▸ Capsule. Rename it Player and set its Position to 0, 1, 0.

  2. Drag Player.mat onto Material 0 in its MeshRenderer.

  3. Add a CapsuleCollider, then a Rigidbody.

    The player selected: Player.mat on Material 0, then its CapsuleCollider and Rigidbody
  4. In the Rigidbody, select freezeRotationX, freezeRotationY and freezeRotationZ.

    The Rigidbody's freezeRotationX, freezeRotationY and freezeRotationZ, selected

The Rigidbody makes the player part of the physics simulation: it falls, it stops at the walls, and your code moves it by setting its velocity. Freezing its rotation keeps the capsule upright when it runs into something.

The pickup sound

  1. Drag a short sound file onto the Asset Browser: a .wav, .ogg, .mp3 or .flac. It is copied into the folder the Asset Browser shows.

    The dropped sound in the Asset Browser, with the import reported in the status bar
  2. Select the player and add an AudioSource. The player plays the sound, not the coin: a sound playing on the coin would stop when the coin disappears.

    The AudioSource added below the player's other components
  3. Select Main Camera and add an AudioListener. It is where the game's sound is heard from, and without one nothing is.

    The AudioListener added to Main Camera

The PlayerController component

  1. Create a component named PlayerController, as you did for the coin, and replace its two files:
Source/PlayerController.h
#pragma once

#include "Shalltear.h"

// moves the player with the keyboard/a gamepad + collects the coins it touches
//
// Behavior FIRST -> PhysicsListener second. A component -> hears physics
// events is a Behavior -> also implements the listener (in that order)
class PlayerController : public Behavior, public PhysicsListener {
public:
	PlayerController();

	SLT_REFLECT_COMPONENT(PlayerController, Behavior)
	SLT_REFLECT_FIELD(speed)
	SLT_REFLECT_FIELD(pickupSound)
	SLT_END_REFLECT()

	// how fast the player runs, in m/s
	f32 speed = 6.0f;
	// played by the player's AudioSource each time a coin is collected
	Handle<AudioClip> pickupSound;

	i32 GetCoins() const { return _coins; }

protected:
	void OnEnable() override;
	void OnDisable() override;
	void OnUpdate() override;
	void OnTriggerEnter(Handle<Collider> other) override;

private:
	// set of actions, each with the keys/gamepad controls bound to it.
	// Reads input only while enabled -> OnEnable/OnDisable turn it on/off
	// with the component
	InputActionMap _controls;
	ActionId _move;
	i32 _coins = 0;
};
Source/PlayerController.cpp
#include "PlayerController.h"

#include "Coin.h"

SLT_REGISTER_TYPE(PlayerController)

PlayerController::PlayerController() {
	// one action with two axes. x = left/right, y = forward/back.
	// Every binding adds share (W + D together move diagonally)
	_move = _controls.RegisterAction("Move"_hash, ActionType::Axis2D);
	_controls.Bind(_move, {.code = u16(Key::D), .scale = 1.0f, .component = 0});
	_controls.Bind(_move, {.code = u16(Key::A), .scale = -1.0f, .component = 0});
	_controls.Bind(_move, {.code = u16(Key::W), .scale = 1.0f, .component = 1});
	_controls.Bind(_move, {.code = u16(Key::S), .scale = -1.0f, .component = 1});
	// left stick feeds the same two axes
	_controls.Bind(_move, {.source = InputBinding::Source::GamepadAxis, .code = u16(GamepadAxis::LeftX), .component = 0});
	_controls.Bind(_move, {.source = InputBinding::Source::GamepadAxis, .code = u16(GamepadAxis::LeftY), .component = 1});
}

void PlayerController::OnEnable() {
	_controls.Enable();
}

void PlayerController::OnDisable() {
	_controls.Disable();
}

void PlayerController::OnUpdate() {
	// reads this frame's input into the map. Once per frame/before query
	_controls.Evaluate();
	Vec2 input = _controls.GetAxis2D(_move);
	// a key + the stick together can add up past full speed
	if (input.Length() > 1.0f) {
		input = input.Normalized();
	}

	Rigidbody *body = GetOwner()->GetComponent<Rigidbody>().Get();
	if (!body) {
		return;
	}
	// set horizontal speed, keep the vertical one (so the player still
	// falls). The physics engine moves the body. Moving the object directly
	// would skip it through walls
	Vec3 velocity = body->GetLinearVelocity();
	velocity.x = input.x * speed;
	velocity.z = input.y * speed;
	body->SetLinearVelocity(velocity);
}

// runs when player touches a trigger collider (like a coin's)
void PlayerController::OnTriggerEnter(Handle<Collider> other) {
	// what did we touch?
	Collider *collider = other.Get();
	SceneObject *object = collider ? collider->GetOwner().Get() : nullptr;
	Coin *coin = object ? object->GetComponent<Coin>().Get() : nullptr;
	if (!coin) {
		return;
	}

	_coins += coin->value;
	// the sound plays on the PLAYER (which stays). A sound playing on the coin
	// would stop the moment the coin is destroyed
	if (AudioSource *audio = GetOwner()->GetComponent<AudioSource>().Get()) {
		audio->PlayOneShot(pickupSound.Get());
	}
	// the coin disappears at the end of this frame
	SceneSystem::Destroy(collider->GetOwner());
}
  1. Save both files, add PlayerController to the player, and drag your sound from the Asset Browser onto its pickupSound field.

    The sound being dragged onto the PlayerController's pickupSound field

Press Ctrl+P. Move with W A S D or the left stick, and each coin you touch disappears with a sound. The keyboard reaches the game while the Runtime View has focus, so click it if the player does not move.

The player among the coins in the Runtime View

Save the scene. Next, add a timer, a goal and a pause menu.