mirror of
https://github.com/kuhyx/praca_magisterska.git
synced 2026-07-04 13:43:05 +02:00
feat: added presentation
This commit is contained in:
parent
6069b5d948
commit
b6ac46401e
259
code/cpp/algorithms.cpp
Normal file
259
code/cpp/algorithms.cpp
Normal file
@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Shortest-path algorithms: Dijkstra, Bellman-Ford, A*
|
||||
* Implementations — no GUI dependencies.
|
||||
*/
|
||||
|
||||
#include "algorithms.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
// ─── Adjacency list (internal) ──────────────────────────────────────────
|
||||
|
||||
using AdjList = std::map<std::string, std::vector<std::pair<std::string, int>>>;
|
||||
|
||||
static AdjList build_adj() {
|
||||
AdjList adj;
|
||||
for (auto& n : NODE_ORDER) adj[n] = {};
|
||||
for (auto& e : EDGES) {
|
||||
adj[e.src].emplace_back(e.dst, e.weight);
|
||||
adj[e.dst].emplace_back(e.src, e.weight);
|
||||
}
|
||||
return adj;
|
||||
}
|
||||
|
||||
static const AdjList ADJ = build_adj();
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
static std::string join_path(const std::vector<std::string>& p) {
|
||||
std::string s;
|
||||
for (size_t i = 0; i < p.size(); i++) {
|
||||
if (i) s += "->";
|
||||
s += p[i];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void AlgoState::init_dists() {
|
||||
for (auto& n : NODE_ORDER) {
|
||||
dist[n] = INF_VAL;
|
||||
prev[n] = "";
|
||||
}
|
||||
}
|
||||
|
||||
void AlgoState::reconstruct_path() {
|
||||
path.clear();
|
||||
std::string node = GOAL;
|
||||
while (!node.empty()) {
|
||||
path.insert(path.begin(), node);
|
||||
auto it = prev.find(node);
|
||||
node = (it != prev.end() && !it->second.empty()) ? it->second : "";
|
||||
}
|
||||
if (path.empty() || path[0] != SOURCE) path.clear();
|
||||
}
|
||||
|
||||
double heuristic(const std::string& node) {
|
||||
auto pn = NODES.at(node);
|
||||
auto pg = NODES.at(GOAL);
|
||||
return std::hypot(pg.x - pn.x, pg.y - pn.y) / 100.0;
|
||||
}
|
||||
|
||||
// ─── Dijkstra ───────────────────────────────────────────────────────────
|
||||
|
||||
AlgoState dijkstra_init() {
|
||||
AlgoState st;
|
||||
st.init_dists();
|
||||
st.dist[SOURCE] = 0;
|
||||
st.frontier.push({0, SOURCE});
|
||||
st.message = "Dijkstra: start from " + SOURCE;
|
||||
return st;
|
||||
}
|
||||
|
||||
void dijkstra_step(AlgoState& st) {
|
||||
auto finish = [&]() {
|
||||
st.finished = true;
|
||||
st.reconstruct_path();
|
||||
st.message = st.path.empty()
|
||||
? "Dijkstra done! No path."
|
||||
: "Dijkstra done! Path: " + join_path(st.path) +
|
||||
", cost=" + std::to_string((int)st.dist[GOAL]);
|
||||
};
|
||||
|
||||
if (st.finished || st.frontier.empty()) { finish(); return; }
|
||||
|
||||
st.current_node = "";
|
||||
while (!st.frontier.empty()) {
|
||||
auto [d, u] = st.frontier.top();
|
||||
st.frontier.pop();
|
||||
if (st.visited.count(u)) continue;
|
||||
|
||||
st.visited.insert(u);
|
||||
st.step++;
|
||||
st.current_node = u;
|
||||
st.message = "Step " + std::to_string(st.step) +
|
||||
": visit " + u + " (dist=" +
|
||||
std::to_string((int)st.dist[u]) + ")";
|
||||
|
||||
for (auto& [v, w] : ADJ.at(u)) {
|
||||
if (!st.visited.count(v)) {
|
||||
double nd = st.dist[u] + w;
|
||||
if (nd < st.dist[v]) {
|
||||
st.dist[v] = nd;
|
||||
st.prev[v] = u;
|
||||
st.frontier.push({nd, v});
|
||||
st.relaxed_edges.insert({u, v});
|
||||
st.message += " | relax " + u + "->" + v + ":" +
|
||||
std::to_string((int)nd);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
// ─── Bellman-Ford ───────────────────────────────────────────────────────
|
||||
|
||||
AlgoState bellman_ford_init() {
|
||||
AlgoState st;
|
||||
st.init_dists();
|
||||
st.dist[SOURCE] = 0;
|
||||
st.message = "Bellman-Ford: " + std::to_string(NODE_ORDER.size() - 1) +
|
||||
" iterations over all edges";
|
||||
return st;
|
||||
}
|
||||
|
||||
void bellman_ford_step(AlgoState& st) {
|
||||
if (st.finished) return;
|
||||
|
||||
int V = (int)NODE_ORDER.size();
|
||||
|
||||
std::vector<Edge> all_edges;
|
||||
for (auto& e : EDGES) {
|
||||
all_edges.push_back(e);
|
||||
all_edges.push_back({e.dst, e.src, e.weight});
|
||||
}
|
||||
|
||||
if (st.bf_iteration < V - 1) {
|
||||
bool changed = false;
|
||||
for (auto& e : all_edges) {
|
||||
if (st.dist[e.src] + e.weight < st.dist[e.dst]) {
|
||||
st.dist[e.dst] = st.dist[e.src] + e.weight;
|
||||
st.prev[e.dst] = e.src;
|
||||
st.relaxed_edges.insert({e.src, e.dst});
|
||||
st.visited.insert(e.src);
|
||||
st.visited.insert(e.dst);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
st.bf_iteration++;
|
||||
st.step++;
|
||||
st.current_node = "";
|
||||
st.message = "Iteration " + std::to_string(st.bf_iteration) + "/" +
|
||||
std::to_string(V - 1) + ": " +
|
||||
(changed ? "changes made" : "no changes (stable)");
|
||||
|
||||
if (!changed) {
|
||||
st.finished = true;
|
||||
st.reconstruct_path();
|
||||
if (!st.path.empty())
|
||||
st.message += " -> Early stop! Path: " + join_path(st.path) +
|
||||
", cost=" + std::to_string((int)st.dist[GOAL]);
|
||||
else
|
||||
st.message += " -> Early stop!";
|
||||
}
|
||||
} else {
|
||||
bool neg = false;
|
||||
for (auto& e : all_edges) {
|
||||
if (st.dist[e.src] + e.weight < st.dist[e.dst]) { neg = true; break; }
|
||||
}
|
||||
st.finished = true;
|
||||
if (neg) {
|
||||
st.message = "Negative cycle detected!";
|
||||
} else {
|
||||
st.reconstruct_path();
|
||||
st.message = st.path.empty()
|
||||
? "Bellman-Ford done! No path."
|
||||
: "Bellman-Ford done! Path: " + join_path(st.path) +
|
||||
", cost=" + std::to_string((int)st.dist[GOAL]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── A* ─────────────────────────────────────────────────────────────────
|
||||
|
||||
AlgoState astar_init() {
|
||||
AlgoState st;
|
||||
st.init_dists();
|
||||
st.dist[SOURCE] = 0;
|
||||
double h = heuristic(SOURCE);
|
||||
st.f_score[SOURCE] = h;
|
||||
st.frontier.push({h, SOURCE});
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "A*: start=%s, goal=%s, h(S)=%.1f",
|
||||
SOURCE.c_str(), GOAL.c_str(), h);
|
||||
st.message = buf;
|
||||
return st;
|
||||
}
|
||||
|
||||
void astar_step(AlgoState& st) {
|
||||
auto finish = [&]() {
|
||||
st.finished = true;
|
||||
st.reconstruct_path();
|
||||
st.message = st.path.empty()
|
||||
? "A* done! No path."
|
||||
: "A* done! Path: " + join_path(st.path) +
|
||||
", cost=" + std::to_string((int)st.dist[GOAL]);
|
||||
};
|
||||
|
||||
if (st.finished || st.frontier.empty()) { finish(); return; }
|
||||
|
||||
st.current_node = "";
|
||||
while (!st.frontier.empty()) {
|
||||
auto [f, u] = st.frontier.top();
|
||||
st.frontier.pop();
|
||||
if (st.visited.count(u)) continue;
|
||||
|
||||
st.visited.insert(u);
|
||||
st.step++;
|
||||
st.current_node = u;
|
||||
|
||||
if (u == GOAL) {
|
||||
st.finished = true;
|
||||
st.reconstruct_path();
|
||||
st.message = "Step " + std::to_string(st.step) +
|
||||
": GOAL reached! Path: " + join_path(st.path) +
|
||||
", cost=" + std::to_string((int)st.dist[GOAL]);
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[200];
|
||||
double h = heuristic(u);
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Step %d: visit %s (g=%.0f, h=%.1f, f=%.1f)",
|
||||
st.step, u.c_str(), st.dist[u], h, f);
|
||||
st.message = buf;
|
||||
|
||||
for (auto& [v, w] : ADJ.at(u)) {
|
||||
if (!st.visited.count(v)) {
|
||||
double nd = st.dist[u] + w;
|
||||
if (nd < st.dist[v]) {
|
||||
st.dist[v] = nd;
|
||||
st.prev[v] = u;
|
||||
double fv = nd + heuristic(v);
|
||||
st.f_score[v] = fv;
|
||||
st.frontier.push({fv, v});
|
||||
st.relaxed_edges.insert({u, v});
|
||||
char b2[100];
|
||||
snprintf(b2, sizeof(b2),
|
||||
" | relax %s->%s: g=%.0f, f=%.1f",
|
||||
u.c_str(), v.c_str(), nd, fv);
|
||||
st.message += b2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
}
|
||||
79
code/cpp/algorithms.h
Normal file
79
code/cpp/algorithms.h
Normal file
@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
/*
|
||||
* Shortest-path algorithms: Dijkstra, Bellman-Ford, A*
|
||||
* Pure data structures and declarations — no GUI dependencies.
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static constexpr double INF_VAL = std::numeric_limits<double>::infinity();
|
||||
|
||||
// ─── Graph ──────────────────────────────────────────────────────────────
|
||||
|
||||
struct Vec2 { int x, y; };
|
||||
|
||||
struct Edge { std::string src, dst; int weight; };
|
||||
|
||||
inline const std::vector<std::string> NODE_ORDER =
|
||||
{"S", "A", "B", "C", "D", "E", "F", "G", "T"};
|
||||
|
||||
inline const std::map<std::string, Vec2> NODES = {
|
||||
{"S", {80, 350}}, {"A", {270, 130}}, {"B", {270, 570}},
|
||||
{"C", {500, 80}}, {"D", {500, 350}}, {"E", {500, 620}},
|
||||
{"F", {730, 180}}, {"G", {730, 520}}, {"T", {950, 350}},
|
||||
};
|
||||
|
||||
inline const std::vector<Edge> EDGES = {
|
||||
{"S","A",4}, {"S","B",2}, {"A","C",5}, {"A","D",10},
|
||||
{"B","D",3}, {"B","E",8}, {"C","F",3}, {"D","F",7},
|
||||
{"D","G",4}, {"E","G",2}, {"F","T",4}, {"G","T",6},
|
||||
{"C","D",2},
|
||||
};
|
||||
|
||||
inline const std::string SOURCE = "S";
|
||||
inline const std::string GOAL = "T";
|
||||
|
||||
// ─── State ──────────────────────────────────────────────────────────────
|
||||
|
||||
using PQ = std::priority_queue<
|
||||
std::pair<double, std::string>,
|
||||
std::vector<std::pair<double, std::string>>,
|
||||
std::greater<std::pair<double, std::string>>>;
|
||||
|
||||
struct AlgoState {
|
||||
std::map<std::string, double> dist;
|
||||
std::map<std::string, std::string> prev; // "" = no predecessor
|
||||
std::set<std::string> visited;
|
||||
bool finished = false;
|
||||
int step = 0;
|
||||
std::string message;
|
||||
std::vector<std::string> path;
|
||||
std::set<std::pair<std::string,std::string>> relaxed_edges;
|
||||
std::string current_node; // "" = none
|
||||
std::map<std::string, double> f_score; // A* only
|
||||
PQ frontier;
|
||||
int bf_iteration = 0; // Bellman-Ford only
|
||||
|
||||
void reconstruct_path();
|
||||
void init_dists();
|
||||
};
|
||||
|
||||
// ─── Algorithm functions ────────────────────────────────────────────────
|
||||
|
||||
double heuristic(const std::string& node);
|
||||
|
||||
AlgoState dijkstra_init();
|
||||
void dijkstra_step(AlgoState& st);
|
||||
|
||||
AlgoState bellman_ford_init();
|
||||
void bellman_ford_step(AlgoState& st);
|
||||
|
||||
AlgoState astar_init();
|
||||
void astar_step(AlgoState& st);
|
||||
15
code/cpp/run.sh
Executable file
15
code/cpp/run.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Build and run the C++ shortest-path visualizer (requires raylib)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if ! command -v pkg-config &>/dev/null || ! pkg-config --exists raylib; then
|
||||
echo "raylib not found. Install with: sudo pacman -S raylib"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Compiling..."
|
||||
g++ -std=c++17 -O2 -o visualizer algorithms.cpp visualizer.cpp \
|
||||
$(pkg-config --cflags --libs raylib) -lm
|
||||
echo "Running..."
|
||||
./visualizer
|
||||
BIN
code/cpp/visualizer
Executable file
BIN
code/cpp/visualizer
Executable file
Binary file not shown.
173
code/cpp/visualizer.cpp
Normal file
173
code/cpp/visualizer.cpp
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Raylib GUI for shortest-path algorithm visualization.
|
||||
*
|
||||
* Controls:
|
||||
* 1 / 2 / 3 — Dijkstra / Bellman-Ford / A*
|
||||
* SPACE — advance one step
|
||||
* R — reset
|
||||
* Q / ESC — quit
|
||||
*/
|
||||
|
||||
#include "algorithms.h"
|
||||
#include "raylib.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
static const int W = 1100;
|
||||
static const int H = 700;
|
||||
static const float NODE_R = 28.0f;
|
||||
static const int FSIZ = 20;
|
||||
static const int FSIZ_S = 16;
|
||||
|
||||
// Colours
|
||||
static const Color BG_COL = {245, 245, 250, 255};
|
||||
static const Color EDGE_DEF = {160, 160, 160, 255};
|
||||
static const Color EDGE_REL = { 60, 160, 60, 255};
|
||||
static const Color EDGE_PATH = { 40, 180, 100, 255};
|
||||
static const Color NODE_DEF = {255, 255, 255, 255};
|
||||
static const Color NODE_VIS = {180, 220, 255, 255};
|
||||
static const Color NODE_CUR = {255, 220, 80, 255};
|
||||
static const Color NODE_PATH = { 60, 200, 120, 255};
|
||||
static const Color COL_BLUE = { 60, 100, 220, 255};
|
||||
static const Color COL_ORANGE = {240, 140, 40, 255};
|
||||
static const Color BAR_COL = {230, 230, 230, 255};
|
||||
|
||||
static const char* ALGO_NAMES[] = {
|
||||
"Dijkstra's Algorithm",
|
||||
"Bellman-Ford Algorithm",
|
||||
"A* Algorithm",
|
||||
};
|
||||
|
||||
// ─── Edge drawing (clipped to node radius) ──────────────────────────────
|
||||
|
||||
static void draw_edge(Vector2 p1, Vector2 p2, Color col, float thick) {
|
||||
float dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||
float len = sqrtf(dx * dx + dy * dy);
|
||||
if (len < 1.0f) return;
|
||||
float ux = dx / len, uy = dy / len;
|
||||
Vector2 a = {p1.x + ux * NODE_R, p1.y + uy * NODE_R};
|
||||
Vector2 b = {p2.x - ux * NODE_R, p2.y - uy * NODE_R};
|
||||
DrawLineEx(a, b, thick, col);
|
||||
}
|
||||
|
||||
// ─── Render ─────────────────────────────────────────────────────────────
|
||||
|
||||
static void render(const AlgoState& st, int algo) {
|
||||
BeginDrawing();
|
||||
ClearBackground(BG_COL);
|
||||
|
||||
// Title
|
||||
const char* title = ALGO_NAMES[algo];
|
||||
int tw = MeasureText(title, FSIZ + 4);
|
||||
DrawText(title, W / 2 - tw / 2, 10, FSIZ + 4, BLACK);
|
||||
|
||||
// Build path edge set
|
||||
std::set<std::pair<std::string, std::string>> path_edges;
|
||||
for (size_t i = 0; i + 1 < st.path.size(); i++) {
|
||||
path_edges.insert({st.path[i], st.path[i + 1]});
|
||||
path_edges.insert({st.path[i + 1], st.path[i]});
|
||||
}
|
||||
|
||||
// Edges
|
||||
for (auto& e : EDGES) {
|
||||
auto p1v = NODES.at(e.src);
|
||||
auto p2v = NODES.at(e.dst);
|
||||
Vector2 p1 = {(float)p1v.x, (float)p1v.y};
|
||||
Vector2 p2 = {(float)p2v.x, (float)p2v.y};
|
||||
|
||||
Color col = EDGE_DEF; float thick = 2.0f;
|
||||
if (path_edges.count({e.src, e.dst})) {
|
||||
col = EDGE_PATH; thick = 5.0f;
|
||||
} else if (st.relaxed_edges.count({e.src, e.dst}) ||
|
||||
st.relaxed_edges.count({e.dst, e.src})) {
|
||||
col = EDGE_REL; thick = 3.0f;
|
||||
}
|
||||
draw_edge(p1, p2, col, thick);
|
||||
|
||||
// Weight label
|
||||
float mx = (p1.x + p2.x) / 2;
|
||||
float my = (p1.y + p2.y) / 2;
|
||||
float dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||
float len = sqrtf(dx * dx + dy * dy);
|
||||
if (len > 0) { mx += (-dy / len) * 14; my += (dx / len) * 14; }
|
||||
const char* wt = TextFormat("%d", e.weight);
|
||||
int wtw = MeasureText(wt, FSIZ_S);
|
||||
DrawText(wt, (int)mx - wtw / 2, (int)my - FSIZ_S / 2, FSIZ_S, DARKGRAY);
|
||||
}
|
||||
|
||||
// Nodes
|
||||
for (auto& [name, pos] : NODES) {
|
||||
Color col = NODE_DEF;
|
||||
if (std::find(st.path.begin(), st.path.end(), name) != st.path.end()
|
||||
&& st.finished)
|
||||
col = NODE_PATH;
|
||||
else if (name == st.current_node)
|
||||
col = NODE_CUR;
|
||||
else if (st.visited.count(name))
|
||||
col = NODE_VIS;
|
||||
|
||||
DrawCircle(pos.x, pos.y, NODE_R, col);
|
||||
DrawCircleLines(pos.x, pos.y, NODE_R, BLACK);
|
||||
|
||||
// Name
|
||||
int nw = MeasureText(name.c_str(), FSIZ);
|
||||
DrawText(name.c_str(), pos.x - nw / 2, pos.y - FSIZ / 2 - 8,
|
||||
FSIZ, BLACK);
|
||||
|
||||
// Distance
|
||||
double d = INF_VAL;
|
||||
auto it = st.dist.find(name);
|
||||
if (it != st.dist.end()) d = it->second;
|
||||
const char* dtxt = (d >= INF_VAL) ? "d=inf" : TextFormat("d=%d", (int)d);
|
||||
int dw = MeasureText(dtxt, FSIZ_S);
|
||||
DrawText(dtxt, pos.x - dw / 2, pos.y + 8, FSIZ_S, COL_BLUE);
|
||||
|
||||
// h(n) for A*
|
||||
if (algo == 2) {
|
||||
const char* ht = TextFormat("h=%.1f", heuristic(name));
|
||||
DrawText(ht, pos.x + (int)NODE_R + 4, pos.y - 8, FSIZ_S, COL_ORANGE);
|
||||
}
|
||||
}
|
||||
|
||||
// Info bar
|
||||
DrawRectangle(0, H - 80, W, 80, BAR_COL);
|
||||
DrawLine(0, H - 80, W, H - 80, DARKGRAY);
|
||||
DrawText(st.message.c_str(), 20, H - 70, FSIZ, BLACK);
|
||||
DrawText("[1] Dijkstra [2] Bellman-Ford [3] A* "
|
||||
"[SPACE] Step [R] Reset [Q] Quit",
|
||||
20, H - 35, FSIZ_S, DARKGRAY);
|
||||
|
||||
EndDrawing();
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
InitWindow(W, H, "Shortest Path Visualizer");
|
||||
SetTargetFPS(60);
|
||||
|
||||
int algo = 0;
|
||||
AlgoState state = dijkstra_init();
|
||||
|
||||
using StepFn = void(*)(AlgoState&);
|
||||
using InitFn = AlgoState(*)();
|
||||
|
||||
InitFn inits[] = {dijkstra_init, bellman_ford_init, astar_init};
|
||||
StepFn steps[] = {dijkstra_step, bellman_ford_step, astar_step};
|
||||
|
||||
while (!WindowShouldClose()) {
|
||||
if (IsKeyPressed(KEY_Q)) break;
|
||||
if (IsKeyPressed(KEY_ONE)) { algo = 0; state = inits[0](); }
|
||||
if (IsKeyPressed(KEY_TWO)) { algo = 1; state = inits[1](); }
|
||||
if (IsKeyPressed(KEY_THREE)) { algo = 2; state = inits[2](); }
|
||||
if (IsKeyPressed(KEY_R)) { state = inits[algo](); }
|
||||
if (IsKeyPressed(KEY_SPACE) && !state.finished)
|
||||
steps[algo](state);
|
||||
|
||||
render(state, algo);
|
||||
}
|
||||
|
||||
CloseWindow();
|
||||
return 0;
|
||||
}
|
||||
304
code/csharp/Algorithms.cs
Normal file
304
code/csharp/Algorithms.cs
Normal file
@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Shortest-path algorithms: Dijkstra, Bellman-Ford, A*
|
||||
* Pure C# implementations — no GUI dependencies.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShortestPathViz;
|
||||
|
||||
// ─── Graph ──────────────────────────────────────────────────────────────
|
||||
|
||||
public record struct Vec2(int X, int Y);
|
||||
public record struct EdgeDef(string Src, string Dst, int Weight);
|
||||
|
||||
public static class Graph
|
||||
{
|
||||
public static readonly Dictionary<string, Vec2> Nodes = new()
|
||||
{
|
||||
["S"] = new(80, 350), ["A"] = new(270, 130), ["B"] = new(270, 570),
|
||||
["C"] = new(500, 80), ["D"] = new(500, 350), ["E"] = new(500, 620),
|
||||
["F"] = new(730, 180), ["G"] = new(730, 520), ["T"] = new(950, 350),
|
||||
};
|
||||
|
||||
public static readonly string[] NodeOrder =
|
||||
["S", "A", "B", "C", "D", "E", "F", "G", "T"];
|
||||
|
||||
public static readonly EdgeDef[] Edges =
|
||||
[
|
||||
new("S","A",4), new("S","B",2), new("A","C",5), new("A","D",10),
|
||||
new("B","D",3), new("B","E",8), new("C","F",3), new("D","F",7),
|
||||
new("D","G",4), new("E","G",2), new("F","T",4), new("G","T",6),
|
||||
new("C","D",2),
|
||||
];
|
||||
|
||||
public static readonly string Source = "S";
|
||||
public static readonly string Goal = "T";
|
||||
|
||||
public static readonly Dictionary<string, List<(string N, int W)>> Adj = BuildAdj();
|
||||
|
||||
private static Dictionary<string, List<(string, int)>> BuildAdj()
|
||||
{
|
||||
var adj = NodeOrder.ToDictionary(n => n, _ => new List<(string, int)>());
|
||||
foreach (var e in Edges)
|
||||
{
|
||||
adj[e.Src].Add((e.Dst, e.Weight));
|
||||
adj[e.Dst].Add((e.Src, e.Weight));
|
||||
}
|
||||
return adj;
|
||||
}
|
||||
|
||||
public static double Heuristic(string node)
|
||||
{
|
||||
var p = Nodes[node];
|
||||
var g = Nodes[Goal];
|
||||
return Math.Sqrt(Math.Pow(g.X - p.X, 2) + Math.Pow(g.Y - p.Y, 2)) / 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── State ──────────────────────────────────────────────────────────────
|
||||
|
||||
public class AlgoState
|
||||
{
|
||||
public Dictionary<string, double> Dist = new();
|
||||
public Dictionary<string, string?> Prev = new();
|
||||
public HashSet<string> Visited = new();
|
||||
public bool Finished;
|
||||
public int Step;
|
||||
public string Message = "";
|
||||
public List<string> Path = new();
|
||||
public HashSet<(string, string)> RelaxedEdges = new();
|
||||
public string? CurrentNode;
|
||||
public Dictionary<string, double> FScore = new();
|
||||
public PriorityQueue<string, double> Frontier = new();
|
||||
public int BfIteration;
|
||||
|
||||
public void InitDists()
|
||||
{
|
||||
foreach (var n in Graph.NodeOrder)
|
||||
{
|
||||
Dist[n] = double.PositiveInfinity;
|
||||
Prev[n] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReconstructPath()
|
||||
{
|
||||
Path.Clear();
|
||||
string? node = Graph.Goal;
|
||||
while (node != null)
|
||||
{
|
||||
Path.Insert(0, node);
|
||||
node = Prev.GetValueOrDefault(node);
|
||||
}
|
||||
if (Path.Count == 0 || Path[0] != Graph.Source)
|
||||
Path.Clear();
|
||||
}
|
||||
|
||||
private static string JoinPath(List<string> p) => string.Join("->", p);
|
||||
|
||||
public string PathString() => JoinPath(Path);
|
||||
}
|
||||
|
||||
// ─── Algorithms ─────────────────────────────────────────────────────────
|
||||
|
||||
public static class Algorithms
|
||||
{
|
||||
// ── Dijkstra ──
|
||||
|
||||
public static AlgoState DijkstraInit()
|
||||
{
|
||||
var st = new AlgoState();
|
||||
st.InitDists();
|
||||
st.Dist[Graph.Source] = 0;
|
||||
st.Frontier.Enqueue(Graph.Source, 0);
|
||||
st.Message = $"Dijkstra: start from {Graph.Source}";
|
||||
return st;
|
||||
}
|
||||
|
||||
public static void DijkstraStep(AlgoState st)
|
||||
{
|
||||
void Finish()
|
||||
{
|
||||
st.Finished = true;
|
||||
st.ReconstructPath();
|
||||
st.Message = st.Path.Count > 0
|
||||
? $"Dijkstra done! Path: {st.PathString()}, cost={st.Dist[Graph.Goal]:F0}"
|
||||
: "Dijkstra done! No path.";
|
||||
}
|
||||
|
||||
if (st.Finished || st.Frontier.Count == 0) { Finish(); return; }
|
||||
|
||||
st.CurrentNode = null;
|
||||
while (st.Frontier.Count > 0)
|
||||
{
|
||||
var u = st.Frontier.Dequeue();
|
||||
if (st.Visited.Contains(u)) continue;
|
||||
st.Visited.Add(u);
|
||||
st.Step++;
|
||||
st.CurrentNode = u;
|
||||
st.Message = $"Step {st.Step}: visit {u} (dist={st.Dist[u]:F0})";
|
||||
|
||||
foreach (var (v, w) in Graph.Adj[u])
|
||||
{
|
||||
if (!st.Visited.Contains(v))
|
||||
{
|
||||
double nd = st.Dist[u] + w;
|
||||
if (nd < st.Dist[v])
|
||||
{
|
||||
st.Dist[v] = nd;
|
||||
st.Prev[v] = u;
|
||||
st.Frontier.Enqueue(v, nd);
|
||||
st.RelaxedEdges.Add((u, v));
|
||||
st.Message += $" | relax {u}->{v}:{nd:F0}";
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Finish();
|
||||
}
|
||||
|
||||
// ── Bellman-Ford ──
|
||||
|
||||
public static AlgoState BellmanFordInit()
|
||||
{
|
||||
var st = new AlgoState();
|
||||
st.InitDists();
|
||||
st.Dist[Graph.Source] = 0;
|
||||
st.Message = $"Bellman-Ford: {Graph.Nodes.Count - 1} iterations over all edges";
|
||||
return st;
|
||||
}
|
||||
|
||||
public static void BellmanFordStep(AlgoState st)
|
||||
{
|
||||
if (st.Finished) return;
|
||||
|
||||
int V = Graph.Nodes.Count;
|
||||
var allEdges = Graph.Edges
|
||||
.SelectMany(e => new[] { (e.Src, e.Dst, e.Weight), (e.Dst, e.Src, e.Weight) })
|
||||
.ToList();
|
||||
|
||||
if (st.BfIteration < V - 1)
|
||||
{
|
||||
bool changed = false;
|
||||
foreach (var (s, d, w) in allEdges)
|
||||
{
|
||||
if (st.Dist[s] + w < st.Dist[d])
|
||||
{
|
||||
st.Dist[d] = st.Dist[s] + w;
|
||||
st.Prev[d] = s;
|
||||
st.RelaxedEdges.Add((s, d));
|
||||
st.Visited.Add(s);
|
||||
st.Visited.Add(d);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
st.BfIteration++;
|
||||
st.Step++;
|
||||
st.CurrentNode = null;
|
||||
st.Message = $"Iteration {st.BfIteration}/{V - 1}: " +
|
||||
(changed ? "changes made" : "no changes (stable)");
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
st.Finished = true;
|
||||
st.ReconstructPath();
|
||||
st.Message += st.Path.Count > 0
|
||||
? $" -> Early stop! Path: {st.PathString()}, cost={st.Dist[Graph.Goal]:F0}"
|
||||
: " -> Early stop!";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool neg = allEdges.Any(e => st.Dist[e.Item1] + e.Item3 < st.Dist[e.Item2]);
|
||||
st.Finished = true;
|
||||
if (neg)
|
||||
{
|
||||
st.Message = "Negative cycle detected!";
|
||||
}
|
||||
else
|
||||
{
|
||||
st.ReconstructPath();
|
||||
st.Message = st.Path.Count > 0
|
||||
? $"Bellman-Ford done! Path: {st.PathString()}, cost={st.Dist[Graph.Goal]:F0}"
|
||||
: "Bellman-Ford done! No path.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── A* ──
|
||||
|
||||
public static AlgoState AStarInit()
|
||||
{
|
||||
var st = new AlgoState();
|
||||
st.InitDists();
|
||||
foreach (var n in Graph.NodeOrder)
|
||||
st.FScore[n] = double.PositiveInfinity;
|
||||
st.Dist[Graph.Source] = 0;
|
||||
double h = Graph.Heuristic(Graph.Source);
|
||||
st.FScore[Graph.Source] = h;
|
||||
st.Frontier.Enqueue(Graph.Source, h);
|
||||
st.Message = $"A*: start={Graph.Source}, goal={Graph.Goal}, h(S)={h:F1}";
|
||||
return st;
|
||||
}
|
||||
|
||||
public static void AStarStep(AlgoState st)
|
||||
{
|
||||
void Finish()
|
||||
{
|
||||
st.Finished = true;
|
||||
st.ReconstructPath();
|
||||
st.Message = st.Path.Count > 0
|
||||
? $"A* done! Path: {st.PathString()}, cost={st.Dist[Graph.Goal]:F0}"
|
||||
: "A* done! No path.";
|
||||
}
|
||||
|
||||
if (st.Finished || st.Frontier.Count == 0) { Finish(); return; }
|
||||
|
||||
st.CurrentNode = null;
|
||||
while (st.Frontier.Count > 0)
|
||||
{
|
||||
var u = st.Frontier.Dequeue();
|
||||
if (st.Visited.Contains(u)) continue;
|
||||
st.Visited.Add(u);
|
||||
st.Step++;
|
||||
st.CurrentNode = u;
|
||||
|
||||
if (u == Graph.Goal)
|
||||
{
|
||||
st.Finished = true;
|
||||
st.ReconstructPath();
|
||||
st.Message = $"Step {st.Step}: GOAL reached! " +
|
||||
$"Path: {st.PathString()}, cost={st.Dist[Graph.Goal]:F0}";
|
||||
return;
|
||||
}
|
||||
|
||||
double hu = Graph.Heuristic(u);
|
||||
double fu = st.Dist[u] + hu;
|
||||
st.Message = $"Step {st.Step}: visit {u} (g={st.Dist[u]:F0}, h={hu:F1}, f={fu:F1})";
|
||||
|
||||
foreach (var (v, w) in Graph.Adj[u])
|
||||
{
|
||||
if (!st.Visited.Contains(v))
|
||||
{
|
||||
double nd = st.Dist[u] + w;
|
||||
if (nd < st.Dist[v])
|
||||
{
|
||||
st.Dist[v] = nd;
|
||||
st.Prev[v] = u;
|
||||
double fv = nd + Graph.Heuristic(v);
|
||||
st.FScore[v] = fv;
|
||||
st.Frontier.Enqueue(v, fv);
|
||||
st.RelaxedEdges.Add((u, v));
|
||||
st.Message += $" | relax {u}->{v}: g={nd:F0}, f={fv:F1}";
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
182
code/csharp/Visualizer.cs
Normal file
182
code/csharp/Visualizer.cs
Normal file
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Raylib GUI for shortest-path algorithm visualization (C#).
|
||||
*
|
||||
* Controls:
|
||||
* 1 / 2 / 3 — Dijkstra / Bellman-Ford / A*
|
||||
* SPACE — advance one step
|
||||
* R — reset
|
||||
* Q / ESC — quit
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Raylib_cs;
|
||||
|
||||
namespace ShortestPathViz;
|
||||
|
||||
static class Visualizer
|
||||
{
|
||||
const int W = 1100;
|
||||
const int H = 700;
|
||||
const float NodeR = 28f;
|
||||
const int FSiz = 20;
|
||||
const int FSizS = 16;
|
||||
|
||||
// Colours
|
||||
static readonly Color BgCol = new(245, 245, 250, 255);
|
||||
static readonly Color EdgeDef = new(160, 160, 160, 255);
|
||||
static readonly Color EdgeRel = new( 60, 160, 60, 255);
|
||||
static readonly Color EdgePath = new( 40, 180, 100, 255);
|
||||
static readonly Color NodeDef = new(255, 255, 255, 255);
|
||||
static readonly Color NodeVis = new(180, 220, 255, 255);
|
||||
static readonly Color NodeCur = new(255, 220, 80, 255);
|
||||
static readonly Color NodePath = new( 60, 200, 120, 255);
|
||||
static readonly Color ColBlue = new( 60, 100, 220, 255);
|
||||
static readonly Color ColOrange = new(240, 140, 40, 255);
|
||||
static readonly Color BarCol = new(230, 230, 230, 255);
|
||||
|
||||
static readonly string[] AlgoNames =
|
||||
["Dijkstra's Algorithm", "Bellman-Ford Algorithm", "A* Algorithm"];
|
||||
|
||||
delegate AlgoState InitFn();
|
||||
delegate void StepFn(AlgoState st);
|
||||
|
||||
static readonly InitFn[] Inits =
|
||||
[Algorithms.DijkstraInit, Algorithms.BellmanFordInit, Algorithms.AStarInit];
|
||||
static readonly StepFn[] Steps =
|
||||
[Algorithms.DijkstraStep, Algorithms.BellmanFordStep, Algorithms.AStarStep];
|
||||
|
||||
// ── Edge drawing ────────────────────────────────────────────────────
|
||||
|
||||
static void DrawEdge(Vector2 p1, Vector2 p2, Color col, float thick)
|
||||
{
|
||||
float dx = p2.X - p1.X, dy = p2.Y - p1.Y;
|
||||
float len = MathF.Sqrt(dx * dx + dy * dy);
|
||||
if (len < 1f) return;
|
||||
float ux = dx / len, uy = dy / len;
|
||||
var a = new Vector2(p1.X + ux * NodeR, p1.Y + uy * NodeR);
|
||||
var b = new Vector2(p2.X - ux * NodeR, p2.Y - uy * NodeR);
|
||||
Raylib.DrawLineEx(a, b, thick, col);
|
||||
}
|
||||
|
||||
// ── Render ──────────────────────────────────────────────────────────
|
||||
|
||||
static void Render(AlgoState st, int algo)
|
||||
{
|
||||
Raylib.BeginDrawing();
|
||||
Raylib.ClearBackground(BgCol);
|
||||
|
||||
// Title
|
||||
string title = AlgoNames[algo];
|
||||
int tw = Raylib.MeasureText(title, FSiz + 4);
|
||||
Raylib.DrawText(title, W / 2 - tw / 2, 10, FSiz + 4, Color.Black);
|
||||
|
||||
// Path edge set
|
||||
var pathEdges = new HashSet<(string, string)>();
|
||||
for (int i = 0; i < st.Path.Count - 1; i++)
|
||||
{
|
||||
pathEdges.Add((st.Path[i], st.Path[i + 1]));
|
||||
pathEdges.Add((st.Path[i + 1], st.Path[i]));
|
||||
}
|
||||
|
||||
// Edges
|
||||
foreach (var e in Graph.Edges)
|
||||
{
|
||||
var p1v = Graph.Nodes[e.Src];
|
||||
var p2v = Graph.Nodes[e.Dst];
|
||||
var p1 = new Vector2(p1v.X, p1v.Y);
|
||||
var p2 = new Vector2(p2v.X, p2v.Y);
|
||||
|
||||
Color col = EdgeDef; float thick = 2f;
|
||||
if (pathEdges.Contains((e.Src, e.Dst)))
|
||||
{ col = EdgePath; thick = 5f; }
|
||||
else if (st.RelaxedEdges.Contains((e.Src, e.Dst)) ||
|
||||
st.RelaxedEdges.Contains((e.Dst, e.Src)))
|
||||
{ col = EdgeRel; thick = 3f; }
|
||||
|
||||
DrawEdge(p1, p2, col, thick);
|
||||
|
||||
// Weight label
|
||||
float mx = (p1.X + p2.X) / 2;
|
||||
float my = (p1.Y + p2.Y) / 2;
|
||||
float dx = p2.X - p1.X, dy = p2.Y - p1.Y;
|
||||
float len = MathF.Sqrt(dx * dx + dy * dy);
|
||||
if (len > 0) { mx += (-dy / len) * 14; my += (dx / len) * 14; }
|
||||
string wt = e.Weight.ToString();
|
||||
int wtw = Raylib.MeasureText(wt, FSizS);
|
||||
Raylib.DrawText(wt, (int)mx - wtw / 2, (int)my - FSizS / 2,
|
||||
FSizS, Color.DarkGray);
|
||||
}
|
||||
|
||||
// Nodes
|
||||
foreach (var (name, pos) in Graph.Nodes)
|
||||
{
|
||||
Color col = NodeDef;
|
||||
if (st.Path.Contains(name) && st.Finished)
|
||||
col = NodePath;
|
||||
else if (name == st.CurrentNode)
|
||||
col = NodeCur;
|
||||
else if (st.Visited.Contains(name))
|
||||
col = NodeVis;
|
||||
|
||||
Raylib.DrawCircle(pos.X, pos.Y, NodeR, col);
|
||||
Raylib.DrawCircleLines(pos.X, pos.Y, NodeR, Color.Black);
|
||||
|
||||
// Name
|
||||
int nw = Raylib.MeasureText(name, FSiz);
|
||||
Raylib.DrawText(name, pos.X - nw / 2, pos.Y - FSiz / 2 - 8,
|
||||
FSiz, Color.Black);
|
||||
|
||||
// Distance
|
||||
double d = st.Dist.GetValueOrDefault(name, double.PositiveInfinity);
|
||||
string dtxt = double.IsPositiveInfinity(d) ? "d=inf" : $"d={d:F0}";
|
||||
int dw = Raylib.MeasureText(dtxt, FSizS);
|
||||
Raylib.DrawText(dtxt, pos.X - dw / 2, pos.Y + 8, FSizS, ColBlue);
|
||||
|
||||
// h(n) for A*
|
||||
if (algo == 2)
|
||||
{
|
||||
string ht = $"h={Graph.Heuristic(name):F1}";
|
||||
Raylib.DrawText(ht, pos.X + (int)NodeR + 4, pos.Y - 8,
|
||||
FSizS, ColOrange);
|
||||
}
|
||||
}
|
||||
|
||||
// Info bar
|
||||
Raylib.DrawRectangle(0, H - 80, W, 80, BarCol);
|
||||
Raylib.DrawLine(0, H - 80, W, H - 80, Color.DarkGray);
|
||||
Raylib.DrawText(st.Message, 20, H - 70, FSiz, Color.Black);
|
||||
Raylib.DrawText("[1] Dijkstra [2] Bellman-Ford [3] A* " +
|
||||
"[SPACE] Step [R] Reset [Q] Quit",
|
||||
20, H - 35, FSizS, Color.DarkGray);
|
||||
|
||||
Raylib.EndDrawing();
|
||||
}
|
||||
|
||||
// ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
static void Main()
|
||||
{
|
||||
Raylib.InitWindow(W, H, "Shortest Path Visualizer");
|
||||
Raylib.SetTargetFPS(60);
|
||||
|
||||
int algo = 0;
|
||||
AlgoState state = Inits[0]();
|
||||
|
||||
while (!Raylib.WindowShouldClose())
|
||||
{
|
||||
if (Raylib.IsKeyPressed(KeyboardKey.Q)) break;
|
||||
if (Raylib.IsKeyPressed(KeyboardKey.One)) { algo = 0; state = Inits[0](); }
|
||||
if (Raylib.IsKeyPressed(KeyboardKey.Two)) { algo = 1; state = Inits[1](); }
|
||||
if (Raylib.IsKeyPressed(KeyboardKey.Three)) { algo = 2; state = Inits[2](); }
|
||||
if (Raylib.IsKeyPressed(KeyboardKey.R)) state = Inits[algo]();
|
||||
if (Raylib.IsKeyPressed(KeyboardKey.Space) && !state.Finished)
|
||||
Steps[algo](state);
|
||||
|
||||
Render(state, algo);
|
||||
}
|
||||
|
||||
Raylib.CloseWindow();
|
||||
}
|
||||
}
|
||||
BIN
code/csharp/bin/Debug/net8.0/ShortestPathViz
Executable file
BIN
code/csharp/bin/Debug/net8.0/ShortestPathViz
Executable file
Binary file not shown.
68
code/csharp/bin/Debug/net8.0/ShortestPathViz.deps.json
Normal file
68
code/csharp/bin/Debug/net8.0/ShortestPathViz.deps.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"ShortestPathViz/1.0.0": {
|
||||
"dependencies": {
|
||||
"Raylib-cs": "6.1.1"
|
||||
},
|
||||
"runtime": {
|
||||
"ShortestPathViz.dll": {}
|
||||
}
|
||||
},
|
||||
"Raylib-cs/6.1.1": {
|
||||
"runtime": {
|
||||
"lib/net6.0/Raylib-cs.dll": {
|
||||
"assemblyVersion": "0.0.0.0",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/linux-x64/native/libraylib.so": {
|
||||
"rid": "linux-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-arm64/native/libraylib.dylib": {
|
||||
"rid": "osx-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-x64/native/libraylib.dylib": {
|
||||
"rid": "osx-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x64/native/raylib.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/raylib.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"ShortestPathViz/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Raylib-cs/6.1.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ty5RQoFK7bW4mTogwMf55Zh73DMJMvFLLJTarhmL1g7loxeLYoHTFufN7JagObdLHo2zB3wVrtEHVCXFoZ9TMg==",
|
||||
"path": "raylib-cs/6.1.1",
|
||||
"hashPath": "raylib-cs.6.1.1.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]
|
||||
22
code/csharp/obj/Debug/net8.0/ShortestPathViz.AssemblyInfo.cs
Normal file
22
code/csharp/obj/Debug/net8.0/ShortestPathViz.AssemblyInfo.cs
Normal file
@ -0,0 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("ShortestPathViz")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+6069b5d94885e5ff265f341da3de76c3955f3280")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("ShortestPathViz")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("ShortestPathViz")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
||||
@ -0,0 +1 @@
|
||||
b71e974c87232aa1c1c268934adf494b936a0e1b2fd3216718a7624b1f4b23c1
|
||||
@ -0,0 +1,17 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net8.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v8.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = ShortestPathViz
|
||||
build_property.ProjectDir = /home/kuhy/praca_magisterska/code/csharp/
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 8.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
BIN
code/csharp/obj/Debug/net8.0/ShortestPathViz.assets.cache
Normal file
BIN
code/csharp/obj/Debug/net8.0/ShortestPathViz.assets.cache
Normal file
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
98e632fbadff1490b599cbadc5029792b1f6a2b87d493ab6d5e333ed3a7f7fe7
|
||||
@ -0,0 +1,23 @@
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/ShortestPathViz
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/ShortestPathViz.deps.json
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/ShortestPathViz.runtimeconfig.json
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/ShortestPathViz.dll
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/ShortestPathViz.pdb
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.GeneratedMSBuildEditorConfig.editorconfig
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.AssemblyInfoInputs.cache
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.AssemblyInfo.cs
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.csproj.CoreCompileInputs.cache
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.sourcelink.json
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.dll
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/refint/ShortestPathViz.dll
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.pdb
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.genruntimeconfig.cache
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ref/ShortestPathViz.dll
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/ShortestPathViz.csproj.AssemblyReference.cache
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/Raylib-cs.dll
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/runtimes/linux-x64/native/libraylib.so
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/runtimes/osx-arm64/native/libraylib.dylib
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/runtimes/osx-x64/native/libraylib.dylib
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/runtimes/win-x64/native/raylib.dll
|
||||
/home/kuhy/praca_magisterska/code/csharp/bin/Debug/net8.0/runtimes/win-x86/native/raylib.dll
|
||||
/home/kuhy/praca_magisterska/code/csharp/obj/Debug/net8.0/Shortest.2EF48F70.Up2Date
|
||||
@ -0,0 +1 @@
|
||||
5e9411043162c2d23b01bac1e6aaf4aacfe65bc12ba829a5b21afdd586ff95cf
|
||||
@ -0,0 +1 @@
|
||||
{"documents":{"/home/kuhy/praca_magisterska/*":"https://raw.githubusercontent.com/kuhyx/praca_magisterska/6069b5d94885e5ff265f341da3de76c3955f3280/*"}}
|
||||
BIN
code/csharp/obj/Debug/net8.0/apphost
Executable file
BIN
code/csharp/obj/Debug/net8.0/apphost
Executable file
Binary file not shown.
87
code/csharp/obj/ShortestPathViz.csproj.nuget.dgspec.json
Normal file
87
code/csharp/obj/ShortestPathViz.csproj.nuget.dgspec.json
Normal file
@ -0,0 +1,87 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"/home/kuhy/praca_magisterska/code/csharp/ShortestPathViz.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"/home/kuhy/praca_magisterska/code/csharp/ShortestPathViz.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "/home/kuhy/praca_magisterska/code/csharp/ShortestPathViz.csproj",
|
||||
"projectName": "ShortestPathViz",
|
||||
"projectPath": "/home/kuhy/praca_magisterska/code/csharp/ShortestPathViz.csproj",
|
||||
"packagesPath": "/home/kuhy/.nuget/packages/",
|
||||
"outputPath": "/home/kuhy/praca_magisterska/code/csharp/obj/",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"/home/kuhy/.nuget/NuGet/NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net8.0"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.100"
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"dependencies": {
|
||||
"Raylib-cs": {
|
||||
"target": "Package",
|
||||
"version": "[6.1.1, )"
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[8.0.24, 8.0.24]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Host.linux-x64",
|
||||
"version": "[8.0.24, 8.0.24]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[8.0.24, 8.0.24]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.103/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
code/csharp/obj/ShortestPathViz.csproj.nuget.g.props
Normal file
15
code/csharp/obj/ShortestPathViz.csproj.nuget.g.props
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/kuhy/.nuget/packages/</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/kuhy/.nuget/packages/</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="/home/kuhy/.nuget/packages/" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
2
code/csharp/obj/ShortestPathViz.csproj.nuget.g.targets
Normal file
2
code/csharp/obj/ShortestPathViz.csproj.nuget.g.targets
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
210
code/csharp/obj/project.assets.json
Normal file
210
code/csharp/obj/project.assets.json
Normal file
@ -0,0 +1,210 @@
|
||||
{
|
||||
"version": 3,
|
||||
"targets": {
|
||||
"net8.0": {
|
||||
"Raylib-cs/6.1.1": {
|
||||
"type": "package",
|
||||
"dependencies": {
|
||||
"System.Numerics.Vectors": "4.5.0"
|
||||
},
|
||||
"compile": {
|
||||
"lib/net6.0/Raylib-cs.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/Raylib-cs.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/linux-x64/native/libraylib.so": {
|
||||
"assetType": "native",
|
||||
"rid": "linux-x64"
|
||||
},
|
||||
"runtimes/osx-arm64/native/libraylib.dylib": {
|
||||
"assetType": "native",
|
||||
"rid": "osx-arm64"
|
||||
},
|
||||
"runtimes/osx-x64/native/libraylib.dylib": {
|
||||
"assetType": "native",
|
||||
"rid": "osx-x64"
|
||||
},
|
||||
"runtimes/win-x64/native/raylib.dll": {
|
||||
"assetType": "native",
|
||||
"rid": "win-x64"
|
||||
},
|
||||
"runtimes/win-x86/native/raylib.dll": {
|
||||
"assetType": "native",
|
||||
"rid": "win-x86"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors/4.5.0": {
|
||||
"type": "package",
|
||||
"compile": {
|
||||
"ref/netcoreapp2.0/_._": {}
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netcoreapp2.0/_._": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Raylib-cs/6.1.1": {
|
||||
"sha512": "ty5RQoFK7bW4mTogwMf55Zh73DMJMvFLLJTarhmL1g7loxeLYoHTFufN7JagObdLHo2zB3wVrtEHVCXFoZ9TMg==",
|
||||
"type": "package",
|
||||
"path": "raylib-cs/6.1.1",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"README.md",
|
||||
"lib/net6.0/Raylib-cs.dll",
|
||||
"lib/net6.0/Raylib-cs.xml",
|
||||
"raylib-cs.6.1.1.nupkg.sha512",
|
||||
"raylib-cs.nuspec",
|
||||
"raylib-cs_64x64.png",
|
||||
"runtimes/linux-x64/native/libraylib.so",
|
||||
"runtimes/osx-arm64/native/libraylib.dylib",
|
||||
"runtimes/osx-x64/native/libraylib.dylib",
|
||||
"runtimes/win-x64/native/raylib.dll",
|
||||
"runtimes/win-x86/native/raylib.dll"
|
||||
]
|
||||
},
|
||||
"System.Numerics.Vectors/4.5.0": {
|
||||
"sha512": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==",
|
||||
"type": "package",
|
||||
"path": "system.numerics.vectors/4.5.0",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"LICENSE.TXT",
|
||||
"THIRD-PARTY-NOTICES.TXT",
|
||||
"lib/MonoAndroid10/_._",
|
||||
"lib/MonoTouch10/_._",
|
||||
"lib/net46/System.Numerics.Vectors.dll",
|
||||
"lib/net46/System.Numerics.Vectors.xml",
|
||||
"lib/netcoreapp2.0/_._",
|
||||
"lib/netstandard1.0/System.Numerics.Vectors.dll",
|
||||
"lib/netstandard1.0/System.Numerics.Vectors.xml",
|
||||
"lib/netstandard2.0/System.Numerics.Vectors.dll",
|
||||
"lib/netstandard2.0/System.Numerics.Vectors.xml",
|
||||
"lib/portable-net45+win8+wp8+wpa81/System.Numerics.Vectors.dll",
|
||||
"lib/portable-net45+win8+wp8+wpa81/System.Numerics.Vectors.xml",
|
||||
"lib/uap10.0.16299/_._",
|
||||
"lib/xamarinios10/_._",
|
||||
"lib/xamarinmac20/_._",
|
||||
"lib/xamarintvos10/_._",
|
||||
"lib/xamarinwatchos10/_._",
|
||||
"ref/MonoAndroid10/_._",
|
||||
"ref/MonoTouch10/_._",
|
||||
"ref/net45/System.Numerics.Vectors.dll",
|
||||
"ref/net45/System.Numerics.Vectors.xml",
|
||||
"ref/net46/System.Numerics.Vectors.dll",
|
||||
"ref/net46/System.Numerics.Vectors.xml",
|
||||
"ref/netcoreapp2.0/_._",
|
||||
"ref/netstandard1.0/System.Numerics.Vectors.dll",
|
||||
"ref/netstandard1.0/System.Numerics.Vectors.xml",
|
||||
"ref/netstandard2.0/System.Numerics.Vectors.dll",
|
||||
"ref/netstandard2.0/System.Numerics.Vectors.xml",
|
||||
"ref/uap10.0.16299/_._",
|
||||
"ref/xamarinios10/_._",
|
||||
"ref/xamarinmac20/_._",
|
||||
"ref/xamarintvos10/_._",
|
||||
"ref/xamarinwatchos10/_._",
|
||||
"system.numerics.vectors.4.5.0.nupkg.sha512",
|
||||
"system.numerics.vectors.nuspec",
|
||||
"useSharedDesignerContext.txt",
|
||||
"version.txt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"projectFileDependencyGroups": {
|
||||
"net8.0": [
|
||||
"Raylib-cs >= 6.1.1"
|
||||
]
|
||||
},
|
||||
"packageFolders": {
|
||||
"/home/kuhy/.nuget/packages/": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "/home/kuhy/praca_magisterska/code/csharp/ShortestPathViz.csproj",
|
||||
"projectName": "ShortestPathViz",
|
||||
"projectPath": "/home/kuhy/praca_magisterska/code/csharp/ShortestPathViz.csproj",
|
||||
"packagesPath": "/home/kuhy/.nuget/packages/",
|
||||
"outputPath": "/home/kuhy/praca_magisterska/code/csharp/obj/",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"/home/kuhy/.nuget/NuGet/NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net8.0"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.100"
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"dependencies": {
|
||||
"Raylib-cs": {
|
||||
"target": "Package",
|
||||
"version": "[6.1.1, )"
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[8.0.24, 8.0.24]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Host.linux-x64",
|
||||
"version": "[8.0.24, 8.0.24]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[8.0.24, 8.0.24]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.103/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
code/csharp/obj/project.nuget.cache
Normal file
14
code/csharp/obj/project.nuget.cache
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "JOOe7X4+/Bk=",
|
||||
"success": true,
|
||||
"projectFilePath": "/home/kuhy/praca_magisterska/code/csharp/ShortestPathViz.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"/home/kuhy/.nuget/packages/raylib-cs/6.1.1/raylib-cs.6.1.1.nupkg.sha512",
|
||||
"/home/kuhy/.nuget/packages/system.numerics.vectors/4.5.0/system.numerics.vectors.4.5.0.nupkg.sha512",
|
||||
"/home/kuhy/.nuget/packages/microsoft.netcore.app.ref/8.0.24/microsoft.netcore.app.ref.8.0.24.nupkg.sha512",
|
||||
"/home/kuhy/.nuget/packages/microsoft.aspnetcore.app.ref/8.0.24/microsoft.aspnetcore.app.ref.8.0.24.nupkg.sha512",
|
||||
"/home/kuhy/.nuget/packages/microsoft.netcore.app.host.linux-x64/8.0.24/microsoft.netcore.app.host.linux-x64.8.0.24.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
5
code/csharp/run.sh
Executable file
5
code/csharp/run.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Build and run the C# shortest-path visualizer (requires .NET SDK + raylib native lib)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
dotnet run
|
||||
129
code/pseudocode.md
Normal file
129
code/pseudocode.md
Normal file
@ -0,0 +1,129 @@
|
||||
# Shortest Path Algorithms — Pseudocode
|
||||
|
||||
## 1. Dijkstra's Algorithm
|
||||
|
||||
Finds shortest paths from a single source to all nodes.
|
||||
**Requirement:** all edge weights must be non-negative.
|
||||
|
||||
```
|
||||
DIJKSTRA(Graph, source):
|
||||
for each node v in Graph:
|
||||
dist[v] ← ∞
|
||||
prev[v] ← NULL
|
||||
dist[source] ← 0
|
||||
|
||||
PQ ← min-priority queue ordered by dist
|
||||
PQ.insert(source, 0)
|
||||
visited ← ∅
|
||||
|
||||
while PQ is not empty:
|
||||
u ← PQ.extract_min()
|
||||
|
||||
if u ∈ visited:
|
||||
continue
|
||||
visited ← visited ∪ {u}
|
||||
|
||||
for each neighbour v of u with edge weight w:
|
||||
if dist[u] + w < dist[v]:
|
||||
dist[v] ← dist[u] + w
|
||||
prev[v] ← u
|
||||
PQ.insert(v, dist[v])
|
||||
|
||||
return dist, prev
|
||||
```
|
||||
|
||||
**Complexity:** O((V + E) log V) with a binary heap.
|
||||
|
||||
|
||||
## 2. Bellman-Ford Algorithm
|
||||
|
||||
Finds shortest paths from a single source to all nodes.
|
||||
**Advantage over Dijkstra:** handles negative edge weights.
|
||||
|
||||
```
|
||||
BELLMAN_FORD(Graph, source):
|
||||
for each node v in Graph:
|
||||
dist[v] ← ∞
|
||||
prev[v] ← NULL
|
||||
dist[source] ← 0
|
||||
|
||||
repeat |V| - 1 times: ← main relaxation loop
|
||||
for each edge (u, v, w) in Graph:
|
||||
if dist[u] + w < dist[v]:
|
||||
dist[v] ← dist[u] + w
|
||||
prev[v] ← u
|
||||
|
||||
for each edge (u, v, w) in Graph: ← negative cycle check
|
||||
if dist[u] + w < dist[v]:
|
||||
error "Graph contains a negative cycle"
|
||||
|
||||
return dist, prev
|
||||
```
|
||||
|
||||
**Complexity:** O(V · E).
|
||||
|
||||
|
||||
## 3. A* Algorithm
|
||||
|
||||
Finds the shortest path from source to a specific goal node.
|
||||
Uses a heuristic h(n) that estimates the cost from n to the goal.
|
||||
**Requirement:** h(n) must be admissible (never overestimates).
|
||||
|
||||
```
|
||||
A_STAR(Graph, source, goal, h):
|
||||
for each node v in Graph:
|
||||
g[v] ← ∞ ← actual cost from source
|
||||
prev[v] ← NULL
|
||||
g[source] ← 0
|
||||
|
||||
PQ ← min-priority queue ordered by f(v) = g[v] + h(v)
|
||||
PQ.insert(source, g[source] + h(source))
|
||||
visited ← ∅
|
||||
|
||||
while PQ is not empty:
|
||||
u ← PQ.extract_min()
|
||||
|
||||
if u = goal:
|
||||
return reconstruct_path(prev, goal), g[goal]
|
||||
|
||||
if u ∈ visited:
|
||||
continue
|
||||
visited ← visited ∪ {u}
|
||||
|
||||
for each neighbour v of u with edge weight w:
|
||||
tentative ← g[u] + w
|
||||
if tentative < g[v]:
|
||||
g[v] ← tentative
|
||||
prev[v] ← u
|
||||
f[v] ← g[v] + h(v)
|
||||
PQ.insert(v, f[v])
|
||||
|
||||
return "no path found"
|
||||
```
|
||||
|
||||
**Complexity:** depends on heuristic quality; worst case O((V + E) log V), same as Dijkstra.
|
||||
|
||||
|
||||
## Path Reconstruction (used by all three)
|
||||
|
||||
```
|
||||
RECONSTRUCT_PATH(prev, target):
|
||||
path ← []
|
||||
node ← target
|
||||
while node ≠ NULL:
|
||||
path.prepend(node)
|
||||
node ← prev[node]
|
||||
return path
|
||||
```
|
||||
|
||||
|
||||
## Quick Comparison
|
||||
|
||||
| Property | Dijkstra | Bellman-Ford | A* |
|
||||
|-----------------------|----------------|----------------|-----------------------|
|
||||
| Negative weights | ✗ | ✓ | ✗ |
|
||||
| Negative cycle detect | ✗ | ✓ | ✗ |
|
||||
| Heuristic required | no | no | yes (admissible h) |
|
||||
| Finds path to | all nodes | all nodes | single goal node |
|
||||
| Time complexity | O((V+E) log V) | O(V · E) | O((V+E) log V) best |
|
||||
| Best use case | general SSSP | negative edges | known goal + good h |
|
||||
BIN
code/python/__pycache__/algorithms.cpython-314.pyc
Normal file
BIN
code/python/__pycache__/algorithms.cpython-314.pyc
Normal file
Binary file not shown.
232
code/python/algorithms.py
Normal file
232
code/python/algorithms.py
Normal file
@ -0,0 +1,232 @@
|
||||
"""
|
||||
Shortest-path algorithms: Dijkstra, Bellman-Ford, A*
|
||||
Pure implementations — no GUI dependencies.
|
||||
"""
|
||||
|
||||
import heapq
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
INF = float("inf")
|
||||
|
||||
# ─── Graph ───────────────────────────────────────────────────────────────
|
||||
|
||||
NODES: dict[str, tuple[int, int]] = {
|
||||
"S": (80, 350), "A": (270, 130), "B": (270, 570),
|
||||
"C": (500, 80), "D": (500, 350), "E": (500, 620),
|
||||
"F": (730, 180), "G": (730, 520), "T": (950, 350),
|
||||
}
|
||||
|
||||
EDGES: list[tuple[str, str, int]] = [
|
||||
("S", "A", 4), ("S", "B", 2), ("A", "C", 5), ("A", "D", 10),
|
||||
("B", "D", 3), ("B", "E", 8), ("C", "F", 3), ("D", "F", 7),
|
||||
("D", "G", 4), ("E", "G", 2), ("F", "T", 4), ("G", "T", 6),
|
||||
("C", "D", 2),
|
||||
]
|
||||
|
||||
ADJ: dict[str, list[tuple[str, int]]] = {n: [] for n in NODES}
|
||||
for _u, _v, _w in EDGES:
|
||||
ADJ[_u].append((_v, _w))
|
||||
ADJ[_v].append((_u, _w))
|
||||
|
||||
SOURCE = "S"
|
||||
GOAL = "T"
|
||||
|
||||
|
||||
def heuristic(node: str) -> float:
|
||||
x1, y1 = NODES[node]
|
||||
x2, y2 = NODES[GOAL]
|
||||
return math.hypot(x2 - x1, y2 - y1) / 100
|
||||
|
||||
|
||||
# ─── State ───────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class AlgoState:
|
||||
dist: dict[str, float] = field(default_factory=dict)
|
||||
prev: dict[str, Optional[str]] = field(default_factory=dict)
|
||||
visited: set[str] = field(default_factory=set)
|
||||
finished: bool = False
|
||||
step: int = 0
|
||||
message: str = ""
|
||||
path: list[str] = field(default_factory=list)
|
||||
relaxed_edges: set[tuple[str, str]] = field(default_factory=set)
|
||||
current_node: Optional[str] = None
|
||||
f_score: dict[str, float] = field(default_factory=dict)
|
||||
frontier: list = field(default_factory=list) # heap for Dijkstra/A*
|
||||
bf_iteration: int = 0
|
||||
|
||||
def _reconstruct_path(self) -> None:
|
||||
path: list[str] = []
|
||||
node: Optional[str] = GOAL
|
||||
while node is not None:
|
||||
path.append(node)
|
||||
node = self.prev.get(node)
|
||||
path.reverse()
|
||||
self.path = path if path and path[0] == SOURCE else []
|
||||
|
||||
|
||||
# ─── Dijkstra ────────────────────────────────────────────────────────────
|
||||
|
||||
def dijkstra_init() -> AlgoState:
|
||||
st = AlgoState()
|
||||
st.dist = {n: INF for n in NODES}
|
||||
st.prev = {n: None for n in NODES}
|
||||
st.dist[SOURCE] = 0
|
||||
st.frontier = [(0, SOURCE)]
|
||||
st.message = f"Dijkstra: start from {SOURCE}, dist[{SOURCE}]=0"
|
||||
return st
|
||||
|
||||
|
||||
def dijkstra_step(st: AlgoState) -> None:
|
||||
if st.finished or not st.frontier:
|
||||
st.finished = True
|
||||
st._reconstruct_path()
|
||||
c = st.dist.get(GOAL, INF)
|
||||
st.message = (f"Dijkstra done! Path: {'→'.join(st.path)}, cost={c:.0f}"
|
||||
if st.path else "Dijkstra done! No path.")
|
||||
return
|
||||
|
||||
st.current_node = None
|
||||
while st.frontier:
|
||||
d, u = heapq.heappop(st.frontier)
|
||||
if u in st.visited:
|
||||
continue
|
||||
st.visited.add(u)
|
||||
st.step += 1
|
||||
st.current_node = u
|
||||
st.message = f"Step {st.step}: visit {u} (dist={st.dist[u]:.0f})"
|
||||
|
||||
for v, w in ADJ[u]:
|
||||
if v not in st.visited:
|
||||
nd = st.dist[u] + w
|
||||
if nd < st.dist[v]:
|
||||
st.dist[v] = nd
|
||||
st.prev[v] = u
|
||||
heapq.heappush(st.frontier, (nd, v))
|
||||
st.relaxed_edges.add((u, v))
|
||||
st.message += f" | relax {u}->{v}:{nd:.0f}"
|
||||
return
|
||||
|
||||
st.finished = True
|
||||
st._reconstruct_path()
|
||||
c = st.dist.get(GOAL, INF)
|
||||
st.message = (f"Dijkstra done! Path: {'→'.join(st.path)}, cost={c:.0f}"
|
||||
if st.path else "Dijkstra done! No path.")
|
||||
|
||||
|
||||
# ─── Bellman-Ford ────────────────────────────────────────────────────────
|
||||
|
||||
def bellman_ford_init() -> AlgoState:
|
||||
st = AlgoState()
|
||||
st.dist = {n: INF for n in NODES}
|
||||
st.prev = {n: None for n in NODES}
|
||||
st.dist[SOURCE] = 0
|
||||
st.message = (f"Bellman-Ford: {len(NODES)-1} iterations "
|
||||
f"over {len(EDGES)*2} directed edges")
|
||||
return st
|
||||
|
||||
|
||||
def bellman_ford_step(st: AlgoState) -> None:
|
||||
if st.finished:
|
||||
return
|
||||
|
||||
V = len(NODES)
|
||||
all_edges = ([(u, v, w) for u, v, w in EDGES]
|
||||
+ [(v, u, w) for u, v, w in EDGES])
|
||||
|
||||
if st.bf_iteration < V - 1:
|
||||
changed = False
|
||||
for src, dst, w in all_edges:
|
||||
if st.dist[src] + w < st.dist[dst]:
|
||||
st.dist[dst] = st.dist[src] + w
|
||||
st.prev[dst] = src
|
||||
st.relaxed_edges.add((src, dst))
|
||||
st.visited.add(src)
|
||||
st.visited.add(dst)
|
||||
changed = True
|
||||
st.bf_iteration += 1
|
||||
st.step += 1
|
||||
st.current_node = None
|
||||
label = "changes made" if changed else "no changes (stable)"
|
||||
st.message = f"Iteration {st.bf_iteration}/{V-1}: {label}"
|
||||
|
||||
if not changed:
|
||||
st.finished = True
|
||||
st._reconstruct_path()
|
||||
c = st.dist.get(GOAL, INF)
|
||||
st.message += (f" -> Early stop! Path: {'→'.join(st.path)}, cost={c:.0f}"
|
||||
if st.path else " -> Early stop!")
|
||||
else:
|
||||
neg = any(st.dist[s] + w < st.dist[d] for s, d, w in all_edges)
|
||||
st.finished = True
|
||||
if neg:
|
||||
st.message = "Negative cycle detected!"
|
||||
else:
|
||||
st._reconstruct_path()
|
||||
c = st.dist.get(GOAL, INF)
|
||||
st.message = (f"Bellman-Ford done! Path: {'→'.join(st.path)}, cost={c:.0f}"
|
||||
if st.path else "Bellman-Ford done! No path.")
|
||||
|
||||
|
||||
# ─── A* ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def astar_init() -> AlgoState:
|
||||
st = AlgoState()
|
||||
st.dist = {n: INF for n in NODES}
|
||||
st.prev = {n: None for n in NODES}
|
||||
st.f_score = {n: INF for n in NODES}
|
||||
st.dist[SOURCE] = 0
|
||||
h = heuristic(SOURCE)
|
||||
st.f_score[SOURCE] = h
|
||||
st.frontier = [(h, SOURCE)]
|
||||
st.message = f"A*: start={SOURCE}, goal={GOAL}, h({SOURCE})={h:.1f}"
|
||||
return st
|
||||
|
||||
|
||||
def astar_step(st: AlgoState) -> None:
|
||||
if st.finished or not st.frontier:
|
||||
st.finished = True
|
||||
st._reconstruct_path()
|
||||
c = st.dist.get(GOAL, INF)
|
||||
st.message = (f"A* done! Path: {'→'.join(st.path)}, cost={c:.0f}"
|
||||
if st.path else "A* done! No path.")
|
||||
return
|
||||
|
||||
st.current_node = None
|
||||
while st.frontier:
|
||||
f, u = heapq.heappop(st.frontier)
|
||||
if u in st.visited:
|
||||
continue
|
||||
st.visited.add(u)
|
||||
st.step += 1
|
||||
st.current_node = u
|
||||
|
||||
if u == GOAL:
|
||||
st.finished = True
|
||||
st._reconstruct_path()
|
||||
st.message = (f"Step {st.step}: GOAL reached! "
|
||||
f"Path: {'→'.join(st.path)}, cost={st.dist[GOAL]:.0f}")
|
||||
return
|
||||
|
||||
h = heuristic(u)
|
||||
st.message = (f"Step {st.step}: visit {u} "
|
||||
f"(g={st.dist[u]:.0f}, h={h:.1f}, f={f:.1f})")
|
||||
|
||||
for v, w in ADJ[u]:
|
||||
if v not in st.visited:
|
||||
nd = st.dist[u] + w
|
||||
if nd < st.dist[v]:
|
||||
st.dist[v] = nd
|
||||
st.prev[v] = u
|
||||
fv = nd + heuristic(v)
|
||||
st.f_score[v] = fv
|
||||
heapq.heappush(st.frontier, (fv, v))
|
||||
st.relaxed_edges.add((u, v))
|
||||
st.message += f" | relax {u}->{v}: g={nd:.0f}, f={fv:.1f}"
|
||||
return
|
||||
|
||||
st.finished = True
|
||||
st._reconstruct_path()
|
||||
st.message = "A* done! No path."
|
||||
5
code/python/run.sh
Executable file
5
code/python/run.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Run the Python shortest-path visualizer (requires pygame)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
python3 visualizer.py
|
||||
188
code/python/visualizer.py
Normal file
188
code/python/visualizer.py
Normal file
@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pygame GUI for shortest-path algorithm visualization.
|
||||
|
||||
Controls:
|
||||
1 / 2 / 3 — Dijkstra / Bellman-Ford / A*
|
||||
SPACE — advance one step
|
||||
R — reset
|
||||
Q / ESC — quit
|
||||
"""
|
||||
|
||||
import math
|
||||
import sys
|
||||
import pygame
|
||||
|
||||
from algorithms import (
|
||||
NODES, EDGES, GOAL, INF, AlgoState, heuristic,
|
||||
dijkstra_init, dijkstra_step,
|
||||
bellman_ford_init, bellman_ford_step,
|
||||
astar_init, astar_step,
|
||||
)
|
||||
|
||||
# ─── Constants ───────────────────────────────────────────────────────────
|
||||
WIDTH, HEIGHT = 1100, 700
|
||||
NODE_RADIUS = 28
|
||||
FONT_SIZE = 18
|
||||
|
||||
# Colours
|
||||
WHITE = (255, 255, 255)
|
||||
BLACK = (0, 0, 0)
|
||||
LIGHT_GREY = (230, 230, 230)
|
||||
DARK_GREY = (100, 100, 100)
|
||||
BLUE = (60, 100, 220)
|
||||
ORANGE = (240, 140, 40)
|
||||
VISITED_COL = (180, 220, 255)
|
||||
PATH_COL = (60, 200, 120)
|
||||
CURRENT_COL = (255, 220, 80)
|
||||
EDGE_DEFAULT= (160, 160, 160)
|
||||
EDGE_RELAXED= (60, 160, 60)
|
||||
EDGE_PATH = (40, 180, 100)
|
||||
BG = (245, 245, 250)
|
||||
|
||||
ALGO_NAMES = ["Dijkstra's Algorithm", "Bellman-Ford Algorithm", "A* Algorithm"]
|
||||
ALGOS = [
|
||||
(dijkstra_init, dijkstra_step),
|
||||
(bellman_ford_init, bellman_ford_step),
|
||||
(astar_init, astar_step),
|
||||
]
|
||||
|
||||
|
||||
# ─── Drawing ─────────────────────────────────────────────────────────────
|
||||
|
||||
def draw_edge(screen: pygame.Surface, p1: tuple, p2: tuple,
|
||||
colour: tuple, width: int = 2) -> None:
|
||||
dx, dy = p2[0] - p1[0], p2[1] - p1[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length == 0:
|
||||
return
|
||||
ux, uy = dx / length, dy / length
|
||||
start = (p1[0] + ux * NODE_RADIUS, p1[1] + uy * NODE_RADIUS)
|
||||
end = (p2[0] - ux * NODE_RADIUS, p2[1] - uy * NODE_RADIUS)
|
||||
pygame.draw.line(screen, colour, start, end, width)
|
||||
|
||||
|
||||
def render(screen: pygame.Surface, font: pygame.font.Font,
|
||||
small_font: pygame.font.Font, state: AlgoState,
|
||||
algo_idx: int) -> None:
|
||||
screen.fill(BG)
|
||||
|
||||
# Title
|
||||
title_surf = font.render(ALGO_NAMES[algo_idx], True, BLACK)
|
||||
screen.blit(title_surf, (WIDTH // 2 - title_surf.get_width() // 2, 10))
|
||||
|
||||
# Path edge set
|
||||
path_edges: set[tuple[str, str]] = set()
|
||||
for i in range(len(state.path) - 1):
|
||||
a, b = state.path[i], state.path[i + 1]
|
||||
path_edges.add((a, b))
|
||||
path_edges.add((b, a))
|
||||
|
||||
# Edges
|
||||
for u, v, w in EDGES:
|
||||
p1, p2 = NODES[u], NODES[v]
|
||||
if (u, v) in path_edges:
|
||||
col, wd = EDGE_PATH, 5
|
||||
elif (u, v) in state.relaxed_edges or (v, u) in state.relaxed_edges:
|
||||
col, wd = EDGE_RELAXED, 3
|
||||
else:
|
||||
col, wd = EDGE_DEFAULT, 2
|
||||
draw_edge(screen, p1, p2, col, wd)
|
||||
|
||||
# Weight label
|
||||
mx = (p1[0] + p2[0]) // 2
|
||||
my = (p1[1] + p2[1]) // 2
|
||||
dx, dy = p2[0] - p1[0], p2[1] - p1[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length > 0:
|
||||
mx += int(-dy / length * 14)
|
||||
my += int(dx / length * 14)
|
||||
wt = small_font.render(str(w), True, DARK_GREY)
|
||||
screen.blit(wt, (mx - wt.get_width() // 2, my - wt.get_height() // 2))
|
||||
|
||||
# Nodes
|
||||
for name, (x, y) in NODES.items():
|
||||
if name in state.path and state.finished:
|
||||
col = PATH_COL
|
||||
elif name == state.current_node:
|
||||
col = CURRENT_COL
|
||||
elif name in state.visited:
|
||||
col = VISITED_COL
|
||||
else:
|
||||
col = WHITE
|
||||
|
||||
pygame.draw.circle(screen, col, (x, y), NODE_RADIUS)
|
||||
pygame.draw.circle(screen, BLACK, (x, y), NODE_RADIUS, 2)
|
||||
|
||||
# Name
|
||||
label = font.render(name, True, BLACK)
|
||||
screen.blit(label, (x - label.get_width() // 2,
|
||||
y - label.get_height() // 2 - 8))
|
||||
|
||||
# Distance
|
||||
d = state.dist.get(name, INF)
|
||||
d_text = "inf" if d == INF else f"{d:.0f}"
|
||||
d_surf = small_font.render(f"d={d_text}", True, BLUE)
|
||||
screen.blit(d_surf, (x - d_surf.get_width() // 2, y + 8))
|
||||
|
||||
# h(n) for A*
|
||||
if algo_idx == 2:
|
||||
h_surf = small_font.render(f"h={heuristic(name):.1f}", True, ORANGE)
|
||||
screen.blit(h_surf, (x + NODE_RADIUS + 4, y - 8))
|
||||
|
||||
# Info bar
|
||||
pygame.draw.rect(screen, LIGHT_GREY, (0, HEIGHT - 80, WIDTH, 80))
|
||||
pygame.draw.line(screen, DARK_GREY, (0, HEIGHT - 80), (WIDTH, HEIGHT - 80), 2)
|
||||
|
||||
info = font.render(state.message, True, BLACK)
|
||||
screen.blit(info, (20, HEIGHT - 70))
|
||||
|
||||
hint = small_font.render(
|
||||
"[1] Dijkstra [2] Bellman-Ford [3] A* "
|
||||
"[SPACE] Step [R] Reset [Q] Quit", True, DARK_GREY)
|
||||
screen.blit(hint, (20, HEIGHT - 35))
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
|
||||
# ─── Main ────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
pygame.init()
|
||||
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||
pygame.display.set_caption("Shortest Path Visualizer — Dijkstra / Bellman-Ford / A*")
|
||||
font = pygame.font.SysFont("DejaVu Sans", FONT_SIZE, bold=True)
|
||||
small_font = pygame.font.SysFont("DejaVu Sans", FONT_SIZE - 4)
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
current = 0
|
||||
state = ALGOS[current][0]()
|
||||
|
||||
running = True
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
if event.key in (pygame.K_q, pygame.K_ESCAPE):
|
||||
running = False
|
||||
elif event.key == pygame.K_1:
|
||||
current = 0; state = ALGOS[current][0]()
|
||||
elif event.key == pygame.K_2:
|
||||
current = 1; state = ALGOS[current][0]()
|
||||
elif event.key == pygame.K_3:
|
||||
current = 2; state = ALGOS[current][0]()
|
||||
elif event.key == pygame.K_r:
|
||||
state = ALGOS[current][0]()
|
||||
elif event.key == pygame.K_SPACE:
|
||||
if not state.finished:
|
||||
ALGOS[current][1](state)
|
||||
|
||||
render(screen, font, small_font, state, current)
|
||||
clock.tick(30)
|
||||
|
||||
pygame.quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
332
games/EXAM_QUESTIONS_CODE.md
Normal file
332
games/EXAM_QUESTIONS_CODE.md
Normal file
@ -0,0 +1,332 @@
|
||||
# Thesis Defense — Exemplary Code Questions & Answers
|
||||
|
||||
Questions that may arise during the defense (obrona) about the game implementations,
|
||||
along with answers that can be demonstrated directly from the source code.
|
||||
|
||||
---
|
||||
|
||||
## Category 1: Architecture & Design Patterns
|
||||
|
||||
### Q1: Why does the Unity version build the entire game from code instead of using a pre-configured scene?
|
||||
|
||||
**Answer (reference: `GameBootstrap.cs`, `GameInitializer.cs`)**:
|
||||
The Unity version uses a **scene-free bootstrap pattern** where `GameBootstrap` (a static class with `[RuntimeInitializeOnLoadMethod]`) creates a `GameInitializer` MonoBehaviour that constructs everything in `Awake()`. This design ensures:
|
||||
1. **Benchmark reproducibility** — no scene serialization variability between test runs
|
||||
2. **No missing references** — all wiring is explicit in code, not serialized in scene files
|
||||
3. **Single source of truth** — the entire game configuration is readable in `GameInitializer.BuildGame()`
|
||||
|
||||
The Unreal version, in contrast, uses level-placed actors — this is more idiomatic for Unreal but introduces scene-dependency.
|
||||
|
||||
---
|
||||
|
||||
### Q2: What design patterns are used in the Unity project, and why?
|
||||
|
||||
**Answer (reference: multiple files)**:
|
||||
- **Singleton** (`GameDirector.Instance`, `ScoreManager.Instance`, etc.) — provides global access to managers; common in Unity game architecture
|
||||
- **Object Pooling** (`BulletPool.cs`, `EffectManager.cs`) — eliminates GC pressure from Instantiate/Destroy in a bullet-hell scenario
|
||||
- **Component Composition** (`[RequireComponent(typeof(Health))]`) — decouples health/damage from entity behavior
|
||||
- **Event-driven Communication** (`Health.Died`, `ScoreManager.ScoreChanged`) — loose coupling via C# delegates
|
||||
- **Factory Method** (`BulletPool.Create()`, `PlayerController.CreatePlayer()`) — encapsulates construction
|
||||
- **Data-driven Configuration** (`EnemyBlueprint`) — enemy archetypes defined as runtime data objects
|
||||
|
||||
---
|
||||
|
||||
### Q3: How does the Unreal version discover and communicate between actors?
|
||||
|
||||
**Answer (reference: `STGGameDirector.cpp`, `STGPawn.cpp`)**:
|
||||
The Unreal version uses `UGameplayStatics::GetAllActorsOfClass()` to find other actors at runtime. For example, the GameDirector finds the HUDManager each tick:
|
||||
```cpp
|
||||
TArray<AActor*> FoundManagers;
|
||||
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASTGHUDManager::StaticClass(), FoundManagers);
|
||||
```
|
||||
This is the standard UE5 approach but has O(N) complexity per call. The Unity version uses singletons (O(1) access) but sacrifices flexibility (only one instance allowed).
|
||||
|
||||
---
|
||||
|
||||
### Q4: Why does the Unreal version use a centralized settings header instead of UPROPERTY defaults?
|
||||
|
||||
**Answer (reference: `STGGameSettings.h`)**:
|
||||
The `STGGameSettings.h` uses `constexpr` namespaces (`STG::Player::MoveSpeed`, `STG::Enemy::Tank::Health`, etc.) to define all game constants in one file. Benefits:
|
||||
1. **Zero runtime cost** — values are compile-time constants
|
||||
2. **Single source of truth** — change a value in one place, it propagates to all classes
|
||||
3. **No editor dependency** — no need to set values in Blueprint/Inspector
|
||||
4. **Self-documenting** — the namespace hierarchy (`STG::Enemy::Tank::Health = 50`) reads as game design documentation
|
||||
|
||||
UPROPERTY defaults are still used as initializers (`float MoveSpeed = STG::Player::MoveSpeed`) so values appear in the editor for debugging.
|
||||
|
||||
---
|
||||
|
||||
## Category 2: Performance & Optimization
|
||||
|
||||
### Q5: How does the Unity bullet pool work, and why is it needed?
|
||||
|
||||
**Answer (reference: `BulletPool.cs`, `Bullet.cs`)**:
|
||||
`BulletPool` uses a `Queue<Bullet>` for O(1) get/return and a `HashSet<Bullet>` for tracking live bullets:
|
||||
- **Spawn**: Dequeue from pool (or create new if empty), activate, configure
|
||||
- **Return**: Deactivate, re-parent to hidden root, enqueue
|
||||
- **ClearLiveBullets**: Iterate HashSet, return all to pool (used by bomb)
|
||||
|
||||
Pre-allocated capacities: 400 player bullets, 900 enemy bullets. Without pooling, `Instantiate`/`Destroy` calls would trigger garbage collection pauses, causing frame hitches — catastrophic in a game running at 240 FPS target with 1000+ active bullets.
|
||||
|
||||
---
|
||||
|
||||
### Q6: Why doesn't the Unreal version use object pooling for bullets?
|
||||
|
||||
**Answer (reference: `STGProjectile.cpp`, `STGPawn.cpp` FireShot)**:
|
||||
The Unreal version uses `World->SpawnActor<ASTGProjectile>()` and `Destroy()` for each bullet — the standard UE5 approach. UE5's actor lifecycle is managed by the engine with its own memory management (no GC like C#), so the penalty for spawn/destroy is lower. However, each `SpawnActor` still creates a new UObject with components, registers overlaps, etc. — this IS measurable overhead.
|
||||
|
||||
**This is a deliberate comparison point**: the thesis measures whether UE5's native actor management can match Unity's explicit pooling. Any performance difference here reflects each engine's memory management strategy.
|
||||
|
||||
---
|
||||
|
||||
### Q7: How do the two engines handle bullet movement differently?
|
||||
|
||||
**Answer (reference: Unity `Bullet.cs` vs Unreal `STGProjectile.cpp`)**:
|
||||
|
||||
**Unity**: Manual position update every frame:
|
||||
```csharp
|
||||
transform.position += (Vector3)(_direction * (_speed * Time.deltaTime));
|
||||
```
|
||||
The `Rigidbody2D` is set to `Kinematic` — it exists only for trigger detection, not physics simulation. This gives full control over movement with zero physics solver overhead.
|
||||
|
||||
**Unreal**: Uses `UProjectileMovementComponent`:
|
||||
```cpp
|
||||
ProjectileMovement->InitialSpeed = InSpeed;
|
||||
ProjectileMovement->ProjectileGravityScale = 0.0f;
|
||||
```
|
||||
This is UE5's built-in optimized component that handles velocity, acceleration, and bouncing. Since gravity is zero, the result is identical straight-line motion, but it leverages UE5's native tick pipeline.
|
||||
|
||||
---
|
||||
|
||||
### Q8: What is the "grazing mechanic" in the Unreal version?
|
||||
|
||||
**Answer (reference: `STGGameSettings.h`, `STGProjectile.cpp` BeginPlay)**:
|
||||
Enemy bullets have a large visual mesh (`BulletVisualScale = 0.25`) but a tiny collision sphere (`BulletCollisionRadius = 3.0`):
|
||||
```cpp
|
||||
CollisionComp->SetSphereRadius(CollisionRadius); // 3.0 units
|
||||
MeshComp->SetRelativeScale3D(FVector(BulletScale)); // 0.25 scale
|
||||
```
|
||||
This means players can visually "graze" through bullets that appear to overlap them, adding skill expression. The Unity version doesn't have this distinction — visual and collision sizes are coupled.
|
||||
|
||||
---
|
||||
|
||||
### Q9: How does frame rate targeting differ between the two implementations?
|
||||
|
||||
**Answer (reference: Unity `GameInitializer.cs`, Unreal project settings)**:
|
||||
Unity explicitly sets `Application.targetFrameRate = 240` in code to uncap rendering. Unreal relies on project settings (default uncapped) or console commands. Both aim for maximum frame throughput to stress-test the render pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Category 3: Gameplay Systems
|
||||
|
||||
### Q10: How does the difficulty progression work in each engine?
|
||||
|
||||
**Answer (reference: Unity `EnemySpawner.cs`, Unreal `STGEnemySpawner.cpp`)**:
|
||||
|
||||
**Unity**:
|
||||
- Spawn delay: linearly interpolates from 1.6s → 0.45s over 90 seconds
|
||||
- Wave size: 2–10 enemies per wave, scaling with elapsed time
|
||||
- Difficulty value: 1.2 → 6.0, scaling enemy HP (+12 per unit), speed, and fire rate
|
||||
- Introduction: first 4 spawns introduce each enemy type once
|
||||
|
||||
**Unreal**:
|
||||
- Spawn interval: 0.25s → 0.08s (linear), with 0.03s "chaos" in final 5 seconds
|
||||
- One enemy per spawn tick (constant stream vs Unity's wave pulses)
|
||||
- Type distribution: Fodder-only at start, gradually adding Runner, Turret, Tank
|
||||
- Maximum 200 simultaneous enemies
|
||||
|
||||
**Key difference**: Unity spawns in **waves** (2–10 at once with delays), Unreal spawns a **constant stream** (one per interval). The Unreal "final rush" at 0.03s intervals creates more extreme end-game stress.
|
||||
|
||||
---
|
||||
|
||||
### Q11: How does the player upgrade/progression system compare?
|
||||
|
||||
**Answer (reference: Unity `PlayerController.cs` HandleScoreChanged, Unreal `STGPawn.cpp` CheckUpgrades)**:
|
||||
|
||||
**Unity**: Exponential thresholds with doubling increments:
|
||||
- Base threshold: 1500, then doubles (1500 → 3000 → 6000 → ...)
|
||||
- Max level 12 with unlockable directional shots (side, diagonal, rear)
|
||||
- Smooth escalation over very high score ranges
|
||||
|
||||
**Unreal**: Fixed score thresholds with explicit upgrade tiers:
|
||||
- Level 1 at 100, Level 2 at 300, Level 3 at 600, Level 4 (max) at 1000
|
||||
- Each level explicitly sets volley size, spread, fire rate
|
||||
- Faster progression — max power reached at 1000 score
|
||||
|
||||
**Why different**: The Unity version has longer duration before max power, creating more gradual performance escalation. Unreal reaches max power quickly, then maintains constant load — better for steady-state benchmarking.
|
||||
|
||||
---
|
||||
|
||||
### Q12: How does the screen-clear special ability work in each engine?
|
||||
|
||||
**Answer (reference: Unity `PlayerController.cs` PerformBomb, Unreal `STGPawn.cpp` UseSpecial)**:
|
||||
|
||||
**Unity**: Coroutine-based dramatic sequence:
|
||||
1. Disable controls, enable invulnerability
|
||||
2. Flash white, spawn screen-clear effects
|
||||
3. Kill all enemies via `EnemySpawner.ClearScreen()` (which also clears enemy bullets)
|
||||
4. Dash upward, then return to starting position
|
||||
5. Re-enable controls
|
||||
|
||||
**Unreal**: Immediate, no animation:
|
||||
1. Find all `ASTGEnemy` actors → `Destroy()` each
|
||||
2. Find all `ASTGProjectile` where `!bIsPlayerBullet` → `Destroy()` each
|
||||
3. Done in a single frame
|
||||
|
||||
**Unity's approach** is more cinematic; **Unreal's approach** is simpler but functional.
|
||||
|
||||
---
|
||||
|
||||
### Q13: How are enemy types defined differently in the two implementations?
|
||||
|
||||
**Answer (reference: Unity `EnemyBlueprint.cs`, Unreal `STGEnemy.h`)**:
|
||||
|
||||
**Unity** uses `EnemyBlueprint` — a sealed immutable C# class with 19 constructor parameters defining every aspect of an enemy archetype. Blueprints are created in `EnemySpawner.BuildBlueprints()` as an array of 4 instances.
|
||||
|
||||
**Unreal** uses `EEnemyType` enum + `InitializeFromType()` switch statement. Each case reads values from the `STGGameSettings.h` constexpr namespaces and sets `UPROPERTY` fields accordingly.
|
||||
|
||||
Both achieve the same goal — data-driven enemy configuration — but:
|
||||
- Unity's approach is more **object-oriented** (data class encapsulation)
|
||||
- Unreal's approach is more **configuration-driven** (header constants + switch)
|
||||
|
||||
---
|
||||
|
||||
## Category 4: Technical Implementation Details
|
||||
|
||||
### Q14: How is collision detection handled in each engine?
|
||||
|
||||
**Answer (reference: Unity `Bullet.cs` OnTriggerEnter2D, Unreal `STGProjectile.cpp` OnOverlapBegin)**:
|
||||
|
||||
**Unity**: Uses 2D physics with `CircleCollider2D` (trigger mode) + `Rigidbody2D` (Kinematic):
|
||||
- `OnTriggerEnter2D` checks for bullet-vs-bullet (same faction → ignore), bullet-vs-Health (opposite faction → damage + despawn)
|
||||
- Respects player invulnerability via `PlayerController.IsInvulnerable()`
|
||||
|
||||
**Unreal**: Uses 3D physics with `USphereComponent` + `UBoxComponent` (overlap events):
|
||||
- `OnOverlapBegin` uses `Cast<>` to identify hit actors: `ASTGEnemy` or `ASTGPawn`
|
||||
- Player bullets damage enemies, enemy bullets damage player
|
||||
|
||||
**Key technical difference**: Unity uses 2D physics (Box2D engine internally). Unreal uses 3D physics (Chaos/PhysX) even though the game is effectively 2D. The 3D physics overhead for a 2D game is a potential benchmark comparison point.
|
||||
|
||||
---
|
||||
|
||||
### Q15: How does each engine generate sprites/meshes at runtime without external assets?
|
||||
|
||||
**Answer (reference: Unity `Bullet.cs` GetOrCreateSprite, `EnemySpawner.cs` GenerateSprite, Unreal `STGEnemy.cpp` constructor)**:
|
||||
|
||||
**Unity**: Procedurally generates `Texture2D` objects and creates `Sprite` from them:
|
||||
- Bullets: 16×16 solid-color squares, cached per color in `static Dictionary<Color, Sprite>`
|
||||
- Enemies: 64×64 textures with shape-specific pixel tests (disc, triangle, diamond, arrow)
|
||||
- Life icons: 32×32 circles via distance-from-center
|
||||
- Effects: 32×32 radial gradients (alpha = (1-dist)²)
|
||||
- Stars: Full-resolution textures with random white pixels
|
||||
|
||||
**Unreal**: Uses built-in engine meshes:
|
||||
- Player: `/Engine/BasicShapes/Cylinder` flattened (0.08×0.08×0.001)
|
||||
- Enemies: `/Engine/BasicShapes/Cube` at various scales (0.25–0.8)
|
||||
- Bullets: `/Engine/BasicShapes/Sphere` with dynamic material for color/emissive
|
||||
- Colors via `UMaterialInstanceDynamic` set at runtime
|
||||
|
||||
**Implication**: Unity creates zero external dependencies — entirely procedural. Unreal reuses engine-built-in meshes — cleaner but requires the engine's asset system.
|
||||
|
||||
---
|
||||
|
||||
### Q16: How is the HUD implemented differently in each engine?
|
||||
|
||||
**Answer (reference: Unity `GameInitializer.cs` CreateHud, Unreal `STGHUDManager.cpp`)**:
|
||||
|
||||
**Unity**: Entire canvas and all UI elements built from C# code:
|
||||
- `Canvas`, `CanvasScaler`, `GraphicRaycaster` components created
|
||||
- `Text` components for score, timer, status
|
||||
- `Image` components for life icons and damage overlay
|
||||
- All positioning via `RectTransform` anchors and offsets
|
||||
|
||||
**Unreal**: C++ creates/manages a widget, but layout is in a UMG Blueprint:
|
||||
- `CreateWidget<UUserWidget>(GetWorld(), HUDWidgetClass)` instantiates the widget
|
||||
- Updates find named elements: `GetWidgetFromName("txt_Score")`
|
||||
- Widget layout, fonts, and positioning defined in the editor
|
||||
|
||||
**Tradeoff**: Unity's approach is fully self-contained but verbose (~100 lines of UI code). Unreal's approach is concise in C++ but requires a separate UMG asset.
|
||||
|
||||
---
|
||||
|
||||
### Q17: How does the camera system differ between engines?
|
||||
|
||||
**Answer (reference: Unity `GameInitializer.cs` BuildCamera, Unreal `STGFixedCamera.cpp`)**:
|
||||
|
||||
**Unity**: Simple orthographic camera:
|
||||
```csharp
|
||||
cam.orthographic = true;
|
||||
cam.orthographicSize = 6.5f;
|
||||
cam.transform.position = new Vector3(0f, 0f, -10f);
|
||||
```
|
||||
6.5 units = half the visible height. The camera is 2D by design.
|
||||
|
||||
**Unreal**: Perspective camera with trigonometric auto-fit:
|
||||
```cpp
|
||||
float HeightForWidth = HalfWidth / FMath::Tan(HalfFOVRad);
|
||||
float HeightForHeight = HalfHeight / FMath::Tan(VerticalHalfFOVRad);
|
||||
return FMath::Max(HeightForWidth, HeightForHeight);
|
||||
```
|
||||
Calculates required Z-height to fit the 1700×900 play area within 60° FOV, assuming 16:9 aspect ratio.
|
||||
|
||||
**Why different**: Unity's 2D pipeline natively supports orthographic. Unreal's 3D pipeline typically uses perspective — even for top-down games, this requires explicit camera distance calculation.
|
||||
|
||||
---
|
||||
|
||||
## Category 5: Testing & Benchmarking
|
||||
|
||||
### Q18: How are the games configured for automated performance testing?
|
||||
|
||||
**Answer (reference: Unity `PlayerController.cs` CheckCommandLineArgs, Unreal `STGPawn.cpp` BeginPlay + `STGGameDirector.cpp`)**:
|
||||
|
||||
Both support command-line flags:
|
||||
|
||||
| Flag | Unity | Unreal |
|
||||
|------|-------|--------|
|
||||
| Invincibility | `--invincible` | `--invincible` |
|
||||
| No player movement | `--stationary` | `--stationary` |
|
||||
| Auto-quit on end | N/A | `--autoquit` |
|
||||
| Custom duration | N/A | `--duration=N` |
|
||||
| Start at time offset | N/A | `--start-time=N` |
|
||||
|
||||
Usage: `./Final.x86_64 --invincible --stationary` (Unity) or passing flags via UE5 launch arguments.
|
||||
|
||||
These modes enable:
|
||||
- **`--invincible`**: Prevents game-over during unattended profiling runs
|
||||
- **`--stationary`**: Isolates enemy/spawner performance by removing player bullet generation
|
||||
- **`--autoquit`**: Clean exit for batch profiling (Nsight capture scripts)
|
||||
- **`--start-time=N`**: Jump to specific difficulty phase for targeted profiling
|
||||
|
||||
---
|
||||
|
||||
### Q19: What makes these implementations suitable for a fair engine comparison?
|
||||
|
||||
**Answer**: The games are designed with parity in mind:
|
||||
1. **Same game concept** — top-down bullet hell with survival timer
|
||||
2. **Same duration** — 90 seconds
|
||||
3. **Same complexity arc** — escalating enemies and bullet counts
|
||||
4. **Same test modes** — invincibility and stationary mode
|
||||
5. **Equivalent enemy variety** — 4 types each, with similar gameplay roles
|
||||
6. **Equivalent mechanics** — player shooting, screen-clear special, score-based upgrades
|
||||
|
||||
**Intentional differences** highlight engine characteristics:
|
||||
- Unity uses object pooling → shows GC management approach
|
||||
- Unreal uses spawn/destroy → shows native memory management
|
||||
- Unity uses 2D physics → lightweight collision
|
||||
- Unreal uses 3D physics → full physics engine for a 2D game
|
||||
- Unity builds UI in code → no external assets
|
||||
- Unreal uses UMG widgets → engine's standard UI system
|
||||
|
||||
---
|
||||
|
||||
### Q20: What specific metrics can be compared using these implementations?
|
||||
|
||||
**Answer**: Both games generate comparable workloads measurable via NVIDIA Nsight:
|
||||
1. **Draw calls** — sprite rendering (Unity 2D) vs mesh rendering (Unreal 3D)
|
||||
2. **GPU frame time** — shader complexity, overdraw, particle effects
|
||||
3. **CPU frame time** — game logic, physics, spawning overhead
|
||||
4. **Memory allocation patterns** — pooled (Unity) vs dynamic (Unreal)
|
||||
5. **Physics overhead** — Box2D 2D triggers (Unity) vs Chaos/PhysX 3D overlaps (Unreal)
|
||||
6. **Actor/object count scaling** — how each engine handles 100→1000+ active entities
|
||||
7. **VFX cost** — sprite scaling (Unity) vs Niagara particles (Unreal)
|
||||
8. **Input latency** — Legacy Input (Unity) vs Enhanced Input (Unreal)
|
||||
343
games/UNITY_CODE_DOCUMENTATION.md
Normal file
343
games/UNITY_CODE_DOCUMENTATION.md
Normal file
@ -0,0 +1,343 @@
|
||||
# Unity Bullet Hell — Complete Code Documentation
|
||||
|
||||
**Project**: `magisterka_2/` (Unity 2D, C#)
|
||||
**Namespace**: `Magisterka.BulletHell`
|
||||
**Purpose**: Performance benchmark bullet-hell shooter for comparing Unity vs Unreal Engine.
|
||||
**Duration**: 90-second survival round with escalating difficulty.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The project follows a **scene-free bootstrap pattern**: the game builds itself entirely at runtime from C# code, with no dependency on a pre-configured Unity scene. This is done via:
|
||||
|
||||
1. **`GameBootstrap`** — Static class with `[RuntimeInitializeOnLoadMethod]` that ensures `GameInitializer` exists.
|
||||
2. **`GameInitializer`** — MonoBehaviour that constructs the entire game hierarchy: camera, player, pools, HUD, spawner, director.
|
||||
|
||||
This architecture means the game can run from a completely empty scene — ideal for reproducible benchmarking across machines.
|
||||
|
||||
### Dependency Graph
|
||||
|
||||
```
|
||||
GameBootstrap → GameInitializer
|
||||
├── Camera (orthographic, 2D)
|
||||
├── BackgroundScroller (parallax starfield)
|
||||
├── EffectManager (VFX pooling)
|
||||
├── ScoreManager (HUD + score)
|
||||
├── BulletPool × 2 (player & enemy)
|
||||
├── PlayerController
|
||||
├── EnemySpawner
|
||||
└── GameDirector (timer, victory, game-over)
|
||||
```
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
| Pattern | Where Used | Why |
|
||||
|---------|-----------|-----|
|
||||
| **Singleton** | `GameDirector`, `EnemySpawner`, `ScoreManager`, `EffectManager`, `PlayerController` | Provides global access to managers via `.Instance` |
|
||||
| **Object Pooling** | `BulletPool`, `EffectManager` | Eliminates per-frame allocation; critical for performance at 1000+ active bullets |
|
||||
| **Bootstrap/Factory** | `GameBootstrap` + `GameInitializer` | Scene-independent initialization for benchmark reproducibility |
|
||||
| **Component Composition** | `[RequireComponent(typeof(Health))]` on `EnemyController` and `PlayerController` | Ensures Health component always present; decouples damage logic from entity logic |
|
||||
| **Event-driven Communication** | `Health.Died`, `Health.Damaged`, `ScoreManager.ScoreChanged` | Loose coupling between systems via C# events/delegates |
|
||||
| **Blueprint/Data-driven Configuration** | `EnemyBlueprint` class | Runtime data objects define enemy archetypes without requiring ScriptableObjects |
|
||||
|
||||
---
|
||||
|
||||
## File-by-File Documentation
|
||||
|
||||
### 1. `Faction.cs` — Combat Side Identifier
|
||||
|
||||
**What it does**: Defines a simple enum with two values: `Player` and `Enemy`.
|
||||
|
||||
**Why it works this way**: Used throughout the collision and damage system to prevent friendly fire. When a bullet hits a `Health` component, factions are compared — bullets only damage entities of the opposite faction. This is the simplest possible friend-or-foe identification, avoiding complex layer setups.
|
||||
|
||||
**Key detail**: Stored as `int` (Player=0, Enemy=1), making comparison extremely cheap.
|
||||
|
||||
---
|
||||
|
||||
### 2. `GameBootstrap.cs` — Runtime Scene Initializer
|
||||
|
||||
**What it does**: A static class with `[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]` that runs automatically after any scene loads. It checks if a `GameInitializer` already exists; if not, it creates one and marks it `DontDestroyOnLoad`.
|
||||
|
||||
**Why it works this way**: This guarantees the game bootstraps correctly regardless of which scene is loaded or how the build is configured. No manual scene setup needed. The `AfterSceneLoad` timing ensures the scene hierarchy is ready before initialization begins.
|
||||
|
||||
**Performance relevance**: Zero runtime cost — runs exactly once at startup.
|
||||
|
||||
---
|
||||
|
||||
### 3. `GameInitializer.cs` — Full Scene Builder
|
||||
|
||||
**What it does**: A MonoBehaviour that constructs the **entire game** programmatically in `Awake()`:
|
||||
- Creates an orthographic 2D camera (size 6.5, black background, positioned at z=-10)
|
||||
- Sets `Application.targetFrameRate = 240` (uncapped for benchmarking)
|
||||
- Hides cursor
|
||||
- Creates `BackgroundScroller`, `EffectManager`, `ScoreManager`
|
||||
- Creates two `BulletPool` instances (player: 400 warm, cyan; enemy: 900 warm, orange-red)
|
||||
- Creates `PlayerController` with calculated movement bounds from camera aspect
|
||||
- Builds the entire HUD (canvas, score label, life icons, timer, game-over text, damage overlay)
|
||||
- Creates `EnemySpawner` and `GameDirector`, wiring them together
|
||||
|
||||
**Why it works this way**: Building everything in code means:
|
||||
1. **No scene dependency** — benchmarks are perfectly reproducible
|
||||
2. **Single source of truth** — all wiring is visible in one file
|
||||
3. **No serialization bugs** — no missing references from scene changes
|
||||
|
||||
**HUD construction detail**: The `CreateHud()` method builds the entire UI canvas from scratch including:
|
||||
- Score text (upper-left, 24pt)
|
||||
- Life icons (programmatically generated circular sprites via `BuildLifeSprite()`)
|
||||
- Timer text (upper-right, 24pt)
|
||||
- Game over/victory text (center, 48pt, initially hidden)
|
||||
- Damage overlay (full-screen red flash, initially transparent)
|
||||
|
||||
**`BuildLifeSprite()`**: Generates a 32×32 pixel circular texture at runtime using distance-from-center calculation. No external assets needed.
|
||||
|
||||
---
|
||||
|
||||
### 4. `GameDirector.cs` — Game Flow Controller
|
||||
|
||||
**What it does**: Singleton MonoBehaviour that manages the overall game timer and end conditions:
|
||||
- Counts down from `totalDuration` (90s) to 0
|
||||
- When timer hits 0: stops the spawner, enters "cleanup phase"
|
||||
- During cleanup: waits until `_spawner.HasActiveEnemies` is false, then triggers victory
|
||||
- On player death (game over): stops the timer
|
||||
- Updates the timer UI label every frame in `MM:SS` format
|
||||
|
||||
**Why it works this way**: The two-phase end condition (timer expired → wait for enemies cleared) prevents an abrupt ending while enemies are still on screen. The player must survive AND clear all remaining enemies after time runs out.
|
||||
|
||||
**State machine**:
|
||||
```
|
||||
RUNNING → _timeRemaining <= 0 → EXPIRED (spawning stops)
|
||||
EXPIRED → no active enemies → VICTORY
|
||||
RUNNING → player dies → GAME OVER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. `Health.cs` — Hit Point System
|
||||
|
||||
**What it does**: Tracks HP for any entity (player or enemy). Exposes:
|
||||
- `Configure(faction, maxHP)` — sets faction and max health
|
||||
- `ApplyDamage(amount, hitPoint)` — reduces HP, spawns hit effect, fires `Damaged` event, triggers death at 0
|
||||
- `RestoreFull()` — resets to max HP (used on player respawn)
|
||||
- `Kill()` — instant death (used by bomb screen-clear)
|
||||
- Events: `Died` (Action<Health>), `Damaged` (Action<float, Vector2>)
|
||||
|
||||
**Why it works this way**: Decoupled from entity logic. `PlayerController` and `EnemyController` both `[RequireComponent(typeof(Health))]` and subscribe to events. This means the damage system works identically regardless of who takes damage — the same `ApplyDamage` path is used for player health, enemy health, and bullet-vs-bullet collision.
|
||||
|
||||
**Hit effect integration**: Every `ApplyDamage` call automatically triggers `EffectManager.SpawnHitEffect()`, so visual feedback is always consistent.
|
||||
|
||||
---
|
||||
|
||||
### 6. `BulletPool.cs` — Object Pool for Bullets
|
||||
|
||||
**What it does**: Manages a pool of `Bullet` objects using a `Queue<Bullet>` (for O(1) get/return) and a `HashSet<Bullet>` for tracking live bullets:
|
||||
- `Create(parent, name, color, faction, warmCount)` — static factory that creates and pre-warms the pool
|
||||
- `Spawn(position, direction, speed, damage)` — activates a pooled bullet or creates a new one if pool is empty
|
||||
- `Return(bullet)` — deactivates and re-queues the bullet
|
||||
- `ClearLiveBullets()` — returns all active bullets to pool (used by bomb and screen clear)
|
||||
|
||||
**Why it works this way**: In a bullet-hell game, thousands of bullets are spawned and destroyed per second. Without pooling, `Instantiate`/`Destroy` would generate massive GC pressure. The `Queue` provides FIFO reuse, and the `HashSet` enables O(1) membership checking for `ClearLiveBullets()`.
|
||||
|
||||
**Warm capacity**: Pre-allocates 400 player bullets and 900 enemy bullets at startup to avoid first-frame allocation spikes. These numbers were tuned empirically to the game's peak bullet counts.
|
||||
|
||||
**Pool hierarchy**: Inactive bullets are parented to a hidden `_poolRoot` transform. When spawned, bullets are unparented (`SetParent(null)`) to avoid transform hierarchy overhead.
|
||||
|
||||
---
|
||||
|
||||
### 7. `Bullet.cs` — Individual Bullet Behavior
|
||||
|
||||
**What it does**: Each bullet moves in a straight line at constant speed, checks bounds, and handles collisions:
|
||||
- Movement: `transform.position += direction * speed * deltaTime` (no physics engine)
|
||||
- Bounds check: despawns if beyond ±14 units from origin
|
||||
- Collision via `OnTriggerEnter2D`:
|
||||
- Bullet vs bullet (opposite faction): both despawn
|
||||
- Bullet vs Health (opposite faction): applies damage, despawns
|
||||
- Respects player invulnerability
|
||||
- **Sprite caching**: `static Dictionary<Color, Sprite> SpriteCache` — generates colored 16×16 pixel textures once and reuses them
|
||||
|
||||
**Why it works this way**: Using `transform.position` directly instead of `Rigidbody2D.velocity` gives deterministic control over movement. The `Rigidbody2D` is set to `Kinematic` with `Interpolation` — it exists only so Unity's physics system can generate trigger events, not for actual physics simulation.
|
||||
|
||||
**Sprite generation**: Bullets generate their own 16×16 solid-color sprites via a static cache. This avoids needing any sprite assets and ensures bullets of the same color share the same `Sprite` object.
|
||||
|
||||
---
|
||||
|
||||
### 8. `EnemyBlueprint.cs` — Enemy Archetype Configuration
|
||||
|
||||
**What it does**: A sealed immutable data class that defines all parameters for an enemy type:
|
||||
- Name, color, scale, sprite
|
||||
- Movement style (`EnemyMovementStyle` enum: ZigZag, Straight, Sine, Orbit)
|
||||
- Fire style (`EnemyFireStyle` enum: None, Radial, Stream, Burst)
|
||||
- Numerical stats: move speed, horizontal amplitude/frequency, fire interval, bullets per volley, bullet speed/damage, contact damage, health, score value, orbit radius, centering bias
|
||||
|
||||
**Why it works this way**: Using a plain C# class instead of `ScriptableObject` keeps everything in code (no Unity asset files). The 19-parameter constructor is intentionally explicit — it makes the 4 enemy definitions in `EnemySpawner.BuildBlueprints()` fully self-documenting.
|
||||
|
||||
**Four archetypes**:
|
||||
| Name | Movement | Fire | Behavior |
|
||||
|------|---------|------|----------|
|
||||
| **Blazer** | ZigZag | Radial | Orange, zig-zags down, fires radial bursts (12+ bullets) |
|
||||
| **Striker** | Straight | Stream | Purple, drops straight down, fires aimed stream + homing shots |
|
||||
| **Cascade** | Sine | Burst | Green, sine-waves, fires tight burst salvos |
|
||||
| **Interceptor** | Orbit | None | Yellow, fast orbital movement, no shooting, relies on collision |
|
||||
|
||||
---
|
||||
|
||||
### 9. `EnemyController.cs` — Enemy AI and Behavior
|
||||
|
||||
**What it does**: Controls individual enemy behavior every frame:
|
||||
- **Movement**: Four movement patterns based on `EnemyBlueprint.Movement`:
|
||||
- `ZigZag`: Moves down + sinusoidal horizontal offset
|
||||
- `Straight`: Pure downward movement
|
||||
- `Sine`: Downward + absolute sine positioning (position-based, not velocity-based)
|
||||
- `Orbit`: Circular/elliptical path with slower vertical descent
|
||||
- **Firing**: Three firing patterns based on `EnemyBlueprint.Fire`:
|
||||
- `Radial`: N bullets evenly around 360° with a rotating offset
|
||||
- `Stream`: Downward spread + one homing shot aimed at the player
|
||||
- `Burst`: Triple salvos of 3 bullets each at increasing spread
|
||||
- **Difficulty scaling**: All stats scale with `_difficulty` parameter: health increases (+12 per difficulty unit), movement speeds up, fire rate decreases, radial bullet count increases
|
||||
- **Death**: Awards score, spawns explosion effect, notifies spawner, destroys self
|
||||
- **Contact damage**: If the enemy's collider touches the player, deals `ContactDamage` and the enemy self-destructs
|
||||
- **Despawn**: If the enemy falls below the camera view by `despawnPadding`, it's cleaned up
|
||||
|
||||
**Why it works this way**: The pattern-based movement system allows each enemy type to feel distinct while sharing the same update loop. Difficulty scaling is multiplicative — the same `AdvanceMovement()` code handles early weak enemies and late-game tough ones.
|
||||
|
||||
**Centering bias**: Some enemies (Blazer with 1.2 bias) tend to drift toward center X, making them cluster dangerously. This creates organic difficulty without specific AI pathfinding.
|
||||
|
||||
---
|
||||
|
||||
### 10. `EnemySpawner.cs` — Wave Generation System
|
||||
|
||||
**What it does**: Singleton that continuously spawns enemy waves with escalating difficulty:
|
||||
- **Spawn routine**: Coroutine that loops for `totalDuration` (90s), spawning waves at decreasing intervals
|
||||
- **Spawn delay curve**: Linearly interpolates from `spawnDelayStart` (1.6s) to `spawnDelayEnd` (0.45s) based on elapsed time
|
||||
- **Introduction phase**: First 4 enemies are spawned one at a time (one of each type) so the player sees all archetypes
|
||||
- **Wave composition**: After introduction, spawns 2–10 enemies per wave, scaling with elapsed time (`1 + elapsed * 0.05`)
|
||||
- **Difficulty value**: Linearly interpolates from 1.2 to 6.0 based on game progress
|
||||
- **Spatial distribution**: Enemies spawn at the top of the camera view, horizontally distributed with slight randomness and centering bias
|
||||
- **Screen clear**: `ClearScreen()` kills all active enemies and clears all enemy bullets (used by player bomb)
|
||||
- **Procedural sprites**: `GenerateSprite()` creates 64×64 pixel textures for four shapes: Disc, Triangle, Diamond, Arrow
|
||||
- **Blueprint definitions**: `BuildBlueprints()` creates all four enemy archetypes with hardcoded stats
|
||||
|
||||
**Why it works this way**: The coroutine-based spawn loop allows natural timing control without complex state machines. The escalating difficulty curve ensures the game gets progressively harder over 90 seconds — from manageable waves of 2–3 enemies to frantic swarms of 10.
|
||||
|
||||
**Enemy tracking**: A `List<EnemyController> _liveEnemies` tracks all active enemies. This is critical for:
|
||||
- `HasActiveEnemies` (checked by GameDirector for victory condition)
|
||||
- `ClearScreen()` (iterating all enemies to kill them)
|
||||
|
||||
---
|
||||
|
||||
### 11. `PlayerController.cs` — Player Input and Progression
|
||||
|
||||
**What it does**: Comprehensive player logic:
|
||||
|
||||
**Movement** (WASD/arrows):
|
||||
- Base speed 12 units/s, focus mode (LeftShift) at 0.6× speed
|
||||
- Clamped to camera bounds with 0.5 unit padding
|
||||
|
||||
**Shooting** (Z key or left mouse):
|
||||
- Auto-fire at 0.12s intervals (8.3 shots/sec)
|
||||
- Fires a volley of bullets in a fan pattern, size based on level
|
||||
- Base volley: 5 bullets with angular spread increasing by level
|
||||
- Extra directional shots unlock at levels 2, 4, 6, 8 (side, diagonal, back shots)
|
||||
|
||||
**Bomb** (X key or space or right mouse):
|
||||
- One-use screen clear: destroys all enemies, clears all bullets
|
||||
- Visual: player flashes white, dashes upward then returns
|
||||
- Grants temporary invulnerability during animation
|
||||
|
||||
**Progression system**:
|
||||
- Level calculated from score using exponential thresholds: `baseLevelThreshold` (1500), doubling each level
|
||||
- Max level: 12
|
||||
- Each level-up: visual flash, explosion effect, camera shake
|
||||
- Stats affected: volley width (more bullets), extra directional shots
|
||||
|
||||
**Health/Lives**:
|
||||
- Starts with `maxLives` (default 1)
|
||||
- On death: explosion VFX, renderer disabled, wait `respawnDelay` (0.7s), clear screen, reposition to bottom, flash invulnerability for 2s
|
||||
- If lives < 0: game over sequence
|
||||
|
||||
**Test modes** (command-line or runtime toggles):
|
||||
- `--invincible` / I key: permanent invulnerability (yellow color indicator)
|
||||
- `--stationary` / P key: disables all movement and shooting (for passive performance testing)
|
||||
|
||||
**Why it works this way**: The progression mechanic (score → level → more bullets) creates a natural performance escalation: as the player gets stronger, more player bullets are on screen, increasing GPU/CPU load. This is deliberate for benchmarking — the workload scales organically over the 90-second run.
|
||||
|
||||
**Test modes rationale**: `--invincible` prevents game-over during unattended benchmark runs. `--stationary` isolates enemy/spawner performance by removing player bullet generation.
|
||||
|
||||
---
|
||||
|
||||
### 12. `ScoreManager.cs` — Score and HUD Controller
|
||||
|
||||
**What it does**: Singleton that manages:
|
||||
- Score accumulation and display (`Score: N`)
|
||||
- Life icon visibility (enabling/disabling `Image` components)
|
||||
- Status messages ("GAME OVER" in red, "YOU WIN" in green)
|
||||
- Damage flash overlay (full-screen red pulse with sine-curve fade)
|
||||
- `ScoreChanged` event (consumed by `PlayerController` for level-up detection)
|
||||
|
||||
**Why it works this way**: The queued initialization pattern (`_queuedLives`, `_queuedStatus`) handles race conditions where `SetLives()` or status messages are called before the HUD is fully constructed. This is essential because `PlayerController.Initialize()` and `ScoreManager.Initialize()` may run in unpredictable order.
|
||||
|
||||
**Damage flash**: The `DamageFlashRoutine` uses a sine curve (`sin(normalized * PI)`) to create a smooth peak-and-fade effect. Strength parameter controls peak alpha (0.35–0.75).
|
||||
|
||||
---
|
||||
|
||||
### 13. `EffectManager.cs` — Visual Effects Hub
|
||||
|
||||
**What it does**: Singleton managing all visual feedback using pooled `EffectPulse` sprites:
|
||||
- `SpawnHitEffect(position, faction, intensity)` — 1–3 expanding circles (color by faction)
|
||||
- `SpawnExplosion(position, faction, size)` — 4 concentric expanding circles
|
||||
- `SpawnScreenClear(position, radius)` — 6 large expanding circles + camera shake
|
||||
- `ShakeCamera(duration, strength)` — delegates to `CameraShaker`
|
||||
- Internal pool of `EffectPulse` objects (warmed to 64, recycled via callbacks)
|
||||
|
||||
**Why it works this way**: Like `BulletPool`, effect objects are pooled to avoid GC spikes. The multi-pulse explosion technique (multiple expanding circles at different speeds) creates convincing explosion effects using only simple scaling sprites — no particle systems, no shaders.
|
||||
|
||||
---
|
||||
|
||||
### 14. `EffectPulse.cs` — Animated Sprite Effect
|
||||
|
||||
**What it does**: A single expanding, fading sprite:
|
||||
- `Play(position, color, startSize, endSize, duration, callback)` — configure and start
|
||||
- Each frame: interpolates scale from start to end, fades alpha from 1 to 0
|
||||
- On completion: deactivates self, invokes recycle callback
|
||||
|
||||
**Sprite generation**: `BuildPulseSprite()` creates a 32×32 radial gradient texture (alpha = (1 - distance)²) giving a soft, circular glow effect.
|
||||
|
||||
**Why it works this way**: The radial gradient sprite combined with scale animation creates the appearance of an expanding shockwave or explosion ring. The quadratic alpha falloff (`alpha * alpha`) ensures edges are smooth.
|
||||
|
||||
---
|
||||
|
||||
### 15. `CameraShaker.cs` — Screen Shake
|
||||
|
||||
**What it does**: Attached to the main camera, provides juicy screen shake:
|
||||
- `Shake(duration, strength)` — sets shake parameters (max of current and new)
|
||||
- Each frame during shake: random offset within `_shakeStrength` circle, strength decaying over time
|
||||
- After shake: smoothly lerps back to original position
|
||||
|
||||
**Why it works this way**: The "max of current and new" approach allows overlapping shakes without resetting, creating more organic screen feedback during intense combat.
|
||||
|
||||
---
|
||||
|
||||
### 16. `BackgroundScroller.cs` — Parallax Starfield
|
||||
|
||||
**What it does**: Creates two large starfield tiles and scrolls them downward at different speeds:
|
||||
- Tile A: scrolls at `scrollSpeed` (full speed)
|
||||
- Tile B: scrolls at `scrollSpeed * parallaxFactor` (0.6× speed, creating depth illusion)
|
||||
- When a tile scrolls off the bottom, it teleports back above the top (infinite loop)
|
||||
- `GenerateStarSprite()`: Creates a procedural starfield texture — dark background with random white pixels (star density: ~1 per 45 pixels)
|
||||
|
||||
**Why it works this way**: The parallax effect gives depth to a 2D game with zero external assets. Two tiles ensure seamless continuous scrolling. The procedural generation means no texture files are needed in the build.
|
||||
|
||||
---
|
||||
|
||||
## Performance-Critical Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Object pooling for bullets and effects | Avoids GC allocation spikes from `Instantiate`/`Destroy` |
|
||||
| `transform.position` movement (no physics forces) | Deterministic movement, no physics solver overhead |
|
||||
| Kinematic Rigidbody2D on bullets | Enables trigger detection without full physics simulation |
|
||||
| Procedural sprite/texture generation | Zero asset loading, reproducible across machines |
|
||||
| `targetFrameRate = 240` | Uncapped rendering for benchmarking |
|
||||
| Scene-free bootstrap | Eliminates scene deserialization variability |
|
||||
| HashSet for live bullet tracking | O(1) membership test during screen clears |
|
||||
| Static sprite caching (Bullet.SpriteCache) | Single shared texture per color |
|
||||
346
games/UNREAL_CODE_DOCUMENTATION.md
Normal file
346
games/UNREAL_CODE_DOCUMENTATION.md
Normal file
@ -0,0 +1,346 @@
|
||||
# Unreal Engine Bullet Hell — Complete Code Documentation
|
||||
|
||||
**Project**: `games/unreal/BulletHellGame/BulletHellCPP/`
|
||||
**Language**: C++ (Unreal Engine 5.5)
|
||||
**Module**: `BulletHellCPP`
|
||||
**Purpose**: Performance benchmark bullet-hell shooter — Unreal counterpart to the Unity version for engine comparison.
|
||||
**Duration**: 90-second survival round with escalating difficulty.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The Unreal version follows a **standard UE5 actor-based architecture**: the game uses Actors placed in a level, with C++ classes providing all game logic. Unlike the Unity version (which bootstraps from an empty scene), this project relies on a pre-configured level with key actors placed:
|
||||
|
||||
1. **`ASTGGameMode`** — Sets default pawn class to `ASTGPawn`
|
||||
2. **`ASTGGameDirector`** — Level-placed actor managing timer and game flow
|
||||
3. **`ASTGEnemySpawner`** — Level-placed actor continuously spawning enemies
|
||||
4. **`ASTGFixedCamera`** — Level-placed camera auto-fitting the play area
|
||||
5. **`ASTGHUDManager`** — Level-placed actor managing UMG widget
|
||||
|
||||
### Dependency Graph
|
||||
|
||||
```
|
||||
ASTGGameMode
|
||||
└── Spawns ASTGPawn (Default Pawn)
|
||||
|
||||
Level contains:
|
||||
├── ASTGGameDirector (timer, victory/game-over)
|
||||
├── ASTGEnemySpawner (wave spawning)
|
||||
├── ASTGFixedCamera (top-down camera, auto-fit)
|
||||
└── ASTGHUDManager (UMG widget wrapper)
|
||||
|
||||
Runtime spawned:
|
||||
├── ASTGEnemy (4 types: Fodder, Runner, Turret, Tank)
|
||||
└── ASTGProjectile (player & enemy bullets)
|
||||
```
|
||||
|
||||
### Key Design Differences from Unity Version
|
||||
|
||||
| Aspect | Unity | Unreal |
|
||||
|--------|-------|--------|
|
||||
| **Initialization** | Scene-free bootstrap (code creates everything) | Level-placed actors (scene-dependent) |
|
||||
| **Bullet management** | Object pooling (`BulletPool`) | Dynamic spawn/destroy (no pooling) |
|
||||
| **Configuration** | Runtime `EnemyBlueprint` C# class | `STGGameSettings.h` constexpr namespace |
|
||||
| **HUD** | Built entirely in C# code | UMG widget (Blueprint-defined layout) |
|
||||
| **Movement** | Manual `transform.position` manipulation | UE5 `ProjectileMovementComponent` for bullets |
|
||||
| **Input** | Legacy `Input.GetKey()` polling | Enhanced Input System (actions + contexts) |
|
||||
| **VFX** | Pooled sprite scaling (`EffectPulse`) | Niagara particle system |
|
||||
| **Enemy types** | Blazer, Striker, Cascade, Interceptor | Fodder, Runner, Turret, Tank |
|
||||
| **Actor discovery** | Singleton pattern (`.Instance`) | `GetAllActorsOfClass()` runtime queries |
|
||||
| **Camera** | Orthographic 2D | Perspective 3D (top-down at calculated height) |
|
||||
|
||||
---
|
||||
|
||||
## File-by-File Documentation
|
||||
|
||||
### 1. `STGGameSettings.h` — Centralized Game Constants
|
||||
|
||||
**What it does**: A header-only configuration file using nested `constexpr` namespaces to define ALL game constants in a single location:
|
||||
|
||||
- **`STG::PlayArea`** — World-space bounds: 1700×900 units centered at origin (±850×±450)
|
||||
- **`STG::Game`** — Default duration (90s), debug quick mode (10s)
|
||||
- **`STG::Player`** — Move speed (750), bullet speed (2200), max lives (3), upgrade thresholds (100/300/600/1000 score), starting vs max fire stats
|
||||
- **`STG::Enemy::Fodder/Runner/Turret/Tank`** — Per-type stats: health, score, speeds, firing parameters
|
||||
- **`STG::Enemy`** — Shared enemy settings: bullet lifetime (10s), visual scale vs collision radius (grazing mechanic)
|
||||
- **`STG::Spawner`** — Spawn intervals (0.25s→0.08s→0.03s chaos), max 200 simultaneous enemies
|
||||
- **`STG::Camera`** — FOV (60°), default height, ortho width
|
||||
- **`STG::VFX`** — Death particle count (500)
|
||||
|
||||
**Why it works this way**: The `constexpr` approach gives:
|
||||
1. **Zero runtime cost** — all values are compile-time constants
|
||||
2. **Single source of truth** — change a value once, everything updates
|
||||
3. **No serialization** — unlike UPROPERTY defaults which require editor interaction
|
||||
4. **Clear hierarchy** — the namespace structure documents the game design
|
||||
|
||||
**Grazing mechanic**: `BulletVisualScale = 0.25` but `BulletCollisionRadius = 3.0` means enemy bullets appear large (easy to see) but have a tiny hitbox, so players can "graze" through near-misses. This is a classic bullet-hell design.
|
||||
|
||||
---
|
||||
|
||||
### 2. `STGGameMode.h/.cpp` — Game Mode Configuration
|
||||
|
||||
**What it does**: Minimal `AGameModeBase` subclass that sets `DefaultPawnClass = ASTGPawn::StaticClass()` and disables spectator start.
|
||||
|
||||
**Why it works this way**: UE5 requires a GameMode to define the default pawn class. This is the simplest possible setup — the GameMode does nothing else. The pawn spawning is handled by the engine's built-in `RestartPlayer()` flow.
|
||||
|
||||
---
|
||||
|
||||
### 3. `STGGameDirector.h/.cpp` — Game Flow Controller
|
||||
|
||||
**What it does**: Level-placed `AActor` that manages the game timer and end conditions:
|
||||
|
||||
**Tick behavior**:
|
||||
1. Increments `ElapsedTime`
|
||||
2. Displays timer via `GEngine->AddOnScreenDebugMessage`
|
||||
3. When `ElapsedTime >= GameDuration`: enters cleanup phase (spawning stops)
|
||||
4. During cleanup: counts active enemies via `GetAllActorsOfClass(ASTGEnemy)`, triggers victory when count reaches 0
|
||||
5. Updates HUD timer via `ASTGHUDManager`
|
||||
|
||||
**End states**:
|
||||
- **Victory**: All enemies cleared after time expires → shows "VICTORY!", pauses game (or auto-quits)
|
||||
- **Game Over**: Player dies → shows "GAME OVER", pauses game (or auto-quits)
|
||||
|
||||
**Command-line arguments** (for automated benchmarking):
|
||||
- `--autoquit` — Calls `FPlatformMisc::RequestExit(false)` when game ends
|
||||
- `--duration=N` — Overrides game duration (e.g., `--duration=95`)
|
||||
- `--start-time=N` — Fast-forwards elapsed time for phased profiling (e.g., `--start-time=30` starts at 30s difficulty)
|
||||
|
||||
**Why it works this way**: Using `GetAllActorsOfClass()` every tick during cleanup is not ideal for performance, but the cleanup phase is brief and occurs only once. The command-line argument system enables fully automated benchmarking — run the game, capture N seconds, auto-exit — perfect for Nsight profiling integration.
|
||||
|
||||
**Actor discovery pattern**: The Director finds the HUDManager via `GetAllActorsOfClass()` each tick. This is different from Unity's singleton pattern — UE5's approach is more flexible (supports multiple instances) but has higher per-query cost.
|
||||
|
||||
---
|
||||
|
||||
### 4. `STGFixedCamera.h/.cpp` — Camera System
|
||||
|
||||
**What it does**: A fixed top-down camera with automatic play-area fitting:
|
||||
|
||||
**Auto-fit mode** (default):
|
||||
1. Finds `ASTGGameDirector` via `GetAllActorsOfClass()`
|
||||
2. Reads play area bounds (`PlayAreaMin`, `PlayAreaMax`)
|
||||
3. Calculates required camera height using `CalculateCameraHeightForPlayArea()`:
|
||||
- Computes height needed for horizontal extent using horizontal FOV
|
||||
- Computes height needed for vertical extent using derived vertical FOV (assuming 16:9)
|
||||
- Takes the maximum to ensure both dimensions fit
|
||||
4. Applies optional margin (percentage of FOV narrowing)
|
||||
|
||||
**Manual mode**: Uses fixed `CameraHeight` and `FieldOfView`.
|
||||
|
||||
**Orthographic mode**: Optional, using `OrthoWidth` for 2D-style projection.
|
||||
|
||||
**Why it works this way**: The auto-fit calculation ensures the camera shows exactly the play area regardless of resolution or aspect ratio. The trigonometric approach (`height = halfWidth / tan(halfFOV)`) is the standard way to calculate camera distance for a given field of view.
|
||||
|
||||
**Key difference from Unity**: Unity uses a simple orthographic camera (set `orthographicSize`). Unreal's perspective camera requires explicit height calculation — this is more complex but gives the 3D depth effect that Unreal's renderer expects.
|
||||
|
||||
---
|
||||
|
||||
### 5. `STGPawn.h/.cpp` — Player Character
|
||||
|
||||
**What it does**: The player pawn with movement, shooting, screen-clear special, and upgrade progression:
|
||||
|
||||
**Construction** (ASTGPawn constructor):
|
||||
- Creates a `UStaticMeshComponent` using `/Engine/BasicShapes/Cylinder` flattened to scale (0.08, 0.08, 0.001) — a flat 2D circle
|
||||
- Creates a `UBoxComponent` hitbox (4×4×4 units) matching the visual size
|
||||
- Creates a hidden `HitboxIndicator` (unused)
|
||||
|
||||
**Movement** (Enhanced Input):
|
||||
- Uses `UInputMappingContext` and `UInputAction` bindings (UE5 Enhanced Input System)
|
||||
- `Move()` reads 2D axis input, `Tick()` applies to position with bounds clamping
|
||||
- Movement is in X (forward/back) and Y (left/right) — UE5's coordinate system for top-down
|
||||
|
||||
**Shooting**:
|
||||
- Hold fire → auto-fire at `FireInterval`
|
||||
- `VolleySize` bullets with `VolleySpread` angular distribution
|
||||
- Bullets spawned as `ASTGProjectile` actors with `bIsPlayerBullet = true`
|
||||
- Bullets colored soft green (0.2, 0.8, 0.3) with low emissive
|
||||
|
||||
**Upgrade system** (score-based):
|
||||
| Level | Score | Volley | Spread | Fire Rate |
|
||||
|-------|-------|--------|--------|-----------|
|
||||
| 0 | 0 | 1 bullet | 0° | 0.50s |
|
||||
| 1 | 100 | 2 bullets | 8° | 0.35s |
|
||||
| 2 | 300 | 3 bullets | 12° | 0.20s |
|
||||
| 3 | 600 | 4 bullets | 16° | 0.12s |
|
||||
| 4 | 1000 | 5 bullets | 20° | 0.08s |
|
||||
|
||||
**Special ability** (one-use screen clear):
|
||||
- Destroys all `ASTGEnemy` actors via `GetAllActorsOfClass()`
|
||||
- Destroys all non-player `ASTGProjectile` actors
|
||||
|
||||
**Damage/death**:
|
||||
- `TakeHit(damage)` — reduces lives, spawns Niagara hit VFX, triggers game over at 0
|
||||
- Debug invincibility ignores all damage
|
||||
- On death: hides actor, notifies `ASTGGameDirector::OnPlayerDied()`
|
||||
|
||||
**Command-line flags**:
|
||||
- `--invincible` — Ignores all damage
|
||||
- `--stationary` — Disables movement (for passive benchmarking)
|
||||
|
||||
**Why it works this way**: The Enhanced Input System is UE5's recommended input approach (Legacy Input is deprecated). The upgrade system creates a natural difficulty escalation — more bullets means more `ASTGProjectile` actors spawned, increasing rendering and physics overlap costs.
|
||||
|
||||
**Key architectural difference from Unity**: No object pooling for bullets. Each `FireShot()` calls `World->SpawnActor<ASTGProjectile>()`, and bullets are destroyed on hit or lifetime expiry. This is the standard UE5 approach but means higher actor creation/destruction overhead per frame.
|
||||
|
||||
---
|
||||
|
||||
### 6. `STGEnemy.h/.cpp` — Enemy Actors
|
||||
|
||||
**What it does**: Base enemy actor with 4 types defined by `EEnemyType` enum:
|
||||
|
||||
**Type initialization** (`InitializeFromType()`):
|
||||
Each type loads the cube mesh at different scales/rotations with dynamic material colors:
|
||||
|
||||
| Type | HP | Score | Speed | Fires | Shape | Color |
|
||||
|------|-----|-------|-------|-------|-------|-------|
|
||||
| **Fodder** | 1 | 10 | 80 | No | Small square (0.25) | Green |
|
||||
| **Runner** | 3 | 25 | 90 | No | Diamond (0.35, rotated 45°) | Cyan |
|
||||
| **Turret** | 15 | 75 | 60 | Yes (4 bullets, 360° burst, 1.2s) | Medium square (0.5) | Orange |
|
||||
| **Tank** | 50 | 200 | 40 | Yes (20 bullets, 360° rapid, 0.3s) | Wide rectangle (0.4×0.8) | Red |
|
||||
|
||||
**Movement** (Tick):
|
||||
- Moves in -X direction (downward in top-down view) at `VerticalSpeed`
|
||||
- Horizontal sinusoidal wave: `Y = StartY + Amplitude * sin(Frequency * (time + seed))`
|
||||
- Y-clamped to play area bounds
|
||||
- Despawns when X < `DespawnY`
|
||||
|
||||
**Firing** (timer-based):
|
||||
- Uses `FTimerHandle` to call `Fire()` at `FireInterval`
|
||||
- Spawns `BulletsPerBurst` projectiles in a 360° radial burst pattern
|
||||
- Each bullet gets orange-red color, large visual scale with tiny collision (grazing)
|
||||
|
||||
**Damage/death**:
|
||||
- `HandleDamage(amount)`: reduces HP, spawns Niagara hit effect, awards score on death
|
||||
- `OnOverlapBegin()`: if overlapping player, damages player and self-destructs
|
||||
|
||||
**Why it works this way**: The `InitializeFromType()` pattern is similar to Unity's `EnemyBlueprint` but uses an enum + switch instead of a data class. All enemies use the same `ASTGEnemy` class with different configs — this avoids needing multiple Blueprint subclasses.
|
||||
|
||||
**Material system**: Each enemy creates a `UMaterialInstanceDynamic` from the basic shape material to set color. This allows runtime color changes without needing separate material assets — analogous to Unity's `SpriteRenderer.color`.
|
||||
|
||||
---
|
||||
|
||||
### 7. `STGEnemySpawner.h/.cpp` — Wave Spawning System
|
||||
|
||||
**What it does**: Level-placed actor that continuously spawns enemies with escalating difficulty:
|
||||
|
||||
**Initial wave**: 5 Fodder enemies spawned in `BeginPlay()` at upper half of play area.
|
||||
|
||||
**Tick spawning**:
|
||||
1. Updates `ElapsedTime` and `CurrentSpawnInterval`
|
||||
2. When `SpawnTimer` expires: checks enemy count (max 200), spawns one enemy
|
||||
3. Stops spawning when `ElapsedTime >= GameDuration`
|
||||
|
||||
**Difficulty progression** (`GetRandomEnemyType()`):
|
||||
| Game Progress | Enemy Distribution |
|
||||
|--------------|-------------------|
|
||||
| 0–25% | 100% Fodder (spam) |
|
||||
| 25–50% | 85% Fodder, 15% Runner |
|
||||
| 50–75% | 70% Fodder, 20% Runner, 10% Turret |
|
||||
| 75–100% | 55% Fodder, 20% Runner, 15% Turret, 10% Tank |
|
||||
|
||||
**Spawn interval** (`CalculateSpawnInterval()`):
|
||||
- Linear interpolation from `BaseSpawnInterval` (0.25s) to `MinSpawnInterval` (0.08s)
|
||||
- Final 5 seconds: `FinalRushInterval` (0.03s) — absolute chaos mode
|
||||
|
||||
**Spawn location**: Top of play area (`STG::Spawner::SpawnX`), random Y within `SpawnAreaHalfWidth`.
|
||||
|
||||
**Command-line support**: `--start-time=N` fast-forwards the spawner's elapsed time and difficulty to match.
|
||||
|
||||
**Why it works this way**: The progressive difficulty ensures the game starts manageable (only harmless Fodder) and escalates to overwhelming (Tanks + rapid spawning). The "final rush" mechanic at 0.03s intervals creates a dramatic climax before the timer expires.
|
||||
|
||||
**Key difference from Unity**: The Unity spawner uses a coroutine-based loop and spawns multiple enemies per wave (2–10). The Unreal spawner uses tick-based timing and spawns one enemy per interval. The net effect is similar but the Unreal version creates a more constant stream while Unity has distinct wave pulses.
|
||||
|
||||
---
|
||||
|
||||
### 8. `STGProjectile.h/.cpp` — Bullet Actor
|
||||
|
||||
**What it does**: Individual bullet actor using UE5's built-in `UProjectileMovementComponent`:
|
||||
|
||||
**Construction**:
|
||||
- `USphereComponent` for collision (radius configurable)
|
||||
- `UStaticMeshComponent` using `/Engine/BasicShapes/Sphere` for visual
|
||||
- `UProjectileMovementComponent` for movement: no gravity, no bouncing, rotation follows velocity
|
||||
|
||||
**BeginPlay configuration**:
|
||||
- Sets `LifeSpan` for automatic destruction
|
||||
- Applies `CollisionRadius` to sphere component
|
||||
- Applies `BulletScale` to mesh component
|
||||
- Creates `UMaterialInstanceDynamic` for color/emissive control
|
||||
|
||||
**Collision** (`OnOverlapBegin`):
|
||||
- Player bullets → hit `ASTGEnemy` → call `HandleDamage()`, destroy self
|
||||
- Enemy bullets → hit `ASTGPawn` → call `TakeHit()`, destroy self
|
||||
|
||||
**Grazing mechanic**: `BulletScale` controls visual size while `CollisionRadius` controls the actual hitbox. Enemy bullets have large visuals (0.25) but tiny collision (3.0 radius), allowing players to weave between bullets that appear to overlap.
|
||||
|
||||
**Why it works this way**: `UProjectileMovementComponent` provides efficient built-in projectile physics including speed, gravity, and bouncing. Since gravity is disabled, bullets travel in straight lines — functionally identical to Unity's manual `transform.position` movement but leveraging UE5's optimized component.
|
||||
|
||||
**No pooling**: Unlike Unity's `BulletPool`, each bullet is a new `AActor` spawned via `SpawnActor()` and destroyed via `Destroy()` or `LifeSpan`. This is the standard UE5 pattern. The performance impact of actor creation/destruction vs pooling is a key comparison point for the thesis.
|
||||
|
||||
---
|
||||
|
||||
### 9. `STGHUDManager.h/.cpp` — HUD Widget Manager
|
||||
|
||||
**What it does**: Actor-based wrapper for a UMG (Unreal Motion Graphics) widget:
|
||||
- `BeginPlay()`: Creates and displays the widget from `HUDWidgetClass` (a Blueprint-defined UMG widget)
|
||||
- `UpdateScore(score)`: Finds `txt_Score` text block, sets text
|
||||
- `UpdateLives(lives)`: Finds `txt_Lives` text block, sets text
|
||||
- `UpdateTimer(timeRemaining)`: Finds `txt_Timer` text block, sets formatted seconds
|
||||
- `ShowVictory()`: Finds `txt_Result`, sets "VICTORY!" in green
|
||||
- `ShowGameOver()`: Finds `txt_Result`, sets "GAME OVER" in red
|
||||
|
||||
**Why it works this way**: UE5's HUD is typically built in UMG (visual editor), not in C++. The C++ code only drives data updates, while the widget layout is defined in a Blueprint asset. This is the standard UE5 pattern — C++ logic + UMG layout.
|
||||
|
||||
**Widget name binding**: Finding widgets by name (`GetWidgetFromName("txt_Score")`) is a common UE5 pattern for C++ ↔ UMG communication. It's less type-safe than Blueprint bindings but allows the C++ code to work with any widget layout that follows the naming convention.
|
||||
|
||||
**Difference from Unity**: Unity's HUD is built entirely in C# code (`GameInitializer.CreateHud()`). Unreal delegates layout to UMG, which is more visual but requires a separate Blueprint asset.
|
||||
|
||||
---
|
||||
|
||||
### 10. `BulletHellCPP.Build.cs` — Build Configuration
|
||||
|
||||
**What it does**: Module build file declaring dependencies:
|
||||
- `Core`, `CoreUObject`, `Engine` — standard UE5 modules
|
||||
- `InputCore`, `EnhancedInput` — for the Enhanced Input System
|
||||
- `UMG` — for HUD widget system
|
||||
- `Niagara` — for particle VFX
|
||||
|
||||
**Why these dependencies**: Each module provides specific functionality:
|
||||
- `EnhancedInput` replaces legacy input for UE5 projects
|
||||
- `UMG` is needed for `CreateWidget<>()` and `UUserWidget`
|
||||
- `Niagara` enables GPU particle effects for enemy death explosions
|
||||
|
||||
---
|
||||
|
||||
### 11. `STGFixedCamera.h/.cpp` — Top-Down Camera (continued)
|
||||
|
||||
The camera rotation is set to `FRotator(-90, 0, 0)` — looking straight down along -Z. The `CalculateCameraHeightForPlayArea()` method uses the horizontal FOV and an assumed 16:9 aspect ratio to compute the exact Z-height needed so the play area fills the viewport.
|
||||
|
||||
---
|
||||
|
||||
## Performance-Critical Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| No bullet pooling (`SpawnActor`/`Destroy` per bullet) | Standard UE5 approach; creates a natural comparison point with Unity's pooled design |
|
||||
| `UProjectileMovementComponent` for bullet movement | Leverages UE5's optimized built-in component vs manual tick movement |
|
||||
| `constexpr` configuration namespace | Zero-cost abstractions; compile-time constants |
|
||||
| Niagara for death VFX (500 particles) | GPU particle system; specifically tests GPU workload |
|
||||
| `GetAllActorsOfClass()` for actor discovery | Standard UE5 pattern; O(N) scan but simple |
|
||||
| Dynamic materials per enemy | One material instance per actor allows per-enemy colors |
|
||||
| Enhanced Input System | UE5 recommended; maps cleanly to gamepad + keyboard |
|
||||
| Large visual / small collision for enemy bullets | Grazing mechanic makes gameplay fair despite high bullet density |
|
||||
| Command-line flags for benchmarking | Enables fully automated profiling runs |
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System Note
|
||||
|
||||
Unreal uses a **left-handed Z-up** coordinate system:
|
||||
- **X** = forward (toward top of screen in top-down view)
|
||||
- **Y** = right (horizontal on screen)
|
||||
- **Z** = up (camera height, perpendicular to play plane)
|
||||
|
||||
This means:
|
||||
- Enemies move in **-X** direction (downward on screen)
|
||||
- Player shoots in **+X** direction (upward on screen)
|
||||
- Camera is at high **Z** looking down with rotation `(-90, 0, 0)`
|
||||
- Play area bounds are on X/Y plane, Z ≈ 0
|
||||
Binary file not shown.
@ -19,7 +19,7 @@
|
||||
Wydział Elektroniki i Technik Informacyjnych \\
|
||||
Politechnika Warszawska
|
||||
}
|
||||
\date{\scriptsize Luty 2026}
|
||||
\date{\scriptsize 10 marca 2026}
|
||||
|
||||
\setbeamertemplate{footline}[frame number]{}
|
||||
\beamertemplatenavigationsymbolsempty
|
||||
@ -57,25 +57,25 @@
|
||||
\item \textbf{Unreal Engine} -- ponad 7\,500 gier na Steam
|
||||
\end{itemize}
|
||||
\item Brak systematycznych badań porównawczych
|
||||
\item Odmienne filozofie projektowe:
|
||||
\begin{itemize}
|
||||
\item Unity: C\#, garbage collector
|
||||
\item Unreal: C++, dostęp do kodu źródłowego
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Cel pracy}
|
||||
Porównanie wydajności i~możliwości Unity oraz Unreal Engine:
|
||||
\begin{enumerate}
|
||||
\item Testy wydajności z~NVIDIA Nsight Systems
|
||||
\item Implementacja identycznej gry w obu silnikach
|
||||
\item Analiza porównawcza funkcjonalności
|
||||
\item Wywiady z~8 deweloperami gier
|
||||
\end{enumerate}
|
||||
\vspace{0.5cm}
|
||||
\textbf{Hipoteza:} Unity osiągnie lepszą wydajność w~grze 2D \emph{bullet hell} dzięki natywnemu wsparciu dla grafiki 2D.
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Hipoteza}
|
||||
\vspace{1cm}
|
||||
\centering
|
||||
\large
|
||||
Unity osiągnie lepszą wydajność w~grze 2D \emph{bullet hell} dzięki natywnemu wsparciu dla grafiki 2D.
|
||||
\end{frame}
|
||||
|
||||
%==============================================================================
|
||||
@ -83,37 +83,27 @@
|
||||
%==============================================================================
|
||||
\section{Metodologia}
|
||||
|
||||
% SKRYPT: Wybór gatunku -- bullet hell
|
||||
% Uzasadnienie:
|
||||
% - Setki/tysiące pocisków na ekranie
|
||||
% - Test zarządzania pamięcią (GC vs ręczne)
|
||||
% - Intensywne wykrywanie kolizji
|
||||
% - Skalowalność obciążenia
|
||||
% - Prosta koncepcja -> fokus na wydajność
|
||||
{
|
||||
\setbeamercolor{footline}{fg=white}
|
||||
\usebackgroundtemplate{
|
||||
\includegraphics[width=\paperwidth, height=\paperheight]{touhou.jpg}}
|
||||
\begin{frame}
|
||||
\frametitle{Wybór gatunku -- bullet hell}
|
||||
\begin{columns}
|
||||
\begin{column}{0.55\textwidth}
|
||||
\textbf{Uzasadnienie:}
|
||||
\begin{itemize}
|
||||
\item Setki/tysiące pocisków na ekranie
|
||||
\item Test zarządzania pamięcią \\ (GC vs ręczne)
|
||||
\item Intensywne wykrywanie kolizji
|
||||
\item Skalowalność obciążenia
|
||||
\item Prosta koncepcja $\rightarrow$ fokus na wydajność
|
||||
\end{itemize}
|
||||
\end{column}
|
||||
\begin{column}{0.45\textwidth}
|
||||
\includegraphics[width=\textwidth]{touhou.jpg}
|
||||
{\tiny Touhou Project -- przykład bullet hell}
|
||||
\end{column}
|
||||
\end{columns}
|
||||
\end{frame}
|
||||
}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Parametry gry testowej}
|
||||
\begin{itemize}
|
||||
\item Czas rozgrywki: \textbf{90 sekund}
|
||||
\item 3 typy przeciwników z różnymi wzorcami strzelania
|
||||
\item Eskalacja trudności w 3 fazach:
|
||||
\begin{enumerate}
|
||||
\item \textbf{0--30s:} Niska trudność (podstawowi przeciwnicy)
|
||||
\item \textbf{30--60s:} Średnia (szybsze jednostki, wieżyczki)
|
||||
\item \textbf{60--90s:} Wysoka (wszystkie typy, max spawnu)
|
||||
\end{enumerate}
|
||||
\item Eskalacja trudności w 3 fazach
|
||||
\item Limit: 200 jednoczesnych przeciwników
|
||||
\item Interwał spawnu: 0,25s $\rightarrow$ 0,08s
|
||||
\end{itemize}
|
||||
@ -123,13 +113,12 @@
|
||||
\frametitle{Środowisko testowe}
|
||||
\begin{table}
|
||||
\centering
|
||||
\small
|
||||
\begin{tabular}{ll}
|
||||
\footnotesize
|
||||
\renewcommand{\arraystretch}{1.1}
|
||||
\begin{tabular}{@{}ll@{}}
|
||||
\toprule
|
||||
\textbf{Komponent} & \textbf{Specyfikacja} \\
|
||||
\midrule
|
||||
CPU & AMD Ryzen 9 7900X3D (24 rdzenie) \\
|
||||
GPU & NVIDIA GeForce RTX 3090 (24 GB) \\
|
||||
CPU & AMD Ryzen 9 7900X3D \\
|
||||
GPU & NVIDIA RTX 3090 (24 GB) \\
|
||||
RAM & 32 GB \\
|
||||
OS & Arch Linux \\
|
||||
Unity & 6.0 LTS \\
|
||||
@ -147,8 +136,6 @@
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Implementacja -- Unity}
|
||||
\begin{columns}
|
||||
\begin{column}{0.5\textwidth}
|
||||
\begin{itemize}
|
||||
\item Język: \textbf{C\#}
|
||||
\item Natywny tryb 2D
|
||||
@ -157,17 +144,18 @@
|
||||
\item Hot reload
|
||||
\item Instalacja: $\sim$30 min
|
||||
\end{itemize}
|
||||
\end{column}
|
||||
\begin{column}{0.5\textwidth}
|
||||
\includegraphics[width=\textwidth]{unity_game.png}
|
||||
\end{column}
|
||||
\end{columns}
|
||||
\end{frame}
|
||||
|
||||
{
|
||||
\setbeamercolor{footline}{fg=white}
|
||||
\usebackgroundtemplate{
|
||||
\includegraphics[width=\paperwidth, height=\paperheight]{unity_game.png}}
|
||||
\begin{frame}
|
||||
\end{frame}
|
||||
}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Implementacja -- Unreal Engine}
|
||||
\begin{columns}
|
||||
\begin{column}{0.5\textwidth}
|
||||
\begin{itemize}
|
||||
\item Język: \textbf{C++} / Blueprinty
|
||||
\item ,,Fałszywe 2D'' w środowisku 3D
|
||||
@ -180,13 +168,16 @@
|
||||
\end{itemize}
|
||||
\item Instalacja: $\sim$2--4h (Linux)
|
||||
\end{itemize}
|
||||
\end{column}
|
||||
\begin{column}{0.5\textwidth}
|
||||
\includegraphics[width=\textwidth]{unreal_game_view.png}
|
||||
\end{column}
|
||||
\end{columns}
|
||||
\end{frame}
|
||||
|
||||
{
|
||||
\setbeamercolor{footline}{fg=white}
|
||||
\usebackgroundtemplate{
|
||||
\includegraphics[width=\paperwidth, height=\paperheight]{unreal_game_view.png}}
|
||||
\begin{frame}
|
||||
\end{frame}
|
||||
}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Porównanie doświadczeń implementacyjnych}
|
||||
\begin{table}
|
||||
@ -206,9 +197,6 @@
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
\vspace{0.3cm}
|
||||
\centering
|
||||
Implementacja w~Unity zajęła \textbf{$\sim$60\%} czasu potrzebnego w~Unreal Engine
|
||||
\end{frame}
|
||||
|
||||
%==============================================================================
|
||||
@ -216,15 +204,14 @@
|
||||
%==============================================================================
|
||||
\section{Narzędzie profilowania}
|
||||
|
||||
% SKRYPT: Dlaczego nie wbudowane profilery?
|
||||
% Wbudowane profilery silników są nieporównywalne:
|
||||
% - Różna definicja metryk
|
||||
% - Różny narzut profilowania
|
||||
% - Różne formaty wyjściowe
|
||||
\begin{frame}
|
||||
\frametitle{Dlaczego NVIDIA Nsight Systems?}
|
||||
\begin{itemize}
|
||||
\item Wbudowane profilery silników są \textbf{nieporównywalne}:
|
||||
\begin{itemize}
|
||||
\item Różna definicja metryk
|
||||
\item Różny narzut profilowania
|
||||
\item Różne formaty wyjściowe
|
||||
\end{itemize}
|
||||
\item NVIDIA Nsight -- niezależne narzędzie:
|
||||
\begin{itemize}
|
||||
\item Zunifikowane metryki sprzętowe
|
||||
@ -252,27 +239,25 @@
|
||||
\frametitle{Unity -- wydajność klatek}
|
||||
\begin{table}
|
||||
\centering
|
||||
\small
|
||||
\begin{tabular}{lr}
|
||||
\footnotesize
|
||||
\renewcommand{\arraystretch}{1.1}
|
||||
\begin{tabular}{@{}lr@{}}
|
||||
\toprule
|
||||
\textbf{Metryka} & \textbf{Wartość} \\
|
||||
\midrule
|
||||
Czas testu & 94,16 s \\
|
||||
Wyrenderowane klatki & 13\,556 \\
|
||||
Średni FPS & 143,96 (V-Sync: 144 Hz) \\
|
||||
Mediana czasu klatki & 6,94 ms \\
|
||||
99. percentyl (1\% low) & 7,58 ms (132 FPS) \\
|
||||
Klatki w przedziale 5--10 ms & \textbf{98,24\%} \\
|
||||
Klatki w 5--10 ms & \textbf{98,24\%} \\
|
||||
IQR czasu klatki & 0,08 ms \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
\vspace{0.3cm}
|
||||
\centering
|
||||
Wysoka stabilność -- IQR zaledwie 0,08 ms
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Unity -- architektura renderowania}
|
||||
\small
|
||||
\begin{itemize}
|
||||
\item \textbf{Prosty potok}: 2 wywołania \texttt{vkQueueSubmit}/klatkę
|
||||
\item \texttt{vkWaitForFences} -- \textbf{95,2\%} czasu Vulkan API
|
||||
@ -310,6 +295,7 @@
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Unreal Engine -- architektura renderowania}
|
||||
\small
|
||||
\begin{itemize}
|
||||
\item \textbf{Złożony potok}: 16 wywołań \texttt{vkQueueSubmit}/klatkę
|
||||
\item Tworzenie potoków: \textbf{47--72\%} czasu Vulkan API
|
||||
@ -331,8 +317,6 @@
|
||||
\toprule
|
||||
\textbf{Metryka} & \textbf{Unity} & \textbf{Unreal} \\
|
||||
\midrule
|
||||
FPS (niskie obciążenie) & 164 (V-Sync) & 332--339 \\
|
||||
FPS (wymagająca scena) & 132 (1\% low) & 162 (faza 3) \\
|
||||
Wykorzystanie GPU & 23\% & 91\% / 50\% \\
|
||||
Wywołania Vulkan API & $\sim$0,5 mln & $\sim$32 mln \\
|
||||
Wywołania sync. OS & 29\,383 & $\sim$9 mln \\
|
||||
@ -344,7 +328,7 @@
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Interpretacja wyników}
|
||||
\frametitle{Interpretacja -- Unity}
|
||||
\begin{block}{Unity}
|
||||
\begin{itemize}
|
||||
\item Prosty, dwuetapowy potok -- wydajny dla gier 2D
|
||||
@ -352,12 +336,16 @@
|
||||
\item Niewykorzystany potencjał GPU (V-Sync: 23\%)
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Interpretacja -- Unreal Engine}
|
||||
\begin{block}{Unreal Engine}
|
||||
\begin{itemize}
|
||||
\item Złożony, wieloetapowy potok -- narzut dla prostych scen
|
||||
\item Ciągła rekompilacja potoków (dynamiczna optymalizacja)
|
||||
\item Pełne wykorzystanie GPU ($\sim$91\%)
|
||||
\item Spadek wydajności przy dużym obciążeniu (>50\%)
|
||||
\item Spadek wydajności przy dużym obciążeniu ($>$50\%)
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\end{frame}
|
||||
@ -368,14 +356,19 @@
|
||||
\section{Wywiady z~deweloperami}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Wywiady -- najważniejsze wnioski}
|
||||
\frametitle{Wywiady -- najważniejsze wnioski (1/2)}
|
||||
8 respondentów (1--10 lat doświadczenia):
|
||||
\vspace{0.2cm}
|
||||
\small
|
||||
\begin{itemize}
|
||||
\item \textbf{Próg wejścia}: Unity niższy, Unreal wyższy
|
||||
\item \textbf{Dokumentacja}: Unity lepsza (przykłady kodu); Unreal -- ,,szkieletowa''
|
||||
\item \textbf{Blueprinty}: Ułatwiają współpracę z~nietechnicznymi, ale problemy z~Git
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Wywiady -- najważniejsze wnioski (2/2)}
|
||||
\begin{itemize}
|
||||
\item \textbf{Architektura}: Unreal wymusza porządek; Unity -- elastyczny
|
||||
\item \textbf{C\# vs C++}: C\# łatwiejszy; C++ w~Unreal ,,niestandardowy''
|
||||
\item \textbf{Materiały}: Unity przewaga ilościowa
|
||||
@ -392,15 +385,11 @@
|
||||
\textit{,,Unity osiągnie lepszą wydajność w~grze bullet hell~\ldots''}
|
||||
\vspace{0.5cm}
|
||||
|
||||
Hipoteza \textbf{częściowo potwierdzona}:
|
||||
\small
|
||||
\begin{itemize}
|
||||
\item[$\checkmark$] Prostsza architektura renderowania
|
||||
\item[$\checkmark$] Stabilniejsze czasy klatek
|
||||
\item[$\checkmark$] Łatwiejszy proces implementacji (60\% czasu)
|
||||
\item[$\checkmark$] Natywne wsparcie 2D
|
||||
\item[$\times$] Zbliżona wydajność w~wymagających scenach (132 vs 162 FPS)
|
||||
\item[$\times$] Unity ograniczone przez V-Sync
|
||||
\item Prostsza architektura renderowania
|
||||
\item Stabilniejsze czasy klatek
|
||||
\item Łatwiejszy proces implementacji (60\% czasu)
|
||||
\item Natywne wsparcie 2D
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
@ -408,16 +397,17 @@
|
||||
\frametitle{Rekomendacje}
|
||||
\begin{table}
|
||||
\centering
|
||||
\small
|
||||
\begin{tabular}{lll}
|
||||
\footnotesize
|
||||
\renewcommand{\arraystretch}{1.1}
|
||||
\begin{tabular}{@{}lll@{}}
|
||||
\toprule
|
||||
\textbf{Typ projektu} & \textbf{Silnik} & \textbf{Uzasadnienie} \\
|
||||
\textbf{Projekt} & \textbf{Silnik} & \textbf{Powód} \\
|
||||
\midrule
|
||||
Gra 2D indie & Unity & Natywne wsparcie 2D \\
|
||||
Gra mobilna & Unity & Optymalizacja, rozmiar \\
|
||||
Prototyp & Unity & Szybki cykl iteracji \\
|
||||
Gra 3D AAA & Unreal & Nanite, Lumen \\
|
||||
Gra VR high-end & Unreal & Zaawansowane oświetlenie \\
|
||||
2D indie & Unity & Natywne 2D \\
|
||||
Mobilna & Unity & Optymalizacja \\
|
||||
Prototyp & Unity & Szybkie iteracje \\
|
||||
3D AAA & Unreal & Nanite, Lumen \\
|
||||
VR high-end & Unreal & Oświetlenie \\
|
||||
Zespół mieszany & Unreal & Blueprinty \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
@ -435,21 +425,21 @@
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Ograniczenia i~dalsze badania}
|
||||
\small
|
||||
\textbf{Ograniczenia:}
|
||||
\frametitle{Ograniczenia}
|
||||
\begin{itemize}
|
||||
\item Jeden gatunek gry (bullet hell)
|
||||
\item Pojedyncza konfiguracja sprzętowa (high-end)
|
||||
\item 8 wywiadów (badanie eksploracyjne)
|
||||
\end{itemize}
|
||||
\vspace{0.2cm}
|
||||
\textbf{Propozycje dalszych badań:}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Dalsze badania}
|
||||
\begin{itemize}
|
||||
\item Testy dla RPG, RTS, puzzle
|
||||
\item Różne platformy (mobile, konsole, low-end PC)
|
||||
\item Automatyczny framework benchmarkowy
|
||||
\item Badanie longitudinalne (2--3 lata)
|
||||
\item Badanie długoterminowe (2--3 lata)
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
91
presentation/my_thesis/skrypt.md
Normal file
91
presentation/my_thesis/skrypt.md
Normal file
@ -0,0 +1,91 @@
|
||||
# Skrypt do prezentacji (~10 minut)
|
||||
|
||||
## Slajd 1 — Strona tytułowa
|
||||
Dzień dobry, nazywam się Krzysztof Rudnicki. Tematem mojej pracy magisterskiej jest porównanie wydajności i możliwości współczesnych silników gier komputerowych. Promotorem pracy jest doktor inżynier Michał Chwesiuk.
|
||||
|
||||
## Slajd 2 — Plan prezentacji
|
||||
Prezentacja składa się z siedmiu części: omówię cel pracy, metodologię badań, implementację gier testowych, narzędzie profilowania, wyniki testów wydajności, wnioski z wywiadów z deweloperami i podsumowanie.
|
||||
|
||||
## Slajd 3 — Motywacja
|
||||
Rynek gier komputerowych jest zdominowany przez dwa silniki. Unity ma prawie 25 tysięcy gier na platformie Steam, a Unreal Engine ponad 7500. Pomimo tej dominacji, brakuje systematycznych badań porównawczych, które w sposób obiektywny zestawiałyby te dwa silniki pod kątem wydajności i możliwości.
|
||||
|
||||
## Slajd 4 — Cel pracy
|
||||
Praca realizuje cztery cele: po pierwsze — testy wydajności z użyciem narzędzia NVIDIA Nsight Systems. Po drugie — implementację identycznej gry w obu silnikach. Po trzecie — analizę porównawczą ich funkcjonalności. I po czwarte — wywiady z ośmioma deweloperami gier, żeby uzyskać perspektywę praktyczną.
|
||||
|
||||
## Slajd 5 — Hipoteza
|
||||
Hipoteza badawcza brzmi: Unity osiągnie lepszą wydajność w grze 2D bullet hell dzięki natywnemu wsparciu dla grafiki dwuwymiarowej.
|
||||
|
||||
## Slajd 6 — Bullet hell (obraz Touhou)
|
||||
Jako gatunek testowy wybrałem bullet hell. To gry, w których na ekranie pojawiają się setki, a nawet tysiące pocisków jednocześnie. Taki gatunek doskonale testuje zarządzanie pamięcią — czyli garbage collector w Unity kontra ręczne zarządzanie w C++ — a także wydajność wykrywania kolizji i ogólną skalowalność silnika przy rosnącym obciążeniu. Dodatkową zaletą jest prosta koncepcja rozgrywki, co pozwala skupić się wyłącznie na aspekcie wydajnościowym.
|
||||
|
||||
## Slajd 7 — Parametry gry testowej
|
||||
Gra testowa trwa 90 sekund. Występują trzy typy przeciwników z różnymi wzorcami strzelania. Trudność eskaluje w trzech fazach — od niskiej, przez średnią, po wysoką, z maksymalną liczbą 200 jednoczesnych przeciwników. Interwał pojawiania się wrogów zmniejsza się z 0,25 do 0,08 sekundy.
|
||||
|
||||
## Slajd 8 — Środowisko testowe
|
||||
Testy przeprowadzono na komputerze z procesorem AMD Ryzen 9 7900X3D, kartą graficzną NVIDIA RTX 3090 i 32 gigabajtami pamięci RAM, pod kontrolą systemu Arch Linux. Użyto Unity w wersji 6.0 LTS oraz Unreal Engine 5.5.3. Profilerem był NVIDIA Nsight Systems.
|
||||
|
||||
## Slajd 9 — Implementacja Unity (tekst)
|
||||
Implementacja w Unity wykorzystuje język C# i natywny tryb 2D silnika. Do fizyki użyto komponentów Rigidbody2D i Collider2D. Object pooling zrealizowano prostą metodą SetActive — włączamy i wyłączamy obiekty zamiast je niszczyć i tworzyć od nowa. Unity oferuje też hot reload, co przyspiesza iterację. Instalacja silnika zajęła około 30 minut.
|
||||
|
||||
## Slajd 10 — Implementacja Unity (zrzut ekranu)
|
||||
Tutaj widzimy zrzut z gry w Unity — widoczni są przeciwnicy, pociski i gracz na ekranie.
|
||||
|
||||
## Slajd 11 — Implementacja Unreal (tekst)
|
||||
W Unreal Engine implementacja wymagała C++ i Blueprintów. Ponieważ Unreal nie ma natywnego trybu 2D, zastosowałem tak zwane fałszywe 2D — gra działa w środowisku trójwymiarowym z kamerą ustawioną prostopadle. Object pooling był bardziej złożony i wymagał trzech oddzielnych wywołań: ukrycie aktora, wyłączenie kolizji i wyłączenie logiki tick. Instalacja na Linuksie zajęła od dwóch do czterech godzin.
|
||||
|
||||
## Slajd 12 — Implementacja Unreal (zrzut ekranu)
|
||||
A tutaj analogiczny widok gry w Unreal Engine.
|
||||
|
||||
## Slajd 13 — Porównanie implementacji (tabela)
|
||||
Zestawienie doświadczeń implementacyjnych pokazuje wyraźne różnice. Unity ma niższy próg wejścia, szybszą kompilację, prostszy object pooling i pełny hot reload. Unreal wymaga więcej czasu na instalację i konfigurację, ale oferuje dostęp do kodu źródłowego silnika.
|
||||
|
||||
## Slajd 14 — Dlaczego Nsight?
|
||||
Dlaczego zastosowałem zewnętrzne narzędzie, a nie wbudowane profilery silników? Wbudowane profilery używają różnych definicji metryk, mają różny narzut profilowania i generują nieporównywalne formaty wyjściowe. NVIDIA Nsight Systems to niezależne narzędzie, które analizuje obie aplikacje na tym samym poziomie — wywołań Vulkan API — z minimalnym narzutem na poziomie sterownika i spójnym formatem danych.
|
||||
|
||||
## Slajd 15 — Nsight (zrzut ekranu)
|
||||
Na ekranie widzimy interfejs Nsight Systems z oś czasu wywołań GPU i CPU, który pozwala na szczegółową analizę każdej klatki.
|
||||
|
||||
## Slajd 16 — Unity: wydajność klatek
|
||||
Przejdźmy do wyników. Unity wyrenderowało prawie 13 i pół tysiąca klatek w 94 sekundach, co daje średnio 144 klatki na sekundę — czyli dokładnie tyle, ile wynosi V-Sync na monitorze testowym. Mediana czasu klatki to 6,94 milisekundy, a 99. percentyl — 7,58 milisekundy. Prawie 98 i ćwierć procenta klatek mieściło się w przedziale 5 do 10 milisekund. Rozstęp międzykwartylowy wynosi zaledwie 0,08 milisekundy, co świadczy o bardzo wysokiej stabilności.
|
||||
|
||||
## Slajd 17 — Unity: architektura renderowania
|
||||
Unity stosuje prosty potok renderowania — zaledwie 2 wywołania vkQueueSubmit na klatkę. Funkcja vkWaitForFences zajmowała ponad 95% czasu Vulkan API, co oznacza, że CPU czekał na GPU, a nie odwrotnie — klasyczny scenariusz GPU-bound. W całym teście Unity użyło tylko 3 potoków graficznych i około 219 tysięcy wywołań Vulkan API. Wykorzystanie GPU wynosiło jedynie 23%, bo V-Sync ograniczał liczbę klatek.
|
||||
|
||||
## Slajd 18 — Unreal: wydajność klatek
|
||||
Unreal Engine działał bez V-Sync, więc osiągał 332 do 339 klatek w fazach pierwszej i drugiej. W wymagającej fazie trzeciej wydajność spadła do 162 FPS — czyli spadek o ponad połowę. Wykorzystanie GPU sięgało 91% w lekkich fazach i spadało do 50% w ciężkiej fazie. Warto zwrócić uwagę, że liczba submitów na klatkę jest stała — 16,2 — niezależnie od obciążenia.
|
||||
|
||||
## Slajd 19 — Unreal: architektura renderowania
|
||||
Unreal ma znacznie bardziej złożony potok — 16 wywołań submit na klatkę, podczas gdy Unity ma tylko 2. Tworzenie potoków graficznych pochłaniało od 47 do 72% czasu Vulkan API. Unreal tworzył około tysiąc potoków na fazę, w porównaniu do zaledwie trzech w Unity. Łącznie Unreal wykonał 32 miliony wywołań Vulkan API i 9 milionów wywołań synchronizacji na poziomie systemu operacyjnego. Intensywnie korzysta też z compute shaderów do operacji takich jak culling i post-processing.
|
||||
|
||||
## Slajd 20 — Porównanie kluczowych wyników
|
||||
Tabela zestawia najważniejsze metryki. Unreal generuje 64-krotnie więcej wywołań Vulkan API, ponad 300-krotnie więcej wywołań synchronizacji OS i 800-krotnie więcej potoków graficznych. To odzwierciedla fundamentalną różnicę w architekturze — Unreal ma potok zaprojektowany pod gry 3D AAA, który w kontekście prostej gry 2D generuje znaczący narzut.
|
||||
|
||||
## Slajd 21 — Interpretacja: Unity
|
||||
Podsumowując wyniki Unity: prosty dwuetapowy potok okazał się bardzo wydajny dla gier 2D. Czasy klatek były niezwykle stabilne. Jednocześnie GPU było wykorzystane jedynie w 23 procentach, co sugeruje duży zapas wydajności.
|
||||
|
||||
## Slajd 22 — Interpretacja: Unreal
|
||||
Z kolei Unreal Engine cechuje złożony, wieloetapowy potok, który w przypadku prostych scen 2D generuje zbędny narzut. Silnik ciągle rekompiluje potoki graficzne w ramach dynamicznej optymalizacji. Wykorzystanie GPU sięgało 91%, ale przy dużym obciążeniu wydajność spadała o ponad połowę.
|
||||
|
||||
## Slajd 23 — Wywiady (1/2)
|
||||
W ramach pracy przeprowadziłem wywiady z ośmioma deweloperami o doświadczeniu od roku do dziesięciu lat. Respondenci potwierdzili, że próg wejścia do Unity jest niższy. Dokumentacja Unity jest lepsza dzięki konkretnym przykładom kodu, podczas gdy dokumentacja Unreala bywa określana jako szkieletowa. Blueprinty ułatwiają współpracę z osobami nietechnicznymi, ale powodują problemy z kontrolą wersji w Gicie.
|
||||
|
||||
## Slajd 24 — Wywiady (2/2)
|
||||
Dalsze wnioski: architektura Unreala wymusza pewien porządek w kodzie, podczas gdy Unity daje większą elastyczność. C# jest postrzegany jako łatwiejszy język, a C++ w Unrealu jest opisywany jako niestandardowy z powodu makr i systemu refleksji. Jeśli chodzi o dostępność materiałów edukacyjnych, Unity ma przewagę ilościową.
|
||||
|
||||
## Slajd 25 — Weryfikacja hipotezy
|
||||
Wracając do hipotezy — wyniki wskazują, że Unity ma prostszą architekturę renderowania, stabilniejsze czasy klatek, łatwiejszy i szybszy proces implementacji oraz natywne wsparcie dla grafiki 2D.
|
||||
|
||||
## Slajd 26 — Rekomendacje
|
||||
Na podstawie wyników przygotowałem macierz rekomendacji. Unity jest lepszym wyborem dla gier 2D indie, gier mobilnych i szybkiego prototypowania. Unreal sprawdzi się lepiej w grach 3D AAA dzięki technologiom Nanite i Lumen, w projektach VR high-end i w zespołach mieszanych, gdzie Blueprinty ułatwiają współpracę.
|
||||
|
||||
## Slajd 27 — Wkład pracy
|
||||
Wkład tej pracy obejmuje: zunifikowaną metodykę pomiaru z użyciem Nsight Systems, szczegółową analizę na poziomie Vulkan API, triangulację metod badawczych łączącą testy, wywiady i implementację, oraz praktyczne rekomendacje w postaci macierzy wyboru silnika.
|
||||
|
||||
## Slajd 28 — Ograniczenia
|
||||
Praca ma swoje ograniczenia: badanie dotyczy jednego gatunku gry, przeprowadzono je na jednej, wydajnej konfiguracji sprzętowej, a wywiady objęły ośmiu respondentów, co kwalifikuje je jako badanie eksploracyjne.
|
||||
|
||||
## Slajd 29 — Dalsze badania
|
||||
W przyszłości warto przeprowadzić testy dla innych gatunków, takich jak RPG, RTS czy gry logiczne. Przydatne byłoby też zbadanie wydajności na różnych platformach — mobile, konsolach, słabszych komputerach. Można też stworzyć automatyczny framework benchmarkowy i powtarzać badanie co kilka lat, śledząc rozwój silników.
|
||||
|
||||
## Slajd 30 — Dziękuję za uwagę
|
||||
Dziękuję za uwagę. Jestem gotowy na pytania.
|
||||
Loading…
Reference in New Issue
Block a user