#include "../app/FrameCounter.hpp"
#include "../app/init.hpp"
#include "../audio/Audio.hpp"
+#include "../audio/SoundBank.hpp"
+#include "../geometry/distance.hpp"
#include "../graphics/Font.hpp"
#include "../graphics/Viewport.hpp"
#include "../io/TokenStreamReader.hpp"
-#include "../model/shapes.hpp"
+#include "../model/bounds.hpp"
+#include "../net/CongestionControl.hpp"
#include "../world/BlockLookup.hpp"
#include "../world/World.hpp"
#include "../world/WorldManipulator.hpp"
#include <map>
#include <sstream>
#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtx/projection.hpp>
#include <glm/gtx/rotate_vector.hpp>
#include <glm/gtx/io.hpp>
: world(world)
, player(player)
, move_dir(0.0f)
-, pitch(0.0f)
-, yaw(0.0f)
, dirty(true)
, aim_world()
, aim_entity() {
+ player.GetEntity().SetController(*this);
+ player.GetEntity().GetSteering().SetAcceleration(5.0f);
+}
+PlayerController::~PlayerController() {
+ if (&player.GetEntity().GetController() == this) {
+ player.GetEntity().UnsetController();
+ }
}
void PlayerController::SetMovement(const glm::vec3 &m) noexcept {
- if (dot(m, m) > 1.0f) {
- move_dir = normalize(m);
+ if (glm::dot(m, m) > 1.0f) {
+ move_dir = glm::normalize(m);
} else {
move_dir = m;
}
}
void PlayerController::TurnHead(float dp, float dy) noexcept {
- pitch += dp;
- if (pitch > PI / 2) {
- pitch = PI / 2;
- } else if (pitch < -PI / 2) {
- pitch = -PI / 2;
- }
- yaw += dy;
- if (yaw > PI) {
- yaw -= PI * 2;
- } else if (yaw < -PI) {
- yaw += PI * 2;
- }
- Invalidate();
+ player.GetEntity().TurnHead(dp, dy);
+}
+
+float PlayerController::GetPitch() const noexcept {
+ return player.GetEntity().Pitch();
+}
+
+float PlayerController::GetYaw() const noexcept {
+ return player.GetEntity().Yaw();
}
void PlayerController::SelectInventory(int i) noexcept {
}
void PlayerController::UpdatePlayer() noexcept {
- constexpr float max_vel = 0.005f;
if (dirty) {
- player.GetEntity().Orientation(glm::quat(glm::vec3(pitch, yaw, 0.0f)));
- player.GetEntity().Velocity(glm::rotateY(move_dir * max_vel, yaw));
-
Ray aim = player.Aim();
- if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity().ChunkCoords(), aim_world)) {
+ Entity &entity = player.GetEntity();
+ if (!world.Intersection(aim, entity.ChunkCoords(), aim_world)) {
aim_world = WorldCollision();
}
- if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity(), aim_entity)) {
+ if (!world.Intersection(aim, entity, aim_entity)) {
aim_entity = EntityCollision();
}
if (aim_world && aim_entity) {
aim_world = WorldCollision();
}
}
+ Steering &steering = entity.GetSteering();
+ if (!iszero(move_dir)) {
+ // scale input by max velocity, apply yaw, and transform to world space
+ steering.SetTargetVelocity(glm::vec3(
+ glm::vec4(glm::rotateY(move_dir * entity.MaxVelocity(), entity.Yaw()), 0.0f)
+ * glm::transpose(entity.Transform())
+ ));
+ steering.Enable(Steering::TARGET_VELOCITY);
+ steering.Disable(Steering::HALT);
+ } else {
+ // target velocity of 0 is the same as halt
+ steering.Enable(Steering::HALT);
+ steering.Disable(Steering::TARGET_VELOCITY);
+ }
dirty = false;
}
}
DirectInput::DirectInput(World &world, Player &player, WorldManipulator &manip)
: PlayerController(world, player)
, manip(manip)
-, place_timer(256)
-, remove_timer(256) {
+, place_timer(0.25f)
+, remove_timer(0.25f) {
}
-void DirectInput::Update(int dt) {
+void DirectInput::Update(Entity &, float dt) {
Invalidate(); // world has changed in the meantime
UpdatePlayer();
}
void DirectInput::PlaceBlock() {
+ // update block focus
UpdatePlayer();
+ // do nothing if not looking at any block
if (!BlockFocus()) return;
+ // determine block adjacent to the face the player is looking at
BlockLookup next_block(BlockFocus().chunk, BlockFocus().BlockPos(), Block::NormalFace(BlockFocus().normal));
+ // abort if it's unavailable
if (!next_block) {
return;
}
- manip.SetBlock(next_block.GetChunk(), next_block.GetBlockIndex(), Block(InventorySlot() + 1));
+
+ // "can replace" check
+ // this prevents players from replacing solid blocks e.g. by looking through slabs
+ // simple for now, should be expanded to include things like
+ // entities in the way or replacable blocks like water and stuff
+ if (next_block.GetBlock().type != 0) {
+ return;
+ }
+
+ Block new_block(InventorySlot() + 1);
+
+ // block's up vector
+ // align with player's up
+ const glm::vec3 player_up(GetPlayer().GetEntity().Up());
+ new_block.SetFace(Block::NormalFace(player_up));
+ // to align with player's local up/down look (like stairs in minecraft), just invert
+ // it if pitch is positive
+ // or, align with focus normal (like logs in minecraft)
+
+ // determine block's turn (local rotation about up axis)
+ // when aligned with player's up (first mode, and currently the only one implemented)
+ // project the player's view forward onto his entity's XZ plane and
+ // use the closest cardinal direction it's pointing in
+ const glm::vec3 view_forward(-GetPlayer().GetEntity().ViewTransform(GetPlayer().GetEntity().ChunkCoords())[2]);
+ // if view is straight up or down, this will be a null vector (NaN after normalization)
+ // in that case maybe the model forward should be used?
+ // the current implementation implicitly falls back to TURN_NONE which is -Z
+ const glm::vec3 local_forward(glm::normalize(view_forward - glm::proj(view_forward, player_up)));
+ // FIXME: I suspect this only works when player_up is positive Y
+ if (local_forward.x > 0.707f) {
+ new_block.SetTurn(Block::TURN_RIGHT);
+ } else if (local_forward.z > 0.707f) {
+ new_block.SetTurn(Block::TURN_AROUND);
+ } else if (local_forward.x < -0.707f) {
+ new_block.SetTurn(Block::TURN_LEFT);
+ }
+ // for mode two ("minecraft stairs") it should work the same, but I haven't properly
+ // thought that through (well, that's also true about the whole face/turn thing, but oh well)
+ // mode three I have absoloutely no clue. that placement would be appropriate for pipe-like
+ // blocks, where turn shouldn't make a difference, but what if it does?
+
+ manip.SetBlock(next_block.GetChunk(), next_block.GetBlockIndex(), new_block);
Invalidate();
}
, block_text()
, show_block(false)
, show_entity(false)
+// net stats
+, bandwidth_text()
+, rtt_text()
+, packet_loss_text()
+, show_net(false)
// message box
, messages(env.assets.small_ui_font)
, msg_timer(5000)
+, msg_keep(false)
// crosshair
, crosshair() {
+ const float ls = env.assets.small_ui_font.LineSkip();
+
// "inventory"
block_transform = glm::translate(block_transform, glm::vec3(50.0f, 50.0f, 0.0f));
block_transform = glm::scale(block_transform, glm::vec3(50.0f));
counter_text.Position(glm::vec3(-25.0f, 25.0f, 0.0f), Gravity::NORTH_EAST);
counter_text.Foreground(glm::vec4(1.0f));
counter_text.Background(glm::vec4(0.5f));
- position_text.Position(glm::vec3(-25.0f, 25.0f + env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
+ position_text.Position(glm::vec3(-25.0f, 25.0f + ls, 0.0f), Gravity::NORTH_EAST);
position_text.Foreground(glm::vec4(1.0f));
position_text.Background(glm::vec4(0.5f));
- orientation_text.Position(glm::vec3(-25.0f, 25.0f + 2 * env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
+ orientation_text.Position(glm::vec3(-25.0f, 25.0f + 2 * ls, 0.0f), Gravity::NORTH_EAST);
orientation_text.Foreground(glm::vec4(1.0f));
orientation_text.Background(glm::vec4(0.5f));
- block_text.Position(glm::vec3(-25.0f, 25.0f + 4 * env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
+ block_text.Position(glm::vec3(-25.0f, 25.0f + 4 * ls, 0.0f), Gravity::NORTH_EAST);
block_text.Foreground(glm::vec4(1.0f));
block_text.Background(glm::vec4(0.5f));
block_text.Set(env.assets.small_ui_font, "Block: none");
- entity_text.Position(glm::vec3(-25.0f, 25.0f + 4 * env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
+ entity_text.Position(glm::vec3(-25.0f, 25.0f + 4 * ls, 0.0f), Gravity::NORTH_EAST);
entity_text.Foreground(glm::vec4(1.0f));
entity_text.Background(glm::vec4(0.5f));
entity_text.Set(env.assets.small_ui_font, "Entity: none");
+ // net stats
+ bandwidth_text.Position(glm::vec3(-25.0f, 25.0f + 6 * ls, 0.0f), Gravity::NORTH_EAST);
+ bandwidth_text.Foreground(glm::vec4(1.0f));
+ bandwidth_text.Background(glm::vec4(0.5f));
+ bandwidth_text.Set(env.assets.small_ui_font, "TX: 0.0KB/s RX: 0.0KB/s");
+ rtt_text.Position(glm::vec3(-25.0f, 25.0f + 7 * ls, 0.0f), Gravity::NORTH_EAST);
+ rtt_text.Foreground(glm::vec4(1.0f));
+ rtt_text.Background(glm::vec4(0.5f));
+ rtt_text.Set(env.assets.small_ui_font, "RTT: unavailable");
+ packet_loss_text.Position(glm::vec3(-25.0f, 25.0f + 8 * ls, 0.0f), Gravity::NORTH_EAST);
+ packet_loss_text.Foreground(glm::vec4(1.0f));
+ packet_loss_text.Background(glm::vec4(0.5f));
+ packet_loss_text.Set(env.assets.small_ui_font, "Packet loss: 0.0%");
+
// message box
- messages.Position(glm::vec3(25.0f, -25.0f, 0.0f), Gravity::SOUTH_WEST);
+ messages.Position(glm::vec3(25.0f, -25.0f - 2 * ls, 0.0f), Gravity::SOUTH_WEST);
messages.Foreground(glm::vec4(1.0f));
messages.Background(glm::vec4(0.5f));
// crosshair
- OutlineModel::Buffer buf;
+ PrimitiveMesh::Buffer buf;
buf.vertices = std::vector<glm::vec3>({
{ -10.0f, 0.0f, 0.0f }, { 10.0f, 0.0f, 0.0f },
{ 0.0f, -10.0f, 0.0f }, { 0.0f, 10.0f, 0.0f },
});
- buf.indices = std::vector<OutlineModel::Index>({
+ buf.indices = std::vector<PrimitiveMesh::Index>({
0, 1, 2, 3
});
- buf.colors.resize(4, { 10.0f, 10.0f, 10.0f });
+ buf.colors.resize(4, { 255, 255, 255, 255 });
crosshair.Update(buf);
}
namespace {
-OutlineModel::Buffer outl_buf;
+PrimitiveMesh::Buffer outl_buf;
}
const Block &block = chunk.BlockAt(index);
const BlockType &type = chunk.Type(index);
outl_buf.Clear();
- type.FillOutlineModel(outl_buf);
+ type.OutlinePrimitiveMesh(outl_buf);
outline.Update(outl_buf);
outline_transform = chunk.Transform(player.GetEntity().ChunkCoords());
outline_transform *= chunk.ToTransform(Chunk::ToPos(index), index);
void HUD::Display(const BlockType &type) {
block_buf.Clear();
- type.FillEntityModel(block_buf);
+ type.FillEntityMesh(block_buf);
block.Update(block_buf);
block_label.Set(env.assets.small_ui_font, type.label);
}
void HUD::UpdateOrientation() {
- //std::stringstream s;
- //s << std::setprecision(3) << "pitch: " << rad2deg(ctrl.Pitch())
- // << ", yaw: " << rad2deg(ctrl.Yaw());
- //orientation_text.Set(env.assets.small_ui_font, s.str());
+ std::stringstream s;
+ s << std::setprecision(3) << "pitch: " << glm::degrees(player.GetEntity().Pitch())
+ << ", yaw: " << glm::degrees(player.GetEntity().Yaw());
+ orientation_text.Set(env.assets.small_ui_font, s.str());
}
void HUD::PostMessage(const char *msg) {
}
+void HUD::UpdateNetStats(const CongestionControl &stat) {
+ if (!config.video.debug) return;
+
+ std::stringstream s;
+ s << std::fixed << std::setprecision(1)
+ << "TX: " << stat.Upstream()
+ << "KB/s, RX: " << stat.Downstream() << "KB/s";
+ bandwidth_text.Set(env.assets.small_ui_font, s.str());
+
+ s.str("");
+ s << "RTT: " << stat.RoundTripTime() << "ms";
+ rtt_text.Set(env.assets.small_ui_font, s.str());
+
+ s.str("");
+ s << "Packet loss: " << (stat.PacketLoss() * 100.0f) << "%";
+ packet_loss_text.Set(env.assets.small_ui_font, s.str());
+
+ show_net = true;
+}
+
+
void HUD::Update(int dt) {
msg_timer.Update(dt);
if (msg_timer.HitOnce()) {
void HUD::Render(Viewport &viewport) noexcept {
// block focus
if (outline_visible && config.video.world) {
- PlainColor &outline_prog = viewport.WorldOutlineProgram();
+ PlainColor &outline_prog = viewport.WorldColorProgram();
outline_prog.SetM(outline_transform);
- outline.Draw();
+ outline.DrawLines();
}
// clear depth buffer so everything renders above the world
if (block_visible) {
DirectionalLighting &world_prog = viewport.HUDProgram();
world_prog.SetLightDirection({ 1.0f, 3.0f, 5.0f });
+ world_prog.SetLightColor({ 1.0f, 1.0f, 1.0f });
+ world_prog.SetAmbientColor({ 0.1f, 0.1f, 0.1f });
// disable distance fog
world_prog.SetFogDensity(0.0f);
}
// message box
- if (msg_timer.Running()) {
+ if (msg_keep || msg_timer.Running()) {
messages.Render(viewport);
}
// crosshair
- PlainColor &outline_prog = viewport.HUDOutlineProgram();
+ PlainColor &outline_prog = viewport.HUDColorProgram();
viewport.EnableInvertBlending();
viewport.SetCursor(glm::vec3(0.0f), Gravity::CENTER);
outline_prog.SetM(viewport.Cursor());
- crosshair.Draw();
+ crosshair.DrawLines();
}
// debug overlay
} else if (show_entity) {
entity_text.Render(viewport);
}
+ if (show_net) {
+ bandwidth_text.Render(viewport);
+ rtt_text.Render(viewport);
+ packet_loss_text.Render(viewport);
+ }
}
}
-InteractiveManipulator::InteractiveManipulator(Environment &env, Entity &player)
+InteractiveManipulator::InteractiveManipulator(Audio &audio, const SoundBank &sounds, Entity &player)
: player(player)
-, audio(env.audio)
-, place_sound(env.loader.LoadSound("thump"))
-, remove_sound(env.loader.LoadSound("plop")) {
+, audio(audio)
+, sounds(sounds) {
}
void InteractiveManipulator::SetBlock(Chunk &chunk, int index, const Block &block) {
+ const BlockType &old_type = chunk.Type(index);
chunk.SetBlock(index, block);
+ const BlockType &new_type = chunk.Type(index);
glm::vec3 coords = chunk.ToSceneCoords(player.ChunkCoords(), Chunk::ToCoords(index));
- // TODO: get sound effect from block type
- if (block.type == 0) {
- audio.Play(remove_sound, coords);
+ if (new_type.id == 0) {
+ if (old_type.remove_sound >= 0) {
+ audio.Play(sounds[old_type.remove_sound], coords);
+ }
} else {
- audio.Play(place_sound, coords);
+ if (new_type.place_sound >= 0) {
+ audio.Play(sounds[new_type.place_sound], coords);
+ }
}
}
, client_ctrl(cc)
, fwd(0)
, rev(0)
-, slot(0)
-, num_slots(10) {
+, num_slots(10)
+, locked(false) {
}
+void Interface::Lock() {
+ fwd = glm::ivec3(0);
+ rev = glm::ivec3(0);
+ locked = true;
+}
+
+void Interface::Unlock() {
+ locked = false;
+}
void Interface::HandlePress(const SDL_KeyboardEvent &event) {
if (!config.input.keyboard) return;
break;
case Keymap::TOGGLE_AUDIO:
- config.audio.enabled = !config.audio.enabled;
- client_ctrl.SetAudio(config.audio.enabled);
+ client_ctrl.SetAudio(!config.audio.enabled);
break;
case Keymap::TOGGLE_VIDEO:
- config.video.world = !config.video.world;
- client_ctrl.SetVideo(config.video.world);
+ client_ctrl.SetVideo(!config.video.world);
break;
case Keymap::TOGGLE_HUD:
- config.video.hud = !config.video.hud;
- client_ctrl.SetHUD(config.video.hud);
+ client_ctrl.SetHUD(!config.video.hud);
break;
case Keymap::TOGGLE_DEBUG:
- config.video.debug = !config.video.debug;
- client_ctrl.SetDebug(config.video.debug);
+ client_ctrl.SetDebug(!config.video.debug);
+ break;
+ case Keymap::CAMERA_NEXT:
+ client_ctrl.NextCamera();
break;
default:
}
void Interface::Handle(const SDL_MouseMotionEvent &event) {
- if (!config.input.mouse) return;
+ if (locked || !config.input.mouse) return;
player_ctrl.TurnHead(
event.yrel * config.input.pitch_sensitivity,
event.xrel * config.input.yaw_sensitivity);
}
void Interface::InvAbs(int s) {
- slot = s % num_slots;
+ int slot = s % num_slots;
while (slot < 0) {
slot += num_slots;
}
}
void Interface::InvRel(int delta) {
- InvAbs(slot + delta);
+ InvAbs(player_ctrl.GetPlayer().GetInventorySlot() + delta);
}
Map(SDL_SCANCODE_0, INV_10);
Map(SDL_SCANCODE_INSERT, SECONDARY);
- Map(SDL_SCANCODE_RETURN, SECONDARY);
Map(SDL_SCANCODE_MENU, TERTIARY);
Map(SDL_SCANCODE_DELETE, PRIMARY);
Map(SDL_SCANCODE_BACKSPACE, PRIMARY);
Map(SDL_SCANCODE_F2, TOGGLE_VIDEO);
Map(SDL_SCANCODE_F3, TOGGLE_DEBUG);
Map(SDL_SCANCODE_F4, TOGGLE_AUDIO);
+ Map(SDL_SCANCODE_F5, CAMERA_NEXT);
Map(SDL_SCANCODE_ESCAPE, EXIT);
}
{ "toggle_video", Keymap::TOGGLE_VIDEO },
{ "toggle_hud", Keymap::TOGGLE_HUD },
{ "toggle_debug", Keymap::TOGGLE_DEBUG },
+ { "camera_next", Keymap::CAMERA_NEXT },
{ "exit", Keymap::EXIT },
};