Move the player
The player object
- In the Hierarchy, select + and choose 3D Object ▸ Capsule. Rename it
Playerand set its Position to0, 1, 0. - Drag
Player.matonto Material 0 in its MeshRenderer. - Add a CapsuleCollider, then a Rigidbody.
- 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
- Drag a short sound file onto the Asset Browser: a
.wav,.ogg,.mp3or.flac. It is copied into the folder the Asset Browser shows. - 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.
- 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
- Create a component named
PlayerController, as you did for the coin, and replace its two files:
#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;
};#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());
}- 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.