4#include "ZEN_RenderCommand.h"
5#include "ZEN_Renderer.h"
7#include "camera/ZEN_CameraController.h"
8#include "editor/src/Obstacle.h"
9#include "editor/src/QuadBuilder.h"
11#include "inputs/ZEN_KeyCodes.h"
12#include "time/ZEN_DeltaTime.h"
13#include <SDL3/SDL_scancode.h>
14#include <glm/gtc/matrix_transform.hpp>
15#include <glm/gtc/random.hpp>
20void GameLayer::onAttach() {
22 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
24 m_camera.setPosition({0, 0, 0});
25 m_camera.setOrthographic(-10, 10, -5.625f, 5.625f);
26 m_cameraController.setWorldBounds(-10.0f, 10.0f, -5.625f, 5.625f);
27 m_cameraController.enableWorldBounds(
true);
29 m_shader = std::make_shared<Shader>(
"data/particle.vert",
"data/particle.frag");
30 m_particleSystem = std::make_unique<ParticleSystem>(5000);
31 m_quadBuilder.init(m_shader);
33 m_player.pos = {-7.0f, 0};
35 m_player.emitter.pos = m_player.pos;
36 m_player.emitter.size = {m_player.width, m_player.height};
37 m_player.emitter.colour = m_player.colour;
38 m_player.emitter.spawnRate = 80.0f;
40 m_player.emitter.props.position = m_player.pos;
41 m_player.emitter.props.velocity = {-5.0f, 0};
42 m_player.emitter.props.lifeTime = 0.6f;
43 m_player.emitter.props.sizeBegin = 0.4f;
44 m_player.emitter.props.sizeEnd = 0;
45 m_player.emitter.props.colourBegin = {1.f, 0, 0, 1.0f};
46 m_player.emitter.props.colourEnd = {0.1f, 0.4f, 0.2f, 0};
47 m_player.emitter.vRand.coneDeg = 30.0f;
48 m_player.emitter.vRand.speedMinMul = 0.5f;
49 m_player.emitter.vRand.speedMaxMul = 2.0f;
50 m_player.emitter.vRand.noiseSigma = 1.0f;
52 for (
int i = 0; i < 10; ++i) {
53 m_obstacles.emplace_back(ObstacleType::SmallBox, 50.0f);
54 m_obstacles[i].active =
false;
58void GameLayer::onUpdate(
DeltaTime deltaTime) {
59 m_cameraController.onUpdate(deltaTime);
61 RenderCommand::setClearColour({0.1f, 0.1f, 0.1f, 1.0f});
62 RenderCommand::clear();
64 if (m_gameState == GameState::Playing) {
65 updateGame(deltaTime);
66 }
else if (m_gameState == GameState::GameOver) {
67 updateGameOver(deltaTime);
68 }
else if (m_gameState == GameState::Paused) {
69 updateGamePaused(deltaTime);
72 m_particleSystem->update(deltaTime);
73 m_particleSystem->upload();
75 Renderer::beginScene(m_camera);
81 Renderer::submit(m_particleSystem->shader(), m_particleSystem->vao());
87bool GameLayer::onEvent(
const ZenEvent &event) {
88 m_cameraController.onEvent(event);
89 if (event.header.type == EventType::WindowResize) {
90 RenderCommand::setViewport(event.windowResize.width, event.windowResize.height);
91 ZEN_LOG_TRACE(
"resize viewport");
96void GameLayer::updateGame(
DeltaTime deltaTime) {
97 if (Input::isKeyPressed(Key::P)) {
98 m_gameState = GameState::Paused;
101 const bool jumpPressed = Input::isKeyPressed(Key::Space);
102 m_player.update(deltaTime, jumpPressed);
104 if (m_emitJumpBurst && m_player.justJumped()) {
107 if (m_playerEmitTrail) {
108 m_particleSystem->updateEmitter(m_player.emitter, deltaTime);
111 m_gameSpeed += m_speedIncreaseRate * deltaTime.seconds();
112 m_gameSpeed = glm::min(m_gameSpeed, 3.0f);
114 for (
auto &obstacle : m_obstacles) {
115 if (obstacle.active) {
117 float originalSpeed = obstacle.speed;
118 obstacle.speed = originalSpeed * m_gameSpeed;
119 obstacle.update(deltaTime);
121 if (m_obstacleEmitTrail) {
122 m_particleSystem->updateEmitter(obstacle.emitter, deltaTime);
125 obstacle.speed = originalSpeed;
129 m_spawnTimer += deltaTime.seconds();
130 if (m_spawnTimer >= m_spawnInterval) {
134 m_spawnInterval = glm::max(m_spawnInterval - 0.05f, m_minSpawnInterval);
139 m_score += deltaTime.seconds() * m_gameSpeed * 5.0f;
142void GameLayer::updateGamePaused(
DeltaTime deltaTime) {
143 if (Input::isKeyPressed(Key::P) || Input::isKeyPressed(Key::Space)) {
144 m_gameState = GameState::Playing;
148 for (
auto &obstacle : m_obstacles) {
149 if (obstacle.active && m_obstacleEmitTrail) {
150 m_particleSystem->updateEmitter(obstacle.emitter, deltaTime);
154void GameLayer::updateGameOver(
DeltaTime deltaTime) {
156 if (Input::isKeyPressed(Key::R) || Input::isKeyPressed(Key::Space)) {
160 for (
auto &obstacle : m_obstacles) {
161 if (obstacle.active && m_obstacleEmitTrail) {
162 m_particleSystem->updateEmitter(obstacle.emitter, deltaTime);
167void GameLayer::spawnObstacle() {
168 Obstacle *obstacle =
nullptr;
169 for (
auto &obs : m_obstacles) {
177 obstacle = &m_obstacles[m_nextObstacleIndex];
178 m_nextObstacleIndex = (m_nextObstacleIndex + 1) % m_obstacles.size();
181 ObstacleType type = getRandomObstacleType();
182 *obstacle = Obstacle(type, 12.0f);
183 obstacle->active =
true;
185 ZEN_LOG_TRACE(
"Spawned obstacle of type {}",
static_cast<int>(type));
188void GameLayer::checkCollisions() {
189 for (
auto &obstacle : m_obstacles) {
190 if (obstacle.active && obstacle.collidesWith(m_player.pos, m_player.getSize())) {
192 ZEN_LOG_INFO(
"Collision detected! Game Over!");
193 m_gameState = GameState::GameOver;
195 if (m_score > m_highScore) {
196 m_highScore = m_score;
197 ZEN_LOG_INFO(
"New high score: {}", m_highScore);
200 emitCollisionEffect();
206void GameLayer::restartGame() {
207 ZEN_LOG_INFO(
"Restarting game...");
209 m_gameState = GameState::Playing;
213 m_spawnInterval = 2.0f;
215 m_player.pos = {-7.0f, 0};
218 for (
auto &obstacle : m_obstacles) {
219 obstacle.active =
false;
222 m_particleSystem->clear();
225void GameLayer::emitJumpBurst() {
226 const int burstCount = 30;
227 for (
int i = 0; i < burstCount; ++i) {
229 p.position = m_player.pos;
231 float angle = glm::linearRand(0.0f, glm::two_pi<float>());
232 float speed = glm::linearRand(3.0f, 6.0f);
233 p.velocity = {glm::cos(angle) * speed, glm::sin(angle) * speed};
238 p.colourBegin = {1.0f, 1.0f, 0.3f, 1.0f};
239 p.colourEnd = {1.0f, 0.5f, 0, 0};
241 m_particleSystem->emit(p);
245void GameLayer::emitCollisionEffect() {
246 const int explosionCount = 100;
247 for (
int i = 0; i < explosionCount; ++i) {
248 ParticleProps explosion;
249 explosion.position = m_player.pos;
252 float angle = glm::linearRand(0.0f, glm::two_pi<float>());
253 float speed = glm::linearRand(3.0f, 10.0f);
254 explosion.velocity = {glm::cos(angle) * speed, glm::sin(angle) * speed};
256 explosion.lifeTime = 1.0f;
257 explosion.sizeBegin = 0.6f;
258 explosion.sizeEnd = 0;
259 explosion.colourBegin = {1.0f, 0.3f, 0.1f, 1.0f};
260 explosion.colourEnd = {0.5f, 0.1f, 0, 0};
262 m_particleSystem->emit(explosion);
266ObstacleType GameLayer::getRandomObstacleType() {
267 int random = rand() % 100;
270 return ObstacleType::SmallBox;
273 return ObstacleType::TallBox;
275 return ObstacleType::FlyingBox;
278void GameLayer::drawPlayer() {
279 glm::vec4 colour = m_player.colour;
280 if (m_gameState == GameState::GameOver) {
281 colour = {0.5f, 0.5f, 0.5f, 1.0f};
283 m_quadBuilder.drawQuad(m_player.pos, m_player.getSize(), colour);
286void GameLayer::drawObstacles() {
287 for (
const auto &obstacle : m_obstacles) {
288 if (obstacle.active) {
289 m_quadBuilder.drawQuad(obstacle.pos, obstacle.size, obstacle.colour);
294void GameLayer::drawGround() { m_quadBuilder.drawQuad(m_groundPos, m_groundSize, m_groundColour); }
296void GameLayer::drawUI() {
297 ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Always);
298 ImGui::SetNextWindowBgAlpha(0.6f);
299 ImGui::Begin(
"Score",
301 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
302 ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove);
304 ImGui::Text(
"Score: %.0f", m_score);
305 ImGui::Text(
"High: %.0f", m_highScore);
306 ImGui::Text(
"Speed: %.1fx", m_gameSpeed);
308 if (m_gameState == GameState::GameOver) {
310 ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f),
"GAME OVER!");
311 ImGui::Text(
"Press R or SPACE to restart");
314 if (m_gameState == GameState::Paused) {
316 ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f),
"PAUSED!");
317 ImGui::Text(
"Press P or SPACE to Resume");
323void GameLayer::onGUIRender() {
324 ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_Once);
325 ImGui::SetNextWindowPos(ImVec2(800, 10), ImGuiCond_Once);
326 ImGui::Begin(
"Editor");
328 ImGui::SeparatorText(
"Game State");
330 const char *stateNames[] = {
"Playing",
"GameOver",
"Paused"};
331 ImGui::Text(
"State: %s", stateNames[
static_cast<int>(m_gameState)]);
332 ImGui::Text(
"Score: %.0f", m_score);
333 ImGui::Text(
"High Score: %.0f", m_highScore);
335 if (ImGui::Button(
"Restart Game (R)")) {
339 if (m_gameState == GameState::Playing) {
340 if (ImGui::Button(
"Pause Game (P)")) {
341 m_gameState = GameState::Paused;
343 }
else if (m_gameState == GameState::Paused) {
344 if (ImGui::Button(
"Resume Game (P)")) {
345 m_gameState = GameState::Playing;
349 if (ImGui::Button(
"Spawn Obstacle")) {
353 ImGui::SeparatorText(
"Difficulty");
354 ImGui::DragFloat(
"Game Speed", &m_gameSpeed, 0.1f, 0.5f, 5.0f);
355 ImGui::DragFloat(
"Speed Increase Rate", &m_speedIncreaseRate, 0.01f, 0, 1.0f);
356 ImGui::DragFloat(
"Spawn Interval", &m_spawnInterval, 0.1f, 0.5f, 5.0f);
357 ImGui::DragFloat(
"Min Spawn Interval", &m_minSpawnInterval, 0.1f, 0.3f, 2.0f);
359 ImGui::SeparatorText(
"Player");
360 ImGui::DragFloat(
"Jump Power", &m_player.jumpPower, 0.1f, 5.0f, 20.0f);
361 ImGui::DragFloat(
"Gravity", &m_player.gravity, 0.5f, -50.0f, -5.0f);
362 ImGui::DragFloat2(
"Player Size", &m_player.width, 0.1f, 0.1f, 3.0f);
363 ImGui::ColorEdit4(
"Player Colour", &m_player.colour[0]);
365 ImGui::SeparatorText(
"Player Particles");
366 ImGui::Checkbox(
"Emit Trail", &m_playerEmitTrail);
367 ImGui::Checkbox(
"Emit Jump Burst", &m_emitJumpBurst);
368 ImGui::DragFloat(
"Trail Spawn Rate", &m_player.emitter.spawnRate, 1.0f, 0, 200.0f);
369 ImGui::DragFloat2(
"Trail Velocity", &m_player.emitter.props.velocity[0], 0.1f, -10.0f, 10.0f);
370 ImGui::ColorEdit4(
"Trail Colour Start", &m_player.emitter.props.colourBegin[0]);
371 ImGui::ColorEdit4(
"Trail Colour End", &m_player.emitter.props.colourEnd[0]);
372 ImGui::DragFloat(
"Trail Life", &m_player.emitter.props.lifeTime, 0.1f, 0.1f, 3.0f);
373 ImGui::DragFloat(
"Cone (deg)", &m_player.emitter.vRand.coneDeg, 0.1f, 0, 60.0f);
374 ImGui::DragFloat(
"Speed min x", &m_player.emitter.vRand.speedMinMul, 0.01f, 0, 2.0f);
375 ImGui::DragFloat(
"Speed max x", &m_player.emitter.vRand.speedMaxMul, 0.01f, 0, 3.0f);
376 ImGui::DragFloat(
"Noise sigma", &m_player.emitter.vRand.noiseSigma, 0.01f, 0, 1.0f);
378 ImGui::SeparatorText(
"Obstacles");
379 ImGui::Checkbox(
"Emit Obstacle Trails", &m_obstacleEmitTrail);
382 for (
const auto &obs : m_obstacles) {
387 ImGui::Text(
"Active Obstacles: %d / %zu", activeCount, m_obstacles.size());
389 if (ImGui::Button(
"Clear All Obstacles")) {
390 for (
auto &obs : m_obstacles) {
395 if (ImGui::TreeNode(
"Obstacle Types")) {
396 for (
size_t i = 0; i < m_obstacles.size() && i < 5; ++i) {
397 ImGui::PushID(
static_cast<int>(i));
398 if (m_obstacles[i].active) {
399 const char *typeNames[] = {
"Small",
"Tall",
"Flying"};
400 ImGui::Text(
"Obstacle %zu: %s (%.1f, %.1f)",
402 typeNames[
static_cast<int>(m_obstacles[i].type)],
403 m_obstacles[i].pos.x,
404 m_obstacles[i].pos.y);
411 ImGui::SeparatorText(
"Test Spawns");
412 if (ImGui::Button(
"Spawn Small Box")) {
413 m_obstacles.push_back(Obstacle(ObstacleType::SmallBox, 12.0f));
415 if (ImGui::Button(
"Spawn Tall Box")) {
416 m_obstacles.push_back(Obstacle(ObstacleType::TallBox, 12.0f));
418 if (ImGui::Button(
"Spawn Flying Box")) {
419 m_obstacles.push_back(Obstacle(ObstacleType::FlyingBox, 12.0f));
422 ImGui::SeparatorText(
"Particle System");
423 ImGui::Text(
"Particles: %zu", m_particleSystem->capacity());
424 if (ImGui::Button(
"Clear All Particles")) {
425 m_particleSystem->clear();