r-type  0.0.0
R-Type main
Loading...
Searching...
No Matches
gameMulti.cpp
Go to the documentation of this file.
2#include <algorithm>
3#include <ranges>
4#include <set>
5#include <vector>
6
7#include "ECS/Component.hpp"
12#include "Utils/Common.hpp"
13#include "Utils/EventBus.hpp"
15
16gme::GameMulti::GameMulti(const eng::id assignedId, const std::shared_ptr<eng::IRenderer> &renderer,
17 const std::shared_ptr<eng::IAudio> &audio, const float skinIndex, bool &showDebug,
18 const uint32_t sessionId)
19 : AScene(assignedId), m_audio(audio), m_renderer(renderer), m_skinIndex(skinIndex), m_showDebug(showDebug),
20 m_sessionId(sessionId)
21{
22 auto &registry = AScene::getRegistry();
23
24 registry.onComponentAdded(
25 [this, &renderer, &audio, &registry](const ecs::Entity e, const std::type_info &type)
26 {
27 const auto *audioComp = registry.getComponent<ecs::Audio>(e);
28 const auto *colorComp = registry.getComponent<ecs::Color>(e);
29 const auto *fontComp = registry.getComponent<ecs::Font>(e);
30 const auto *rectComp = registry.getComponent<ecs::Rect>(e);
31 const auto *scaleComp = registry.getComponent<ecs::Scale>(e);
32 const auto *textComp = registry.getComponent<ecs::Text>(e);
33 const auto *textureComp = registry.getComponent<ecs::Texture>(e);
34 const auto *transform = registry.getComponent<ecs::Transform>(e);
35 const auto *hitBox = registry.getComponent<ecs::Hitbox>(e);
36
37 if (hitBox && transform)
38 {
39 renderer->createCircleShape({.name = "hitbox_" + std::to_string(e),
40 .radius = hitBox->radius,
41 .color = {.r = 255, .g = 0, .b = 0, .a = 100},
42 .x = transform->x + hitBox->offsetX,
43 .y = transform->y + hitBox->offsetY,
44 .outline_thickness = 1.0f,
45 .outline_color = {.r = 255, .g = 0, .b = 0, .a = 200}});
46 }
47 if (type == typeid(ecs::Text))
48 {
49 if (textComp && transform && fontComp)
50 {
51 // Only create font if not already loaded (cache)
52 if (this->m_loadedFonts.find(fontComp->id) == this->m_loadedFonts.end())
53 {
54 renderer->createFont(fontComp->id, fontComp->path);
55 this->m_loadedFonts.insert(fontComp->id);
56 }
57 renderer->createText(
58 {.font_name = fontComp->id,
59 .color = {.r = colorComp->r, .g = colorComp->g, .b = colorComp->b, .a = colorComp->a},
60 .content = textComp->content,
61 .size = textComp->font_size,
62 .x = transform->x,
63 .y = transform->y,
64 .name = textComp->id});
65 }
66 }
67 else if (type == typeid(ecs::Texture))
68 {
69 const float scale_x = scaleComp ? scaleComp->x : 1.F;
70 const float scale_y = scaleComp ? scaleComp->y : 1.F;
71
72 // Only create texture if not already loaded (cache)
73 if (this->m_loadedTextures.find(textureComp->id) == this->m_loadedTextures.end())
74 {
75 renderer->createTexture(textureComp->id, textureComp->path);
76 this->m_loadedTextures.insert(textureComp->id);
77 }
78
79 if (transform && textureComp)
80 {
81 if (rectComp)
82 {
83 renderer->createSprite(textureComp->id + std::to_string(e), textureComp->id, transform->x,
84 transform->y, scale_x, scale_y, static_cast<int>(rectComp->pos_x),
85 static_cast<int>(rectComp->pos_y), rectComp->size_x, rectComp->size_y);
86 }
87 else
88 {
89 renderer->createSprite(textureComp->id + std::to_string(e), textureComp->id, transform->x,
90 transform->y);
91 }
92 }
93 }
94 else if (type == typeid(ecs::Audio))
95 {
96 if (audioComp)
97 {
98 audio->createAudio(audioComp->path, audioComp->volume, audioComp->loop,
99 audioComp->id + std::to_string(e));
100 }
101 }
102 });
103
104 registry.createEntity().with<ecs::Score>("score", 0).build();
105
106 m_playerController = std::make_unique<PlayerControllerMulti>(renderer, m_sessionId);
107 m_hudSystem = std::make_unique<HUDSystem>(renderer, registry);
108
109 m_localPlayerEntity = m_playerController->createPlayer(registry, 200.F, 100.F);
110
111 if (auto *playerRect = registry.getComponent<ecs::Rect>(m_localPlayerEntity))
112 {
113 uint32_t skinIndex = 0;
115 {
116 skinIndex = m_playerSkinMap[m_sessionId];
117 }
118 float skinPosY = static_cast<float>(skinIndex) * utl::GameConfig::Player::SPRITE_HEIGHT;
119 playerRect->pos_y = skinPosY;
120 }
121
122 if (auto *playerRect = registry.getComponent<ecs::Rect>(m_localPlayerEntity))
123 {
124 uint32_t skinIndex = 0;
126 {
127 skinIndex = m_playerSkinMap[m_sessionId];
128 }
129 float skinPosY = static_cast<float>(skinIndex) * utl::GameConfig::Player::SPRITE_HEIGHT;
130 playerRect->pos_y = skinPosY;
131 }
132
133 auto beginSoundEntity = registry.createEntity()
134 .with<ecs::Audio>("game_begin", utl::Path::Audio::AUDIO_BEGIN, 1.0F, false, false)
135 .build();
136 if (auto *audioComp = registry.getComponent<ecs::Audio>(beginSoundEntity))
137 {
138 audioComp->play = true;
139 }
140 m_beginSoundEntity = beginSoundEntity;
141
142 m_stageManager = std::make_unique<StageManager>();
143
144 // Preload all common textures to avoid lag spikes during gameplay
146
148}
149
151{
152 // Preload all textures that will be used during gameplay
153 // This prevents lag when entities are first created from world state
154
155 std::vector<std::pair<std::string, std::string>> texturesToLoad = {
164
165 for (const auto &[id, path] : texturesToLoad)
166 {
167 if (m_loadedTextures.find(id) == m_loadedTextures.end())
168 {
169 m_renderer->createTexture(id, path);
170 m_loadedTextures.insert(id);
171 }
172 }
173}
174
176{
178 eventBus.registerComponent(m_eventComponentId, "GameMulti");
179 eventBus.subscribe(m_eventComponentId, utl::EventType::GAME_START);
180 eventBus.subscribe(m_eventComponentId, utl::EventType::PLAYER_INPUT_RECEIVED);
181 eventBus.subscribe(m_eventComponentId, utl::EventType::WORLD_STATE_RECEIVED);
182 eventBus.subscribe(m_eventComponentId, utl::EventType::GAME_OVER);
183}
184
186{
187 auto &eventBus = utl::EventBus::getInstance();
188 for (const std::vector<utl::Event> events = eventBus.consumeForTarget(m_eventComponentId);
189 const auto &event : events)
190 {
191 switch (event.type)
192 {
194 {
195 break;
196 }
198 break;
200 handleWorldStateUpdate(event);
201 break;
203 {
204 utl::Logger::log("GameMulti: Received GAME_OVER event", utl::LogLevel::INFO);
205 if (onGameOver)
206 {
207 onGameOver();
208 }
209 break;
210 }
211 default:
212 break;
213 }
214 }
215}
216
218{
219 try
220 {
221 rnp::Serializer deserializer(event.data);
222 rnp::PacketWorldState worldState = deserializer.deserializeWorldState();
223
224 auto &registry = getRegistry();
225
226 // Update local player score from world state
227 for (const auto &entityState : worldState.entities)
228 {
229 if (entityState.id == m_sessionId && entityState.type == static_cast<uint16_t>(rnp::EntityType::PLAYER))
230 {
231 // Update the Score component with the server's score
232 for (auto &score : registry.getAll<ecs::Score>() | std::views::values)
233 {
234 score.value = static_cast<int>(entityState.score);
235 break;
236 }
237 }
238 }
239
240 // Track entities present in this world state
241 std::set<uint32_t> currentEnemyIds;
242 std::set<uint32_t> currentProjectileIds;
243
244 if (m_firstWorldState)
245 {
246 uint32_t playerIndex = 0;
247 std::vector<uint32_t> playerIds;
248
249 playerIds.push_back(m_sessionId);
250 for (const auto &entityState : worldState.entities)
251 {
252 if (entityState.type == static_cast<std::uint16_t>(rnp::EntityType::PLAYER))
253 {
254 if (entityState.id != m_sessionId)
255 {
256 playerIds.push_back(entityState.id);
257 }
258 }
259 }
260
261 for (uint32_t playerId : playerIds)
262 {
263 m_playerSkinMap[playerId] = playerIndex;
264 playerIndex++;
265 }
266
267 m_firstWorldState = false;
268 }
269
270 // First pass: collect IDs of entities in the world state
271 for (const auto &entityState : worldState.entities)
272 {
273 if (entityState.type == static_cast<std::uint16_t>(rnp::EntityType::ENEMY) ||
274 entityState.type == static_cast<std::uint16_t>(rnp::EntityType::BOSS))
275 {
276 currentEnemyIds.insert(entityState.id);
277 }
278 else if (entityState.type == static_cast<std::uint16_t>(rnp::EntityType::PROJECTILE))
279 {
280 currentProjectileIds.insert(entityState.id);
281 }
282 }
283
284 // Second pass: process entities
285 for (const auto &entityState : worldState.entities)
286 {
287 if (entityState.id == m_sessionId)
288 {
289 if (auto *transform = registry.getComponent<ecs::Transform>(m_localPlayerEntity))
290 {
291 float deltaX = std::abs(transform->x - entityState.x);
292 float deltaY = std::abs(transform->y - entityState.y);
293 const float CORRECTION_THRESHOLD = 5.0f;
294
295 if (deltaX > CORRECTION_THRESHOLD || deltaY > CORRECTION_THRESHOLD)
296 {
297 float t = 0.2f;
298 transform->x = transform->x + t * (entityState.x - transform->x);
299 transform->y = transform->y + t * (entityState.y - transform->y);
300 }
301 }
302 if (auto *velocity = registry.getComponent<ecs::Velocity>(m_localPlayerEntity))
303 {
304 velocity->x = entityState.vx;
305 velocity->y = entityState.vy;
306 }
307
308 // Synchronize health from server
309 if (auto *health = registry.getComponent<ecs::Health>(m_localPlayerEntity))
310 {
311 if (entityState.healthPercent != 255) // 255 means no health data
312 {
313 float previousHealth = health->current;
314 health->current = (static_cast<float>(entityState.healthPercent) / 100.0f) * health->max;
315
316 // Detect player death
317 if (health->current <= 0.0f && previousHealth > 0.0f)
318 {
319 utl::Logger::log("GameMulti: Local player died - disconnecting and showing game over",
321
322 // Send disconnect event to network
324 std::vector<std::uint8_t> emptyData;
325 eventBus.publish(utl::EventType::REQUEST_DISCONNECT, emptyData, m_eventComponentId,
327
328 // Trigger game over after a short delay to allow disconnect to process
329 if (onGameOver)
330 {
331 onGameOver();
332 }
333 }
334 }
335 }
336
337 // Synchronize beam charge from server
338 if (auto *beamCharge = registry.getComponent<ecs::BeamCharge>(m_localPlayerEntity))
339 {
340 // Always accept server charge value
341 beamCharge->current_charge = static_cast<float>(entityState.stateFlags) / 255.0f;
342 }
343 }
344 else if (entityState.type == static_cast<std::uint16_t>(rnp::EntityType::PLAYER))
345 {
346 if (!m_remotePlayers.contains(entityState.id))
347 {
348 uint32_t skinIndex = 0;
349 if (m_playerSkinMap.contains(entityState.id))
350 {
351 skinIndex = m_playerSkinMap[entityState.id];
352 }
353 float skinPosY = static_cast<float>(skinIndex) * utl::GameConfig::Player::SPRITE_HEIGHT;
357
358 ecs::Entity remotePlayer =
359 registry.createEntity()
360 .with<ecs::Transform>("remote_player_" + std::to_string(entityState.id), entityState.x,
361 entityState.y, 0.F)
362 .with<ecs::Velocity>("remote_velocity_" + std::to_string(entityState.id), entityState.vx,
363 entityState.vy)
364 .with<ecs::Rect>("remote_rect_" + std::to_string(entityState.id), 0.F, skinPosY,
367 .with<ecs::Scale>("remote_scale_" + std::to_string(entityState.id),
369 .with<ecs::Texture>("remote_texture_" + std::to_string(entityState.id),
371 .with<ecs::Player>("remote_player_comp_" + std::to_string(entityState.id), false)
372 .with<ecs::Hitbox>("hitbox", utl::GameConfig::Hitbox::BOSS_RADIUS, offsetX, offsetY)
373 .with<ecs::Health>("remote_player_health_" + std::to_string(entityState.id), 100.0f, 100.0f)
374 .build();
375 m_remotePlayers[entityState.id] = remotePlayer;
376
377 m_remotePlayerData[entityState.id] = {.targetX = entityState.x,
378 .targetY = entityState.y,
379 .targetVx = entityState.vx,
380 .targetVy = entityState.vy,
381 .currentX = entityState.x,
382 .currentY = entityState.y,
383 .smoothFactor = REMOTE_PLAYER_SMOOTH_FACTOR,
384 .targetRotation = 0.0f,
385 .currentRotation = 0.0f};
386 }
387 else
388 {
389 m_remotePlayerData[entityState.id].targetX = entityState.x;
390 m_remotePlayerData[entityState.id].targetY = entityState.y;
391 m_remotePlayerData[entityState.id].targetVx = entityState.vx;
392 m_remotePlayerData[entityState.id].targetVy = entityState.vy;
393
394 // Update remote player health
395 if (auto *health = registry.getComponent<ecs::Health>(m_remotePlayers[entityState.id]))
396 {
397 if (entityState.healthPercent != 255)
398 {
399 health->current = (static_cast<float>(entityState.healthPercent) / 100.0f) * health->max;
400 }
401 }
402 }
403 }
404 else if (entityState.type == static_cast<std::uint16_t>(rnp::EntityType::PROJECTILE))
405 {
406
407 if (!m_projectileEntities.contains(entityState.id))
408 {
409 // Use subtype field to determine projectile type
410 bool isEnemyProjectile =
411 (entityState.subtype == static_cast<std::uint8_t>(rnp::EntitySubtype::PROJECTILE_ENEMY));
412 bool isSupercharged =
413 (entityState.subtype ==
414 static_cast<std::uint8_t>(rnp::EntitySubtype::PROJECTILE_PLAYER_SUPERCHARGED));
415
416 std::string texturePath;
417 float width, height, scale;
418
419 if (isEnemyProjectile)
420 {
421 // Enemy projectile
423 width = 16.0f;
424 height = 16.0f;
425 scale = 1.0f;
426 }
427 else
428 {
429 // Player projectile
430 texturePath = isSupercharged ? utl::Path::Texture::TEXTURE_SHOOT_CHARGED
432 width = isSupercharged ? 29.0f : 20.0f;
433 height = isSupercharged ? 24.0f : 10.0f;
434 scale = isSupercharged ? 1.5f : 1.0f;
435 }
436
437 auto entityBuilder =
438 registry.createEntity()
439 .with<ecs::Transform>("projectile_" + std::to_string(entityState.id), entityState.x,
440 entityState.y, 0.F)
441 .with<ecs::Velocity>("projectile_velocity_" + std::to_string(entityState.id),
442 entityState.vx, entityState.vy)
443 .with<ecs::Rect>("projectile_rect_" + std::to_string(entityState.id), 0.F, 0.F,
444 static_cast<int>(width), static_cast<int>(height))
445 .with<ecs::Scale>("projectile_scale_" + std::to_string(entityState.id), scale, scale)
446 .with<ecs::Texture>("projectile_texture_" + std::to_string(entityState.id), texturePath);
447
448 if (isSupercharged && !isEnemyProjectile)
449 {
450 entityBuilder.with<ecs::Animation>("projectile_animation_" + std::to_string(entityState.id), 0,
451 4, 0.15f, 0.0f, 29, 24, 4);
452 }
453
454 ecs::Entity projectile = entityBuilder.build();
455 m_projectileEntities[entityState.id] = projectile;
456
457 // Don't play sound for every projectile to avoid audio spam and lag
458 // Sound should be played on player action locally, not on world state sync
459 }
460 else
461 {
462 // Update existing projectile position and velocity
463 // Server handles cleanup of off-screen projectiles
464 if (auto *transform = registry.getComponent<ecs::Transform>(m_projectileEntities[entityState.id]))
465 {
466 transform->x = entityState.x;
467 transform->y = entityState.y;
468 }
469 if (auto *velocity = registry.getComponent<ecs::Velocity>(m_projectileEntities[entityState.id]))
470 {
471 velocity->x = entityState.vx;
472 velocity->y = entityState.vy;
473 }
474 }
475 }
476 else if (entityState.type == static_cast<std::uint16_t>(rnp::EntityType::ENEMY))
477 {
478 if (!m_enemyEntities.contains(entityState.id))
479 {
480 // Use subtype field to determine enemy type
481 std::string texturePath;
482 float width, height, scale;
483 int animFrames = 4;
484
485 if (entityState.subtype == static_cast<std::uint8_t>(rnp::EntitySubtype::ENEMY_ADVANCED))
486 {
488 width = 33.0f;
489 height = 36.0f;
490 scale = 2.0f;
491 }
492 else // ENEMY_BASIC
493 {
495 width = 32.0f;
496 height = 32.0f;
497 scale = 2.0f;
498 }
499 auto [offsetX, offsetY] = utl::calculateHitboxOffsets(width, height, scale);
500
501 ecs::Entity enemy =
502 registry.createEntity()
503 .with<ecs::Transform>("enemy_" + std::to_string(entityState.id), entityState.x,
504 entityState.y, 0.F)
505 .with<ecs::Velocity>("enemy_velocity_" + std::to_string(entityState.id), entityState.vx,
506 entityState.vy)
507 .with<ecs::Rect>("enemy_rect_" + std::to_string(entityState.id), 0.F, 0.F,
508 static_cast<int>(width), static_cast<int>(height))
509 .with<ecs::Scale>("enemy_scale_" + std::to_string(entityState.id), scale, scale)
510 .with<ecs::Texture>("enemy_texture_" + std::to_string(entityState.id), texturePath)
511 .with<ecs::Hitbox>("enemy_hitbox", utl::GameConfig::Hitbox::ENEMY_RADIUS, offsetX, offsetY)
512 .with<ecs::Animation>("enemy_animation_" + std::to_string(entityState.id), 0, animFrames,
513 0.5f, 0.0f, static_cast<int>(width), static_cast<int>(height),
514 animFrames)
515 .build();
516
517 m_enemyEntities[entityState.id] = enemy;
518
519 m_enemyData[entityState.id] = {.targetX = entityState.x,
520 .targetY = entityState.y,
521 .targetVx = entityState.vx,
522 .targetVy = entityState.vy,
523 .currentX = entityState.x,
524 .currentY = entityState.y,
525 .smoothFactor = ENEMY_SMOOTH_FACTOR,
526 .targetRotation = 0.0F,
527 .currentRotation = 0.0F};
528 }
529 else
530 {
531 m_enemyData[entityState.id].targetX = entityState.x;
532 m_enemyData[entityState.id].targetY = entityState.y;
533 m_enemyData[entityState.id].targetVx = entityState.vx;
534 m_enemyData[entityState.id].targetVy = entityState.vy;
535 }
536 }
537 else if (entityState.type == static_cast<std::uint16_t>(rnp::EntityType::BOSS))
538 {
539 if (!m_enemyEntities.contains(entityState.id))
540 {
541 // Boss entity
542 std::string texturePath = utl::Path::Texture::TEXTURE_BOSS;
543 float width = 64.0f;
544 float height = 64.0f;
545 float scale = 3.0f;
546 auto [offsetX, offsetY] = utl::calculateHitboxOffsets(width, height, scale);
547 ecs::Entity boss =
548 registry.createEntity()
549 .with<ecs::Transform>("boss_" + std::to_string(entityState.id), entityState.x,
550 entityState.y, 0.F)
551 .with<ecs::Velocity>("boss_velocity_" + std::to_string(entityState.id), entityState.vx,
552 entityState.vy)
553 .with<ecs::Rect>("boss_rect_" + std::to_string(entityState.id), 0.F, 0.F,
554 static_cast<int>(width), static_cast<int>(height))
555 .with<ecs::Scale>("boss_scale_" + std::to_string(entityState.id), scale, scale)
556 .with<ecs::Texture>("boss_texture_" + std::to_string(entityState.id), texturePath)
557 .with<ecs::Hitbox>("boss_hitbox", utl::GameConfig::Hitbox::BOSS_RADIUS, offsetX, offsetY)
558 .with<ecs::Animation>("boss_animation_" + std::to_string(entityState.id), 0, 2, 0.3f, 0.0f,
559 static_cast<int>(width), static_cast<int>(height), 2)
560 .build();
561
562 m_enemyEntities[entityState.id] = boss;
563
564 m_enemyData[entityState.id] = {.targetX = entityState.x,
565 .targetY = entityState.y,
566 .targetVx = entityState.vx,
567 .targetVy = entityState.vy,
568 .currentX = entityState.x,
569 .currentY = entityState.y,
570 .smoothFactor = ENEMY_SMOOTH_FACTOR,
571 .targetRotation = 0.0f,
572 .currentRotation = 0.0f};
573
574 utl::Logger::log("GameMulti: Boss spawned with ID " + std::to_string(entityState.id),
576 }
577 else
578 {
579 m_enemyData[entityState.id].targetX = entityState.x;
580 m_enemyData[entityState.id].targetY = entityState.y;
581 m_enemyData[entityState.id].targetVx = entityState.vx;
582 m_enemyData[entityState.id].targetVy = entityState.vy;
583 }
584 }
585 }
586
587 // Clean up entities that are no longer in the world state
588 // Remove enemies that disappeared (create explosion before removal)
589 std::vector<uint32_t> enemiesToRemove;
590 for (const auto &enemyId : m_enemyEntities | std::views::keys)
591 {
592 if (!currentEnemyIds.contains(enemyId))
593 {
594 enemiesToRemove.push_back(enemyId);
595 }
596 }
597
598 for (uint32_t enemyId : enemiesToRemove)
599 {
600 ecs::Entity enemyEntity = m_enemyEntities[enemyId];
601
602 // Create explosion at enemy position before removing
603 if (auto *transform = registry.getComponent<ecs::Transform>(enemyEntity))
604 {
605 registry.createEntity()
606 .with<ecs::Transform>("explosion_transform", transform->x, transform->y, 0.0f)
607 .with<ecs::Rect>("explosion_rect", 0.0f, 0.0f,
610 .with<ecs::Scale>("explosion_scale", utl::GameConfig::Explosion::SCALE,
612 .with<ecs::Texture>("explosion_texture", utl::Path::Texture::TEXTURE_EXPLOSION)
613 .with<ecs::Explosion>(
618 .build();
619 }
620
621 // Remove all components
622 if (registry.hasComponent<ecs::Transform>(enemyEntity))
623 registry.removeComponent<ecs::Transform>(enemyEntity);
624 if (registry.hasComponent<ecs::Velocity>(enemyEntity))
625 registry.removeComponent<ecs::Velocity>(enemyEntity);
626 if (registry.hasComponent<ecs::Rect>(enemyEntity))
627 registry.removeComponent<ecs::Rect>(enemyEntity);
628 if (registry.hasComponent<ecs::Scale>(enemyEntity))
629 registry.removeComponent<ecs::Scale>(enemyEntity);
630 if (registry.hasComponent<ecs::Texture>(enemyEntity))
631 registry.removeComponent<ecs::Texture>(enemyEntity);
632 if (registry.hasComponent<ecs::Animation>(enemyEntity))
633 registry.removeComponent<ecs::Animation>(enemyEntity);
634
635 m_enemyEntities.erase(enemyId);
636 m_enemyData.erase(enemyId);
637 }
638
639 // Remove projectiles that disappeared
640 std::vector<uint32_t> projectilesToRemove;
641 for (const auto &projectileId : m_projectileEntities | std::views::keys)
642 {
643 if (!currentProjectileIds.contains(projectileId))
644 {
645 projectilesToRemove.push_back(projectileId);
646 }
647 }
648
649 for (uint32_t projectileId : projectilesToRemove)
650 {
651 ecs::Entity projectileEntity = m_projectileEntities[projectileId];
652
653 // Remove all components
654 if (registry.hasComponent<ecs::Transform>(projectileEntity))
655 {
656 registry.removeComponent<ecs::Transform>(projectileEntity);
657 }
658 if (registry.hasComponent<ecs::Velocity>(projectileEntity))
659 {
660 registry.removeComponent<ecs::Velocity>(projectileEntity);
661 }
662 if (registry.hasComponent<ecs::Rect>(projectileEntity))
663 {
664 registry.removeComponent<ecs::Rect>(projectileEntity);
665 }
666 if (registry.hasComponent<ecs::Scale>(projectileEntity))
667 {
668 registry.removeComponent<ecs::Scale>(projectileEntity);
669 }
670 if (registry.hasComponent<ecs::Texture>(projectileEntity))
671 {
672 registry.removeComponent<ecs::Texture>(projectileEntity);
673 }
674 if (registry.hasComponent<ecs::Animation>(projectileEntity))
675 {
676 registry.removeComponent<ecs::Animation>(projectileEntity);
677 }
678
679 m_projectileEntities.erase(projectileId);
680 }
681 }
682 catch (const std::exception &e)
683 {
684 utl::Logger::log("GameMulti: Error handling world state: " + std::string(e.what()), utl::LogLevel::WARNING);
685 }
686}
687
688void gme::GameMulti::update(const float dt, const eng::WindowSize &size)
689{
690 auto &reg = getRegistry();
691
692 if (m_playerController)
693 {
694 m_playerController->update(reg, dt);
695 }
696
697 if (m_hudSystem)
698 {
699 m_hudSystem->update(reg, dt);
700 }
701 for (const auto &audios = reg.getAll<ecs::Audio>(); const auto &audio : audios | std::views::values)
702 {
703 if (!audio.play && audio.loop && (m_audio->isPlaying(audio.id) == eng::Status::Playing))
704 {
705 m_audio->stopAudio(audio.id);
706 }
707 }
708 if (m_beginSoundEntity != ecs::Entity{} && m_stageManager && !m_bossMusicStarted)
709 {
710 thread_local std::string beginNameCache;
711 beginNameCache.clear();
712 beginNameCache = "game_begin";
713 beginNameCache += std::to_string(m_beginSoundEntity);
714
715 if (m_audio->isPlaying(beginNameCache) != eng::Status::Playing)
716 {
717 if (auto *beginAudio = reg.getComponent<ecs::Audio>(m_beginSoundEntity))
718 {
719 beginAudio->play = false;
720 }
722 m_bossMusicEntity = reg.createEntity()
723 .with<ecs::Audio>("boss_music", utl::Path::Audio::AUDIO_BOSS, 1.0F, true, false)
724 .build();
725 if (auto *bossAudio = reg.getComponent<ecs::Audio>(m_bossMusicEntity))
726 {
727 bossAudio->loop = true;
728 bossAudio->play = true;
729 }
730 m_bossMusicStarted = true;
731 m_bossMusicTimer = 0.0f;
732 m_beginSoundEntity = {};
733 }
734 }
735 if (m_bossMusicStarted)
736 {
737 m_bossMusicTimer += dt;
738 if (m_bossMusicTimer >= BOSS_MUSIC_DURATION)
739 {
740 if (auto *bossAudio = reg.getComponent<ecs::Audio>(m_bossMusicEntity))
741 {
742 bossAudio->loop = false;
743 bossAudio->play = false;
744 }
745 m_bossMusicStarted = false;
746 }
747 else
748 {
749 if (auto *bossAudio = reg.getComponent<ecs::Audio>(m_bossMusicEntity))
750 {
751 bossAudio->loop = true;
752 bossAudio->play = true;
753 }
754 }
755 }
756
757 updateInterpolation(m_remotePlayerData, m_remotePlayers, REMOTE_PLAYER_SMOOTH_FACTOR, dt, reg);
758 updateInterpolation(m_projectileData, m_projectileEntities, PROJECTILE_SMOOTH_FACTOR, dt, reg);
759 updateInterpolation(m_enemyData, m_enemyEntities, ENEMY_SMOOTH_FACTOR, dt, reg);
760
761 m_stageManager->update(reg, dt, size);
762
763 processEventBus();
764}
765
767{
768 auto &reg = getRegistry();
769 m_playerController->handleInput(reg, event);
770}
771
772void gme::GameMulti::updateInterpolation(std::unordered_map<uint32_t, InterpolationData> &dataMap,
773 std::unordered_map<uint32_t, ecs::Entity> &entityMap, float smoothFactor,
774 float dt, ecs::Registry &registry)
775{
776 for (auto &[entityId, interpData] : dataMap)
777 {
778 if (&dataMap == &m_remotePlayerData && entityId == m_sessionId)
779 {
780 continue;
781 }
782
783 if (entityMap.contains(entityId))
784 {
785 const ecs::Entity entity = entityMap[entityId];
786 if (auto *transform = registry.getComponent<ecs::Transform>(entity))
787 {
788 interpData.currentX += (interpData.targetX - interpData.currentX) * interpData.smoothFactor;
789 interpData.currentY += (interpData.targetY - interpData.currentY) * interpData.smoothFactor;
790
791 transform->x = interpData.currentX;
792 transform->y = interpData.currentY;
793
794 if (auto *velocity = registry.getComponent<ecs::Velocity>(entity))
795 {
796 velocity->x = interpData.targetVx;
797 velocity->y = interpData.targetVy;
798 }
799
800 interpData.currentRotation +=
801 (interpData.targetRotation - interpData.currentRotation) * interpData.smoothFactor;
802 transform->rotation = interpData.currentRotation;
803 }
804 }
805 }
806}
807
809{
810 auto &registry = getRegistry();
811
812 if (auto *playerRect = registry.getComponent<ecs::Rect>(m_localPlayerEntity); playerRect != nullptr)
813 {
814 const float skinPosY = m_skinIndex * utl::GameConfig::Player::SPRITE_HEIGHT;
815 playerRect->pos_y = skinPosY;
816 }
817}
This file contains the component definitions.
Thread-safe event bus implementation for inter-component communication.
Configuration constants for the multiplayer game.
Main multiplayer game scene for R-Type.
Utility functions for hitbox calculations.
This file contains the Audio interface.
Multiplayer player controller system for R-Type client.
Class for managing entities and their components.
Definition Registry.hpp:25
T * getComponent(Entity e)
Definition Registry.hpp:71
GameMulti(eng::id assignedId, const std::shared_ptr< eng::IRenderer > &renderer, const std::shared_ptr< eng::IAudio > &audio, float skinIndex, bool &showDebug, uint32_t sessionId)
Constructor.
Definition gameMulti.cpp:16
void processEventBus()
Process pending events from the event bus.
std::unique_ptr< PlayerControllerMulti > m_playerController
Player input controller.
void event(const eng::Event &event) override
Handle input events.
void updatePlayerSkin()
Update the local player's visual skin/appearance.
void update(float dt, const eng::WindowSize &size) override
Update the game scene (called each frame)
std::unordered_set< std::string > m_loadedFonts
Cache of loaded font paths.
void handleWorldStateUpdate(const utl::Event &event)
Handle world state update from server.
void preloadCommonTextures()
Preload commonly used textures.
std::unordered_set< std::string > m_loadedTextures
Cache of loaded texture paths.
std::unique_ptr< StageManager > m_stageManager
Stage/level manager.
ecs::Entity m_beginSoundEntity
Entity for game start sound.
void updateInterpolation(std::unordered_map< uint32_t, InterpolationData > &dataMap, std::unordered_map< uint32_t, ecs::Entity > &entityMap, float smoothFactor, float dt, ecs::Registry &registry)
Update entity position interpolation.
std::unordered_map< uint32_t, uint32_t > m_playerSkinMap
Map of player session ID to skin index.
ecs::Entity m_localPlayerEntity
Local player entity.
void setupEventSubscriptions() const
Subscribe to event bus events.
std::unique_ptr< HUDSystem > m_hudSystem
HUD rendering system.
uint32_t m_sessionId
Local player session ID.
static void stopScrolling(ecs::Registry &registry)
Binary serializer for RNP protocol packets.
PacketWorldState deserializeWorldState()
Deserialize WORLD_STATE packet.
Thread-safe event bus for decoupled component communication.
Definition EventBus.hpp:31
bool publish(const Event &event)
Publish an event to the bus.
Definition EventBus.hpp:118
static EventBus & getInstance()
Get singleton instance of EventBus.
Definition EventBus.hpp:82
void registerComponent(std::uint32_t componentId, const std::string &name)
Register component name for better debugging.
Definition EventBus.hpp:423
void subscribe(std::uint32_t componentId, EventType type)
Subscribe component to specific event types.
Definition EventBus.hpp:384
Event structure for inter-component communication.
Definition Event.hpp:89
std::vector< std::uint8_t > data
Serialized event data.
Definition Event.hpp:96
static void log(const std::string &message, const LogLevel &logLevel)
Definition Logger.hpp:51
This file contains common definitions and constants.
std::uint32_t Entity
Definition Entity.hpp:13
unsigned int id
Definition IScene.hpp:20
constexpr float SPRITE_HEIGHT
constexpr float SPRITE_WIDTH
constexpr float ANIMATION_DURATION
constexpr float BOSS_RADIUS
constexpr float ENEMY_RADIUS
constexpr float SCALE
constexpr float SPRITE_HEIGHT
constexpr float SPRITE_WIDTH
constexpr auto AUDIO_BOSS
Definition Common.hpp:93
constexpr auto AUDIO_BEGIN
Definition Common.hpp:91
constexpr auto TEXTURE_SHOOT
Definition Common.hpp:115
constexpr auto TEXTURE_ENEMY_PROJECTILE
Definition Common.hpp:122
constexpr auto TEXTURE_PLAYER
Definition Common.hpp:114
constexpr auto TEXTURE_ENEMY_ADVANCED
Definition Common.hpp:120
constexpr auto TEXTURE_ENEMY_BASIC
Definition Common.hpp:119
constexpr auto TEXTURE_EXPLOSION
Definition Common.hpp:123
constexpr auto TEXTURE_SHOOT_CHARGED
Definition Common.hpp:116
constexpr auto TEXTURE_BOSS
Definition Common.hpp:121
std::pair< float, float > calculateHitboxOffsets(const float spriteWidth, const float spriteHeight, const float scale)
Calculate hitbox offsets to center the hitbox on the sprite.
static constexpr std::uint32_t NETWORK_CLIENT
Definition Event.hpp:17
std::string id
Definition Component.hpp:15
WORLD_STATE packet payload.
Definition Protocol.hpp:198
std::vector< EntityState > entities
Definition Protocol.hpp:201