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.
  4. In the Rigidbody, select freezeRotationX, freezeRotationY and freezeRotationZ.

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.
  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.
  3. Select Main Camera and add an AudioListener. It is where the game's sound is heard from, and without one nothing is.

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"
#include "Shalltear/Audio/AudioClip.h"
#include "Shalltear/Physics3D/PhysicsListener.h"

// moves the player with the keyboard or a gamepad, and collects the coins it
// touches.
//
// Behavior FIRST, PhysicsListener second: a component that hears physics
// events is a Behavior that 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 meters per second
	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:
	// a set of named actions, each with the keys and gamepad controls bound to
	// it. It reads input only while enabled, so OnEnable and OnDisable turn it
	// on and off with the component
	InputActionMap _controls;
	ActionId _move;
	i32 _coins = 0;
};
Source/PlayerController.cpp
#include "PlayerController.h"

#include "Coin.h"

#include "Shalltear/Audio/AudioSource.h"
#include "Shalltear/Physics3D/Colliders.h"
#include "Shalltear/Physics3D/Rigidbody.h"

SLT_REGISTER_TYPE(PlayerController)

PlayerController::PlayerController() : Behavior(BehaviorTick::Update) {
	// one action with two axes: x is left and right, y is forward and back.
	// Every binding adds its share, so W and 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});
	// the 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 any query
	_controls.Evaluate();
	Vec2 input = _controls.GetAxis2D(_move);
	// a key and 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 the horizontal speed and 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 the player touches a trigger collider, such as a coin's
void PlayerController::OnTriggerEnter(Handle<Collider> other) {
	// what did we touch? Only an object with a Coin on it counts
	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.

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.

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