NinjaSnowWar.as 38 KB

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