NinjaSnowWar.as 37 KB

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