/
yaseeeeechka
/
CenterControl
Обзор
Документация
Войти
/
yaseeeeechka
/
CenterControl
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
CenterControl_TCP/include/client/GameScreen.h
853 строки
31 KB
Yaroslava
New structure of Repo. TCP impl
11 дек 2025, 21:12
11 дек 2025, 21:12
6fdf9d7
Код
Авторство
О чём код?
#ifndef CENTERCONTROL_GAMESCREEN_H #define CENTERCONTROL_GAMESCREEN_H // ==================== GameScreen ====================================== class GameScreen : public IScreen { public: // Локальные ачивки одного матча enum class MatchAchievement { None, Leader, // ты лидер по очкам GoodScore, // очки > 0, но не лидер ZeroScore // дошёл до этого состояния с 0 очков }; struct PlayerVisualState { sf::Vector2f pos; // текущая позиция на экране sf::Vector2f targetPos; // целевая позиция (из GameToScreen) bool initialized = false; }; GameScreen(IScreenApp *app, const sf::Font &font) : app_(app), font_(font), title_(font, "Game", 24), info_txt_(font, "", 20), btn_exit_(sf::Vector2f(180.f, 40.f)), btn_exit_txt_(font, "Exit game", 18), chat_label_(font, "Room chat:", 18), chat_send_txt_(font, "Send", 16), quick_txt_hello_(font, "Hello!", 14), quick_txt_bye_(font, "Bye!", 14), quick_txt_go_(font, "Let's go!", 14), // === ACHIEVEMENTS: текст для ачивки achievement_text_(font, "", 20) { bg_.setSize({900, 720}); bg_.setFillColor(sf::Color(50, 50, 70)); title_.setFillColor(sf::Color::White); title_.setPosition({20.f, 20.f}); info_txt_.setFillColor(sf::Color::White); info_txt_.setPosition({20.f, 60.f}); // Немного сузим и сдвинем поле влево, чтобы справа была панель чата field_.setSize({560.f, 500.f}); field_.setFillColor(sf::Color(30, 30, 40)); field_.setOutlineColor(sf::Color(200, 200, 200)); field_.setOutlineThickness(2.f); field_.setPosition({20.f, 140.f}); // слева, оставили справа место под чат // Центральный круг захвата central_circle_.setFillColor(sf::Color(80, 80, 160, 60)); // полупрозрачный central_circle_.setOutlineColor(sf::Color(150, 150, 220, 150)); central_circle_.setOutlineThickness(2.f); // Аура толчка (видна только для локального игрока) push_field_circle_.setFillColor(sf::Color(0, 255, 0, 40)); push_field_circle_.setOutlineColor(sf::Color(0, 200, 0, 120)); push_field_circle_.setOutlineThickness(1.f); btn_exit_.setPosition({900.f - 180.f - 20.f, 20.f}); btn_exit_.setFillColor(sf::Color(180, 80, 80)); btn_exit_txt_.setFillColor(sf::Color::White); btn_exit_txt_.setPosition({ btn_exit_.getPosition().x + 20.f, btn_exit_.getPosition().y + 8.f }); player_circle_.setRadius(10.f); player_circle_.setFillColor(sf::Color::Green); // ---------- ЧАТ (общий с комнатой) ---------- float chatX = field_.getPosition().x + field_.getSize().x + 20.f; // правее поля float chatY = field_.getPosition().y; float chatW = 260.f; float chatH = 360.f; chat_bg_.setPosition({chatX, chatY}); chat_bg_.setSize({chatW, chatH}); chat_bg_.setFillColor(sf::Color(20, 20, 30, 220)); chat_bg_.setOutlineColor(sf::Color(180, 180, 180)); chat_bg_.setOutlineThickness(1.f); chat_label_.setFillColor(sf::Color::White); chat_label_.setPosition({chatX, chatY - 24.f}); // Поле ввода float inputY = chatY + chatH + 10.f; float inputX = chatX; float inputHeight = 30.f; float inputWidth = chatW - 100.f; // оставляем место под кнопку Send chat_input_ = std::make_unique<TextBox>(inputX, inputY, inputWidth, inputHeight, font_); chat_input_rect_ = sf::FloatRect({inputX, inputY}, {inputWidth, inputHeight}); // Кнопка Send chat_send_btn_.setSize({80.f, inputHeight}); chat_send_btn_.setPosition({inputX + inputWidth + 5.f, inputY}); chat_send_btn_.setFillColor(sf::Color(100, 150, 255)); chat_send_txt_.setFillColor(sf::Color::White); chat_send_txt_.setPosition({ chat_send_btn_.getPosition().x + 15.f, chat_send_btn_.getPosition().y + 4.f }); float quickY = inputY + 40.f; // размещаем ниже поля ввода, чтобы не перекрывать чат quick_btn_hello_.setSize({80.f, 30.f}); quick_btn_hello_.setPosition({chatX, quickY}); quick_btn_hello_.setFillColor(sf::Color(100, 150, 255)); quick_txt_hello_.setFillColor(sf::Color::White); quick_txt_hello_.setPosition({ quick_btn_hello_.getPosition().x + 8.f, quick_btn_hello_.getPosition().y + 5.f }); quick_btn_bye_.setSize({80.f, 30.f}); quick_btn_bye_.setPosition({chatX + 85.f, quickY}); quick_btn_bye_.setFillColor(sf::Color(100, 150, 255)); quick_txt_bye_.setFillColor(sf::Color::White); quick_txt_bye_.setPosition({ quick_btn_bye_.getPosition().x + 16.f, quick_btn_bye_.getPosition().y + 5.f }); quick_btn_go_.setSize({80.f, 30.f}); quick_btn_go_.setPosition({chatX + 170.f, quickY}); quick_btn_go_.setFillColor(sf::Color(100, 150, 255)); quick_txt_go_.setFillColor(sf::Color::White); quick_txt_go_.setPosition({ quick_btn_go_.getPosition().x + 4.f, quick_btn_go_.getPosition().y + 5.f }); // === ACHIEVEMENTS: оформление текста ачивки === achievement_text_.setFillColor(sf::Color::Yellow); achievement_text_.setOutlineColor(sf::Color::Black); achievement_text_.setOutlineThickness(2.f); // расположим под строкой info_txt_ achievement_text_.setPosition({20.f, 90.f}); } bool AllowWindowClose() const override { return false; } void OnShow() override { if (auto sess = app_->GetSession()) { // Раз мы уже зашли в игру, авто-возврат опять разрешён sess->SetSuppressAutoGame(false); room_name_ = sess->GetCurrentRoom(); // Подписываемся на тот же чат комнаты sess->SetChatCallback([this](const std::vector<ChatMessageDTO> &msgs) { chat_messages_ = msgs; }); if (!room_name_.empty()) { sess->RequestRoomChatHistory(room_name_); } sess->SendGameReady(room_name_); } spawn_pos_inited_ = false; countdown_ = 255; info_txt_.setString("Waiting for players..."); pollAccum_ = 0.f; chatPollAccum_ = 0.f; chat_messages_.clear(); chat_input_focused_ = false; is_spectator_ = false; my_score_ = 0; // === ACHIEVEMENTS: сброс на начало матча === match_achievement_ = MatchAchievement::None; achievement_text_.setString(""); // сбрасываем визуальные позиции players_visual_.clear(); keyUpHeld_ = keyDownHeld_ = keyLeftHeld_ = keyRightHeld_ = false; moveAccum_ = 0.f; } void HandleEvent(const sf::Event &e) override { // Сначала даём поработать TextBox-у if (chat_input_) { chat_input_->HandleEvent(e, app_->GetWindow()); } if (auto mb = e.getIf<sf::Event::MouseButtonPressed>()) { auto m = sf::Mouse::getPosition(app_->GetWindow()); sf::Vector2f mp{(float) m.x, (float) m.y}; // Фокус на поле ввода чата if (chat_input_rect_.contains(mp)) { chat_input_focused_ = true; } else if (!chat_send_btn_.getGlobalBounds().contains(mp) && !quick_btn_hello_.getGlobalBounds().contains(mp) && !quick_btn_bye_.getGlobalBounds().contains(mp) && !quick_btn_go_.getGlobalBounds().contains(mp)) { // клик вне элементов чата — снимаем фокус chat_input_focused_ = false; } // Кнопка выхода из игры if (btn_exit_.getGlobalBounds().contains(mp)) { if (auto sess = app_->GetSession()) { // Всегда выходим из игры sess->LeaveGame(room_name_); // Сигнал: сознательный выход, не нужно авто-возврата в GameScreen sess->SetSuppressAutoGame(true); // Если это наблюдатель — как и раньше, выходим и из комнаты if (is_spectator_ && !room_name_.empty()) { sess->LeaveRoom(room_name_); } } if (is_spectator_) { app_->SwitchScreen("main"); } else { app_->SwitchScreen("room"); } return; } // Отправка сообщения кнопкой Send if (chat_send_btn_.getGlobalBounds().contains(mp)) { if (auto sess = app_->GetSession()) { auto txt = chat_input_->GetText(); Trim(txt); if (!txt.empty() && !room_name_.empty()) { sess->SendRoomChatMessage(room_name_, txt); } chat_input_->Clear(); } return; } // Быстрые сообщения if (!room_name_.empty()) { if (quick_btn_hello_.getGlobalBounds().contains(mp)) { if (auto sess = app_->GetSession()) { sess->SendRoomChatMessage(room_name_, "Hello!"); } return; } else if (quick_btn_bye_.getGlobalBounds().contains(mp)) { if (auto sess = app_->GetSession()) { sess->SendRoomChatMessage(room_name_, "Bye!"); } return; } else if (quick_btn_go_.getGlobalBounds().contains(mp)) { if (auto sess = app_->GetSession()) { sess->SendRoomChatMessage(room_name_, "Let's go!"); } return; } } } if (auto key = e.getIf<sf::Event::KeyPressed>()) { using Sc = sf::Keyboard::Scancode; // Если фокус в TextBox — WASD не двигает игрока if (chat_input_focused_) { if (key->scancode == Sc::Enter) { if (auto sess = app_->GetSession()) { auto txt = chat_input_->GetText(); Trim(txt); if (!txt.empty() && !room_name_.empty()) { sess->SendRoomChatMessage(room_name_, txt); } chat_input_->Clear(); } } return; } // Ещё не заспавнились или игра не началась — игнорим управление if (!spawn_pos_inited_ || countdown_ != 0) { return; } auto sess = app_->GetSession(); if (!sess) return; MOVE_DIR dir = MOVE_DIR::UP; bool moveNow = false; bool doPush = false; switch (key->scancode) { case Sc::W: if (!keyUpHeld_) { // только при первом нажатии keyUpHeld_ = true; dir = MOVE_DIR::UP; moveNow = true; // один мгновенный шаг moveAccum_ = 0.f; // таймер для автодвижения с нуля } break; case Sc::S: if (!keyDownHeld_) { keyDownHeld_ = true; dir = MOVE_DIR::DOWN; moveNow = true; moveAccum_ = 0.f; } break; case Sc::A: if (!keyLeftHeld_) { keyLeftHeld_ = true; dir = MOVE_DIR::LEFT; moveNow = true; moveAccum_ = 0.f; } break; case Sc::D: if (!keyRightHeld_) { keyRightHeld_ = true; dir = MOVE_DIR::RIGHT; moveNow = true; moveAccum_ = 0.f; } break; case Sc::Space: doPush = true; break; default: break; } // Один мгновенный шаг на первое нажатие if (moveNow) { auto gs = sess->SendGameMove(room_name_, dir); if (gs.status == STATUS::RET_SUCCESS) { countdown_ = gs.countdown; players_ = std::move(gs.players); UpdateVisualTargets(); UpdateInfoText(); UpdateAchievement(); } } else if (doPush) { auto gs = sess->SendGamePush(room_name_); if (gs.status == STATUS::RET_SUCCESS || gs.status == STATUS::RET_ALREADY_LOGGED_IN) { countdown_ = gs.countdown; players_ = std::move(gs.players); UpdateVisualTargets(); UpdateInfoText(); UpdateAchievement(); } } } if (auto keyRel = e.getIf<sf::Event::KeyReleased>()) { using Sc = sf::Keyboard::Scancode; switch (keyRel->scancode) { case Sc::W: keyUpHeld_ = false; break; case Sc::S: keyDownHeld_ = false; break; case Sc::A: keyLeftHeld_ = false; break; case Sc::D: keyRightHeld_ = false; break; default: break; } } } void Update(float dt) override { // --- опрос состояния игры --- pollAccum_ += dt; if (pollAccum_ >= pollIntervalSec_) { pollAccum_ = 0.f; if (auto sess = app_->GetSession()) { auto gs = sess->QueryGameState(room_name_); if (gs.status == STATUS::RET_ERROR) { // В комнате больше нет активной игры if (is_spectator_) { app_->SwitchScreen("main"); } else { app_->SwitchScreen("room"); } return; } if (gs.status == STATUS::RET_SUCCESS) { // ты участник is_spectator_ = false; if (!spawn_pos_inited_) { spawn_index_ = gs.spawnIndex; UpdateSpawnPosition(); spawn_pos_inited_ = true; } countdown_ = gs.countdown; players_ = std::move(gs.players); UpdateVisualTargets(); const std::string currentUser = sess->GetCurrentUser(); int s = 0; for (const auto &p : players_) { if (p.user == currentUser) { s = p.score; break; } } my_score_ = s; UpdateInfoText(); UpdateAchievement(); // === ACHIEVEMENTS } else if (gs.status == STATUS::RET_ALREADY_LOGGED_IN) { // ты зритель is_spectator_ = true; countdown_ = gs.countdown; players_ = std::move(gs.players); UpdateVisualTargets(); const std::string currentUser = sess->GetCurrentUser(); int s = 0; for (const auto &p : players_) { if (p.user == currentUser) { s = p.score; break; } } my_score_ = s; UpdateInfoText(); UpdateAchievement(); // === ACHIEVEMENTS } } } // --- опрос истории чата комнаты --- chatPollAccum_ += dt; if (chatPollAccum_ >= chatPollIntervalSec_) { chatPollAccum_ = 0.f; if (auto sess = app_->GetSession()) { if (!room_name_.empty()) { sess->RequestRoomChatHistory(room_name_); } } } // непрерывное движение по удержанию WASD UpdateContinuousMovement(dt); // плавная интерполяция (то, что мы делали раньше) UpdateVisualPositions(dt); } void Render(sf::RenderWindow &w) override { w.draw(bg_); w.draw(title_); w.draw(info_txt_); // === ACHIEVEMENTS: рисуем, если есть if (match_achievement_ != MatchAchievement::None && !achievement_text_.getString().isEmpty()) { auto b = achievement_text_.getGlobalBounds(); // b.position, b.size sf::RectangleShape bg; bg.setPosition({ b.position.x - 6.f, b.position.y - 4.f }); bg.setSize({ b.size.x + 12.f, b.size.y + 8.f }); bg.setFillColor(sf::Color(0, 0, 0, 150)); w.draw(bg); w.draw(achievement_text_); } w.draw(field_); // масштаб из координат сервера в экранные float scaleX = field_.getSize().x / kServerFieldWidth; float scaleY = field_.getSize().y / kServerFieldHeight; float scale = std::min(scaleX, scaleY); // центральный круг float centerScreenRadius = kCenterCircleRadius * scale; central_circle_.setRadius(centerScreenRadius); central_circle_.setOrigin({centerScreenRadius, centerScreenRadius}); sf::Vector2f centerPos = GameToScreen(kCenterCircleX, kCenterCircleY); central_circle_.setPosition(centerPos); w.draw(central_circle_); w.draw(btn_exit_); w.draw(btn_exit_txt_); sf::Vector2f myPosScreen; bool haveMyPos = false; if (auto sess = app_->GetSession()) { const std::string currentUser = sess->GetCurrentUser(); for (const auto &p: players_) { // по умолчанию — позиция по данным сервера sf::Vector2f pos = GameToScreen(p.x, p.y); // если есть плавное состояние — используем его auto it = players_visual_.find(p.user); if (it != players_visual_.end() && it->second.initialized) { pos = it->second.pos; } sf::CircleShape circle; circle.setRadius(player_circle_.getRadius()); circle.setOrigin({player_circle_.getRadius(), player_circle_.getRadius()}); circle.setPosition(pos); if (p.user == currentUser) { circle.setFillColor(sf::Color::Green); myPosScreen = pos; haveMyPos = true; } else { circle.setFillColor(sf::Color::Red); } w.draw(circle); sf::Text nickText(title_.getFont(), p.user, 14); nickText.setFillColor(sf::Color::White); nickText.setPosition({pos.x - 10.f, pos.y - 25.f}); w.draw(nickText); } if (haveMyPos && countdown_ == 0 && !is_spectator_) { float pushRadiusScreen = kPushRadius * scale; push_field_circle_.setRadius(pushRadiusScreen); push_field_circle_.setOrigin({pushRadiusScreen, pushRadiusScreen}); push_field_circle_.setPosition(myPosScreen); w.draw(push_field_circle_); } } // -------- ЧАТ -------- w.draw(chat_label_); w.draw(chat_bg_); int maxLines = 14; int total = static_cast<int>(chat_messages_.size()); int start = std::max(0, total - maxLines); float cx = chat_bg_.getPosition().x + 6.f; float cy = chat_bg_.getPosition().y + 6.f; for (int i = start; i < total; ++i) { const auto &m = chat_messages_[i]; std::string line = m.user + ": " + m.text; sf::Text t(title_.getFont(), line, 14); t.setFillColor(sf::Color::White); t.setPosition({cx, cy}); w.draw(t); cy += 16.f; } if (chat_input_) chat_input_->Draw(w); w.draw(chat_send_btn_); w.draw(chat_send_txt_); w.draw(quick_btn_hello_); w.draw(quick_txt_hello_); w.draw(quick_btn_bye_); w.draw(quick_txt_bye_); w.draw(quick_btn_go_); w.draw(quick_txt_go_); } private: void UpdateInfoText() { std::string prefix = "Score: " + std::to_string(my_score_) + " "; if (countdown_ == 255) { info_txt_.setString(prefix + "Waiting for players..."); pollIntervalSec_ = 0.3f; } else if (countdown_ > 0) { info_txt_.setString(prefix + "Starting in " + std::to_string(countdown_) + "..."); pollIntervalSec_ = 0.3f; } else { info_txt_.setString(prefix + "GO!"); pollIntervalSec_ = 0.05f; } } // === ACHIEVEMENTS: вычисляем ачивку по текущему состоянию players_ === void UpdateAchievement() { match_achievement_ = MatchAchievement::None; achievement_text_.setString(""); auto sess = app_->GetSession(); if (!sess) return; const std::string &me = sess->GetCurrentUser(); if (me.empty() || players_.empty()) return; int myScore = 0; bool foundMe = false; int bestScore = std::numeric_limits<int>::min(); for (const auto &p : players_) { if (p.score > bestScore) bestScore = p.score; if (p.user == me) { myScore = p.score; foundMe = true; } } if (!foundMe) return; if (myScore == 0) { match_achievement_ = MatchAchievement::ZeroScore; achievement_text_.setString("You are incredible loser, still 0 points!"); } else if (myScore == bestScore) { match_achievement_ = MatchAchievement::Leader; achievement_text_.setString("You are the best player in PRUE and lead now!"); } else { match_achievement_ = MatchAchievement::GoodScore; achievement_text_.setString("Well, but you can better! Points: " + std::to_string(myScore)); } } void UpdateVisualTargets() { if (players_.empty()) { return; } for (const auto& p : players_) { sf::Vector2f target = GameToScreen(p.x, p.y); auto& v = players_visual_[p.user]; if (!v.initialized) { // первый раз видим игрока — ставим сразу в целевую позицию v.pos = target; v.targetPos = target; v.initialized = true; } else { // просто обновляем цель v.targetPos = target; } } // (опционально можно чистить игроков, которых больше нет в players_, // но это не критично и можно не делать) } void UpdateVisualPositions(float dt) { if (players_visual_.empty()) return; for (auto& [login, v] : players_visual_) { if (!v.initialized) continue; sf::Vector2f delta = v.targetPos - v.pos; float dist2 = delta.x * delta.x + delta.y * delta.y; if (dist2 < 0.01f) { v.pos = v.targetPos; continue; } float dist = std::sqrt(dist2); float maxStep = moveSpeed_ * dt; if (dist <= maxStep) { v.pos = v.targetPos; } else { // нормализуем направление и двигаемся с фиксированной скоростью delta.x /= dist; delta.y /= dist; v.pos.x += delta.x * maxStep; v.pos.y += delta.y * maxStep; } } } void UpdateSpawnPosition() { sf::Vector2f fieldPos = field_.getPosition(); float left = fieldPos.x; float top = fieldPos.y; float right = left + field_.getSize().x; float bottom = top + field_.getSize().y; sf::Vector2f pos; switch (spawn_index_) { case 0: // Bottom pos.x = (left + right) * 0.5f; pos.y = bottom - 20.f; break; case 1: // Left pos.x = left + 20.f; pos.y = (top + bottom) * 0.5f; break; case 2: // Top pos.x = (left + right) * 0.5f; pos.y = top + 20.f; break; case 3: // Right pos.x = right - 20.f; pos.y = (top + bottom) * 0.5f; break; default: pos.x = (left + right) * 0.5f; pos.y = bottom - 20.f; break; } player_circle_.setPosition({ pos.x - player_circle_.getRadius(), pos.y - player_circle_.getRadius() }); } void UpdateContinuousMovement(float dt) { // игра не началась — не двигаем if (!spawn_pos_inited_ || countdown_ != 0) return; if (chat_input_focused_) return; // пока печатаем — не едем auto sess = app_->GetSession(); if (!sess) return; moveAccum_ += dt; if (moveAccum_ < moveIntervalSec_) return; moveAccum_ = 0.f; MOVE_DIR dir; bool any = false; // Приоритет направления — на твой вкус, я сделал W > S > A > D if (keyUpHeld_) { dir = MOVE_DIR::UP; any = true; } else if (keyDownHeld_) { dir = MOVE_DIR::DOWN; any = true; } else if (keyLeftHeld_) { dir = MOVE_DIR::LEFT; any = true; } else if (keyRightHeld_) { dir = MOVE_DIR::RIGHT; any = true; } if (!any) return; auto gs = sess->SendGameMove(room_name_, dir); if (gs.status == STATUS::RET_SUCCESS) { countdown_ = gs.countdown; players_ = std::move(gs.players); UpdateVisualTargets(); UpdateInfoText(); UpdateAchievement(); } } static void Trim(std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); } // значения такие же, как на сервере static constexpr float kServerFieldWidth = static_cast<float>(kFieldWidth); static constexpr float kServerFieldHeight = static_cast<float>(kFieldHeight - kHeightUpToBottom); sf::Vector2f GameToScreen(float gx, float gy) const { sf::Vector2f fieldPos = field_.getPosition(); sf::Vector2f fieldSize = field_.getSize(); float nx = gx / kServerFieldWidth; // 0..1 float ny = gy / kServerFieldHeight; // 0..1 float sx = fieldPos.x + nx * fieldSize.x; float sy = fieldPos.y + ny * fieldSize.y; return {sx, sy}; } private: IScreenApp *app_; const sf::Font &font_; sf::RectangleShape bg_; sf::RectangleShape field_; sf::CircleShape player_circle_; sf::Text title_; sf::Text info_txt_; sf::RectangleShape btn_exit_; sf::Text btn_exit_txt_; std::string room_name_; bool spawn_pos_inited_ = false; uint8_t spawn_index_ = 0; uint8_t countdown_ = 255; float pollAccum_ = 0.f; float pollIntervalSec_ = 0.3f; std::vector<PlayerStateDTO> players_; bool is_spectator_ = false; sf::CircleShape central_circle_; sf::CircleShape push_field_circle_; int my_score_ = 0; // ---- ЧАТ комнаты на экране игры ---- sf::Text chat_label_; sf::RectangleShape chat_bg_; std::unique_ptr<TextBox> chat_input_; sf::RectangleShape chat_send_btn_; sf::Text chat_send_txt_; std::vector<ChatMessageDTO> chat_messages_; sf::RectangleShape quick_btn_hello_; sf::RectangleShape quick_btn_bye_; sf::RectangleShape quick_btn_go_; sf::Text quick_txt_hello_; sf::Text quick_txt_bye_; sf::Text quick_txt_go_; sf::FloatRect chat_input_rect_; bool chat_input_focused_ = false; float chatPollAccum_ = 0.f; const float chatPollIntervalSec_ = 0.7f; // === ACHIEVEMENTS: поля MatchAchievement match_achievement_ = MatchAchievement::None; sf::Text achievement_text_; // Плавная анимация игроков std::unordered_map<std::string, PlayerVisualState> players_visual_; float moveSpeed_ = 600.f; // пикселей в секунду // --- управление движением по удержанию клавиш --- bool keyUpHeld_ = false; bool keyDownHeld_ = false; bool keyLeftHeld_ = false; bool keyRightHeld_ = false; float moveAccum_ = 0.f; const float moveIntervalSec_ = 0.06f; // как часто слать шаг на сервер (можно 0.05–0.1) }; #endif //CENTERCONTROL_GAMESCREEN_H