NinjaSnowWar.as 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155
  1. // Remake of NinjaSnowWar in script
  2. #include "Scripts/NinjaSnowWar/FootSteps.as"
  3. #include "Scripts/NinjaSnowWar/LightFlash.as"
  4. #include "Scripts/NinjaSnowWar/Ninja.as"
  5. #include "Scripts/NinjaSnowWar/Player.as"
  6. #include "Scripts/NinjaSnowWar/Potion.as"
  7. #include "Scripts/NinjaSnowWar/SnowBall.as"
  8. #include "Scripts/NinjaSnowWar/SnowCrate.as"
  9. #include "Scripts/Utilities/Network.as"
  10. const float mouseSensitivity = 0.125;
  11. const float touchSensitivity = 2.0;
  12. const float joySensitivity = 0.5;
  13. const float joyMoveDeadZone = 0.333;
  14. const float joyLookDeadZone = 0.05;
  15. const float cameraMinDist = 0.25;
  16. const float cameraMaxDist = 5;
  17. const float cameraSafetyDist = 0.3;
  18. const int initialMaxEnemies = 5;
  19. const int finalMaxEnemies = 25;
  20. const int maxPowerups = 5;
  21. const int incrementEach = 10;
  22. const int playerHealth = 20;
  23. const float enemySpawnRate = 1;
  24. const float powerupSpawnRate = 15;
  25. const float spawnAreaSize = 5;
  26. Scene@ gameScene;
  27. Node@ gameCameraNode;
  28. Node@ musicNode;
  29. Camera@ gameCamera;
  30. Text@ scoreText;
  31. Text@ hiscoreText;
  32. Text@ messageText;
  33. BorderImage@ healthBar;
  34. BorderImage@ sight;
  35. BorderImage@ moveButton;
  36. BorderImage@ fireButton;
  37. SoundSource@ musicSource;
  38. Controls playerControls;
  39. Controls prevPlayerControls;
  40. bool singlePlayer = true;
  41. bool gameOn = false;
  42. bool drawDebug = false;
  43. bool drawOctreeDebug = false;
  44. int maxEnemies = 0;
  45. int incrementCounter = 0;
  46. float enemySpawnTimer = 0;
  47. float powerupSpawnTimer = 0;
  48. uint clientNodeID = 0;
  49. int clientScore = 0;
  50. bool touchEnabled = false;
  51. int touchButtonSize = 96;
  52. int touchButtonBorder = 12;
  53. int moveTouchID = -1;
  54. int rotateTouchID = -1;
  55. int fireTouchID = -1;
  56. Array<Player> players;
  57. Array<HiscoreEntry> hiscores;
  58. void Start()
  59. {
  60. if (engine.headless)
  61. OpenConsoleWindow();
  62. ParseNetworkArguments();
  63. if (runServer || runClient)
  64. singlePlayer = false;
  65. InitAudio();
  66. InitConsole();
  67. InitScene();
  68. InitNetworking();
  69. CreateCamera();
  70. CreateOverlays();
  71. SubscribeToEvent(gameScene, "SceneUpdate", "HandleUpdate");
  72. if (gameScene.physicsWorld !is null)
  73. SubscribeToEvent(gameScene.physicsWorld, "PhysicsPreStep", "HandleFixedUpdate");
  74. SubscribeToEvent(gameScene, "ScenePostUpdate", "HandlePostUpdate");
  75. SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate");
  76. SubscribeToEvent("KeyDown", "HandleKeyDown");
  77. SubscribeToEvent("Points", "HandlePoints");
  78. SubscribeToEvent("Kill", "HandleKill");
  79. SubscribeToEvent("ScreenMode", "HandleScreenMode");
  80. SubscribeToEvent("TouchBegin", "HandleTouchBegin");
  81. SubscribeToEvent("TouchEnd", "HandleTouchEnd");
  82. if (singlePlayer)
  83. {
  84. StartGame(null);
  85. engine.pauseMinimized = true;
  86. }
  87. }
  88. void InitAudio()
  89. {
  90. if (engine.headless)
  91. return;
  92. // Lower mastervolumes slightly.
  93. audio.masterGain[SOUND_MASTER] = 0.75;
  94. audio.masterGain[SOUND_MUSIC] = 0.9;
  95. if (!nobgm)
  96. {
  97. Sound@ musicFile = cache.GetResource("Sound", "Music/Ninja Gods.ogg");
  98. musicFile.looped = true;
  99. // Note: the non-positional sound source component need to be attached to a node to become effective
  100. // Due to networked mode clearing the scene on connect, do not attach to the scene itself
  101. musicNode = Node();
  102. musicSource = musicNode.CreateComponent("SoundSource");
  103. musicSource.soundType = SOUND_MUSIC;
  104. musicSource.Play(musicFile);
  105. }
  106. }
  107. void InitConsole()
  108. {
  109. if (engine.headless)
  110. return;
  111. XMLFile@ uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
  112. Console@ console = engine.CreateConsole();
  113. console.defaultStyle = uiStyle;
  114. console.numRows = 16;
  115. engine.CreateDebugHud();
  116. debugHud.defaultStyle = uiStyle;
  117. }
  118. void InitScene()
  119. {
  120. gameScene = Scene("NinjaSnowWar");
  121. // Enable access to this script file & scene from the console
  122. script.defaultScene = gameScene;
  123. script.defaultScriptFile = scriptFile;
  124. // For the multiplayer client, do not load the scene, let it load from the server
  125. if (runClient)
  126. return;
  127. gameScene.LoadXML(cache.GetFile("Scenes/NinjaSnowWar.xml"));
  128. // On mobile devices render the shadowmap first
  129. if (GetPlatform() == "Android" || GetPlatform() == "iOS")
  130. renderer.reuseShadowMaps = false;
  131. }
  132. void InitNetworking()
  133. {
  134. network.updateFps = 25; // 1/4 of physics FPS
  135. network.RegisterRemoteEvent("PlayerSpawned");
  136. network.RegisterRemoteEvent("UpdateScore");
  137. network.RegisterRemoteEvent("UpdateHiscores");
  138. network.RegisterRemoteEvent("ParticleEffect");
  139. if (runServer)
  140. {
  141. network.StartServer(serverPort);
  142. // Disable physics interpolation to ensure clients get sent physically correct transforms
  143. gameScene.physicsWorld.interpolation = false;
  144. SubscribeToEvent("ClientIdentity", "HandleClientIdentity");
  145. SubscribeToEvent("ClientSceneLoaded", "HandleClientSceneLoaded");
  146. SubscribeToEvent("ClientDisconnected", "HandleClientDisconnected");
  147. }
  148. if (runClient)
  149. {
  150. VariantMap identity;
  151. identity["UserName"] = userName;
  152. network.updateFps = 50; // Increase controls send rate for better responsiveness
  153. network.Connect(serverAddress, serverPort, gameScene, identity);
  154. SubscribeToEvent("PlayerSpawned", "HandlePlayerSpawned");
  155. SubscribeToEvent("UpdateScore", "HandleUpdateScore");
  156. SubscribeToEvent("UpdateHiscores", "HandleUpdateHiscores");
  157. SubscribeToEvent("NetworkUpdateSent", "HandleNetworkUpdateSent");
  158. SubscribeToEvent("ParticleEffect", "HandleParticleEffect");
  159. }
  160. }
  161. void InitTouchInput()
  162. {
  163. touchEnabled = true;
  164. moveButton = BorderImage();
  165. moveButton.texture = cache.GetResource("Texture2D", "Textures/TouchInput.png");
  166. moveButton.imageRect = IntRect(0, 0, 96, 96);
  167. moveButton.SetAlignment(HA_LEFT, VA_BOTTOM);
  168. moveButton.SetPosition(touchButtonBorder, -touchButtonBorder);
  169. moveButton.SetSize(touchButtonSize, touchButtonSize);
  170. ui.root.AddChild(moveButton);
  171. fireButton = BorderImage();
  172. fireButton.texture = cache.GetResource("Texture2D", "Textures/TouchInput.png");
  173. fireButton.imageRect = IntRect(96, 0, 192, 96);
  174. fireButton.SetAlignment(HA_RIGHT, VA_BOTTOM);
  175. fireButton.SetPosition(-touchButtonBorder, -touchButtonBorder);
  176. fireButton.SetSize(touchButtonSize, touchButtonSize);
  177. ui.root.AddChild(fireButton);
  178. }
  179. void CreateCamera()
  180. {
  181. // Note: the camera is not in the scene
  182. gameCameraNode = Node();
  183. gameCameraNode.position = Vector3(0, 2, -10);
  184. gameCamera = gameCameraNode.CreateComponent("Camera");
  185. gameCamera.nearClip = 0.5;
  186. gameCamera.farClip = 160;
  187. if (!engine.headless)
  188. {
  189. renderer.viewports[0] = Viewport(gameScene, gameCamera);
  190. audio.listener = gameCameraNode.CreateComponent("SoundListener");
  191. }
  192. }
  193. void CreateOverlays()
  194. {
  195. if (engine.headless || runServer)
  196. return;
  197. int height = graphics.height / 22;
  198. if (height > 64)
  199. height = 64;
  200. sight = BorderImage();
  201. sight.texture = cache.GetResource("Texture2D", "Textures/Sight.png");
  202. sight.SetAlignment(HA_CENTER, VA_CENTER);
  203. sight.SetSize(height, height);
  204. ui.root.AddChild(sight);
  205. Font@ font = cache.GetResource("Font", "Fonts/BlueHighway.ttf");
  206. scoreText = Text();
  207. scoreText.SetFont(font, 17);
  208. scoreText.SetAlignment(HA_LEFT, VA_TOP);
  209. scoreText.SetPosition(5, 5);
  210. scoreText.colors[C_BOTTOMLEFT] = Color(1, 1, 0.25);
  211. scoreText.colors[C_BOTTOMRIGHT] = Color(1, 1, 0.25);
  212. ui.root.AddChild(scoreText);
  213. @hiscoreText = Text();
  214. hiscoreText.SetFont(font, 17);
  215. hiscoreText.SetAlignment(HA_RIGHT, VA_TOP);
  216. hiscoreText.SetPosition(-5, 5);
  217. hiscoreText.colors[C_BOTTOMLEFT] = Color(1, 1, 0.25);
  218. hiscoreText.colors[C_BOTTOMRIGHT] = Color(1, 1, 0.25);
  219. ui.root.AddChild(hiscoreText);
  220. @messageText = Text();
  221. messageText.SetFont(font, 17);
  222. messageText.SetAlignment(HA_CENTER, VA_CENTER);
  223. messageText.SetPosition(0, -height * 2);
  224. messageText.color = Color(1, 0, 0);
  225. ui.root.AddChild(messageText);
  226. BorderImage@ healthBorder = BorderImage();
  227. healthBorder.texture = cache.GetResource("Texture2D", "Textures/HealthBarBorder.png");
  228. healthBorder.SetAlignment(HA_CENTER, VA_TOP);
  229. healthBorder.SetPosition(0, 8);
  230. healthBorder.SetSize(120, 20);
  231. ui.root.AddChild(healthBorder);
  232. healthBar = BorderImage();
  233. healthBar.texture = cache.GetResource("Texture2D", "Textures/HealthBarInside.png");
  234. healthBar.SetPosition(2, 2);
  235. healthBar.SetSize(116, 16);
  236. healthBorder.AddChild(healthBar);
  237. if (GetPlatform() == "Android" || GetPlatform() == "iOS")
  238. InitTouchInput();
  239. }
  240. void SetMessage(const String&in message)
  241. {
  242. if (messageText !is null)
  243. messageText.text = message;
  244. }
  245. void StartGame(Connection@ connection)
  246. {
  247. // Clear the scene of all existing scripted objects
  248. {
  249. Array<Node@> scriptedNodes = gameScene.GetChildrenWithScript(true);
  250. for (uint i = 0; i < scriptedNodes.length; ++i)
  251. scriptedNodes[i].Remove();
  252. }
  253. players.Clear();
  254. SpawnPlayer(connection);
  255. ResetAI();
  256. gameOn = true;
  257. maxEnemies = initialMaxEnemies;
  258. incrementCounter = 0;
  259. enemySpawnTimer = 0;
  260. powerupSpawnTimer = 0;
  261. if (singlePlayer)
  262. {
  263. playerControls.yaw = 0;
  264. playerControls.pitch = 0;
  265. SetMessage("");
  266. }
  267. }
  268. void SpawnPlayer(Connection@ connection)
  269. {
  270. Vector3 spawnPosition;
  271. if (singlePlayer)
  272. spawnPosition = Vector3(0, 0.97, 0);
  273. else
  274. spawnPosition = Vector3(Random(spawnAreaSize) - spawnAreaSize * 0.5, 0.97, Random(spawnAreaSize) - spawnAreaSize);
  275. Node@ playerNode = SpawnObject(spawnPosition, Quaternion(), "Ninja");
  276. // Set owner connection. Owned nodes are always updated to the owner at full frequency
  277. playerNode.owner = connection;
  278. playerNode.name = "Player";
  279. // Initialize variables
  280. Ninja@ playerNinja = cast<Ninja>(playerNode.scriptObject);
  281. playerNinja.health = playerNinja.maxHealth = playerHealth;
  282. playerNinja.side = SIDE_PLAYER;
  283. // Make sure the player can not shoot on first frame by holding the button down
  284. if (connection is null)
  285. playerNinja.controls = playerNinja.prevControls = playerControls;
  286. else
  287. playerNinja.controls = playerNinja.prevControls = connection.controls;
  288. // Check if player entry already exists
  289. int playerIndex = -1;
  290. for (uint i = 0; i < players.length; ++i)
  291. {
  292. if (players[i].connection is connection)
  293. {
  294. playerIndex = i;
  295. break;
  296. }
  297. }
  298. // Does not exist, create new
  299. if (playerIndex < 0)
  300. {
  301. playerIndex = players.length;
  302. players.Resize(players.length + 1);
  303. players[playerIndex].connection = connection;
  304. if (connection !is null)
  305. {
  306. players[playerIndex].name = connection.identity["UserName"].GetString();
  307. // In multiplayer, send current hiscores to the new player
  308. SendHiscores(playerIndex);
  309. }
  310. else
  311. {
  312. players[playerIndex].name = "Player";
  313. // In singleplayer, create also the default hiscore entry immediately
  314. HiscoreEntry newHiscore;
  315. newHiscore.name = players[playerIndex].name;
  316. newHiscore.score = 0;
  317. hiscores.Push(newHiscore);
  318. }
  319. }
  320. players[playerIndex].nodeID = playerNode.id;
  321. players[playerIndex].score = 0;
  322. if (connection !is null)
  323. {
  324. // In multiplayer, send initial score, then send a remote event that tells the spawned node's ID
  325. // It is important for the event to be in-order so that the node has been replicated first
  326. SendScore(playerIndex);
  327. VariantMap eventData;
  328. eventData["NodeID"] = playerNode.id;
  329. connection.SendRemoteEvent("PlayerSpawned", true, eventData);
  330. // Create name tag (Text3D component) for players in multiplayer
  331. Node@ textNode = playerNode.CreateChild("NameTag");
  332. textNode.position = Vector3(0, 1.2, 0);
  333. Text3D@ text3D = textNode.CreateComponent("Text3D");
  334. Font@ font = cache.GetResource("Font", "Fonts/BlueHighway.ttf");
  335. text3D.SetFont(font, 24);
  336. text3D.color = Color(1, 1, 0);
  337. text3D.text = players[playerIndex].name;
  338. text3D.horizontalAlignment = HA_CENTER;
  339. text3D.verticalAlignment = VA_CENTER;
  340. text3D.faceCamera = true;
  341. }
  342. }
  343. void HandleUpdate(StringHash eventType, VariantMap& eventData)
  344. {
  345. float timeStep = eventData["TimeStep"].GetFloat();
  346. UpdateControls();
  347. CheckEndAndRestart();
  348. if (engine.headless)
  349. {
  350. String command = GetConsoleInput();
  351. if (command.length > 0)
  352. script.Execute(command);
  353. }
  354. else
  355. {
  356. if (debugHud.mode != DEBUGHUD_SHOW_NONE)
  357. {
  358. Node@ playerNode = FindOwnNode();
  359. if (playerNode !is null)
  360. {
  361. debugHud.SetAppStats("Player Pos", playerNode.worldPosition.ToString());
  362. debugHud.SetAppStats("Player Yaw", Variant(playerNode.worldRotation.yaw));
  363. }
  364. else
  365. debugHud.ClearAppStats();
  366. }
  367. }
  368. }
  369. void HandleFixedUpdate(StringHash eventType, VariantMap& eventData)
  370. {
  371. float timeStep = eventData["TimeStep"].GetFloat();
  372. // Spawn new objects, singleplayer or server only
  373. if (singlePlayer || runServer)
  374. SpawnObjects(timeStep);
  375. }
  376. void HandlePostUpdate()
  377. {
  378. UpdateCamera();
  379. UpdateStatus();
  380. }
  381. void HandlePostRenderUpdate()
  382. {
  383. if (engine.headless)
  384. return;
  385. if (drawDebug)
  386. gameScene.physicsWorld.DrawDebugGeometry(true);
  387. if (drawOctreeDebug)
  388. gameScene.octree.DrawDebugGeometry(true);
  389. }
  390. void HandleTouchBegin(StringHash eventType, VariantMap& eventData)
  391. {
  392. // On some platforms like Windows the presence of touch input can only be detected dynamically
  393. if (!touchEnabled)
  394. InitTouchInput();
  395. int touchID = eventData["TouchID"].GetInt();
  396. IntVector2 pos(eventData["X"].GetInt(), eventData["Y"].GetInt());
  397. UIElement@ element = ui.GetElementAt(pos, false);
  398. if (element is moveButton)
  399. moveTouchID = touchID;
  400. else if (element is fireButton)
  401. fireTouchID = touchID;
  402. else
  403. rotateTouchID = touchID;
  404. }
  405. void HandleTouchEnd(StringHash eventType, VariantMap& eventData)
  406. {
  407. int touchID = eventData["TouchID"].GetInt();
  408. if (touchID == moveTouchID)
  409. moveTouchID = -1;
  410. if (touchID == rotateTouchID)
  411. rotateTouchID = -1;
  412. if (touchID == fireTouchID)
  413. fireTouchID = -1;
  414. }
  415. void HandleKeyDown(StringHash eventType, VariantMap& eventData)
  416. {
  417. int key = eventData["Key"].GetInt();
  418. if (key == KEY_ESC)
  419. {
  420. if (!console.visible)
  421. engine.Exit();
  422. else
  423. console.visible = false;
  424. }
  425. if (key == KEY_F1)
  426. console.Toggle();
  427. if (key == KEY_F2)
  428. debugHud.ToggleAll();
  429. if (key == KEY_F3)
  430. drawDebug = !drawDebug;
  431. if (key == KEY_F4)
  432. drawOctreeDebug = !drawOctreeDebug;
  433. // Allow pause only in singleplayer
  434. if (key == 'P' && singlePlayer && !console.visible && gameOn)
  435. {
  436. gameScene.updateEnabled = !gameScene.updateEnabled;
  437. if (!gameScene.updateEnabled)
  438. SetMessage("PAUSED");
  439. else
  440. SetMessage("");
  441. }
  442. }
  443. void HandlePoints(StringHash eventType, VariantMap& eventData)
  444. {
  445. if (eventData["DamageSide"].GetInt() == SIDE_PLAYER)
  446. {
  447. // Get node ID of the object that should receive points -> use it to find player index
  448. int playerIndex = FindPlayerIndex(eventData["Receiver"].GetInt());
  449. if (playerIndex >= 0)
  450. {
  451. players[playerIndex].score += eventData["Points"].GetInt();
  452. SendScore(playerIndex);
  453. bool newHiscore = CheckHiscore(playerIndex);
  454. if (newHiscore)
  455. SendHiscores(-1);
  456. }
  457. }
  458. }
  459. void HandleKill(StringHash eventType, VariantMap& eventData)
  460. {
  461. if (eventData["DamageSide"].GetInt() == SIDE_PLAYER)
  462. {
  463. MakeAIHarder();
  464. // Increment amount of simultaneous enemies after enough kills
  465. incrementCounter++;
  466. if (incrementCounter >= incrementEach)
  467. {
  468. incrementCounter = 0;
  469. if (maxEnemies < finalMaxEnemies)
  470. maxEnemies++;
  471. }
  472. }
  473. }
  474. void HandleClientIdentity(StringHash eventType, VariantMap& eventData)
  475. {
  476. Connection@ connection = GetEventSender();
  477. // If user has empty name, invent one
  478. if (connection.identity["UserName"].GetString().Trimmed().empty)
  479. connection.identity["UserName"] = "user" + RandomInt(1000);
  480. // Assign scene to begin replicating it to the client
  481. connection.scene = gameScene;
  482. }
  483. void HandleClientSceneLoaded(StringHash eventType, VariantMap& eventData)
  484. {
  485. // Now client is actually ready to begin. If first player, clear the scene and restart the game
  486. Connection@ connection = GetEventSender();
  487. if (players.empty)
  488. StartGame(connection);
  489. else
  490. SpawnPlayer(connection);
  491. }
  492. void HandleClientDisconnected(StringHash eventType, VariantMap& eventData)
  493. {
  494. Connection@ connection = GetEventSender();
  495. // Erase the player entry, and make the player's ninja commit seppuku (if exists)
  496. for (uint i = 0; i < players.length; ++i)
  497. {
  498. if (players[i].connection is connection)
  499. {
  500. players[i].connection = null;
  501. Node@ playerNode = FindPlayerNode(i);
  502. if (playerNode !is null)
  503. {
  504. Ninja@ playerNinja = cast<Ninja>(playerNode.scriptObject);
  505. playerNinja.health = 0;
  506. playerNinja.lastDamageSide = SIDE_NEUTRAL; // No-one scores from this
  507. }
  508. players.Erase(i);
  509. return;
  510. }
  511. }
  512. }
  513. void HandlePlayerSpawned(StringHash eventType, VariantMap& eventData)
  514. {
  515. // Store our node ID and mark the game as started
  516. clientNodeID = eventData["NodeID"].GetInt();
  517. gameOn = true;
  518. SetMessage("");
  519. // Copy initial yaw from the player node (we should have it replicated now)
  520. Node@ playerNode = FindOwnNode();
  521. if (playerNode !is null)
  522. {
  523. playerControls.yaw = playerNode.rotation.yaw;
  524. playerControls.pitch = 0;
  525. // Disable the nametag from own character
  526. Node@ nameTag = playerNode.GetChild("NameTag");
  527. nameTag.enabled = false;
  528. }
  529. }
  530. void HandleUpdateScore(StringHash eventType, VariantMap& eventData)
  531. {
  532. clientScore = eventData["Score"].GetInt();
  533. scoreText.text = "Score " + clientScore;
  534. }
  535. void HandleUpdateHiscores(StringHash eventType, VariantMap& eventData)
  536. {
  537. VectorBuffer data = eventData["Hiscores"].GetBuffer();
  538. hiscores.Resize(data.ReadVLE());
  539. for (uint i = 0; i < hiscores.length; ++i)
  540. {
  541. hiscores[i].name = data.ReadString();
  542. hiscores[i].score = data.ReadInt();
  543. }
  544. String allHiscores;
  545. for (uint i = 0; i < hiscores.length; ++i)
  546. allHiscores += hiscores[i].name + " " + hiscores[i].score + "\n";
  547. hiscoreText.text = allHiscores;
  548. }
  549. void HandleNetworkUpdateSent()
  550. {
  551. // Clear accumulated buttons from the network controls
  552. if (network.serverConnection !is null)
  553. network.serverConnection.controls.Set(CTRL_ALL, false);
  554. }
  555. void HandleParticleEffect(StringHash eventType, VariantMap& eventData)
  556. {
  557. Vector3 position = eventData["Position"].GetVector3();
  558. String effectName = eventData["EffectName"].GetString();
  559. float duration = eventData["Duration"].GetFloat();
  560. SpawnParticleEffect(position, effectName, duration, LOCAL);
  561. }
  562. int FindPlayerIndex(uint nodeID)
  563. {
  564. for (uint i = 0; i < players.length; ++i)
  565. {
  566. if (players[i].nodeID == nodeID)
  567. return i;
  568. }
  569. return -1;
  570. }
  571. Node@ FindPlayerNode(int playerIndex)
  572. {
  573. if (playerIndex >= 0 && playerIndex < int(players.length))
  574. return gameScene.GetNode(players[playerIndex].nodeID);
  575. else
  576. return null;
  577. }
  578. Node@ FindOwnNode()
  579. {
  580. if (singlePlayer)
  581. return gameScene.GetChild("Player", true);
  582. else
  583. return gameScene.GetNode(clientNodeID);
  584. }
  585. bool CheckHiscore(int playerIndex)
  586. {
  587. for (uint i = 0; i < hiscores.length; ++i)
  588. {
  589. if (hiscores[i].name == players[playerIndex].name)
  590. {
  591. if (players[playerIndex].score > hiscores[i].score)
  592. {
  593. hiscores[i].score = players[playerIndex].score;
  594. SortHiscores();
  595. return true;
  596. }
  597. else
  598. return false; // No update to individual hiscore
  599. }
  600. }
  601. // Not found, create new hiscore entry
  602. HiscoreEntry newHiscore;
  603. newHiscore.name = players[playerIndex].name;
  604. newHiscore.score = players[playerIndex].score;
  605. hiscores.Push(newHiscore);
  606. SortHiscores();
  607. return true;
  608. }
  609. void SortHiscores()
  610. {
  611. for (int i = 1; i < int(hiscores.length); ++i)
  612. {
  613. HiscoreEntry temp = hiscores[i];
  614. int j = i;
  615. while (j > 0 && temp.score > hiscores[j - 1].score)
  616. {
  617. hiscores[j] = hiscores[j - 1];
  618. --j;
  619. }
  620. hiscores[j] = temp;
  621. }
  622. }
  623. void SendScore(int playerIndex)
  624. {
  625. if (!runServer || playerIndex < 0 || playerIndex >= int(players.length))
  626. return;
  627. VariantMap eventData;
  628. eventData["Score"] = players[playerIndex].score;
  629. players[playerIndex].connection.SendRemoteEvent("UpdateScore", true, eventData);
  630. }
  631. void SendHiscores(int playerIndex)
  632. {
  633. if (!runServer)
  634. return;
  635. VectorBuffer data;
  636. data.WriteVLE(hiscores.length);
  637. for (uint i = 0; i < hiscores.length; ++i)
  638. {
  639. data.WriteString(hiscores[i].name);
  640. data.WriteInt(hiscores[i].score);
  641. }
  642. VariantMap eventData;
  643. eventData["Hiscores"] = data;
  644. if (playerIndex >= 0 && playerIndex < int(players.length))
  645. players[playerIndex].connection.SendRemoteEvent("UpdateHiscores", true, eventData);
  646. else
  647. network.BroadcastRemoteEvent(gameScene, "UpdateHiscores", true, eventData); // Send to all in scene
  648. }
  649. Node@ SpawnObject(const Vector3&in position, const Quaternion&in rotation, const String&in className)
  650. {
  651. XMLFile@ xml = cache.GetResource("XMLFile", "Objects/" + className + ".xml");
  652. return scene.InstantiateXML(xml, position, rotation);
  653. }
  654. Node@ SpawnParticleEffect(const Vector3&in position, const String&in effectName, float duration, CreateMode mode = REPLICATED)
  655. {
  656. if (runServer && mode == REPLICATED)
  657. {
  658. VariantMap eventData;
  659. eventData["Position"] = position;
  660. eventData["EffectName"] = effectName;
  661. eventData["Duration"] = duration;
  662. network.BroadcastRemoteEvent(gameScene, "ParticleEffect", false, eventData);
  663. }
  664. Node@ newNode = scene.CreateChild("Effect", LOCAL);
  665. newNode.position = position;
  666. // Create the particle emitter
  667. ParticleEmitter@ emitter = newNode.CreateComponent("ParticleEmitter");
  668. emitter.Load(cache.GetResource("XMLFile", effectName));
  669. // Create a GameObject for managing the effect lifetime
  670. GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL));
  671. object.duration = duration;
  672. return newNode;
  673. }
  674. Node@ SpawnSound(const Vector3&in position, const String&in soundName, float duration)
  675. {
  676. Node@ newNode = scene.CreateChild();
  677. newNode.position = position;
  678. // Create the sound source
  679. SoundSource3D@ source = newNode.CreateComponent("SoundSource3D");
  680. Sound@ sound = cache.GetResource("Sound", soundName);
  681. source.SetDistanceAttenuation(200, 5000, 1);
  682. source.Play(sound);
  683. // Create a GameObject for managing the sound lifetime
  684. GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL));
  685. object.duration = duration;
  686. return newNode;
  687. }
  688. void SpawnObjects(float timeStep)
  689. {
  690. // If game not running, run only the random generator
  691. if (!gameOn)
  692. {
  693. Random();
  694. return;
  695. }
  696. // Spawn powerups
  697. powerupSpawnTimer += timeStep;
  698. if (powerupSpawnTimer >= powerupSpawnRate)
  699. {
  700. powerupSpawnTimer = 0;
  701. int numPowerups = gameScene.GetChildrenWithScript("SnowCrate", true).length + gameScene.GetChildrenWithScript("Potion", true).length;
  702. if (numPowerups < maxPowerups)
  703. {
  704. const float maxOffset = 40;
  705. float xOffset = Random(maxOffset * 2.0) - maxOffset;
  706. float zOffset = Random(maxOffset * 2.0) - maxOffset;
  707. SpawnObject(Vector3(xOffset, 50, zOffset), Quaternion(), "SnowCrate");
  708. }
  709. }
  710. // Spawn enemies
  711. enemySpawnTimer += timeStep;
  712. if (enemySpawnTimer > enemySpawnRate)
  713. {
  714. enemySpawnTimer = 0;
  715. int numEnemies = 0;
  716. Array<Node@> ninjaNodes = gameScene.GetChildrenWithScript("Ninja", true);
  717. for (uint i = 0; i < ninjaNodes.length; ++i)
  718. {
  719. Ninja@ ninja = cast<Ninja>(ninjaNodes[i].scriptObject);
  720. if (ninja.side == SIDE_ENEMY)
  721. ++numEnemies;
  722. }
  723. if (numEnemies < maxEnemies)
  724. {
  725. const float maxOffset = 40;
  726. float offset = Random(maxOffset * 2.0) - maxOffset;
  727. // Random north/east/south/west direction
  728. int dir = RandomInt() & 3;
  729. dir *= 90;
  730. Quaternion rotation(0, dir, 0);
  731. Node@ enemyNode = SpawnObject(rotation * Vector3(offset, 10, -120), rotation, "Ninja");
  732. // Initialize variables
  733. Ninja@ enemyNinja = cast<Ninja>(enemyNode.scriptObject);
  734. enemyNinja.side = SIDE_ENEMY;
  735. @enemyNinja.controller = AIController();
  736. RigidBody@ enemyBody = enemyNode.GetComponent("RigidBody");
  737. enemyBody.linearVelocity = rotation * Vector3(0, 10, 30);
  738. }
  739. }
  740. }
  741. void CheckEndAndRestart()
  742. {
  743. // Only check end of game if singleplayer or client
  744. if (runServer)
  745. return;
  746. // Check if player node has vanished
  747. Node@ playerNode = FindOwnNode();
  748. if (gameOn && playerNode is null)
  749. {
  750. gameOn = false;
  751. SetMessage("Press Fire or Jump to restart!");
  752. return;
  753. }
  754. // Check for restart (singleplayer only)
  755. if (!gameOn && singlePlayer && playerControls.IsPressed(CTRL_FIRE | CTRL_JUMP, prevPlayerControls))
  756. StartGame(null);
  757. }
  758. void UpdateControls()
  759. {
  760. if (singlePlayer || runClient)
  761. {
  762. prevPlayerControls = playerControls;
  763. playerControls.Set(CTRL_ALL, false);
  764. if (touchEnabled)
  765. {
  766. for (uint i = 0; i < input.numTouches; ++i)
  767. {
  768. TouchState@ touch = input.touches[i];
  769. if (touch.touchID == rotateTouchID)
  770. {
  771. playerControls.yaw += touchSensitivity * gameCamera.fov / graphics.height * touch.delta.x;
  772. playerControls.pitch += touchSensitivity * gameCamera.fov / graphics.height * touch.delta.y;
  773. }
  774. if (touch.touchID == moveTouchID)
  775. {
  776. int relX = touch.position.x - moveButton.screenPosition.x - touchButtonSize / 2;
  777. int relY = touch.position.y - moveButton.screenPosition.y - touchButtonSize / 2;
  778. if (relY < 0 && Abs(relX * 3 / 2) < Abs(relY))
  779. playerControls.Set(CTRL_UP, true);
  780. if (relY > 0 && Abs(relX * 3 / 2) < Abs(relY))
  781. playerControls.Set(CTRL_DOWN, true);
  782. if (relX < 0 && Abs(relY * 3 / 2) < Abs(relX))
  783. playerControls.Set(CTRL_LEFT, true);
  784. if (relX > 0 && Abs(relY * 3 / 2) < Abs(relX))
  785. playerControls.Set(CTRL_RIGHT, true);
  786. }
  787. }
  788. if (fireTouchID >= 0)
  789. playerControls.Set(CTRL_FIRE, true);
  790. }
  791. if (input.numJoysticks > 0)
  792. {
  793. JoystickState@ joystick = input.joysticks[0];
  794. if (joystick.numButtons > 0)
  795. {
  796. if (joystick.buttonDown[0])
  797. playerControls.Set(CTRL_JUMP, true);
  798. if (joystick.buttonDown[1])
  799. playerControls.Set(CTRL_FIRE, true);
  800. if (joystick.numButtons >= 6)
  801. {
  802. if (joystick.buttonDown[4])
  803. playerControls.Set(CTRL_JUMP, true);
  804. if (joystick.buttonDown[5])
  805. playerControls.Set(CTRL_FIRE, true);
  806. }
  807. if (joystick.numHats > 0)
  808. {
  809. if (joystick.hatPosition[0] & HAT_LEFT != 0)
  810. playerControls.Set(CTRL_LEFT, true);
  811. if (joystick.hatPosition[0] & HAT_RIGHT != 0)
  812. playerControls.Set(CTRL_RIGHT, true);
  813. if (joystick.hatPosition[0] & HAT_UP != 0)
  814. playerControls.Set(CTRL_UP, true);
  815. if (joystick.hatPosition[0] & HAT_DOWN != 0)
  816. playerControls.Set(CTRL_DOWN, true);
  817. }
  818. if (joystick.numAxes >= 2)
  819. {
  820. if (joystick.axisPosition[0] < -joyMoveDeadZone)
  821. playerControls.Set(CTRL_LEFT, true);
  822. if (joystick.axisPosition[0] > joyMoveDeadZone)
  823. playerControls.Set(CTRL_RIGHT, true);
  824. if (joystick.axisPosition[1] < -joyMoveDeadZone)
  825. playerControls.Set(CTRL_UP, true);
  826. if (joystick.axisPosition[1] > joyMoveDeadZone)
  827. playerControls.Set(CTRL_DOWN, true);
  828. }
  829. if (joystick.numAxes >= 4)
  830. {
  831. float lookX = joystick.axisPosition[joystick.numAxes - 2];
  832. float lookY = joystick.axisPosition[joystick.numAxes - 1];
  833. if (lookX < -joyLookDeadZone)
  834. playerControls.yaw -= joySensitivity * lookX * lookX;
  835. if (lookX > joyLookDeadZone)
  836. playerControls.yaw += joySensitivity * lookX * lookX;
  837. if (lookY < -joyLookDeadZone)
  838. playerControls.pitch -= joySensitivity * lookY * lookY;
  839. if (lookY > joyLookDeadZone)
  840. playerControls.pitch += joySensitivity * lookY * lookY;
  841. }
  842. }
  843. }
  844. // For the triggered actions (fire & jump) check also for press, in case the FPS is low
  845. // and the key was already released
  846. if ((console is null) || (!console.visible))
  847. {
  848. if (input.keyDown['W'])
  849. playerControls.Set(CTRL_UP, true);
  850. if (input.keyDown['S'])
  851. playerControls.Set(CTRL_DOWN, true);
  852. if (input.keyDown['A'])
  853. playerControls.Set(CTRL_LEFT, true);
  854. if (input.keyDown['D'])
  855. playerControls.Set(CTRL_RIGHT, true);
  856. if (input.keyDown[KEY_LCTRL] || input.keyPress[KEY_LCTRL])
  857. playerControls.Set(CTRL_FIRE, true);
  858. if (input.keyDown[' '] || input.keyPress[' '])
  859. playerControls.Set(CTRL_JUMP, true);
  860. }
  861. if (input.mouseButtonDown[MOUSEB_LEFT] || input.mouseButtonPress[MOUSEB_LEFT])
  862. playerControls.Set(CTRL_FIRE, true);
  863. if (input.mouseButtonDown[MOUSEB_RIGHT] || input.mouseButtonPress[MOUSEB_RIGHT])
  864. playerControls.Set(CTRL_JUMP, true);
  865. playerControls.yaw += mouseSensitivity * input.mouseMoveX;
  866. playerControls.pitch += mouseSensitivity * input.mouseMoveY;
  867. playerControls.pitch = Clamp(playerControls.pitch, -60.0, 60.0);
  868. // In singleplayer, set controls directly on the player's ninja. In multiplayer, transmit to server
  869. if (singlePlayer)
  870. {
  871. Node@ playerNode = gameScene.GetChild("Player", true);
  872. if (playerNode !is null)
  873. {
  874. Ninja@ playerNinja = cast<Ninja>(playerNode.scriptObject);
  875. playerNinja.controls = playerControls;
  876. }
  877. }
  878. else if (network.serverConnection !is null)
  879. {
  880. // Set the latest yaw & pitch to server controls, and accumulate the buttons so that we do not miss any presses
  881. network.serverConnection.controls.yaw = playerControls.yaw;
  882. network.serverConnection.controls.pitch = playerControls.pitch;
  883. network.serverConnection.controls.buttons |= playerControls.buttons;
  884. // Tell the camera position to server for interest management
  885. network.serverConnection.position = gameCameraNode.worldPosition;
  886. }
  887. }
  888. if (runServer)
  889. {
  890. // Apply each connection's controls to the ninja they control
  891. for (uint i = 0; i < players.length; ++i)
  892. {
  893. Node@ playerNode = FindPlayerNode(i);
  894. if (playerNode !is null)
  895. {
  896. Ninja@ playerNinja = cast<Ninja>(playerNode.scriptObject);
  897. playerNinja.controls = players[i].connection.controls;
  898. }
  899. else
  900. {
  901. // If player has no ninja, respawn if fire/jump is pressed
  902. if (players[i].connection.controls.IsPressed(CTRL_FIRE | CTRL_JUMP, players[i].lastControls))
  903. SpawnPlayer(players[i].connection);
  904. }
  905. players[i].lastControls = players[i].connection.controls;
  906. }
  907. }
  908. }
  909. void UpdateCamera()
  910. {
  911. if (engine.headless)
  912. return;
  913. // On the server, use a simple freelook camera
  914. if (runServer)
  915. {
  916. UpdateFreelookCamera();
  917. return;
  918. }
  919. Node@ playerNode = FindOwnNode();
  920. if (playerNode is null)
  921. return;
  922. Vector3 pos = playerNode.position;
  923. Quaternion dir;
  924. // Make controls seem more immediate by forcing the current mouse yaw to player ninja's Y-axis rotation
  925. if (playerNode.vars["Health"].GetInt() > 0)
  926. playerNode.rotation = Quaternion(0, playerControls.yaw, 0);
  927. dir = dir * Quaternion(playerNode.rotation.yaw, Vector3(0, 1, 0));
  928. dir = dir * Quaternion(playerControls.pitch, Vector3(1, 0, 0));
  929. Vector3 aimPoint = pos + Vector3(0, 1, 0);
  930. Vector3 minDist = aimPoint + dir * Vector3(0, 0, -cameraMinDist);
  931. Vector3 maxDist = aimPoint + dir * Vector3(0, 0, -cameraMaxDist);
  932. // Collide camera ray with static objects (collision mask 2)
  933. Vector3 rayDir = (maxDist - minDist).Normalized();
  934. float rayDistance = cameraMaxDist - cameraMinDist + cameraSafetyDist;
  935. PhysicsRaycastResult result = gameScene.physicsWorld.RaycastSingle(Ray(minDist, rayDir), rayDistance, 2);
  936. if (result.body !is null)
  937. rayDistance = Min(rayDistance, result.distance - cameraSafetyDist);
  938. gameCameraNode.position = minDist + rayDir * rayDistance;
  939. gameCameraNode.rotation = dir;
  940. }
  941. void UpdateFreelookCamera()
  942. {
  943. float timeStep = time.timeStep;
  944. float speedMultiplier = 1.0;
  945. if (input.keyDown[KEY_LSHIFT])
  946. speedMultiplier = 5.0;
  947. if (input.keyDown[KEY_LCTRL])
  948. speedMultiplier = 0.1;
  949. if (input.keyDown['W'])
  950. gameCameraNode.TranslateRelative(Vector3(0, 0, 10) * timeStep * speedMultiplier);
  951. if (input.keyDown['S'])
  952. gameCameraNode.TranslateRelative(Vector3(0, 0, -10) * timeStep * speedMultiplier);
  953. if (input.keyDown['A'])
  954. gameCameraNode.TranslateRelative(Vector3(-10, 0, 0) * timeStep * speedMultiplier);
  955. if (input.keyDown['D'])
  956. gameCameraNode.TranslateRelative(Vector3(10, 0, 0) * timeStep * speedMultiplier);
  957. playerControls.yaw += mouseSensitivity * input.mouseMoveX;
  958. playerControls.pitch += mouseSensitivity * input.mouseMoveY;
  959. playerControls.pitch = Clamp(playerControls.pitch, -90.0, 90.0);
  960. gameCameraNode.rotation = Quaternion(playerControls.pitch, playerControls.yaw, 0);
  961. }
  962. void UpdateStatus()
  963. {
  964. if (engine.headless || runServer)
  965. return;
  966. if (singlePlayer)
  967. {
  968. if (players.length > 0)
  969. scoreText.text = "Score " + players[0].score;
  970. if (hiscores.length > 0)
  971. hiscoreText.text = "Hiscore " + hiscores[0].score;
  972. }
  973. Node@ playerNode = FindOwnNode();
  974. if (playerNode !is null)
  975. {
  976. int health = 0;
  977. if (singlePlayer)
  978. {
  979. GameObject@ object = cast<GameObject>(playerNode.scriptObject);
  980. health = object.health;
  981. }
  982. else
  983. {
  984. // In multiplayer the client does not have script logic components, but health is replicated via node user variables
  985. health = playerNode.vars["Health"].GetInt();
  986. }
  987. healthBar.width = 116 * health / playerHealth;
  988. }
  989. }
  990. void HandleScreenMode()
  991. {
  992. int height = graphics.height / 22;
  993. if (height > 64)
  994. height = 64;
  995. sight.SetSize(height, height);
  996. messageText.SetPosition(0, -height * 2);
  997. }