NinjaSnowWar.as 37 KB

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