This commit is contained in:
Martin Slachta
2026-07-18 14:31:15 +02:00
commit a04f0dc262
3343 changed files with 1140208 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
#include "runtime.hpp"
int main(int argc, char** argv) {
tw::Runtime runtime(argc, argv);
runtime.run();
return 0;
}
@@ -0,0 +1,20 @@
#pragma once
#include <imgui.h>
#include "debug/ComponentGui.hpp"
#include "world/CharacterBody.hpp"
template<>
class tw::dbg::ComponentGui<tw::CharacterBody> {
public:
void draw(CharacterBody* instance) {
ImGui::SeparatorText("Character Body Component");
auto position = instance->m_character->GetPosition();
ImGui::InputFloat3("Position", (float*)&position);
auto new_velocity = instance->m_character->GetLinearVelocity();
ImGui::InputFloat3("New Velocity", (float*)&new_velocity);
}
};
@@ -0,0 +1,43 @@
#pragma once
#include <imgui.h>
#include "debug/ComponentGui.hpp"
#include "world/CharacterController.hpp"
template<>
class tw::dbg::ComponentGui<tw::CharacterController> {
public:
void draw(tw::CharacterController* instance) {
ImGui::SeparatorText("Character Controller Component");
if(ImGui::BeginTable("history", 3)) {
for(auto key : instance->input_history().buffer()) {
// ImGui::TableNextRow();
// ImGui::TableNextColumn();
// ImGui::Text("%u", key.first);
// ImGui::TableNextColumn();
// if(instance->input_history().get(key.first).has_value()) {
// glm::vec3 input_vec = *instance->input_history().get(key.first).value();
// std::string input = std::format("{} {} {}", input_vec.x, input_vec.y, input_vec.z);
// ImGui::Text("%s", input.c_str());
// } else {
// ImGui::Text("None");
// }
// ImGui::TableNextColumn();
// if(instance->position_history().get(key).has_value()) {
// glm::vec3 position_vec = *instance->position_history().get(key).value();
// std::string position = std::format("{} {} {}", position_vec.x, position_vec.y, position_vec.z);
// ImGui::Text("%s", position.c_str());
// } else {
// ImGui::Text("None");
// }
}
ImGui::EndTable();
}
}
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
namespace tw::dbg {
template<typename T>
class ComponentGui {
public:
void draw(T* instance);
};
}
@@ -0,0 +1,21 @@
#pragma once
#include <string>
namespace tw::dbg {
class DebugComponent {
private:
std::string m_name;
public:
inline const std::string& name() const {
return m_name;
}
DebugComponent(const std::string& name) :
m_name{name} {
}
};
}
@@ -0,0 +1,92 @@
#pragma once
#include <imgui.h>
#include <chrono>
#include <format>
#include <ratio>
#include "debug/ComponentGui.hpp"
#include <implot.h>
#include <implot_internal.h>
#include "network/EntityInterpolation.hpp"
template<>
class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
public:
void draw(net::EntityPositionInterpolation* instance) {
ImGui::SeparatorText("Entity Interpolation");
bool m_is_scrolling = true;
static float m_metric_history = 10.0f;
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
if(m_is_scrolling) {
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
}
static int32_t time_buffer_len = 1000;
ImGui::SliderInt("Buffer length (ms)", &time_buffer_len, 0, 1000);
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
auto [from, to, mix] = instance->get_values_around(now);
auto mixed_value = glm::mix(from, to, mix);
ImGui::Text("Mixed value: (%f, %f)", mixed_value.x, mixed_value.y);
if(ImPlot::BeginPlot("Interpolation")) {
auto plot_x_from = instance->times().begin()->time_since_epoch().count();
std::vector<float> times(instance->times().max_size());
for(auto it = instance->times().begin(); it != instance->times().end(); ++it) {
times.push_back(it->time_since_epoch().count() - plot_x_from);
}
float interpolated_time = now.time_since_epoch().count() - plot_x_from;
std::vector<float> values(instance->values().max_size());
for(auto it = instance->values().begin(); it != instance->values().end(); ++it) {
values.push_back(it->x);
}
values.push_back(mixed_value.x);
ImPlot::PlotScatter("Server", times.data(), values.data(), times.size() - 1);
ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.25f);
ImPlot::SetNextMarkerStyle(ImPlotMarker_Square, 6, ImPlot::GetColormapColor(1), IMPLOT_AUTO, ImPlot::GetColormapColor(1));
ImPlot::PlotScatter("Client", &interpolated_time, &mixed_value.x, 1);
ImPlot::PopStyleVar();
ImPlot::EndPlot();
}
if(ImGui::BeginTable("entityInterpolation", 2)) {
for(int i = 0; i < instance->values().size(); i++) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
auto value = instance->values()[i];
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
if(instance->times()[i] > now && (i == instance->values().size() - 1 || instance->times()[i + 1] <= now)) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.7f, 0.3f, 0.3f, 0.65f)));
ImGui::Text("%f %f %f", mixed_value.x, mixed_value.y, mixed_value.z);
ImGui::TableNextColumn();
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.0f)));
}
ImGui::Text("%f %f %f", value.x, value.y, value.z);
auto point = instance->times()[i].time_since_epoch();
auto hours = std::chrono::duration_cast<std::chrono::hours>(point);
point -= hours;
hours %= 24;
auto minutes = std::chrono::duration_cast<std::chrono::minutes>(point);
point -= minutes;
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(point);
point -= seconds;
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(point);
ImGui::TableNextColumn();
auto fmt = std::format("{}:{}:{}.{}", hours, minutes, seconds, milliseconds);
ImGui::Text(fmt.c_str());
}
ImGui::EndTable();
}
}
};
@@ -0,0 +1,94 @@
#include "EntityManagerGui.hpp"
#include "debug/tools/MatrixWidget.hpp"
#include "debug/EntityInterpolationDebug.hpp"
#include "debug/CharacterBodyDebug.hpp"
#include "debug/CharacterControllerDebug.hpp"
#include "network/EntityInterpolation.hpp"
#include "world/CharacterBody.hpp"
#include "world/CharacterController.hpp"
#include "world/Transform.hpp"
#include "world/WorldEntity.hpp"
#include "draw/Mesh.hpp"
#include <imgui.h>
#include <entt/entity/entity.hpp>
#include <spdlog/spdlog.h>
namespace tw::dbg::tools {
EntityManagerGui::EntityManagerGui(World* world) :
m_world(world) {
}
void draw_transform_controls(Transform* transform) {
}
void EntityManagerGui::draw_entity_components() {
ImGui::Text("Entity ID: %d", (uint32_t)m_selected);
ImGui::BeginChild("Transform Component", ImVec2(0, 0), ImGuiChildFlags_Border);
auto transform = m_world->registry().try_get<Transform>(m_selected);
if(transform != nullptr) {
ImGui::SeparatorText("Transform Component");
draw_matrix_widget(transform->transform, true);
draw_transform_controls(transform);
}
auto mesh = m_world->registry().try_get<tw::drw::Mesh>(m_selected);
if(mesh != nullptr) {
ImGui::SeparatorText("Mesh Component");
ImGui::Text("Num vertices: %i", mesh->num_vertices());
ImGui::Text("Vertex offset: %i", mesh->vertex_offset());
}
draw_debug_components<tw::CharacterBody, tw::CharacterController, tw::net::EntityPositionInterpolation>();
// auto character = m_world->registry().try_get<tw::CharacterBody>(m_selected);
// if(character != nullptr) {
// tw::dbg::ComponentGui<tw::CharacterBody>().draw(character);
// }
//
// auto entity_interpolation = m_world->registry().try_get<EntityInterpolation>(m_selected);
// if(entity_interpolation != nullptr) {
// tw::dbg::ComponentGui<EntityInterpolation>().draw(entity_interpolation);
// }
ImGui::EndChild();
}
void EntityManagerGui::draw() {
ImGui::Begin("Transforms");
ImGui::BeginChild("Entities", ImVec2(0, 260), ImGuiChildFlags_Border);
ImGui::SeparatorText("Entities");
auto view = m_world->registry().view<const WorldEntity>();
view.each([&](const auto entity, const WorldEntity& info) {
ImGui::PushID((uint32_t)info.entity_id);
if(ImGui::Selectable(info.name.c_str(), m_selected_entity == info.entity_id)) {
m_selected_entity = info.entity_id;
m_selected = entity;
}
ImGui::PopID();
});
ImGui::EndChild();
if(m_selected_entity.has_value()) {
draw_entity_components();
}
ImGui::End();
}
}
@@ -0,0 +1,42 @@
#pragma once
#include "debug/ComponentGui.hpp"
#include "entt/entity/fwd.hpp"
#include "world/World.hpp"
#include <entt/entt.hpp>
namespace tw::dbg::tools {
class EntityManagerGui {
private:
World* m_world;
entt::entity m_selected;
std::optional<uint32_t> m_selected_entity;
void draw_entity_components();
template<class... Ts>
void draw_debug_components() {
(
[&]() {
auto character = m_world->registry().try_get<Ts>(m_selected);
if(character != nullptr) {
tw::dbg::ComponentGui<Ts>().draw(character);
}
}(),
...);
}
public:
entt::entity& selected() {
return m_selected;
}
EntityManagerGui(World* world);
void draw();
};
}
@@ -0,0 +1,27 @@
#include "MatrixWidget.hpp"
namespace tw::dbg::tools {
void draw_matrix_widget(const glm::mat4 &mat, bool display_headers) {
if(ImGui::BeginTable("transform_component", 4, ImGuiTableFlags_Borders)) {
if(display_headers) {
ImGui::TableSetupColumn("Right");
ImGui::TableSetupColumn("Front");
ImGui::TableSetupColumn("Up");
ImGui::TableSetupColumn("Translation");
ImGui::TableHeadersRow();
}
for(int row = 0; row < 4; row++) {
ImGui::TableNextRow();
for(int col = 0; col < 4; col++) {
ImGui::TableSetColumnIndex(col);
ImGui::Text("%f", mat[col][row]);
}
}
ImGui::EndTable();
}
}
}
@@ -0,0 +1,10 @@
#pragma once
#include <glm/glm.hpp>
#include <imgui.h>
namespace tw::dbg::tools {
void draw_matrix_widget(const glm::mat4& mat, bool display_headers);
}
@@ -0,0 +1,101 @@
#pragma once
#include <imgui.h>
#include <implot.h>
#include <implot_internal.h>
#include "metrics/BucketMetric.hpp"
namespace tw::dbg::tools {
template<typename T, typename Interval, typename Op = net::SumOp<T>>
class MetricWidget {
std::string m_name;
net::BucketMetric<T, Interval, Op>& m_metric;
using Self = MetricWidget<T, Interval, Op>;
struct {
T constraint_from;
T constraint_to;
T from;
T to;
} y_axis;
bool m_is_scrolling = true;
public:
MetricWidget(
const std::string& name,
net::BucketMetric<T, Interval, Op>& metric
) :
m_name(name),
m_metric(metric)
{
y_axis = {
.constraint_from = 0,
.constraint_to = 500,
.from = 0,
.to = 250
};
}
Self& set_x_axis_limits_contraints(T from, T to) {
ImPlot::SetupAxisLimits(ImAxis_X1, from, to);
return *this;
}
Self& set_y_axis_limits(double from, double to) {
return *this;
}
Self& enable_scrolling() {
m_is_scrolling = true;
}
Self& disable_scrolling() {
m_is_scrolling = true;
}
void draw() {
ImGui::PushID(m_name.c_str());
auto head = m_metric.get_head();
auto head_timeline = m_metric.get_head_timeline();
auto tail = m_metric.get_tail();
auto tail_timeline = m_metric.get_tail_timeline();
static float m_metric_history = 10.0f;
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
if(m_is_scrolling) {
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
}
ImGui::Text("Min: %i", m_metric.min());
ImGui::Text("Max: %i", m_metric.max());
if(ImPlot::BeginPlot(m_name.c_str())) {
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
ImPlot::SetupAxes("Time", m_name.c_str(), ImPlotAxisFlags_None, ImPlotAxisFlags_None);
if(m_is_scrolling) {
ImPlot::SetupAxisLimits(ImAxis_X1, from - m_metric_history, from, ImGuiCond_Always);
}
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, m_metric.max() * 2, ImGuiCond_Always);
// ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
ImPlot::PlotLine(m_name.c_str(), head_timeline.data(), head.data(), head.size());
ImPlot::PlotLine(m_name.c_str(), tail_timeline.data(), tail.data(), tail.size());
ImPlot::EndPlot();
}
ImGui::PopID();
}
};
}
@@ -0,0 +1,62 @@
#pragma once
#include "metrics/BucketMetric.hpp"
#include "metrics/NetworkStatsLogger.hpp"
#include "debug/tools/MetricWidget.hpp"
#include <implot.h>
#include <implot_internal.h>
namespace tw::dbg::tools {
class NetworkStatsGui {
private:
// MetricWidget<uint32_t, std::chrono::seconds, net::AverageOp<uint32_t>> m_ping_widget;
// MetricWidget<uint32_t, std::chrono::seconds> m_outgoing_widget;
// MetricWidget<uint32_t, std::chrono::seconds> m_incoming_widget;
public:
// NetworkStatsGui() :
// m_ping_widget("Ping", net::NetworkStatsLogger::instance()->ping()),
// m_outgoing_widget("Outgoing", net::NetworkStatsLogger::instance()->outgoing()),
// m_incoming_widget("Incoming", net::NetworkStatsLogger::instance()->incoming())
// {
// }
void draw() {
// auto* instance = net::NetworkStatsLogger::instance();
// auto& ping = instance->ping();
ImGui::Begin("Network Stats");
/* auto head = ping.get_head();
auto head_timeline = ping.get_head_timeline();
auto tail = ping.get_tail();
auto tail_timeline = ping.get_tail_timeline();
static float ping_history = 10.0f;
ImGui::SliderFloat("Ping History", &ping_history,1,30,"%.1f s");
if(ImPlot::BeginPlot("Ping")) {
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
ImPlot::SetupAxes("FrameIdx","FPS", ImPlotAxisFlags_None, ImPlotAxisFlags_None);
ImPlot::SetupAxisLimits(ImAxis_X1, from - ping_history, from, ImGuiCond_Always);
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 120);
ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
ImPlot::PlotLine("Ping", head_timeline.data(), head.data(), head.size());
ImPlot::PlotLine("Ping", tail_timeline.data(), tail.data(), tail.size());
ImPlot::EndPlot();
} */
// m_ping_widget.draw();
// m_outgoing_widget.draw();
// m_incoming_widget.draw();
ImGui::End();
}
};
}
@@ -0,0 +1,57 @@
#pragma once
#include <format>
#include <vector>
#include "imgui.h"
#include "metrics/NetworkStatsLogger.hpp"
namespace tw::dbg::tools {
class PacketBacklogGui {
private:
std::vector<uint32_t> m_buckets;
uint32_t m_last_backlog_idx;
public:
void draw() {
// auto* instance = net::NetworkStatsLogger::instance();
// size_t size = instance->get_size();
// if(ImGui::BeginTable("Network Packets", 5)) {
// ImGui::TableSetupColumn("Message Type");
// ImGui::TableSetupColumn("Time");
// ImGui::TableSetupColumn("Is From Us");
// ImGui::TableSetupColumn("Target");
// ImGui::TableSetupColumn("Size");
// for(int32_t i = size-1; i >= 0; i--) {
// auto& item = instance->get_item(i);
// ImGui::PushID(item.timepoint.time_since_epoch().count());
// ImGui::TableNextRow();
// ImGui::TableNextColumn();
// ImGui::Text("%i", item.message_type);
// ImGui::TableNextColumn();
// ImGui::Text(std::format("{}", item.timepoint.time_since_epoch()).c_str());
// ImGui::TableNextColumn();
// ImGui::Checkbox("is_sent_from_us", &item.is_sent_by_us);
// ImGui::TableNextColumn();
// ImGui::Text(item.target.to_string().c_str());
// ImGui::TableNextColumn();
// ImGui::Text("%ld", item.buffer.size());
// ImGui::PopID();
// }
// ImGui::EndTable();
// }
}
};
}
@@ -0,0 +1,55 @@
#pragma once
#include "Address.hpp"
#include "ByteBuffer.hpp"
#include "packets/Packet.hpp"
#include <cstring>
namespace tw::dbg::tools {
class SavedPacket {
public:
PacketType type;
bool is_from_client;
net::Address origin;
net::Address target;
net::ByteBuffer content;
public:
SavedPacket() :
origin({}, 0),
target({}, 0),
content(1024)
{
}
};
class PacketLogger {
private:
std::vector<SavedPacket> m_packets;
uint32_t m_left, m_right;
public:
inline const std::vector<SavedPacket>& packets() const {
return m_packets;
}
PacketLogger(uint32_t max_packets) :
m_packets(max_packets) {
}
void push(PacketType type, bool is_from_client,
net::Address origin, net::Address target,
net::ByteBuffer& content) {
SavedPacket& packet = m_packets[m_right];
packet.type = type;
packet.is_from_client = is_from_client;
packet.origin = origin;
packet.target = target;
memcpy(packet.content.data().data(), content.data().data(), content.writing_head());
}
};
}
@@ -0,0 +1,41 @@
#include "PerformanceStatsGui.hpp"
#include <imgui.h>
#include <implot.h>
#include <implot_internal.h>
namespace tw::dbg::tools {
PerformanceStatsGui::PerformanceStatsGui(LockStep& lock_step) :
m_lockstep(lock_step),
fps_history(1000),
frame_idxs(1000)
{
}
void PerformanceStatsGui::draw() {
ImGui::Begin("Stats");
ImGui::Text("FPS: %ld", m_lockstep.fps());
fps_history[fps_history_idx] = m_lockstep.fps();
frame_idxs[fps_history_idx] = frame_idx++;
if(fps_history_idx == fps_history.size() - 1) {
is_plot_filled = true;
}
fps_history_idx = (fps_history_idx + 1) % fps_history.size();
if(ImPlot::BeginPlot("FPS Plot")) {
ImPlot::SetupAxes("FrameIdx","FPS", ImPlotAxisFlags_None, ImPlotAxisFlags_None);
ImPlot::SetupAxisLimits(ImAxis_X1, std::max((double)frame_idx - 1000.0, 0.0), frame_idx, ImGuiCond_Always);
ImPlot::SetupAxisLimits(ImAxis_Y1,0,120);
ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 120);
ImPlot::PlotLine("FPS", frame_idxs.data(), fps_history.data(), is_plot_filled ? (int)fps_history.size() : (int)fps_history_idx - 1);
ImPlot::EndPlot();
}
ImGui::End();
}
}
@@ -0,0 +1,24 @@
#pragma once
#include "runtime/LockStep.hpp"
namespace tw::dbg::tools {
class PerformanceStatsGui {
private:
LockStep& m_lockstep;
std::vector<uint32_t> fps_history;
std::vector<uint32_t> frame_idxs;
uint32_t fps_history_idx = 0;
uint32_t frame_idx = 0;
bool is_plot_filled = false;
public:
PerformanceStatsGui(LockStep& lock_step);
void draw();
};
}
@@ -0,0 +1,25 @@
#pragma once
#include <chrono>
#include <ratio>
class FrameRateCounter {
using TimePoint = std::chrono::high_resolution_clock::time_point;
TimePoint m_last_frame;
double m_frame_count;
public:
uint64_t frame_count() {
return (uint64_t)m_frame_count;
}
void next_frame() {
TimePoint current = std::chrono::high_resolution_clock::now();
m_frame_count = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(current - m_last_frame).count();
m_last_frame = current;
}
};
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <cstdint>
namespace tw::drw {
/**
* Stores location data about Mesh on the GPU.
*/
class Mesh {
uint32_t m_vertex_offset;
uint32_t m_num_vertices;
public:
inline uint32_t num_vertices() const {
return m_num_vertices;
}
inline uint32_t vertex_offset() const {
return m_vertex_offset;
}
Mesh(uint32_t vertex_offset, uint32_t num_vertices) :
m_vertex_offset{vertex_offset},
m_num_vertices{num_vertices} {
}
};
}
+41
View File
@@ -0,0 +1,41 @@
#include "MeshBuffer.hpp"
namespace tw::drw {
void MeshBuffer::resize_buffer(size_t num_vertices) {
BufferCreateInfo buffer_info = {
.size = sizeof(glm::vec3) * 2 * num_vertices,
.usage = VK_BUFFER_USAGE_2_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_2_TRANSFER_DST_BIT,
.isExclusive = true,
};
MemoryAllocationInfo memory_info = {
.usage = MEMORY_USAGE_AUTO_PREFER_DEVICE
};
m_gpu->memory()->create_buffer(&buffer_info, &memory_info, &m_vertex_buffer);
m_max_vertex_count = num_vertices;
}
MeshBuffer::MeshBuffer(const Gpu* gpu) :
m_gpu(gpu),
m_stream(gpu, 1000 * sizeof(glm::vec3) * 1),
m_num_vertices(0) {
resize_buffer(1000);
}
Mesh MeshBuffer::push_mesh(const MeshData& mesh_data) {
m_stream.write(&m_vertex_buffer, (void*)mesh_data.positions().data(), m_num_vertices * sizeof(glm::vec3), mesh_data.positions().size() * sizeof(glm::vec3));
m_stream.write(&m_vertex_buffer, (void*)mesh_data.normals().data(), (m_num_vertices + 1000) * sizeof(glm::vec3), mesh_data.normals().size() * sizeof(glm::vec3));
m_stream.flush();
Mesh result(m_num_vertices, mesh_data.positions().size());
m_num_vertices += mesh_data.positions().size();
return result;
}
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "Gpu.hpp"
#include "resources/BufferBusWriter.h"
#include "Mesh.hpp"
#include "MeshData.hpp"
namespace tw::drw {
class MeshBuffer {
private:
const Gpu* m_gpu;
Buffer m_vertex_buffer;
uint32_t m_max_vertex_count;
uint32_t m_num_vertices;
// streams data from CPU to the GPU
BufferBusWriter m_stream;
void resize_buffer(size_t num_vertices);
public:
inline const Buffer& buffer() const { return m_vertex_buffer; }
MeshBuffer(const Gpu* gpu);
MeshBuffer(const MeshBuffer&) = delete;
MeshBuffer& operator=(const MeshBuffer&) = delete;
Mesh push_mesh(const MeshData& mesh_data);
};
}
+159
View File
@@ -0,0 +1,159 @@
#pragma once
#include <vector>
#include "glm/glm.hpp"
namespace tw::drw {
class MeshData {
private:
std::vector<glm::vec3> m_positions;
std::vector<glm::vec3> m_normals;
public:
inline const std::vector<glm::vec3>& positions() const {
return m_positions;
}
inline const std::vector<glm::vec3>& normals() const {
return m_normals;
}
MeshData(std::vector<glm::vec3> positions, std::vector<glm::vec3> normals) :
m_positions{positions},
m_normals{normals}
{
}
static MeshData plane(glm::vec2 scale) {
return MeshData({
{-0.5f * scale.x, 0.0f, -0.5f * scale.y},
{ 0.5f * scale.x, 0.0f, -0.5f * scale.y},
{ 0.5f * scale.x, 0.0f, 0.5f * scale.y},
{ 0.5f * scale.x, 0.0f, 0.5f * scale.y},
{-0.5f * scale.x, 0.0f, 0.5f * scale.y},
{-0.5f * scale.x, 0.0f, -0.5f * scale.y},
}, {
{0.0f, 0.0f, 1.0f},
{0.0f, 0.0f, 1.0f},
{0.0f, 0.0f, 1.0f},
{0.0f, 0.0f, 1.0f},
{0.0f, 0.0f, 1.0f},
{0.0f, 0.0f, 1.0f},
} );
}
static MeshData cube(glm::vec3 scale) {
return MeshData({
{-0.5f, -0.5f, 0.5f},
{ 0.5f, -0.5f, 0.5f},
{ 0.5f, 0.5f, 0.5f},
{ 0.5f, 0.5f, 0.5f},
{-0.5f, 0.5f, 0.5f},
{-0.5f, -0.5f, 0.5f},
// Back face (Z)
{ 0.5f, -0.5f, -0.5f},
{-0.5f, -0.5f, -0.5f},
{-0.5f, 0.5f, -0.5f},
{-0.5f, 0.5f, -0.5f},
{ 0.5f, 0.5f, -0.5f},
{ 0.5f, -0.5f, -0.5f},
// Left face (X)
{-0.5f, -0.5f, -0.5f},
{-0.5f, -0.5f, 0.5f},
{-0.5f, 0.5f, 0.5f},
{-0.5f, 0.5f, 0.5f},
{-0.5f, 0.5f, -0.5f},
{-0.5f, -0.5f, -0.5f},
// Right face (+X)
{ 0.5f, -0.5f, 0.5f},
{ 0.5f, -0.5f, -0.5f},
{ 0.5f, 0.5f, -0.5f},
{ 0.5f, 0.5f, -0.5f},
{ 0.5f, 0.5f, 0.5f},
{ 0.5f, -0.5f, 0.5f},
// Top face (+Y)
{-0.5f, 0.5f, 0.5f},
{ 0.5f, 0.5f, 0.5f},
{ 0.5f, 0.5f, -0.5f},
{ 0.5f, 0.5f, -0.5f},
{-0.5f, 0.5f, -0.5f},
{-0.5f, 0.5f, 0.5f},
// Bottom face (Y)
{-0.5f, -0.5f, -0.5f},
{ 0.5f, -0.5f, -0.5f},
{ 0.5f, -0.5f, 0.5f},
{ 0.5f, -0.5f, 0.5f},
{-0.5f, -0.5f, 0.5f},
{-0.5f, -0.5f, -0.5f},
}, {
{ 0.0f, 0.0f, 1.0f},
{ 0.0f, 0.0f, 1.0f},
{ 0.0f, 0.0f, 1.0f},
{ 0.0f, 0.0f, 1.0f},
{ 0.0f, 0.0f, 1.0f},
{ 0.0f, 0.0f, 1.0f},
{ 0.0f, 0.0f, -1.0f},
{ 0.0f, 0.0f, -1.0f},
{ 0.0f, 0.0f, -1.0f},
{ 0.0f, 0.0f, -1.0f},
{ 0.0f, 0.0f, -1.0f},
{ 0.0f, 0.0f, -1.0f},
{-1.0f, 0.0f, 0.0f},
{-1.0f, 0.0f, 0.0f},
{-1.0f, 0.0f, 0.0f},
{-1.0f, 0.0f, 0.0f},
{-1.0f, 0.0f, 0.0f},
{-1.0f, 0.0f, 0.0f},
{ 1.0f, 0.0f, 0.0f},
{ 1.0f, 0.0f, 0.0f},
{ 1.0f, 0.0f, 0.0f},
{ 1.0f, 0.0f, 0.0f},
{ 1.0f, 0.0f, 0.0f},
{ 1.0f, 0.0f, 0.0f},
{ 0.0f, 1.0f, 0.0f},
{ 0.0f, 1.0f, 0.0f},
{ 0.0f, 1.0f, 0.0f},
{ 0.0f, 1.0f, 0.0f},
{ 0.0f, 1.0f, 0.0f},
{ 0.0f, 1.0f, 0.0f},
{ 0.0f, -1.0f, 0.0f},
{ 0.0f, -1.0f, 0.0f},
{ 0.0f, -1.0f, 0.0f},
{ 0.0f, -1.0f, 0.0f},
{ 0.0f, -1.0f, 0.0f},
{ 0.0f, -1.0f, 0.0f},
});
}
};
}
@@ -0,0 +1,105 @@
#pragma once
#include "draw/WorldRenderer.hpp"
#include "io/ShaderManager.hpp"
#include "world/World.hpp"
#include "draw/MeshBuffer.hpp"
#include "shaders/PipelineBuilder.h"
#include <spdlog/spdlog.h>
namespace tw::drw {
class CharacterRenderPass {
public:
const WorldRenderer *m_world_renderer;
io::ShaderManager *m_shader_manager;
VkPipelineLayout m_pipeline_layout;
Pipeline m_pipeline;
lft::rg::RenderTaskBuilder m_task;
lft::rg::RenderTaskBuilder create_render_task() {
return lft::rg::render_task<CharacterRenderPass>(
"character", this,
[](const lft::rg::TaskBuildInfo& info,
CharacterRenderPass* context) {
auto vertex_shader = context->m_shader_manager->load_shader("Opaque.vert.spirv");
auto fragment_shader = context->m_shader_manager->load_shader("Opaque.frag.spirv");
context->m_pipeline_layout = PipelineLayoutBuilder()
.input_set(0, context->m_world_renderer->global_descriptor_set_layout())
.push_constant_range(0, sizeof(glm::mat4), VK_SHADER_STAGE_VERTEX_BIT)
.build(info.gpu());
context->m_pipeline = PipelineBuilder(info.gpu(), info.viewport(),
context->m_pipeline_layout, info.renderpass(),
1,
&vertex_shader, &fragment_shader)
.set_vertex_input_info({
{
.binding = 0,
.stride = sizeof(glm::vec3),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX
},
}, {
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 },
{ 1, 0, VK_FORMAT_R32G32B32_SFLOAT, sizeof(glm::vec3) * 1000 },
})
.build();
},
[](const lft::rg::TaskRecordInfo& info,
CharacterRenderPass* context) {
info.recording()
.bind_vertex_buffers({context->m_world_renderer->mesh_buffer()->buffer()}, {0});
auto binded_pipeline = info.recording()
.bind_graphics_pipeline(context->m_pipeline)
.bind_descriptor_set(0, context->m_world_renderer->global_descriptor_set());
auto viewport = info.viewport();
VkRect2D rect = {
.offset = {
.x = 0,
.y = 0
}, .extent = {
.width = (uint32_t)viewport.width,
.height = (uint32_t)std::abs(viewport.height)
}
};
vkCmdSetViewport(info.recording().cmdbuf(), 0, 1, &viewport);
vkCmdSetScissor(info.recording().cmdbuf(), 0, 1, &rect);
context->m_world_renderer->world()->registry()
.view<Mesh, Transform>()
.each([&](const Mesh& mesh, const Transform& transform) {
binded_pipeline.push_constants(VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(glm::mat4), &transform.transform);
info.recording().draw(mesh.num_vertices(), 1, mesh.vertex_offset(), 0);
});
}
)
.add_dependency("terrain")
.set_depth_output("gbuf_depth", VK_FORMAT_D32_SFLOAT_S8_UINT)
.set_output_to_final();
}
public:
CharacterRenderPass(
const WorldRenderer* world_renderer,
io::ShaderManager* shader_manager
) :
m_world_renderer(world_renderer),
m_shader_manager(shader_manager),
m_task(create_render_task()),
m_pipeline(VK_NULL_HANDLE, VK_NULL_HANDLE)
{
}
lft::rg::RenderTaskBuilder& get_render_task() {
return m_task;
}
};
}
@@ -0,0 +1,128 @@
#pragma once
#include "Instance.hpp"
#include "draw/WorldRenderer.hpp"
#include "io/ShaderManager.hpp"
#include "shaders/PipelineBuilder.h"
#include "world/ChunkData.hpp"
#include <spdlog/spdlog.h>
namespace tw::drw {
class TerrainRenderPass {
private:
using Self = TerrainRenderPass;
const WorldRenderer *m_world_renderer;
io::ShaderManager *m_shader_manager;
VkPipelineLayout m_pipeline_layout;
Pipeline m_pipeline;
lft::rg::RenderTaskBuilder m_task;
Mesh m_chunk_mesh;
Mesh& get_chunk_mesh(glm::ivec2 position) {
return m_chunk_mesh;
}
lft::rg::RenderTaskBuilder create_render_task() {
return lft::rg::render_task<Self>(
"terrain", this,
[](const lft::rg::TaskBuildInfo& info,
Self* context) {
auto vertex_shader = context->m_shader_manager->load_shader("Opaque2.vert.spirv");
auto fragment_shader = context->m_shader_manager->load_shader("Opaque2.frag.spirv");
context->m_pipeline_layout = PipelineLayoutBuilder()
.input_set(0, context->m_world_renderer->global_descriptor_set_layout())
.push_constant_range(0, sizeof(glm::mat4), VK_SHADER_STAGE_VERTEX_BIT)
.build(info.gpu());
std::println("Viewport: {} {}", info.viewport().width, info.viewport().height);
context->m_pipeline = PipelineBuilder(info.gpu(), info.viewport(),
context->m_pipeline_layout, info.renderpass(),
1,
&vertex_shader, &fragment_shader)
.set_vertex_input_info({
{
.binding = 0,
.stride = sizeof(glm::vec3),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX
},
}, {
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 },
{ 1, 0, VK_FORMAT_R32G32B32_SFLOAT, sizeof(glm::vec3) * 1000 },
})
.build();
},
[](const lft::rg::TaskRecordInfo& info,
Self* context) {
info.recording()
.bind_vertex_buffers({context->m_world_renderer->mesh_buffer()->buffer()}, {0});
auto binded_pipeline = info.recording()
.bind_graphics_pipeline(context->m_pipeline)
.bind_descriptor_set(0, context->m_world_renderer->global_descriptor_set());
auto viewport = info.viewport();
VkRect2D rect = {
.offset = {
.x = 0,
.y = 0
}, .extent = {
.width = (uint32_t)viewport.width,
.height = (uint32_t)std::abs(viewport.height)
}
};
vkCmdSetViewport(info.recording().cmdbuf(), 0, 1, &viewport);
vkCmdSetScissor(info.recording().cmdbuf(), 0, 1, &rect);
glm::vec3 center = context->m_world_renderer->camera().position();
uint32_t render_distance = 4;
glm::vec3 from = glm::floor(center - glm::vec3(render_distance));
glm::vec3 to = glm::ceil(center + glm::vec3(render_distance));
from = glm::vec3(-4, 0, -4);
to = glm::vec3(4, 0, 4);
for(int32_t x = from.x; x < to.x; x++) {
for(int32_t z = from.z; z < to.z; z++) {
auto& mesh = context->get_chunk_mesh(glm::ivec2(x, z));
glm::mat4 ts = glm::translate(glm::mat4(1.0f), glm::vec3(x * CHUNK_WIDTH, 0, z * CHUNK_WIDTH));
binded_pipeline.push_constants(VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(glm::mat4), &ts);
info.recording().draw(mesh.num_vertices(), 1, mesh.vertex_offset(), 0);
}
}
}
)
.set_depth_output("gbuf_depth", VK_FORMAT_D32_SFLOAT_S8_UINT)
.set_output_to_final();
}
public:
TerrainRenderPass(
WorldRenderer* world_renderer,
io::ShaderManager* shader_manager
) :
m_world_renderer(world_renderer),
m_shader_manager(shader_manager),
m_task(create_render_task()),
m_pipeline(VK_NULL_HANDLE, VK_NULL_HANDLE),
m_chunk_mesh(world_renderer->mesh_buffer()->push_mesh(MeshData::plane(glm::vec2(16.0f))))
{
}
lft::rg::RenderTaskBuilder& get_render_task() {
return m_task;
}
};
}
+179
View File
@@ -0,0 +1,179 @@
#include "WorldRenderer.hpp"
#include <memory>
#include <vector>
#include <string>
#include <print>
#include "imgui.h"
#include "shaders/ShaderInputSet.h"
#include "shaders/ShaderInputSetLayoutBuilder.hpp"
#include "SDLWindow.h"
#include "Swapchain.hpp"
#include "RenderGraphBuilder.hpp"
#include "draw/RenderPasses/CharacterRenderPass.hpp"
#include "ImGuiRenderPass.hpp"
#include "draw/RenderPasses/TerrainRenderPass.hpp"
#include "world/Camera.hpp"
#include "world/World.hpp"
namespace tw::drw {
void lft_dbg_callback(lft::dbg::LogMessageSeverity severity,
lft::dbg::LogMessageType type,
const char *__restrict format,
va_list args) {
const char* titles[3] = {
"\033[0;34m[info]:",
"\033[0;33m[warn]:",
"\033[0;31m[fail]:"
};
fwrite(titles[severity], 14, 1, stdout);
if(args != nullptr) {
vfprintf(stdout, format, args);
} else {
fwrite(format, 1, strlen(format), stdout);
}
fwrite("\033[0m", 4, 1, stdout);
printf("\n");
}
Instance create_instance(const std::string& name, const lft::win::Window* window) {
std::vector<std::string> required_extensions = window->get_required_extensions();
// required_extensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME);
std::vector<std::string> required_layers = {
"VK_LAYER_KHRONOS_validation"
};
/**
* Instance initializes a connection with Vulkan driver
*/
return Instance(
name, name,
required_extensions,
required_layers,
lft_dbg_callback);
}
Surface create_surface(lft::win::SDLWindow* window, const Instance* instance) {
return window->create_surface(instance);
}
lft::rg::RenderGraph WorldRenderer::init_render_graph() {
auto m_imgui_rp = new gui::ImGuiRenderPass(&m_gpu, (const lft::win::SDLWindow*)m_window);
auto m_character_rp = new CharacterRenderPass(this, &m_shader_manager);
auto m_terrain_rp = new TerrainRenderPass(this, &m_shader_manager);
m_rendergraph_builder.add_task(m_imgui_rp->get_render_task().build());
m_rendergraph_builder.add_task(m_character_rp->get_render_task().build());
m_rendergraph_builder.add_task(m_terrain_rp->get_render_task().build());
return m_rendergraph_builder.build();
}
VkDescriptorSetLayout create_global_descriptor_set_layout(const Gpu* gpu) {
return ShaderInputSetLayoutBuilder()
.uniform_buffer(0)
.build(gpu);
}
VkDescriptorSet create_global_descriptor_set(
const Gpu* gpu,
VkDescriptorSetLayout descriptor_set_layout,
Buffer camera_buffer
) {
return ShaderInputSetBuilder()
.buffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, camera_buffer, 0, sizeof(glm::mat4) * 2 + sizeof(glm::vec4))
.build(gpu, descriptor_set_layout);
}
Buffer create_camera_buffer(const Gpu *gpu) {
MemoryAllocationInfo allocInfo = {
.usage = MEMORY_USAGE_AUTO_PREFER_DEVICE,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
};
BufferCreateInfo bufferInfo = {
.size = sizeof(CameraData),
.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
.isExclusive = true
};
Buffer buffer;
gpu->memory()->create_buffer(&bufferInfo, &allocInfo, &buffer);
return buffer;
}
WorldRenderer::WorldRenderer(
const std::string& name,
const lft::win::Window* window,
const World* world,
const io::Files* files
) :
m_window(window),
m_surface(create_surface((lft::win::SDLWindow*)(m_window), &m_instance)),
m_instance(create_instance(name, m_window)),
m_gpu(&m_instance, &m_surface),
m_swapchain(&m_gpu, m_window->get_size(), &m_surface),
m_files(files),
m_shader_manager(&m_gpu, m_files),
m_wait_on_image_fence(m_gpu.create_fence(false)),
m_camera_buffer(create_camera_buffer(&m_gpu)),
m_global_descriptor_set_layout(create_global_descriptor_set_layout(&m_gpu)),
m_global_descriptor_set(create_global_descriptor_set(&m_gpu, m_global_descriptor_set_layout, m_camera_buffer)),
m_world(world),
m_mesh_buffer(&m_gpu),
m_rendergraph_builder(&m_gpu, ImageChain::from_swapchain(m_swapchain), "swapchain"),
m_rendergraph(init_render_graph()),
m_camera(720.0f / 480.0f)
{
}
void WorldRenderer::update_camera_buffer() {
void *pData = nullptr;
m_gpu.memory()->map(m_camera_buffer.allocation, &pData);
memcpy(pData, m_camera.data(), sizeof(CameraData));
m_gpu.memory()->unmap(m_camera_buffer.allocation);
}
void WorldRenderer::render() {
if(!m_window->has_size(m_last_window_size)) {
vkDeviceWaitIdle(m_gpu.dev());
m_swapchain.resize(m_window->get_size());
m_rendergraph_builder.set_image_chain(ImageChain::from_swapchain(m_swapchain));
m_rendergraph = m_rendergraph_builder.build();
m_last_window_size = m_window->get_size();
}
uint32_t imageIdx = 0;
auto result = m_swapchain.get_next_image_idx(
VK_NULL_HANDLE, m_wait_on_image_fence, &imageIdx);
if(result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
std::println("Rebuilding");
m_rendergraph_builder.build();
}
update_camera_buffer();
m_rendergraph.run(imageIdx, VK_NULL_HANDLE, m_wait_on_image_fence);
m_swapchain.present({ m_rendergraph.buffer(0).final_signal(imageIdx) }, imageIdx);
}
}
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <memory>
#include "Gpu.hpp"
#include "Window.hpp"
#include "Swapchain.hpp"
#include "draw/MeshBuffer.hpp"
#include "io/Files.hpp"
#include "io/ShaderManager.hpp"
#include "world/World.hpp"
#include "world/Camera.hpp"
#include "RenderGraphBuilder.hpp"
namespace tw::drw {
class WorldRenderer {
private:
const lft::win::Window* m_window;
VkExtent2D m_last_window_size;
Instance m_instance;
Surface m_surface;
Gpu m_gpu;
Swapchain m_swapchain;
const io::Files* m_files;
io::ShaderManager m_shader_manager;
VkFence m_wait_on_image_fence;
Buffer m_camera_buffer;
VkDescriptorSetLayout m_global_descriptor_set_layout;
VkDescriptorSet m_global_descriptor_set;
const World *m_world;
MeshBuffer m_mesh_buffer;
lft::rg::Builder m_rendergraph_builder;
lft::rg::RenderGraph m_rendergraph;
Camera m_camera;
lft::rg::RenderGraph init_render_graph();
void update_camera_buffer();
public:
GET_MUT_REF(m_camera, camera);
MeshBuffer* mesh_buffer() { return &m_mesh_buffer; }
const MeshBuffer* mesh_buffer() const { return &m_mesh_buffer; }
const World* world() const { return m_world; }
const VkDescriptorSetLayout global_descriptor_set_layout() const {
return m_global_descriptor_set_layout;
}
const VkDescriptorSet global_descriptor_set() const {
return m_global_descriptor_set;
}
WorldRenderer(const std::string& name,
const lft::win::Window* window,
const World* world,
const io::Files* files);
WorldRenderer(WorldRenderer&) = delete;
WorldRenderer& operator=(WorldRenderer&) = delete;
Mesh add_mesh(const MeshData& mesh_data) {
return m_mesh_buffer.push_mesh(mesh_data);
}
void render();
};
}
@@ -0,0 +1,10 @@
#pragma once
namespace tw::drw {
class WorldRendererGraph {
public:
WorldRendererGraph(const WorldRendererGraph* graph);
};
}
+15
View File
@@ -0,0 +1,15 @@
#include "ShaderManager.hpp"
namespace tw::io {
ShaderManager::ShaderManager(const Gpu* gpu, const Files* files) :
m_files(files),
m_shader_builder(gpu)
{
}
Shader ShaderManager::load_shader(const std::string& name) {
return m_shader_builder.from_binary(m_files->read_file_binary(path("shaders") / name));
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <shaders/Shader.hpp>
#include <shaders/SpirvShaderBuilder.hpp>
#include "io/Files.hpp"
namespace tw::io {
/**
* Unifies and simplifies shader loading
*/
class ShaderManager {
const Files *m_files;
const SpirvShaderBuilder m_shader_builder;
public:
ShaderManager(const Gpu* gpu, const Files *files);
Shader load_shader(const std::string& name);
};
}
@@ -0,0 +1,10 @@
#pragma once
/**
* Writes entity derivation message
*/
template<typename TMesg>
class EntityDerivationWriter {
};
@@ -0,0 +1,12 @@
#pragma once
#include <glm/glm.hpp>
#include "InterpolatedProperty.hpp"
namespace tw::net {
typedef InterpolatedProperty<glm::vec3, 60> EntityPositionInterpolation;
}
@@ -0,0 +1,57 @@
#include "EntityPositionInterpolator.hpp"
#include "world/Transform.hpp"
#include "InterpolatedProperty.hpp"
#include "EntityInterpolation.hpp"
#include <spdlog/spdlog.h>
namespace tw::net {
EntityPositionInterpolator::EntityPositionInterpolator(
entt::registry* registry, entt::entity player_entity, size_t bufferingIntervalInMillis
) : m_registry(registry),
m_player_entity(player_entity),
m_bufferingIntervalInMillis(bufferingIntervalInMillis)
{
}
void EntityPositionInterpolator::register_entity(entt::entity entity) {
m_registry->emplace<EntityPositionInterpolation>(entity, glm::vec3());
}
void EntityPositionInterpolator::add_position_for_entity(entt::entity entity, glm::vec3 position) {
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
if(interpolation == nullptr) {
spdlog::warn("Attempt to add position for non-registered entity {}", (uint32_t)entity);
return;
}
interpolation->push(Clock::now(), position);
}
glm::vec3 EntityPositionInterpolator::get_position_for_entity(entt::entity entity) {
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
if(interpolation == nullptr) {
spdlog::warn("Attempt to get position for non-registered entity {}", (uint32_t)entity);
return glm::vec3();
}
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
auto [from, to, value] = interpolation->get_values_around(now);
return glm::mix(from, to, value);
}
void EntityPositionInterpolator::update() {
m_registry->view<EntityPositionInterpolation, Transform>()
.each([&](const auto entity, const EntityPositionInterpolation& interpolation, Transform& ts) {
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
auto [from, to, value] = interpolation.get_values_around(now);
ts.set_position(glm::mix(from, to, value));
});
}
}
@@ -0,0 +1,34 @@
#include <chrono>
#include <entt/entt.hpp>
#include <glm/glm.hpp>
namespace tw::net {
class EntityPositionInterpolator {
using Clock = std::chrono::high_resolution_clock;
entt::registry* m_registry;
entt::entity m_player_entity;
size_t m_bufferingIntervalInMillis;
public:
EntityPositionInterpolator(
entt::registry* registry,
entt::entity player_entity,
size_t bufferingIntervalInMillis);
void register_entity(entt::entity entity);
void add_position_for_entity(entt::entity entity, glm::vec3 position);
glm::vec3 get_position_for_entity(entt::entity entity);
void update();
};
}
@@ -0,0 +1,88 @@
#pragma once
#include <array>
#include <chrono>
#include "common.hpp"
template<typename T, int length = 3>
class InterpolatedProperty {
using Clock = std::chrono::high_resolution_clock;
std::array<T, length> m_buffer;
std::array<Clock::time_point, length> m_time_buffer;
public:
InterpolatedProperty(T initial_value) {
for(int i = 0; i < length; i++) {
m_buffer[i] = initial_value;
m_time_buffer[i] = Clock::now();
}
}
GET_REF(m_buffer, values);
GET_REF(m_time_buffer, times);
void push(Clock::time_point point, T value) {
if(point < m_time_buffer.at(0)) {
return;
}
int i;
for(i = 1; i < length; i++) {
if(m_time_buffer.at(i) > point) {
m_time_buffer[i - 1] = point;
m_buffer[i - 1] = value;
return;
} else {
m_time_buffer[i - 1] = m_time_buffer[i];
m_buffer[i - 1] = m_buffer[i];
}
}
m_time_buffer[i - 1] = point;
m_buffer[i - 1] = value;
return;
// for(int i = 0; i < length; i++) {
// if(m_time_buffer.at(i) > point) {
// for(int o = 1; o < i; o++) {
// m_time_buffer[o - 1] = m_time_buffer[o];
// m_buffer[o - 1] = m_buffer[o];
// }
//
// m_time_buffer[i] = point;
// m_buffer[i] = value;
//
// return;
// }
// }
//
// m_time_buffer[length] = point;
// m_buffer[length] = value;
}
inline std::tuple<T, T, float> get_values_around(Clock::time_point point) const {
T prev = m_buffer.at(0);
Clock::time_point prev_point = m_time_buffer.at(0);
for(int i = 0; i < length; i++) {
if(m_time_buffer.at(i) > point) {
float value;
if(i == 0) {
value = 1.0f;
} else {
value = (float)(point - prev_point).count() / (m_time_buffer[i] - prev_point).count();
}
return std::make_tuple(prev, m_buffer.at(i), value);
}
prev = m_buffer[i];
prev_point = m_time_buffer[i];
}
return std::make_tuple(m_buffer[m_buffer.size() - 1], m_buffer[m_buffer.size() - 1], 1.0f);
}
};
+161
View File
@@ -0,0 +1,161 @@
#include "runtime.hpp"
#include "Address.hpp"
#include "SDLWindow.h"
#include "debug/tools/NetworkStatsGui.hpp"
#include "debug/tools/PacketBacklogGui.hpp"
#include "entt/entity/fwd.hpp"
#include "io/InputState.hpp"
#include "debug/tools/EntityManagerGui.hpp"
#include "debug/tools/PerformanceStatsGui.hpp"
#include "draw/MeshData.hpp"
#include "imgui.h"
#include "imgui_impl_vulkan.h"
#include "imgui_impl_sdl2.h"
#include "implot.h"
#include "implot_internal.h"
#include "world/Transform.hpp"
#include <SDL_events.h>
#include <glm/glm.hpp>
#include <tracy/Tracy.hpp>
#include <memory>
namespace tw {
std::unique_ptr<lft::win::Window> create_window(const std::string& name, VkExtent2D extent) {
return std::make_unique<lft::win::SDLWindow>(name, (VkRect2D){
0, 0,
extent.width, extent.height
});
}
int get_port_from_args(int argc, char** argv) {
try {
if(argc > 1) {
return atoi(argv[1]);
} else {
return 8080;
}
} catch(std::exception& e) {
std::println("Could not parse port from arguments, using default.");
return 8080;
}
}
Runtime::Runtime(int argc, char** argv) :
m_files(argv, argc),
m_window(create_window("towards", {1920, 1200})),
m_world{},
m_physics_world(&m_world),
m_world_renderer("towards", m_window.get(), &m_world, &m_files),
m_input_manager(m_window.get()),
m_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }),
m_lockstep(60)
{
}
// bool Runtime::world_state_packet_handler(uint32_t* p_frame_idx, WorldSnapshotMessage* mesg) {
// uint32_t frame_idx = *p_frame_idx;
// if(mesg->frame_idx < frame_idx) {
// return false;
// }
// for(int i = 0; i < mesg->player_states.size(); i++) {
// if(!m_players.contains(mesg->player_states[i].id)) {
// auto mesh = m_world_renderer.add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
// const auto entity = m_world.registry().create();
// m_players.insert({mesg->player_states[i].id, entity});
// m_world.registry().emplace<Transform>(entity, Transform(mesg->player_states[i].position));
// m_world.registry().emplace<PlayerInfoComponent>(entity,
// PlayerInfoComponent(
// mesg->player_states[i].id,
// mesg->player_states[i].name));
// m_world.registry().emplace<drw::Mesh>(entity, mesh);
// } else {
// auto entity = (entt::entity)mesg->player_states[i].id;
// auto entity_ts = m_world.registry()
// .try_get<Transform>(entity);
// if(entity_ts) {
// entity_ts->transform = glm::translate(glm::mat4(1.0f), mesg->player_states[i].position);
// }
// }
// }
// return true;
// }
void Runtime::run() {
dbg::tools::EntityManagerGui entity_manager(&m_world);
dbg::tools::PerformanceStatsGui perf_stats(m_lockstep);
dbg::tools::PacketBacklogGui packet_backlog;
dbg::tools::NetworkStatsGui network_stats;
ImPlot::CreateContext();
m_is_running = true;
while(is_running()) {
if(m_lockstep.wait_for_next_step()) {
continue;
}
ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
// ImGui::DockSpaceOverViewport();
entity_manager.draw();
ImGui::Begin("History");
if(ImGui::BeginTable("historyTable", 2)) {
// for(auto key : m_world_controller.position_history().keys()) {
// ImGui::TableNextRow();
//
// // auto v = *m_world_controller.player_history().get(key).value();
// // auto input = std::format("{} {} {}", v.x, v.y, v.z);
// auto p = *m_world_controller.position_history().get(key).value();
// auto position = std::format("{} {} {}", p.x, p.y, p.z);
//
// ImGui::TableNextColumn();
// ImGui::Text("%u", key);
// // ImGui::TableNextColumn();
// // ImGui::Text(input.c_str());
// ImGui::TableNextColumn();
// ImGui::Text(position.c_str());
// }
ImGui::EndTable();
}
ImGui::End();
bool change_imgui = false;
m_input_manager.update();
if(m_input_manager.is_quit()) {
m_is_running = false;
}
m_world_controller.update(m_lockstep.delta_time());
perf_stats.draw();
packet_backlog.draw();
network_stats.draw();
tw::dbg::ComponentGui<tw::io::InputManager>().draw(&m_input_manager);
m_world.step(m_lockstep.delta_time());
m_world_renderer.render();
FrameMark;
}
}
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "draw/WorldRenderer.hpp"
#include "io/InputState.hpp"
#include "runtime/LockStep.hpp"
#include "world/ClientWorldController.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/World.hpp"
#include "messages/PlayerMoveMessage.hpp"
namespace tw {
/**
* Handles runtime of the client application.
*/
class Runtime {
private:
io::Files m_files;
std::unique_ptr<lft::win::Window> m_window;
tw::World m_world;
JoltPhysicsWorld m_physics_world;
tw::drw::WorldRenderer m_world_renderer;
tw::io::InputManager m_input_manager;
tw::ClientWorldController m_world_controller;
tw::LockStep m_lockstep;
bool m_is_running;
std::unordered_map<uint32_t, entt::entity> m_players;
void send_player_positions();
public:
const bool is_running() const {
return m_is_running;
}
Runtime(int argc, char** argv);
void run();
};
}
+12
View File
@@ -0,0 +1,12 @@
#include "Camera.hpp"
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
namespace tw {
Camera::Camera(float aspect_ratio) :
m_data( glm::perspective(40.0f, aspect_ratio, 0.1f, 1000.0f),
Transform(glm::vec3(10.0f, 10.0f, 10.0f))) {
}
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include "common.hpp"
#include "glm/ext/matrix_transform.hpp"
#include <glm/glm.hpp>
#include "world/Transform.hpp"
namespace tw {
struct CameraData {
glm::mat4 projection;
Transform view;
CameraData(glm::mat4 projection, Transform view) :
projection(projection),
view(view)
{
}
};
class Camera {
CameraData m_data;
public:
GET_MUT_REF(m_data.view, view);
glm::vec3 position() const {
return m_data.view.position();
}
void look_at(glm::vec3 from, glm::vec3 to) {
m_data.view = glm::lookAt(from, to, glm::vec3(0.0f, 1.0f, 0.0f));
}
/**
* For uploading data to the GPU buffer
*/
inline const CameraData* data() const { return &m_data; }
/**
*
*/
Camera(float aspect_ratio);
};
}
@@ -0,0 +1,276 @@
#include "ClientWorldController.hpp"
#include <glm/glm.hpp>
#include <chrono>
#include <spdlog/spdlog.h>
#include "Address.hpp"
#include "Entity.pb.h"
#include "Login.pb.h"
#include "WorldState.pb.h"
#include "PlayerMove.pb.h"
#include "entt/entity/entity.hpp"
#include "messenger/MessageHandler.hpp"
#include "messenger/Messenger.hpp"
#include "TcpStream.hpp"
#include "messages/PlayerMoveMessage.hpp"
#include "metrics/HistoryBuffer.hpp"
#include "world/CharacterBody.hpp"
#include "world/CharacterController.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/WorldEntity.hpp"
#include "tw/serial/WorldStateWriter.hpp"
namespace tw {
typedef HistoryBuffer<long, glm::vec3> EntityPositionHistory;
entt::entity create_player_entity(World* world, JoltPhysicsWorld* physics_world, drw::WorldRenderer* renderer) {
// entt::entity entity = world->registry().create();
// drw::Mesh mesh = renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
// world->registry()
// .emplace<Transform>(entity,
// Transform(glm::vec3(0.0f, 10.0f, 0.0f)));
// world->registry()
// .emplace<EntityPositionHistory>(entity, 0, glm::vec3(0.0f, 10.0f, 0.0f), 1000);
// world->registry()
// .emplace<tw::WorldEntity>(entity,
// tw::WorldEntity(std::string("player"), (uint32_t)entity));
// world->registry()
// .emplace<tw::drw::Mesh>(entity, mesh);
// world->registry()
// .emplace<tw::CharacterController>(entity, 20.0f);
// auto* body = &world->registry()
// .emplace<CharacterBody>(entity, physics_world->create_character(
// new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
// glm::vec3(0.0f, 10.0f, 0.0f)));
// return entity;
return entt::entity(0);
}
entt::entity
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
const auto entity = m_world->registry().create();
if(!m_mesh.has_value()) {
m_mesh = m_world_renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
}
spdlog::info("Creating entity {}", name);
m_world->registry()
.emplace<Transform>(entity,
Transform(position));
m_world->registry()
.emplace<tw::WorldEntity>(entity,
tw::WorldEntity(name, (uint32_t)entity));
m_world->registry()
.emplace<tw::drw::Mesh>(entity,
m_mesh.value());
return entity;
}
net::TcpStream create_stream(tw::net::Address address) {
auto stream = net::TcpStream::connect(address);
if(!stream.has_value()) {
spdlog::error("Failed to connect to server");
throw std::runtime_error("Failed to connect to server");
}
stream.value().set_non_blocking();
return std::move(stream.value());
}
std::optional<entt::entity> ClientWorldController::map_from_server_entity(int id) {
if(m_entity_mapping.contains(id)) {
return m_entity_mapping[id];
}
return {};
}
void ClientWorldController::map_server_entity(int server_id, entt::entity local_id) {
m_entity_mapping[server_id] = local_id;
}
void ClientWorldController::apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count) {
for(int i = 0; i < count; i++) {
const mmo::EntityPosition* const position = positions[i];
std::optional<entt::entity> entity = map_from_server_entity(position->id());
if(!entity.has_value()) {
spdlog::warn("Received position update for unknown entity {}", position->id());
continue;
}
glm::vec3 p = {position->x(), position->y(), position->z()};
m_entity_interpolator.add_position_for_entity(entity.value(), p);
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
if(history != nullptr) {
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
history->set(millis, p);
}
}
}
ClientWorldController::ClientWorldController(
const io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
tw::net::Address address
) :
m_input_manager(inputs),
m_world(world),
m_physics_world(physics_world),
m_world_renderer(world_renderer),
m_player_entity(create_player_entity(world, physics_world, world_renderer)),
m_player_controller(&world_renderer->camera(), glm::vec3()),
m_messenger{address},
m_tick_step(20),
m_is_connected(false),
m_position_history_exporter("/home/martin/output.csv"),
m_entity_interpolator(&m_world->registry(), m_player_entity, 300)
{
m_messenger->set_handler<mmo::LoginResponse>(
[&](mmo::LoginResponse* mesg) {
if(!m_is_connected) {
spdlog::info("Joined the game!");
}
});
m_messenger->set_raw_handler(Message<mmo::WorldStateMessage>::value,
[&](std::span<std::byte> data) -> tl::expected<void, net::NetworkError> {
serial::WorldStateReader reader(data);
auto header = reader.read_header();
while(reader.has_spawn()) {
auto spawn = reader.read_spawn();
auto entity = create_entity("test", glm::vec3());
spdlog::info("Spawning entity {}", spawn);
map_server_entity(spawn, entity);
m_entity_interpolator.register_entity(entity);
}
while(reader.has_entity()) {
auto entity_r = reader.read_entity();
std::optional<entt::entity> entity = map_from_server_entity(entity_r.id);
if(!entity.has_value()) {
spdlog::warn("Received position update for unknown entity {}", entity_r.id);
continue;
}
glm::vec3 p = {entity_r.position.x, entity_r.position.y, entity_r.position.z};
m_entity_interpolator.add_position_for_entity(entity.value(), p);
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
if(history != nullptr) {
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
history->set(millis, p);
}
}
// apply_entity_positions();
return {};
});
m_messenger->set_handler<mmo::EntitySpawnMessage>(
[&](mmo::EntitySpawnMessage* mesg) {
auto entity = create_entity(mesg->name(), glm::vec3());
map_server_entity(mesg->entity_id(), entity);
m_entity_interpolator.register_entity(entity);
});
}
ClientWorldController::~ClientWorldController() {
}
void ClientWorldController::export_entity_history() {
}
void ClientWorldController::update(double delta_time) {
m_player_controller.update(m_input_manager, delta_time);
ImGui::Begin("Player Controller");
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity);
ImGui::Text("Player count: %ld", m_entity_mapping.size());
for(auto mapping : m_entity_mapping) {
ImGui::Text("%d -> %d", (uint32_t)mapping.first, mapping.second);
}
ImGui::End();
if(m_tick_step.update()) {
m_messenger->update();
if(!m_is_connected && false) {
return;
} else {
// CharacterController& character = m_world->registry().get<CharacterController>(m_player_entity);
// character.set_input(m_frame_idx, m_player_controller.input());
mmo::PlayerMoveMessage player_move_message = {};
player_move_message.set_frame_idx(m_frame_idx);
mmo::PlayerInput* player_input = new mmo::PlayerInput();
player_input->set_x(m_player_controller.input().x);
player_input->set_y(m_player_controller.input().y);
player_input->set_z(m_player_controller.input().z);
player_move_message.set_allocated_input(player_input);
auto r = m_messenger->send(player_move_message);
// CharacterBody& ts = m_world->registry().get<CharacterBody>(m_player_entity);
// auto position = ts.m_character->GetPosition();
// character.position_history().set(m_frame_idx, glm::vec3(position[0], position[1], position[2]));
// EntityInterpolation& interpolation = m_world->registry().get<EntityInterpolation>(m_player_entity);
// interpolation.push(std::chrono::steady_clock::now(), glm::vec3(position[0], position[1], position[2]));
// m_player_controller.set_target(glm::vec3(position[0], position[1], position[2]));
//
export_entity_history();
m_frame_idx++;
m_world->registry().view<Transform>()
.each([&](const auto e, Transform& t) {
m_position_history_exporter.write((uint32_t)e,
std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count(), t.position());
});
}
}
m_physics_world->step(m_frame_idx, delta_time);
m_entity_interpolator.update();
}
}
@@ -0,0 +1,97 @@
#pragma once
#include <entt/entt.hpp>
#include <glm/gtx/io.hpp>
#include "Address.hpp"
#include "TcpStream.hpp"
#include "messenger/MessageHandler.hpp"
#include "entt/entity/fwd.hpp"
#include "io/InputState.hpp"
#include "metrics/HistoryBufferExporter.hpp"
#include "runtime/LockStep.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/World.hpp"
#include "draw/WorldRenderer.hpp"
#include "world/ThirdPersonPlayerController.hpp"
#include "network/EntityPositionInterpolator.hpp"
namespace tw {
/**
* Multiplayer player controller. Requires address of a server to work.
*/
class ClientWorldController {
/**
* Inputs
*/
const io::InputManager* m_input_manager;
/**
* Pointers to the world
*/
World* m_world;
drw::WorldRenderer* m_world_renderer;
JoltPhysicsWorld* m_physics_world;
entt::entity m_player_entity;
ThirdPersonPlayerController m_player_controller;
std::optional<tw::net::MessageHandler> m_messenger;
LockStep m_tick_step;
uint32_t m_frame_idx = 1;
entt::entity m_entity_id;
glm::vec3 m_input;
bool m_is_connected;
std::optional<drw::Mesh> m_mesh;
using Clock = std::chrono::steady_clock;
HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;
net::EntityPositionInterpolator m_entity_interpolator;
/**
* Mapping from the server entity_id to local entity_id
* Server might have the same entity under different name
* TODO: Figure out if entt supports custUntitledom IDs to sync them
*/
std::unordered_map<uint32_t, entt::entity> m_entity_mapping;
entt::entity create_entity(const std::string& name, glm::vec3 position);
net::MessageHandler create_messenger();
std::optional<entt::entity> map_from_server_entity(int id);
void map_server_entity(int server_id, entt::entity local_id);
void apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count);
/**
* Exports entity history to CSV file for analysis
*/
void export_entity_history();
public:
ClientWorldController(
const io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
tw::net::Address address
);
~ClientWorldController();
void update(double delta_time);
};
}
@@ -0,0 +1,13 @@
#pragma once
#include <entt/entt.hpp>
#include <glm/glm.hpp>
namespace tw {
class EntityFactory {
public:
entt::entity create_player_entity(glm::vec3 position);
};
}
@@ -0,0 +1,51 @@
#include "LocalWorldController.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/WorldEntity.hpp"
namespace tw {
CharacterBody* create_player_body(World* world, JoltPhysicsWorld* physics_world, drw::WorldRenderer* renderer) {
const auto entity = world->registry().create();
drw::Mesh mesh = renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
world->registry()
.emplace<Transform>(entity,
Transform(glm::vec3(0.0f, 10.0f, 0.0f)));
world->registry()
.emplace<tw::WorldEntity>(entity,
tw::WorldEntity(std::string("player"), (uint32_t)entity));
world->registry()
.emplace<tw::drw::Mesh>(entity,
mesh);
auto* body = &world->registry()
.emplace<CharacterBody>(entity, physics_world->create_character(
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
glm::vec3(0.0f, 10.0f, 0.0f)));
return body;
}
LocalWorldController::LocalWorldController(
io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer) :
m_input_manager(inputs),
m_world(world),
m_physics_world(physics_world),
m_world_renderer(world_renderer),
m_player_body(create_player_body(world, physics_world, world_renderer)),
m_player_controller(&world_renderer->camera(), glm::vec3())
{
}
void LocalWorldController::update(double delta_time) {
m_player_controller.update(m_input_manager, delta_time);
}
}
@@ -0,0 +1,33 @@
#pragma once
#include "draw/WorldRenderer.hpp"
#include "io/InputState.hpp"
#include "world/CharacterBody.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/ThirdPersonPlayerController.hpp"
#include "world/World.hpp"
namespace tw {
class LocalWorldController {
private:
io::InputManager* m_input_manager;
World* m_world;
JoltPhysicsWorld* m_physics_world;
drw::WorldRenderer* m_world_renderer;
CharacterBody* m_player_body;
ThirdPersonPlayerController m_player_controller;
public:
LocalWorldController(
io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer);
void update(double delta_time);
};
}
@@ -0,0 +1,56 @@
#include "ThirdPersonPlayerController.hpp"
#include <glm/glm.hpp>
#include <entt/entt.hpp>
namespace tw {
ThirdPersonPlayerController::ThirdPersonPlayerController(Camera* camera, glm::vec3 target) :
m_camera(camera),
m_camera_rotation(1.0f, 0.0f, 0.0f, 0.0f),
m_camera_zoom(10.0f),
m_sensitivity(0.01f),
m_target(target),
m_input(0.0f, 0.0f, 0.0f)
{
}
glm::vec3 ThirdPersonPlayerController::get_axis() {
glm::vec3 forward = m_camera_rotation * glm::vec3(0, 0, -1);
forward.y = 0.0f;
return glm::normalize(forward);
}
void ThirdPersonPlayerController::update(const tw::io::InputManager* input, double delta_time) {
// glm::vec3 velocity = {
// input->axis_x(),
// 0.0f,
// input->axis_y()
// };
// velocity *= m_player_body->m_speed * delta_time;
glm::vec3 forward = m_camera_rotation * glm::vec3(0, 0, -1);
forward.y = 0.0f;
forward = glm::normalize(forward);
glm::vec3 right = glm::cross(forward, glm::vec3(0.0f, 1.0f, 0.0f));
glm::vec3 velocity = forward * input->axis_y() + right * -input->axis_x();
if(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z > 0.0f) {
m_input = glm::normalize(velocity);
} else {
m_input = glm::vec3(0.0f);
}
glm::quat yaw = glm::angleAxis(-input->motion_x() * m_sensitivity, glm::vec3(0, 1, 0));
glm::quat pitch = glm::angleAxis(-input->motion_y() * m_sensitivity, glm::vec3(1, 0, 0));
m_camera_rotation = yaw * m_camera_rotation * pitch;
glm::vec3 offset = m_camera_rotation * glm::vec3(0, 0, m_camera_zoom);
m_camera->view().look_at(get_target_position() + offset, get_target_position());
}
}
@@ -0,0 +1,41 @@
#pragma once
#include <glm/gtc/quaternion.hpp>
#include "world/Camera.hpp"
#include "io/InputState.hpp"
namespace tw {
/**
* Player controller from third person
*/
class ThirdPersonPlayerController {
Camera* m_camera;
glm::vec3 m_input;
glm::quat m_camera_rotation;
float m_camera_zoom;
float m_sensitivity;
glm::vec3 m_target;
glm::vec3 get_target_position() {
return m_target;
}
public:
GET_REF(m_input, input);
void set_target(glm::vec3 target) {
m_target = target;
}
ThirdPersonPlayerController(Camera* camera, glm::vec3 target);
glm::vec3 get_axis();
void update(const tw::io::InputManager* input, double delta_time);
};
}