main.cpp 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207
  1. // Copyright (c) 2022-2023 the Dviglo project
  2. // Copyright (c) 2008-2023 the Urho3D project
  3. // License: MIT
  4. /*
  5. Простой запуск игры - одиночная игра.
  6. При запуске с параметром "-nobgm" фоновая музыка будет отключена.
  7. С параметром "-server" игра запустится в режиме сервера.
  8. Сервер удобно запускать в виде консольного приложения, используя параметры "-server -headless".
  9. С параметром "-address 127.0.0.1" игра запустится в режиме клиента и
  10. подключится к серверу, запущенному на этом же компьютере.
  11. Также игра поддерживает геймпады и сенсорные экраны.
  12. Параметр "-touch" позволяет сэмулировать тачскрин на ПК.
  13. Друг другу игроки урон не наносят.
  14. */
  15. #include <Urho3D/Urho3DAll.h>
  16. #include "foot_steps.h"
  17. #include "light_flash.h"
  18. #include "ninja.h"
  19. #include "player.h"
  20. #include "potion.h"
  21. #include "snow_crate.h"
  22. #include "snowball.h"
  23. #include "utilities/network.h"
  24. #include "utilities/spawn.h"
  25. namespace Urho3D
  26. {
  27. static constexpr float MOUSE_SENSITIVITY = 0.125f;
  28. static constexpr float TOUCH_SENSITIVITY = 2.0f;
  29. static constexpr float JOY_SENSITIVITY = 0.5f;
  30. static constexpr float JOY_MOVE_DEAD_ZONE = 0.333f;
  31. static constexpr float JOY_LOOK_DEAD_ZONE = 0.05f;
  32. static constexpr float CAMERA_MIN_DIST = 0.25f;
  33. static constexpr float CAMERA_MAX_DIST = 5.0f;
  34. static constexpr float CAMERA_SAFETY_DIST = 0.3f;
  35. static constexpr i32 INITIAL_MAX_ENEMIES = 5;
  36. static constexpr i32 FINAL_MAX_ENEMIES = 25;
  37. static constexpr i32 MAX_POWERUPS = 5;
  38. static constexpr i32 INCREMENT_EACH = 10;
  39. static constexpr i32 PLAYER_HEALTH = 20;
  40. static constexpr float ENEMY_SPAWN_RATE = 1.0f;
  41. static constexpr float POWERUP_SPAWN_RATE = 15.0f;
  42. static constexpr float SPAWN_AREA_SIZE = 5.0f;
  43. class App : public Application
  44. {
  45. URHO3D_OBJECT(App, Application);
  46. private:
  47. SharedPtr<Scene> gameScene;
  48. SharedPtr<Node> gameCameraNode;
  49. Camera* gameCamera = nullptr;
  50. SharedPtr<Node> musicNode;
  51. SoundSource* musicSource = nullptr;
  52. SharedPtr<Text> scoreText;
  53. SharedPtr<Text> hiscoreText;
  54. SharedPtr<Text> messageText;
  55. SharedPtr<BorderImage> healthBar;
  56. SharedPtr<BorderImage> sight;
  57. Controls playerControls;
  58. Controls prevPlayerControls;
  59. bool singlePlayer = true;
  60. bool gameOn = false;
  61. bool drawDebug = false;
  62. bool drawOctreeDebug = false;
  63. i32 maxEnemies = 0;
  64. i32 incrementCounter = 0;
  65. float enemySpawnTimer = 0.f;
  66. float powerupSpawnTimer = 0.f;
  67. NodeId clientNodeID = 0;
  68. i32 clientScore = 0;
  69. i32 screenJoystickID = -1;
  70. i32 screenJoystickSettingsID = -1;
  71. bool touchEnabled = false;
  72. Vector<Player> players;
  73. Vector<HiscoreEntry> hiscores;
  74. public:
  75. App(Context* context)
  76. : Application(context)
  77. {
  78. Ninja::RegisterObject(context);
  79. FootSteps::RegisterObject(context);
  80. GameObject::RegisterObject(context);
  81. LightFlash::RegisterObject(context);
  82. Potion::RegisterObject(context);
  83. SnowCrate::RegisterObject(context);
  84. Snowball::RegisterObject(context);
  85. }
  86. void Setup() override
  87. {
  88. // Modify engine startup parameters
  89. engineParameters_[EP_WINDOW_TITLE] = "Ninja Snow Wars Native";
  90. engineParameters_[EP_LOG_NAME] = GetSubsystem<FileSystem>()->GetAppPreferencesDir("urho3d", "logs") + "80_ninja_snow_wars_native.log";
  91. //engineParameters_[EP_FULL_SCREEN] = false;
  92. }
  93. void Start() override
  94. {
  95. if (GetSubsystem<Engine>()->IsHeadless())
  96. OpenConsoleWindow();
  97. ParseNetworkArguments();
  98. if (runServer || runClient)
  99. singlePlayer = false;
  100. InitAudio();
  101. InitConsole();
  102. InitScene();
  103. InitNetworking();
  104. CreateCamera();
  105. CreateOverlays();
  106. SubscribeToEvent(gameScene, E_SCENEUPDATE, URHO3D_HANDLER(App, HandleUpdate));
  107. PhysicsWorld* physicsWorld = gameScene->GetComponent<PhysicsWorld>();
  108. if (physicsWorld)
  109. SubscribeToEvent(physicsWorld, E_PHYSICSPRESTEP, URHO3D_HANDLER(App, HandleFixedUpdate));
  110. SubscribeToEvent(gameScene, E_SCENEPOSTUPDATE, URHO3D_HANDLER(App, HandlePostUpdate));
  111. SubscribeToEvent(E_POSTRENDERUPDATE, URHO3D_HANDLER(App, HandlePostRenderUpdate));
  112. SubscribeToEvent(E_KEYDOWN, URHO3D_HANDLER(App, HandleKeyDown));
  113. SubscribeToEvent("Points", URHO3D_HANDLER(App, HandlePoints));
  114. SubscribeToEvent("Kill", URHO3D_HANDLER(App, HandleKill));
  115. SubscribeToEvent(E_SCREENMODE, URHO3D_HANDLER(App, HandleScreenMode));
  116. if (singlePlayer)
  117. {
  118. StartGame(nullptr);
  119. GetSubsystem<Engine>()->SetPauseMinimized(true);
  120. }
  121. }
  122. void InitAudio()
  123. {
  124. if (GetSubsystem<Engine>()->IsHeadless())
  125. return;
  126. Audio* audio = GetSubsystem<Audio>();
  127. // Lower mastervolumes slightly.
  128. audio->SetMasterGain(SOUND_MASTER, 0.75f);
  129. audio->SetMasterGain(SOUND_MUSIC, 0.9f);
  130. if (!nobgm)
  131. {
  132. Sound* musicFile = GetSubsystem<ResourceCache>()->GetResource<Sound>("Music/Ninja Gods.ogg");
  133. musicFile->SetLooped(true);
  134. // Note: the non-positional sound source component need to be attached to a node to become effective
  135. // Due to networked mode clearing the scene on connect, do not attach to the scene itself
  136. musicNode = new Node(context_);
  137. musicSource = musicNode->CreateComponent<SoundSource>();
  138. musicSource->SetSoundType(SOUND_MUSIC);
  139. musicSource->Play(musicFile);
  140. }
  141. }
  142. void InitConsole()
  143. {
  144. Engine* engine = GetSubsystem<Engine>();
  145. if (engine->IsHeadless())
  146. return;
  147. XMLFile* uiStyle = GetSubsystem<ResourceCache>()->GetResource<XMLFile>("UI/DefaultStyle.xml");
  148. GetSubsystem<UI>()->GetRoot()->SetDefaultStyle(uiStyle);
  149. Console* console = engine->CreateConsole();
  150. console->SetDefaultStyle(uiStyle);
  151. console->GetBackground()->SetOpacity(0.8f);
  152. DebugHud* debug_hud = engine->CreateDebugHud();
  153. debug_hud->SetDefaultStyle(uiStyle);
  154. }
  155. void InitScene()
  156. {
  157. gameScene = new Scene(context_);
  158. gameScene->SetName("NinjaSnowWar");
  159. // For the multiplayer client, do not load the scene, let it load from the server
  160. if (runClient)
  161. return;
  162. ResourceCache* cache = GetSubsystem<ResourceCache>();
  163. Renderer* renderer = GetSubsystem<Renderer>();
  164. Engine* engine = GetSubsystem<Engine>();
  165. Graphics* graphics = GetSubsystem<Graphics>();
  166. gameScene->LoadXML(*cache->GetFile("Scenes/NinjaSnowWar.xml"));
  167. // On mobile devices render the shadowmap first for better performance, adjust the cascaded shadows
  168. String platform = GetPlatform();
  169. if (platform == "Android" || platform == "iOS" || platform == "Raspberry Pi")
  170. {
  171. renderer->SetReuseShadowMaps(false);
  172. // Adjust the directional light shadow range slightly further, as only the first
  173. // cascade is supported
  174. Node* dirLightNode = gameScene->GetChild("GlobalLight", true);
  175. if (dirLightNode)
  176. {
  177. Light* dirLight = dirLightNode->GetComponent<Light>();
  178. dirLight->SetShadowCascade(CascadeParameters(15.0f, 0.0f, 0.0f, 0.0f, 0.9f));
  179. }
  180. }
  181. // Precache shaders if possible
  182. if (!engine->IsHeadless() && cache->Exists("NinjaSnowWarShaders.xml"))
  183. graphics->PrecacheShaders(*cache->GetFile("NinjaSnowWarShaders.xml"));
  184. }
  185. void InitNetworking()
  186. {
  187. Network* network = GetSubsystem<Network>();
  188. network->SetUpdateFps(25); // 1/4 of physics FPS
  189. // Remote events sent between client & server must be explicitly registered or else they are not allowed to be received
  190. network->RegisterRemoteEvent("PlayerSpawned");
  191. network->RegisterRemoteEvent("UpdateScore");
  192. network->RegisterRemoteEvent("UpdateHiscores");
  193. network->RegisterRemoteEvent("ParticleEffect");
  194. if (runServer)
  195. {
  196. network->StartServer(serverPort);
  197. // Disable physics interpolation to ensure clients get sent physically correct transforms
  198. gameScene->GetComponent<PhysicsWorld>()->SetInterpolation(false);
  199. SubscribeToEvent(E_CLIENTIDENTITY, URHO3D_HANDLER(App, HandleClientIdentity));
  200. SubscribeToEvent(E_CLIENTSCENELOADED, URHO3D_HANDLER(App, HandleClientSceneLoaded));
  201. SubscribeToEvent(E_CLIENTDISCONNECTED, URHO3D_HANDLER(App, HandleClientDisconnected));
  202. }
  203. if (runClient)
  204. {
  205. VariantMap identity;
  206. identity["UserName"] = userName;
  207. network->SetUpdateFps(50); // Increase controls send rate for better responsiveness
  208. network->Connect(serverAddress, serverPort, gameScene, identity);
  209. SubscribeToEvent("PlayerSpawned", URHO3D_HANDLER(App, HandlePlayerSpawned));
  210. SubscribeToEvent("UpdateScore", URHO3D_HANDLER(App, HandleUpdateScore));
  211. SubscribeToEvent("UpdateHiscores", URHO3D_HANDLER(App, HandleUpdateHiscores));
  212. SubscribeToEvent(E_NETWORKUPDATESENT, URHO3D_HANDLER(App, HandleNetworkUpdateSent));
  213. }
  214. }
  215. void InitTouchInput()
  216. {
  217. Input* input = GetSubsystem<Input>();
  218. ResourceCache* cache = GetSubsystem<ResourceCache>();
  219. touchEnabled = true;
  220. screenJoystickID = input->AddScreenJoystick(cache->GetResource<XMLFile>("UI/ScreenJoystick_NinjaSnowWar.xml"));
  221. }
  222. void CreateCamera()
  223. {
  224. // Note: the camera is not in the scene
  225. gameCameraNode = new Node(context_);
  226. gameCameraNode->SetPosition(Vector3(0.f, 2.f, -10.f));
  227. gameCamera = gameCameraNode->CreateComponent<Camera>();
  228. gameCamera->SetNearClip(0.5f);
  229. gameCamera->SetFarClip(160.f);
  230. Engine* engine = GetSubsystem<Engine>();
  231. Renderer* renderer = GetSubsystem<Renderer>();
  232. Audio* audio = GetSubsystem<Audio>();
  233. if (!engine->IsHeadless())
  234. {
  235. SharedPtr<Viewport> viewport(new Viewport(context_, gameScene, gameCamera));
  236. renderer->SetViewport(0, viewport);
  237. audio->SetListener(gameCameraNode->CreateComponent<SoundListener>());
  238. }
  239. }
  240. void CreateOverlays()
  241. {
  242. if (GetSubsystem<Engine>()->IsHeadless() || runServer)
  243. return;
  244. i32 height = GetSubsystem<Graphics>()->GetHeight() / 22;
  245. if (height > 64)
  246. height = 64;
  247. ResourceCache* cache = GetSubsystem<ResourceCache>();
  248. UI* ui = GetSubsystem<UI>();
  249. Input* input = GetSubsystem<Input>();
  250. sight = new BorderImage(context_);
  251. sight->SetTexture(cache->GetResource<Texture2D>("Textures/NinjaSnowWar/Sight.png"));
  252. sight->SetAlignment(HA_CENTER, VA_CENTER);
  253. sight->SetSize(height, height);
  254. ui->GetRoot()->AddChild(sight);
  255. Font* font = cache->GetResource<Font>("Fonts/BlueHighway.ttf");
  256. scoreText = new Text(context_);
  257. scoreText->SetFont(font, 13.f);
  258. scoreText->SetAlignment(HA_LEFT, VA_TOP);
  259. scoreText->SetPosition(5, 5);
  260. scoreText->SetColor(C_BOTTOMLEFT, Color(1.f, 1.f, 0.25f));
  261. scoreText->SetColor(C_BOTTOMRIGHT, Color(1.f, 1.f, 0.25f));
  262. ui->GetRoot()->AddChild(scoreText);
  263. hiscoreText = new Text(context_);
  264. hiscoreText->SetFont(font, 13.f);
  265. hiscoreText->SetAlignment(HA_RIGHT, VA_TOP);
  266. hiscoreText->SetPosition(-5, 5);
  267. hiscoreText->SetColor(C_BOTTOMLEFT, Color(1.f, 1.f, 0.25f));
  268. hiscoreText->SetColor(C_BOTTOMRIGHT, Color(1.f, 1.f, 0.25f));
  269. ui->GetRoot()->AddChild(hiscoreText);
  270. messageText = new Text(context_);
  271. messageText->SetFont(font, 13.f);
  272. messageText->SetAlignment(HA_CENTER, VA_CENTER);
  273. messageText->SetPosition(0, -height * 2);
  274. messageText->SetColor(Color(1.f, 0.f, 0.f));
  275. ui->GetRoot()->AddChild(messageText);
  276. SharedPtr<BorderImage> healthBorder(new BorderImage(context_));
  277. healthBorder->SetTexture(cache->GetResource<Texture2D>("Textures/NinjaSnowWar/HealthBarBorder.png"));
  278. healthBorder->SetAlignment(HA_CENTER, VA_TOP);
  279. healthBorder->SetPosition(0, 8);
  280. healthBorder->SetSize(120, 20);
  281. ui->GetRoot()->AddChild(healthBorder);
  282. healthBar = new BorderImage(context_);
  283. healthBar->SetTexture(cache->GetResource<Texture2D>("Textures/NinjaSnowWar/HealthBarInside.png"));
  284. healthBar->SetPosition(2, 2);
  285. healthBar->SetSize(116, 16);
  286. healthBorder->AddChild(healthBar);
  287. if (GetPlatform() == "Android" || GetPlatform() == "iOS")
  288. // On mobile platform, enable touch by adding a screen joystick
  289. InitTouchInput();
  290. else if (input->GetNumJoysticks() == 0)
  291. // On desktop platform, do not detect touch when we already got a joystick
  292. SubscribeToEvent(E_TOUCHBEGIN, URHO3D_HANDLER(App, HandleTouchBegin));
  293. }
  294. void SetMessage(const String& message)
  295. {
  296. if (messageText)
  297. messageText->SetText(message);
  298. }
  299. void StartGame(Connection* connection)
  300. {
  301. // Clear the scene of all existing scripted objects
  302. {
  303. Vector<Node*> scriptedNodes;
  304. for (Node* child : gameScene->GetChildren())
  305. {
  306. for (Component* component : child->GetComponents())
  307. {
  308. // Проверяем, что компонент - производный от GameObject
  309. GameObject* gameObject = dynamic_cast<GameObject*>(component);
  310. if (gameObject)
  311. scriptedNodes.Push(child);
  312. }
  313. }
  314. for (Node* node : scriptedNodes)
  315. node->Remove();
  316. }
  317. players.Clear();
  318. SpawnPlayer(connection);
  319. ResetAI();
  320. gameOn = true;
  321. maxEnemies = INITIAL_MAX_ENEMIES;
  322. incrementCounter = 0;
  323. enemySpawnTimer = 0;
  324. powerupSpawnTimer = 0;
  325. if (singlePlayer)
  326. {
  327. playerControls.yaw_ = 0;
  328. playerControls.pitch_ = 0;
  329. SetMessage("");
  330. }
  331. }
  332. void SpawnPlayer(Connection* connection)
  333. {
  334. Vector3 spawnPosition;
  335. if (singlePlayer)
  336. spawnPosition = Vector3(0.f, 0.97f, 0.f);
  337. else
  338. spawnPosition = Vector3(Random(SPAWN_AREA_SIZE) - SPAWN_AREA_SIZE * 0.5f, 0.97f, Random(SPAWN_AREA_SIZE) - SPAWN_AREA_SIZE);
  339. Node* playerNode = SpawnObject(gameScene, spawnPosition, Quaternion(), "ninja");
  340. // Set owner connection. Owned nodes are always updated to the owner at full frequency
  341. playerNode->SetOwner(connection);
  342. playerNode->SetName("Player");
  343. // Initialize variables
  344. Ninja* playerNinja = playerNode->GetComponent<Ninja>();
  345. playerNinja->health = playerNinja->maxHealth = PLAYER_HEALTH;
  346. playerNinja->side = SIDE_PLAYER;
  347. // Make sure the player can not shoot on first frame by holding the button down
  348. if (!connection)
  349. playerNinja->controls = playerNinja->prevControls = playerControls;
  350. else
  351. playerNinja->controls = playerNinja->prevControls = connection->GetControls();
  352. // Check if player entry already exists
  353. i32 playerIndex = -1;
  354. for (i32 i = 0; i < players.Size(); ++i)
  355. {
  356. if (players[i].connection == connection)
  357. {
  358. playerIndex = i;
  359. break;
  360. }
  361. }
  362. // Does not exist, create new
  363. if (playerIndex < 0)
  364. {
  365. playerIndex = players.Size();
  366. players.Resize(players.Size() + 1);
  367. players[playerIndex].connection = connection;
  368. if (connection)
  369. {
  370. players[playerIndex].name = connection->identity_["UserName"].GetString();
  371. // In multiplayer, send current hiscores to the new player
  372. SendHiscores(playerIndex);
  373. }
  374. else
  375. {
  376. players[playerIndex].name = "Player";
  377. // In singleplayer, create also the default hiscore entry immediately
  378. HiscoreEntry newHiscore;
  379. newHiscore.name = players[playerIndex].name;
  380. newHiscore.score = 0;
  381. hiscores.Push(newHiscore);
  382. }
  383. }
  384. players[playerIndex].nodeID = playerNode->GetID();
  385. players[playerIndex].score = 0;
  386. if (connection)
  387. {
  388. // In multiplayer, send initial score, then send a remote event that tells the spawned node's ID
  389. // It is important for the event to be in-order so that the node has been replicated first
  390. SendScore(playerIndex);
  391. VariantMap eventData;
  392. eventData["NodeID"] = playerNode->GetID();
  393. connection->SendRemoteEvent("PlayerSpawned", true, eventData);
  394. // Create name tag (Text3D component) for players in multiplayer
  395. Node* textNode = playerNode->CreateChild("NameTag");
  396. textNode->SetPosition(Vector3(0.f, 1.2f, 0.f));
  397. Text3D* text3D = textNode->CreateComponent<Text3D>();
  398. Font* font = GetSubsystem<ResourceCache>()->GetResource<Font>("Fonts/BlueHighway.ttf");
  399. text3D->SetFont(font, 19.f);
  400. text3D->SetColor(Color(1.f, 1.f, 0.f));
  401. text3D->SetText(players[playerIndex].name);
  402. text3D->SetHorizontalAlignment(HA_CENTER);
  403. text3D->SetVerticalAlignment(VA_CENTER);
  404. text3D->SetFaceCameraMode(FC_ROTATE_XYZ);
  405. }
  406. }
  407. void HandleUpdate(StringHash eventType, VariantMap& eventData)
  408. {
  409. float timeStep = eventData["TimeStep"].GetFloat();
  410. UpdateControls();
  411. CheckEndAndRestart();
  412. if (GetSubsystem<Engine>()->IsHeadless())
  413. {
  414. String command = GetConsoleInput();
  415. if (command.Length() > 0)
  416. GetSubsystem<Script>()->Execute(command);
  417. }
  418. else
  419. {
  420. DebugHud* debugHud = GetSubsystem<DebugHud>();
  421. if (debugHud->GetMode() != DebugHudElements::None)
  422. {
  423. Node* playerNode = FindOwnNode();
  424. if (playerNode)
  425. {
  426. debugHud->SetAppStats("Player Pos", playerNode->GetWorldPosition().ToString());
  427. debugHud->SetAppStats("Player Yaw", Variant(playerNode->GetWorldRotation().YawAngle()));
  428. }
  429. else
  430. {
  431. debugHud->ClearAppStats();
  432. }
  433. }
  434. }
  435. }
  436. void HandleFixedUpdate(StringHash eventType, VariantMap& eventData)
  437. {
  438. float timeStep = eventData["TimeStep"].GetFloat();
  439. // Spawn new objects, singleplayer or server only
  440. if (singlePlayer || runServer)
  441. SpawnObjects(timeStep);
  442. }
  443. void HandlePostUpdate(StringHash eventType, VariantMap& eventData)
  444. {
  445. UpdateCamera();
  446. UpdateStatus();
  447. }
  448. void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
  449. {
  450. if (GetSubsystem<Engine>()->IsHeadless())
  451. return;
  452. if (drawDebug)
  453. gameScene->GetComponent<PhysicsWorld>()->DrawDebugGeometry(true);
  454. if (drawOctreeDebug)
  455. gameScene->GetComponent<Octree>()->DrawDebugGeometry(true);
  456. }
  457. void HandleTouchBegin(StringHash eventType, VariantMap& eventData)
  458. {
  459. // On some platforms like Windows the presence of touch input can only be detected dynamically
  460. InitTouchInput();
  461. UnsubscribeFromEvent("TouchBegin");
  462. }
  463. void HandleKeyDown(StringHash eventType, VariantMap& eventData)
  464. {
  465. Console* console = GetSubsystem<Console>();
  466. DebugHud* debugHud = GetSubsystem<DebugHud>();
  467. Engine* engine = GetSubsystem<Engine>();
  468. Graphics* graphics = GetSubsystem<Graphics>();
  469. FileSystem* fileSystem = GetSubsystem<FileSystem>();
  470. Time* time = GetSubsystem<Time>();
  471. Audio* audio = GetSubsystem<Audio>();
  472. Input* input = GetSubsystem<Input>();
  473. ResourceCache* cache = GetSubsystem<ResourceCache>();
  474. i32 key = eventData["Key"].GetI32();
  475. if (key == KEY_ESCAPE)
  476. {
  477. if (!console->IsVisible())
  478. engine->Exit();
  479. else
  480. console->SetVisible(false);
  481. }
  482. else if (key == KEY_F1)
  483. console->Toggle();
  484. else if (key == KEY_F2)
  485. debugHud->ToggleAll();
  486. else if (key == KEY_F3)
  487. drawDebug = !drawDebug;
  488. else if (key == KEY_F4)
  489. drawOctreeDebug = !drawOctreeDebug;
  490. else if (key == KEY_F5)
  491. debugHud->Toggle(DebugHudElements::EventProfiler);
  492. // Take screenshot
  493. else if (key == KEY_F6)
  494. {
  495. Image screenshot(context_);
  496. graphics->TakeScreenShot(screenshot);
  497. // Here we save in the Data folder with date and time appended
  498. screenshot.SavePNG(fileSystem->GetProgramDir() + "Data/Screenshot_" +
  499. time->GetTimeStamp().Replaced(':', '_').Replaced('.', '_').Replaced(' ', '_') + ".png");
  500. }
  501. // Allow pause only in singleplayer
  502. if (key == KEY_P && singlePlayer && !console->IsVisible() && gameOn)
  503. {
  504. gameScene->SetUpdateEnabled(!gameScene->IsUpdateEnabled());
  505. if (!gameScene->IsUpdateEnabled())
  506. {
  507. SetMessage("PAUSED");
  508. audio->PauseSoundType(SOUND_EFFECT);
  509. // Open the settings joystick only if the controls screen joystick was already open
  510. if (screenJoystickID >= 0)
  511. {
  512. // Lazy initialization
  513. if (screenJoystickSettingsID < 0)
  514. screenJoystickSettingsID = input->AddScreenJoystick(cache->GetResource<XMLFile>("UI/ScreenJoystickSettings_NinjaSnowWar.xml"));
  515. else
  516. input->SetScreenJoystickVisible(screenJoystickSettingsID, true);
  517. }
  518. }
  519. else
  520. {
  521. SetMessage("");
  522. audio->ResumeSoundType(SOUND_EFFECT);
  523. // Hide the settings joystick
  524. if (screenJoystickSettingsID >= 0)
  525. input->SetScreenJoystickVisible(screenJoystickSettingsID, false);
  526. }
  527. }
  528. }
  529. void HandlePoints(StringHash eventType, VariantMap& eventData)
  530. {
  531. if (eventData["DamageSide"].GetI32() == SIDE_PLAYER)
  532. {
  533. // Get node ID of the object that should receive points -> use it to find player index
  534. i32 playerIndex = FindPlayerIndex(eventData["Receiver"].GetI32());
  535. if (playerIndex >= 0)
  536. {
  537. players[playerIndex].score += eventData["Points"].GetI32();
  538. SendScore(playerIndex);
  539. bool newHiscore = CheckHiscore(playerIndex);
  540. if (newHiscore)
  541. SendHiscores(-1);
  542. }
  543. }
  544. }
  545. void HandleKill(StringHash eventType, VariantMap& eventData)
  546. {
  547. if (eventData["DamageSide"].GetI32() == SIDE_PLAYER)
  548. {
  549. MakeAIHarder();
  550. // Increment amount of simultaneous enemies after enough kills
  551. incrementCounter++;
  552. if (incrementCounter >= INCREMENT_EACH)
  553. {
  554. incrementCounter = 0;
  555. if (maxEnemies < FINAL_MAX_ENEMIES)
  556. maxEnemies++;
  557. }
  558. }
  559. }
  560. void HandleClientIdentity(StringHash eventType, VariantMap& eventData)
  561. {
  562. Connection* connection = (Connection*)GetEventSender();
  563. // If user has empty name, invent one
  564. if (connection->identity_["UserName"].GetString().Trimmed().Empty())
  565. connection->identity_["UserName"] = "user" + String(Random(1000));
  566. // Assign scene to begin replicating it to the client
  567. connection->SetScene(gameScene);
  568. }
  569. void HandleClientSceneLoaded(StringHash eventType, VariantMap& eventData)
  570. {
  571. // Now client is actually ready to begin. If first player, clear the scene and restart the game
  572. Connection* connection = (Connection*)GetEventSender();
  573. if (players.Empty())
  574. StartGame(connection);
  575. else
  576. SpawnPlayer(connection);
  577. }
  578. void HandleClientDisconnected(StringHash eventType, VariantMap& eventData)
  579. {
  580. Connection* connection = (Connection*)GetEventSender();
  581. // Erase the player entry, and make the player's ninja commit seppuku (if exists)
  582. for (i32 i = 0; i < players.Size(); ++i)
  583. {
  584. if (players[i].connection == connection)
  585. {
  586. players[i].connection = nullptr;
  587. Node* playerNode = FindPlayerNode(i);
  588. if (playerNode)
  589. {
  590. Ninja* playerNinja = playerNode->GetComponent<Ninja>();
  591. playerNinja->health = 0;
  592. playerNinja->lastDamageSide = SIDE_NEUTRAL; // No-one scores from this
  593. }
  594. players.Erase(i);
  595. return;
  596. }
  597. }
  598. }
  599. void HandlePlayerSpawned(StringHash eventType, VariantMap& eventData)
  600. {
  601. // Store our node ID and mark the game as started
  602. clientNodeID = eventData["NodeID"].GetI32();
  603. gameOn = true;
  604. SetMessage("");
  605. // Copy initial yaw from the player node (we should have it replicated now)
  606. Node* playerNode = FindOwnNode();
  607. if (playerNode)
  608. {
  609. playerControls.yaw_ = playerNode->GetRotation().YawAngle();
  610. playerControls.pitch_ = 0.f;
  611. // Disable the nametag from own character
  612. Node* nameTag = playerNode->GetChild("NameTag");
  613. nameTag->SetEnabled(false);
  614. }
  615. }
  616. void HandleUpdateScore(StringHash eventType, VariantMap& eventData)
  617. {
  618. clientScore = eventData["Score"].GetI32();
  619. scoreText->SetText("Score " + String(clientScore));
  620. }
  621. void HandleUpdateHiscores(StringHash eventType, VariantMap& eventData)
  622. {
  623. VectorBuffer data(eventData["Hiscores"].GetBuffer());
  624. hiscores.Resize(data.ReadVLE());
  625. for (i32 i = 0; i < hiscores.Size(); ++i)
  626. {
  627. hiscores[i].name = data.ReadString();
  628. hiscores[i].score = data.ReadI32();
  629. }
  630. String allHiscores;
  631. for (i32 i = 0; i < hiscores.Size(); ++i)
  632. allHiscores += hiscores[i].name + " " + String(hiscores[i].score) + "\n";
  633. hiscoreText->SetText(allHiscores);
  634. }
  635. void HandleNetworkUpdateSent(StringHash eventType, VariantMap& eventData)
  636. {
  637. Network* network = GetSubsystem<Network>();
  638. // Clear accumulated buttons from the network controls
  639. if (network->GetServerConnection() != nullptr)
  640. network->GetServerConnection()->controls_.Set(CTRL_ALL, false);
  641. }
  642. i32 FindPlayerIndex(NodeId nodeID)
  643. {
  644. for (i32 i = 0; i < players.Size(); ++i)
  645. {
  646. if (players[i].nodeID == nodeID)
  647. return i;
  648. }
  649. return -1;
  650. }
  651. Node* FindPlayerNode(i32 playerIndex)
  652. {
  653. if (playerIndex >= 0 && playerIndex < players.Size())
  654. return gameScene->GetNode(players[playerIndex].nodeID);
  655. else
  656. return nullptr;
  657. }
  658. Node* FindOwnNode()
  659. {
  660. if (singlePlayer)
  661. return gameScene->GetChild("Player", true);
  662. else
  663. return gameScene->GetNode(clientNodeID);
  664. }
  665. bool CheckHiscore(i32 playerIndex)
  666. {
  667. for (i32 i = 0; i < hiscores.Size(); ++i)
  668. {
  669. if (hiscores[i].name == players[playerIndex].name)
  670. {
  671. if (players[playerIndex].score > hiscores[i].score)
  672. {
  673. hiscores[i].score = players[playerIndex].score;
  674. SortHiscores();
  675. return true;
  676. }
  677. else
  678. return false; // No update to individual hiscore
  679. }
  680. }
  681. // Not found, create new hiscore entry
  682. HiscoreEntry newHiscore;
  683. newHiscore.name = players[playerIndex].name;
  684. newHiscore.score = players[playerIndex].score;
  685. hiscores.Push(newHiscore);
  686. SortHiscores();
  687. return true;
  688. }
  689. void SortHiscores()
  690. {
  691. for (i32 i = 1; i < hiscores.Size(); ++i)
  692. {
  693. HiscoreEntry temp = hiscores[i];
  694. i32 j = i;
  695. while (j > 0 && temp.score > hiscores[j - 1].score)
  696. {
  697. hiscores[j] = hiscores[j - 1];
  698. --j;
  699. }
  700. hiscores[j] = temp;
  701. }
  702. }
  703. void SendScore(i32 playerIndex)
  704. {
  705. if (!runServer || playerIndex < 0 || playerIndex >= players.Size())
  706. return;
  707. VariantMap eventData;
  708. eventData["Score"] = players[playerIndex].score;
  709. players[playerIndex].connection->SendRemoteEvent("UpdateScore", true, eventData);
  710. }
  711. void SendHiscores(int playerIndex)
  712. {
  713. if (!runServer)
  714. return;
  715. VectorBuffer data;
  716. data.WriteVLE(hiscores.Size());
  717. for (i32 i = 0; i < hiscores.Size(); ++i)
  718. {
  719. data.WriteString(hiscores[i].name);
  720. data.WriteI32(hiscores[i].score);
  721. }
  722. VariantMap eventData;
  723. eventData["Hiscores"] = data;
  724. if (playerIndex >= 0 && playerIndex < players.Size())
  725. players[playerIndex].connection->SendRemoteEvent("UpdateHiscores", true, eventData);
  726. else
  727. GetSubsystem<Network>()->BroadcastRemoteEvent(gameScene, "UpdateHiscores", true, eventData); // Send to all in scene
  728. }
  729. void SpawnObjects(float timeStep)
  730. {
  731. // If game not running, run only the random generator
  732. if (!gameOn)
  733. {
  734. Random();
  735. return;
  736. }
  737. // Spawn powerups
  738. powerupSpawnTimer += timeStep;
  739. if (powerupSpawnTimer >= POWERUP_SPAWN_RATE)
  740. {
  741. powerupSpawnTimer = 0;
  742. i32 numPowerups = gameScene->GetChildrenWithComponent("SnowCrate", true).Size() + gameScene->GetChildrenWithComponent("Potion", true).Size();
  743. if (numPowerups < MAX_POWERUPS)
  744. {
  745. const float maxOffset = 40.f;
  746. float xOffset = Random(maxOffset * 2.0f) - maxOffset;
  747. float zOffset = Random(maxOffset * 2.0f) - maxOffset;
  748. SpawnObject(gameScene, Vector3(xOffset, 50.f, zOffset), Quaternion(), "snow_crate");
  749. }
  750. }
  751. // Spawn enemies
  752. enemySpawnTimer += timeStep;
  753. if (enemySpawnTimer > ENEMY_SPAWN_RATE)
  754. {
  755. enemySpawnTimer = 0;
  756. i32 numEnemies = 0;
  757. Vector<Node*> ninjaNodes = gameScene->GetChildrenWithComponent("Ninja", true);
  758. for (i32 i = 0; i < ninjaNodes.Size(); ++i)
  759. {
  760. Ninja* ninja = ninjaNodes[i]->GetComponent<Ninja>();
  761. if (ninja->side == SIDE_ENEMY)
  762. ++numEnemies;
  763. }
  764. if (numEnemies < maxEnemies)
  765. {
  766. const float maxOffset = 40.f;
  767. float offset = Random(maxOffset * 2.0f) - maxOffset;
  768. // Random north/east/south/west direction
  769. i32 dir = Rand() & 3;
  770. dir *= 90;
  771. Quaternion rotation(0.f, (float)dir, 0.f);
  772. Node* enemyNode = SpawnObject(gameScene, rotation * Vector3(offset, 10.f, -120.f), rotation, "ninja");
  773. // Initialize variables
  774. Ninja* enemyNinja = enemyNode->GetComponent<Ninja>();
  775. enemyNinja->side = SIDE_ENEMY;
  776. enemyNinja->controller.reset(new AIController());
  777. RigidBody* enemyBody = enemyNode->GetComponent<RigidBody>();
  778. enemyBody->SetLinearVelocity(rotation * Vector3(0.f, 10.f, 30.f));
  779. }
  780. }
  781. }
  782. void CheckEndAndRestart()
  783. {
  784. // Only check end of game if singleplayer or client
  785. if (runServer)
  786. return;
  787. // Check if player node has vanished
  788. Node* playerNode = FindOwnNode();
  789. if (gameOn && !playerNode)
  790. {
  791. gameOn = false;
  792. SetMessage("Press Fire or Jump to restart!");
  793. return;
  794. }
  795. // Check for restart (singleplayer only)
  796. if (!gameOn && singlePlayer && playerControls.IsPressed(CTRL_FIRE | CTRL_JUMP, prevPlayerControls))
  797. StartGame(nullptr);
  798. }
  799. void UpdateControls()
  800. {
  801. Input* input = GetSubsystem<Input>();
  802. Graphics* graphics = GetSubsystem<Graphics>();
  803. Console* console = GetSubsystem<Console>();
  804. Network* network = GetSubsystem<Network>();
  805. if (singlePlayer || runClient)
  806. {
  807. prevPlayerControls = playerControls;
  808. playerControls.Set(CTRL_ALL, false);
  809. if (touchEnabled)
  810. {
  811. for (i32 i = 0; i < input->GetNumTouches(); ++i)
  812. {
  813. TouchState* touch = input->GetTouch(i);
  814. if (!touch->touchedElement_)
  815. {
  816. // Touch on empty space
  817. playerControls.yaw_ += TOUCH_SENSITIVITY * gameCamera->GetFov() / graphics->GetHeight() * touch->delta_.x_;
  818. playerControls.pitch_ += TOUCH_SENSITIVITY * gameCamera->GetFov() / graphics->GetHeight() * touch->delta_.y_;
  819. }
  820. }
  821. }
  822. if (input->GetNumJoysticks() > 0)
  823. {
  824. JoystickState* joystick = touchEnabled ? input->GetJoystick(screenJoystickID) : input->GetJoystickByIndex(0);
  825. if (joystick->GetNumButtons() > 0)
  826. {
  827. if (joystick->GetButtonDown(0))
  828. playerControls.Set(CTRL_JUMP, true);
  829. if (joystick->GetButtonDown(1))
  830. playerControls.Set(CTRL_FIRE, true);
  831. if (joystick->GetNumButtons() >= 6)
  832. {
  833. if (joystick->GetButtonDown(4))
  834. playerControls.Set(CTRL_JUMP, true);
  835. if (joystick->GetButtonDown(5))
  836. playerControls.Set(CTRL_FIRE, true);
  837. }
  838. if (joystick->GetNumHats() > 0)
  839. {
  840. if ((joystick->GetHatPosition(0) & HAT_LEFT) != 0)
  841. playerControls.Set(CTRL_LEFT, true);
  842. if ((joystick->GetHatPosition(0) & HAT_RIGHT) != 0)
  843. playerControls.Set(CTRL_RIGHT, true);
  844. if ((joystick->GetHatPosition(0) & HAT_UP) != 0)
  845. playerControls.Set(CTRL_UP, true);
  846. if ((joystick->GetHatPosition(0) & HAT_DOWN) != 0)
  847. playerControls.Set(CTRL_DOWN, true);
  848. }
  849. if (joystick->GetNumAxes() >= 2)
  850. {
  851. if (joystick->GetAxisPosition(0) < -JOY_MOVE_DEAD_ZONE)
  852. playerControls.Set(CTRL_LEFT, true);
  853. if (joystick->GetAxisPosition(0) > JOY_MOVE_DEAD_ZONE)
  854. playerControls.Set(CTRL_RIGHT, true);
  855. if (joystick->GetAxisPosition(1) < -JOY_MOVE_DEAD_ZONE)
  856. playerControls.Set(CTRL_UP, true);
  857. if (joystick->GetAxisPosition(1) > JOY_MOVE_DEAD_ZONE)
  858. playerControls.Set(CTRL_DOWN, true);
  859. }
  860. if (joystick->GetNumAxes() >= 4)
  861. {
  862. float lookX = joystick->GetAxisPosition(2);
  863. float lookY = joystick->GetAxisPosition(3);
  864. if (lookX < -JOY_LOOK_DEAD_ZONE)
  865. playerControls.yaw_ -= JOY_SENSITIVITY * lookX * lookX;
  866. if (lookX > JOY_LOOK_DEAD_ZONE)
  867. playerControls.yaw_ += JOY_SENSITIVITY * lookX * lookX;
  868. if (lookY < -JOY_LOOK_DEAD_ZONE)
  869. playerControls.pitch_ -= JOY_SENSITIVITY * lookY * lookY;
  870. if (lookY > JOY_LOOK_DEAD_ZONE)
  871. playerControls.pitch_ += JOY_SENSITIVITY * lookY * lookY;
  872. }
  873. }
  874. }
  875. // For the triggered actions (fire & jump) check also for press, in case the FPS is low
  876. // and the key was already released
  877. if (!console || !console->IsVisible())
  878. {
  879. if (input->GetKeyDown(KEY_W))
  880. playerControls.Set(CTRL_UP, true);
  881. if (input->GetKeyDown(KEY_S))
  882. playerControls.Set(CTRL_DOWN, true);
  883. if (input->GetKeyDown(KEY_A))
  884. playerControls.Set(CTRL_LEFT, true);
  885. if (input->GetKeyDown(KEY_D))
  886. playerControls.Set(CTRL_RIGHT, true);
  887. if (input->GetKeyDown(KEY_LCTRL) || input->GetKeyPress(KEY_LCTRL))
  888. playerControls.Set(CTRL_FIRE, true);
  889. if (input->GetKeyDown(KEY_SPACE) || input->GetKeyPress(KEY_SPACE))
  890. playerControls.Set(CTRL_JUMP, true);
  891. if (input->GetMouseButtonDown(MOUSEB_LEFT) || input->GetMouseButtonPress(MOUSEB_LEFT))
  892. playerControls.Set(CTRL_FIRE, true);
  893. if (input->GetMouseButtonDown(MOUSEB_RIGHT) || input->GetMouseButtonPress(MOUSEB_RIGHT))
  894. playerControls.Set(CTRL_JUMP, true);
  895. playerControls.yaw_ += MOUSE_SENSITIVITY * input->GetMouseMoveX();
  896. playerControls.pitch_ += MOUSE_SENSITIVITY * input->GetMouseMoveY();
  897. playerControls.pitch_ = Clamp(playerControls.pitch_, -60.0f, 60.0f);
  898. }
  899. // In singleplayer, set controls directly on the player's ninja. In multiplayer, transmit to server
  900. if (singlePlayer)
  901. {
  902. Node* playerNode = gameScene->GetChild("Player", true);
  903. if (playerNode)
  904. {
  905. Ninja* playerNinja = playerNode->GetComponent<Ninja>();
  906. playerNinja->controls = playerControls;
  907. }
  908. }
  909. else if (network->GetServerConnection() != nullptr)
  910. {
  911. // Set the latest yaw & pitch to server controls, and accumulate the buttons so that we do not miss any presses
  912. network->GetServerConnection()->controls_.yaw_ = playerControls.yaw_;
  913. network->GetServerConnection()->controls_.pitch_ = playerControls.pitch_;
  914. network->GetServerConnection()->controls_.buttons_ |= playerControls.buttons_;
  915. // Tell the camera position to server for interest management
  916. network->GetServerConnection()->SetPosition(gameCameraNode->GetWorldPosition());
  917. }
  918. }
  919. if (runServer)
  920. {
  921. // Apply each connection's controls to the ninja they control
  922. for (i32 i = 0; i < players.Size(); ++i)
  923. {
  924. Node* playerNode = FindPlayerNode(i);
  925. if (playerNode)
  926. {
  927. Ninja* playerNinja = playerNode->GetComponent<Ninja>();
  928. playerNinja->controls = players[i].connection->controls_;
  929. }
  930. else
  931. {
  932. // If player has no ninja, respawn if fire/jump is pressed
  933. if (players[i].connection->controls_.IsPressed(CTRL_FIRE | CTRL_JUMP, players[i].lastControls))
  934. SpawnPlayer(players[i].connection);
  935. }
  936. players[i].lastControls = players[i].connection->controls_;
  937. }
  938. }
  939. }
  940. void UpdateCamera()
  941. {
  942. if (GetSubsystem<Engine>()->IsHeadless())
  943. return;
  944. // On the server, use a simple freelook camera
  945. if (runServer)
  946. {
  947. UpdateFreelookCamera();
  948. return;
  949. }
  950. Node* playerNode = FindOwnNode();
  951. if (!playerNode)
  952. return;
  953. Vector3 pos = playerNode->GetPosition();
  954. Quaternion dir;
  955. // Make controls seem more immediate by forcing the current mouse yaw to player ninja's Y-axis rotation
  956. if (playerNode->GetVar("Health").GetI32() > 0)
  957. playerNode->SetRotation(Quaternion(0.f, playerControls.yaw_, 0.f));
  958. dir = dir * Quaternion(playerNode->GetRotation().YawAngle(), Vector3(0.f, 1.f, 0.f));
  959. dir = dir * Quaternion(playerControls.pitch_, Vector3(1.f, 0.f, 0.f));
  960. Vector3 aimPoint = pos + Vector3(0.f, 1.f, 0.f);
  961. Vector3 minDist = aimPoint + dir * Vector3(0.f, 0.f, -CAMERA_MIN_DIST);
  962. Vector3 maxDist = aimPoint + dir * Vector3(0.f, 0.f, -CAMERA_MAX_DIST);
  963. // Collide camera ray with static objects (collision mask 2)
  964. Vector3 rayDir = (maxDist - minDist).Normalized();
  965. float rayDistance = CAMERA_MAX_DIST - CAMERA_MIN_DIST + CAMERA_SAFETY_DIST;
  966. PhysicsRaycastResult result;
  967. gameScene->GetComponent<PhysicsWorld>()->RaycastSingle(result, Ray(minDist, rayDir), rayDistance, 2);
  968. if (result.body_)
  969. rayDistance = Min(rayDistance, result.distance_ - CAMERA_SAFETY_DIST);
  970. gameCameraNode->SetPosition(minDist + rayDir * rayDistance);
  971. gameCameraNode->SetRotation(dir);
  972. }
  973. void UpdateFreelookCamera()
  974. {
  975. Console* console = GetSubsystem<Console>();
  976. Time* time = GetSubsystem<Time>();
  977. Input* input = GetSubsystem<Input>();
  978. if (!console || !console->IsVisible())
  979. {
  980. float timeStep = time->GetTimeStep();
  981. float speedMultiplier = 1.0f;
  982. if (input->GetKeyDown(KEY_LSHIFT))
  983. speedMultiplier = 5.0f;
  984. if (input->GetKeyDown(KEY_LCTRL))
  985. speedMultiplier = 0.1f;
  986. if (input->GetKeyDown(KEY_W))
  987. gameCameraNode->Translate(Vector3(0.f, 0.f, 10.f) * timeStep * speedMultiplier);
  988. if (input->GetKeyDown(KEY_S))
  989. gameCameraNode->Translate(Vector3(0.f, 0.f, -10.f) * timeStep * speedMultiplier);
  990. if (input->GetKeyDown(KEY_A))
  991. gameCameraNode->Translate(Vector3(-10.f, 0.f, 0.f) * timeStep * speedMultiplier);
  992. if (input->GetKeyDown(KEY_D))
  993. gameCameraNode->Translate(Vector3(10.f, 0.f, 0.f) * timeStep * speedMultiplier);
  994. playerControls.yaw_ += MOUSE_SENSITIVITY * input->GetMouseMoveX();
  995. playerControls.pitch_ += MOUSE_SENSITIVITY * input->GetMouseMoveY();
  996. playerControls.pitch_ = Clamp(playerControls.pitch_, -90.0f, 90.0f);
  997. gameCameraNode->SetRotation(Quaternion(playerControls.pitch_, playerControls.yaw_, 0.f));
  998. }
  999. }
  1000. void UpdateStatus()
  1001. {
  1002. Engine* engine = GetSubsystem<Engine>();
  1003. if (engine->IsHeadless() || runServer)
  1004. return;
  1005. if (singlePlayer)
  1006. {
  1007. if (players.Size() > 0)
  1008. scoreText->SetText("Score " + String(players[0].score));
  1009. if (hiscores.Size() > 0)
  1010. hiscoreText->SetText("Hiscore " + String(hiscores[0].score));
  1011. }
  1012. Node* playerNode = FindOwnNode();
  1013. if (playerNode)
  1014. {
  1015. i32 health = 0;
  1016. if (singlePlayer)
  1017. {
  1018. GameObject* object = playerNode->GetDerivedComponent<GameObject>();
  1019. health = object->health;
  1020. }
  1021. else
  1022. {
  1023. // In multiplayer the client does not have script logic components, but health is replicated via node user variables
  1024. health = playerNode->GetVar("Health").GetI32();
  1025. }
  1026. healthBar->SetWidth(116 * health / PLAYER_HEALTH);
  1027. }
  1028. }
  1029. void HandleScreenMode(StringHash eventType, VariantMap& eventData)
  1030. {
  1031. Graphics* graphics = GetSubsystem<Graphics>();
  1032. i32 height = graphics->GetHeight() / 22;
  1033. if (height > 64)
  1034. height = 64;
  1035. sight->SetSize(height, height);
  1036. messageText->SetPosition(0, -height * 2);
  1037. }
  1038. };
  1039. } // namespace Urho3D
  1040. URHO3D_DEFINE_APPLICATION_MAIN(App);