// Remake of NinjaSnowWar in script #include "Scripts/NinjaSnowWar/FootSteps.as" #include "Scripts/NinjaSnowWar/LightFlash.as" #include "Scripts/NinjaSnowWar/Ninja.as" #include "Scripts/NinjaSnowWar/Player.as" #include "Scripts/NinjaSnowWar/Potion.as" #include "Scripts/NinjaSnowWar/SnowBall.as" #include "Scripts/NinjaSnowWar/SnowCrate.as" #include "Scripts/Utilities/Network.as" const float mouseSensitivity = 0.125; const float touchSensitivity = 2.0; const float joySensitivity = 0.5; const float joyMoveDeadZone = 0.333; const float joyLookDeadZone = 0.05; const float cameraMinDist = 0.25; const float cameraMaxDist = 5; const float cameraSafetyDist = 0.3; const int initialMaxEnemies = 5; const int finalMaxEnemies = 25; const int maxPowerups = 5; const int incrementEach = 10; const int playerHealth = 20; const float enemySpawnRate = 1; const float powerupSpawnRate = 15; const float spawnAreaSize = 5; Scene@ gameScene; Node@ gameCameraNode; Node@ musicNode; Camera@ gameCamera; Text@ scoreText; Text@ hiscoreText; Text@ messageText; BorderImage@ healthBar; BorderImage@ sight; BorderImage@ moveButton; BorderImage@ fireButton; SoundSource@ musicSource; Controls playerControls; Controls prevPlayerControls; bool singlePlayer = true; bool gameOn = false; bool drawDebug = false; bool drawOctreeDebug = false; int maxEnemies = 0; int incrementCounter = 0; float enemySpawnTimer = 0; float powerupSpawnTimer = 0; uint clientNodeID = 0; int clientScore = 0; int screenJoystickID = -1; int screenJoystickSettingsID = -1; bool touchEnabled = false; Array players; Array hiscores; void Start() { if (engine.headless) OpenConsoleWindow(); ParseNetworkArguments(); if (runServer || runClient) singlePlayer = false; InitAudio(); InitConsole(); InitScene(); InitNetworking(); CreateCamera(); CreateOverlays(); SubscribeToEvent(gameScene, "SceneUpdate", "HandleUpdate"); if (gameScene.physicsWorld !is null) SubscribeToEvent(gameScene.physicsWorld, "PhysicsPreStep", "HandleFixedUpdate"); SubscribeToEvent(gameScene, "ScenePostUpdate", "HandlePostUpdate"); SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate"); SubscribeToEvent("KeyDown", "HandleKeyDown"); SubscribeToEvent("Points", "HandlePoints"); SubscribeToEvent("Kill", "HandleKill"); SubscribeToEvent("ScreenMode", "HandleScreenMode"); if (singlePlayer) { StartGame(null); engine.pauseMinimized = true; } } void InitAudio() { if (engine.headless) return; // Lower mastervolumes slightly. audio.masterGain[SOUND_MASTER] = 0.75; audio.masterGain[SOUND_MUSIC] = 0.9; if (!nobgm) { Sound@ musicFile = cache.GetResource("Sound", "Music/Ninja Gods.ogg"); musicFile.looped = true; // Note: the non-positional sound source component need to be attached to a node to become effective // Due to networked mode clearing the scene on connect, do not attach to the scene itself musicNode = Node(); musicSource = musicNode.CreateComponent("SoundSource"); musicSource.soundType = SOUND_MUSIC; musicSource.Play(musicFile); } } void InitConsole() { if (engine.headless) return; XMLFile@ uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml"); ui.root.defaultStyle = uiStyle; Console@ console = engine.CreateConsole(); console.defaultStyle = uiStyle; console.background.opacity = 0.8; engine.CreateDebugHud(); debugHud.defaultStyle = uiStyle; } void InitScene() { gameScene = Scene("NinjaSnowWar"); // Enable access to this script file & scene from the console script.defaultScene = gameScene; script.defaultScriptFile = scriptFile; // For the multiplayer client, do not load the scene, let it load from the server if (runClient) return; gameScene.LoadXML(cache.GetFile("Scenes/NinjaSnowWar.xml")); // On mobile devices render the shadowmap first. Also adjust the shadow quality for performance String platform = GetPlatform(); if (platform == "Android" || platform == "iOS" || platform == "Raspberry Pi") { renderer.reuseShadowMaps = false; renderer.shadowQuality = SHADOWQUALITY_LOW_16BIT; // Adjust the directional light shadow range slightly further, as only the first // cascade is supported Node@ dirLightNode = gameScene.GetChild("GlobalLight", true); if (dirLightNode !is null) { Light@ dirLight = dirLightNode.GetComponent("Light"); dirLight.shadowCascade = CascadeParameters(15.0f, 0.0f, 0.0f, 0.0f, 0.9f); dirLight.shadowIntensity = 0.333f; } } // Precache shaders if possible if (!engine.headless && cache.Exists("NinjaSnowWarShaders.xml")) graphics.PrecacheShaders(cache.GetFile("NinjaSnowWarShaders.xml")); } void InitNetworking() { network.updateFps = 25; // 1/4 of physics FPS // Remote events sent between client & server must be explicitly registered or else they are not allowed to be received network.RegisterRemoteEvent("PlayerSpawned"); network.RegisterRemoteEvent("UpdateScore"); network.RegisterRemoteEvent("UpdateHiscores"); network.RegisterRemoteEvent("ParticleEffect"); if (runServer) { network.StartServer(serverPort); // Disable physics interpolation to ensure clients get sent physically correct transforms gameScene.physicsWorld.interpolation = false; SubscribeToEvent("ClientIdentity", "HandleClientIdentity"); SubscribeToEvent("ClientSceneLoaded", "HandleClientSceneLoaded"); SubscribeToEvent("ClientDisconnected", "HandleClientDisconnected"); } if (runClient) { VariantMap identity; identity["UserName"] = userName; network.updateFps = 50; // Increase controls send rate for better responsiveness network.Connect(serverAddress, serverPort, gameScene, identity); SubscribeToEvent("PlayerSpawned", "HandlePlayerSpawned"); SubscribeToEvent("UpdateScore", "HandleUpdateScore"); SubscribeToEvent("UpdateHiscores", "HandleUpdateHiscores"); SubscribeToEvent("NetworkUpdateSent", "HandleNetworkUpdateSent"); } } void InitTouchInput() { touchEnabled = true; screenJoystickID = input.AddScreenJoystick(cache.GetResource("XMLFile", "UI/ScreenJoystick_NinjaSnowWar.xml")); } void CreateCamera() { // Note: the camera is not in the scene gameCameraNode = Node(); gameCameraNode.position = Vector3(0, 2, -10); gameCamera = gameCameraNode.CreateComponent("Camera"); gameCamera.nearClip = 0.5; gameCamera.farClip = 160; if (!engine.headless) { renderer.viewports[0] = Viewport(gameScene, gameCamera); audio.listener = gameCameraNode.CreateComponent("SoundListener"); } } void CreateOverlays() { if (engine.headless || runServer) return; int height = graphics.height / 22; if (height > 64) height = 64; sight = BorderImage(); sight.texture = cache.GetResource("Texture2D", "Textures/NinjaSnowWar/Sight.png"); sight.SetAlignment(HA_CENTER, VA_CENTER); sight.SetSize(height, height); ui.root.AddChild(sight); Font@ font = cache.GetResource("Font", "Fonts/BlueHighway.ttf"); scoreText = Text(); scoreText.SetFont(font, 13); scoreText.SetAlignment(HA_LEFT, VA_TOP); scoreText.SetPosition(5, 5); scoreText.colors[C_BOTTOMLEFT] = Color(1, 1, 0.25); scoreText.colors[C_BOTTOMRIGHT] = Color(1, 1, 0.25); ui.root.AddChild(scoreText); @hiscoreText = Text(); hiscoreText.SetFont(font, 13); hiscoreText.SetAlignment(HA_RIGHT, VA_TOP); hiscoreText.SetPosition(-5, 5); hiscoreText.colors[C_BOTTOMLEFT] = Color(1, 1, 0.25); hiscoreText.colors[C_BOTTOMRIGHT] = Color(1, 1, 0.25); ui.root.AddChild(hiscoreText); @messageText = Text(); messageText.SetFont(font, 13); messageText.SetAlignment(HA_CENTER, VA_CENTER); messageText.SetPosition(0, -height * 2); messageText.color = Color(1, 0, 0); ui.root.AddChild(messageText); BorderImage@ healthBorder = BorderImage(); healthBorder.texture = cache.GetResource("Texture2D", "Textures/NinjaSnowWar/HealthBarBorder.png"); healthBorder.SetAlignment(HA_CENTER, VA_TOP); healthBorder.SetPosition(0, 8); healthBorder.SetSize(120, 20); ui.root.AddChild(healthBorder); healthBar = BorderImage(); healthBar.texture = cache.GetResource("Texture2D", "Textures/NinjaSnowWar/HealthBarInside.png"); healthBar.SetPosition(2, 2); healthBar.SetSize(116, 16); healthBorder.AddChild(healthBar); if (GetPlatform() == "Android" || GetPlatform() == "iOS") // On mobile platform, enable touch by adding a screen joystick InitTouchInput(); else if (input.numJoysticks == 0) // On desktop platform, do not detect touch when we already got a joystick SubscribeToEvent("TouchBegin", "HandleTouchBegin"); } void SetMessage(const String&in message) { if (messageText !is null) messageText.text = message; } void StartGame(Connection@ connection) { // Clear the scene of all existing scripted objects { Array scriptedNodes = gameScene.GetChildrenWithScript(true); for (uint i = 0; i < scriptedNodes.length; ++i) scriptedNodes[i].Remove(); } players.Clear(); SpawnPlayer(connection); ResetAI(); gameOn = true; maxEnemies = initialMaxEnemies; incrementCounter = 0; enemySpawnTimer = 0; powerupSpawnTimer = 0; if (singlePlayer) { playerControls.yaw = 0; playerControls.pitch = 0; SetMessage(""); } } void SpawnPlayer(Connection@ connection) { Vector3 spawnPosition; if (singlePlayer) spawnPosition = Vector3(0, 0.97, 0); else spawnPosition = Vector3(Random(spawnAreaSize) - spawnAreaSize * 0.5, 0.97, Random(spawnAreaSize) - spawnAreaSize); Node@ playerNode = SpawnObject(spawnPosition, Quaternion(), "Ninja"); // Set owner connection. Owned nodes are always updated to the owner at full frequency playerNode.owner = connection; playerNode.name = "Player"; // Initialize variables Ninja@ playerNinja = cast(playerNode.scriptObject); playerNinja.health = playerNinja.maxHealth = playerHealth; playerNinja.side = SIDE_PLAYER; // Make sure the player can not shoot on first frame by holding the button down if (connection is null) playerNinja.controls = playerNinja.prevControls = playerControls; else playerNinja.controls = playerNinja.prevControls = connection.controls; // Check if player entry already exists int playerIndex = -1; for (uint i = 0; i < players.length; ++i) { if (players[i].connection is connection) { playerIndex = i; break; } } // Does not exist, create new if (playerIndex < 0) { playerIndex = players.length; players.Resize(players.length + 1); players[playerIndex].connection = connection; if (connection !is null) { players[playerIndex].name = connection.identity["UserName"].GetString(); // In multiplayer, send current hiscores to the new player SendHiscores(playerIndex); } else { players[playerIndex].name = "Player"; // In singleplayer, create also the default hiscore entry immediately HiscoreEntry newHiscore; newHiscore.name = players[playerIndex].name; newHiscore.score = 0; hiscores.Push(newHiscore); } } players[playerIndex].nodeID = playerNode.id; players[playerIndex].score = 0; if (connection !is null) { // In multiplayer, send initial score, then send a remote event that tells the spawned node's ID // It is important for the event to be in-order so that the node has been replicated first SendScore(playerIndex); VariantMap eventData; eventData["NodeID"] = playerNode.id; connection.SendRemoteEvent("PlayerSpawned", true, eventData); // Create name tag (Text3D component) for players in multiplayer Node@ textNode = playerNode.CreateChild("NameTag"); textNode.position = Vector3(0, 1.2, 0); Text3D@ text3D = textNode.CreateComponent("Text3D"); Font@ font = cache.GetResource("Font", "Fonts/BlueHighway.ttf"); text3D.SetFont(font, 19); text3D.color = Color(1, 1, 0); text3D.text = players[playerIndex].name; text3D.horizontalAlignment = HA_CENTER; text3D.verticalAlignment = VA_CENTER; text3D.faceCameraMode = FC_ROTATE_XYZ; } } void HandleUpdate(StringHash eventType, VariantMap& eventData) { float timeStep = eventData["TimeStep"].GetFloat(); UpdateControls(); CheckEndAndRestart(); if (engine.headless) { String command = GetConsoleInput(); if (command.length > 0) script.Execute(command); } else { if (debugHud.mode != DEBUGHUD_SHOW_NONE) { Node@ playerNode = FindOwnNode(); if (playerNode !is null) { debugHud.SetAppStats("Player Pos", playerNode.worldPosition.ToString()); debugHud.SetAppStats("Player Yaw", Variant(playerNode.worldRotation.yaw)); } else debugHud.ClearAppStats(); } } } void HandleFixedUpdate(StringHash eventType, VariantMap& eventData) { float timeStep = eventData["TimeStep"].GetFloat(); // Spawn new objects, singleplayer or server only if (singlePlayer || runServer) SpawnObjects(timeStep); } void HandlePostUpdate() { UpdateCamera(); UpdateStatus(); } void HandlePostRenderUpdate() { if (engine.headless) return; if (drawDebug) gameScene.physicsWorld.DrawDebugGeometry(true); if (drawOctreeDebug) gameScene.octree.DrawDebugGeometry(true); } void HandleTouchBegin(StringHash eventType, VariantMap& eventData) { // On some platforms like Windows the presence of touch input can only be detected dynamically InitTouchInput(); UnsubscribeFromEvent("TouchBegin"); } void HandleKeyDown(StringHash eventType, VariantMap& eventData) { int key = eventData["Key"].GetInt(); if (key == KEY_ESC) { if (!console.visible) engine.Exit(); else console.visible = false; } if (key == KEY_F1) console.Toggle(); if (key == KEY_F2) debugHud.ToggleAll(); if (key == KEY_F3) drawDebug = !drawDebug; if (key == KEY_F4) drawOctreeDebug = !drawOctreeDebug; // Allow pause only in singleplayer if (key == 'P' && singlePlayer && !console.visible && gameOn) { gameScene.updateEnabled = !gameScene.updateEnabled; if (!gameScene.updateEnabled) { SetMessage("PAUSED"); // Open the settings joystick only if the controls screen joystick was already open if (screenJoystickID >= 0) { // Lazy initialization if (screenJoystickSettingsID < 0) screenJoystickSettingsID = input.AddScreenJoystick(cache.GetResource("XMLFile", "UI/ScreenJoystickSettings_NinjaSnowWar.xml")); else input.screenJoystickVisible[screenJoystickSettingsID] = true; } } else { SetMessage(""); // Hide the settings joystick if (screenJoystickSettingsID >= 0) input.screenJoystickVisible[screenJoystickSettingsID] = false; } } } void HandlePoints(StringHash eventType, VariantMap& eventData) { if (eventData["DamageSide"].GetInt() == SIDE_PLAYER) { // Get node ID of the object that should receive points -> use it to find player index int playerIndex = FindPlayerIndex(eventData["Receiver"].GetInt()); if (playerIndex >= 0) { players[playerIndex].score += eventData["Points"].GetInt(); SendScore(playerIndex); bool newHiscore = CheckHiscore(playerIndex); if (newHiscore) SendHiscores(-1); } } } void HandleKill(StringHash eventType, VariantMap& eventData) { if (eventData["DamageSide"].GetInt() == SIDE_PLAYER) { MakeAIHarder(); // Increment amount of simultaneous enemies after enough kills incrementCounter++; if (incrementCounter >= incrementEach) { incrementCounter = 0; if (maxEnemies < finalMaxEnemies) maxEnemies++; } } } void HandleClientIdentity(StringHash eventType, VariantMap& eventData) { Connection@ connection = GetEventSender(); // If user has empty name, invent one if (connection.identity["UserName"].GetString().Trimmed().empty) connection.identity["UserName"] = "user" + RandomInt(1000); // Assign scene to begin replicating it to the client connection.scene = gameScene; } void HandleClientSceneLoaded(StringHash eventType, VariantMap& eventData) { // Now client is actually ready to begin. If first player, clear the scene and restart the game Connection@ connection = GetEventSender(); if (players.empty) StartGame(connection); else SpawnPlayer(connection); } void HandleClientDisconnected(StringHash eventType, VariantMap& eventData) { Connection@ connection = GetEventSender(); // Erase the player entry, and make the player's ninja commit seppuku (if exists) for (uint i = 0; i < players.length; ++i) { if (players[i].connection is connection) { players[i].connection = null; Node@ playerNode = FindPlayerNode(i); if (playerNode !is null) { Ninja@ playerNinja = cast(playerNode.scriptObject); playerNinja.health = 0; playerNinja.lastDamageSide = SIDE_NEUTRAL; // No-one scores from this } players.Erase(i); return; } } } void HandlePlayerSpawned(StringHash eventType, VariantMap& eventData) { // Store our node ID and mark the game as started clientNodeID = eventData["NodeID"].GetInt(); gameOn = true; SetMessage(""); // Copy initial yaw from the player node (we should have it replicated now) Node@ playerNode = FindOwnNode(); if (playerNode !is null) { playerControls.yaw = playerNode.rotation.yaw; playerControls.pitch = 0; // Disable the nametag from own character Node@ nameTag = playerNode.GetChild("NameTag"); nameTag.enabled = false; } } void HandleUpdateScore(StringHash eventType, VariantMap& eventData) { clientScore = eventData["Score"].GetInt(); scoreText.text = "Score " + clientScore; } void HandleUpdateHiscores(StringHash eventType, VariantMap& eventData) { VectorBuffer data = eventData["Hiscores"].GetBuffer(); hiscores.Resize(data.ReadVLE()); for (uint i = 0; i < hiscores.length; ++i) { hiscores[i].name = data.ReadString(); hiscores[i].score = data.ReadInt(); } String allHiscores; for (uint i = 0; i < hiscores.length; ++i) allHiscores += hiscores[i].name + " " + hiscores[i].score + "\n"; hiscoreText.text = allHiscores; } void HandleNetworkUpdateSent() { // Clear accumulated buttons from the network controls if (network.serverConnection !is null) network.serverConnection.controls.Set(CTRL_ALL, false); } int FindPlayerIndex(uint nodeID) { for (uint i = 0; i < players.length; ++i) { if (players[i].nodeID == nodeID) return i; } return -1; } Node@ FindPlayerNode(int playerIndex) { if (playerIndex >= 0 && playerIndex < int(players.length)) return gameScene.GetNode(players[playerIndex].nodeID); else return null; } Node@ FindOwnNode() { if (singlePlayer) return gameScene.GetChild("Player", true); else return gameScene.GetNode(clientNodeID); } bool CheckHiscore(int playerIndex) { for (uint i = 0; i < hiscores.length; ++i) { if (hiscores[i].name == players[playerIndex].name) { if (players[playerIndex].score > hiscores[i].score) { hiscores[i].score = players[playerIndex].score; SortHiscores(); return true; } else return false; // No update to individual hiscore } } // Not found, create new hiscore entry HiscoreEntry newHiscore; newHiscore.name = players[playerIndex].name; newHiscore.score = players[playerIndex].score; hiscores.Push(newHiscore); SortHiscores(); return true; } void SortHiscores() { for (int i = 1; i < int(hiscores.length); ++i) { HiscoreEntry temp = hiscores[i]; int j = i; while (j > 0 && temp.score > hiscores[j - 1].score) { hiscores[j] = hiscores[j - 1]; --j; } hiscores[j] = temp; } } void SendScore(int playerIndex) { if (!runServer || playerIndex < 0 || playerIndex >= int(players.length)) return; VariantMap eventData; eventData["Score"] = players[playerIndex].score; players[playerIndex].connection.SendRemoteEvent("UpdateScore", true, eventData); } void SendHiscores(int playerIndex) { if (!runServer) return; VectorBuffer data; data.WriteVLE(hiscores.length); for (uint i = 0; i < hiscores.length; ++i) { data.WriteString(hiscores[i].name); data.WriteInt(hiscores[i].score); } VariantMap eventData; eventData["Hiscores"] = data; if (playerIndex >= 0 && playerIndex < int(players.length)) players[playerIndex].connection.SendRemoteEvent("UpdateHiscores", true, eventData); else network.BroadcastRemoteEvent(gameScene, "UpdateHiscores", true, eventData); // Send to all in scene } Node@ SpawnObject(const Vector3&in position, const Quaternion&in rotation, const String&in className) { XMLFile@ xml = cache.GetResource("XMLFile", "Objects/" + className + ".xml"); return scene.InstantiateXML(xml, position, rotation); } Node@ SpawnParticleEffect(const Vector3&in position, const String&in effectName, float duration, CreateMode mode = REPLICATED) { Node@ newNode = scene.CreateChild("Effect", mode); newNode.position = position; // Create the particle emitter ParticleEmitter@ emitter = newNode.CreateComponent("ParticleEmitter"); emitter.effect = cache.GetResource("ParticleEffect", effectName); // Create a GameObject for managing the effect lifetime. This is always local, so for server-controlled effects it // exists only on the server GameObject@ object = cast(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL)); object.duration = duration; return newNode; } Node@ SpawnSound(const Vector3&in position, const String&in soundName, float duration) { Node@ newNode = scene.CreateChild(); newNode.position = position; // Create the sound source SoundSource3D@ source = newNode.CreateComponent("SoundSource3D"); Sound@ sound = cache.GetResource("Sound", soundName); source.SetDistanceAttenuation(200, 5000, 1); source.Play(sound); // Create a GameObject for managing the sound lifetime GameObject@ object = cast(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL)); object.duration = duration; return newNode; } void SpawnObjects(float timeStep) { // If game not running, run only the random generator if (!gameOn) { Random(); return; } // Spawn powerups powerupSpawnTimer += timeStep; if (powerupSpawnTimer >= powerupSpawnRate) { powerupSpawnTimer = 0; int numPowerups = gameScene.GetChildrenWithScript("SnowCrate", true).length + gameScene.GetChildrenWithScript("Potion", true).length; if (numPowerups < maxPowerups) { const float maxOffset = 40; float xOffset = Random(maxOffset * 2.0) - maxOffset; float zOffset = Random(maxOffset * 2.0) - maxOffset; SpawnObject(Vector3(xOffset, 50, zOffset), Quaternion(), "SnowCrate"); } } // Spawn enemies enemySpawnTimer += timeStep; if (enemySpawnTimer > enemySpawnRate) { enemySpawnTimer = 0; int numEnemies = 0; Array ninjaNodes = gameScene.GetChildrenWithScript("Ninja", true); for (uint i = 0; i < ninjaNodes.length; ++i) { Ninja@ ninja = cast(ninjaNodes[i].scriptObject); if (ninja.side == SIDE_ENEMY) ++numEnemies; } if (numEnemies < maxEnemies) { const float maxOffset = 40; float offset = Random(maxOffset * 2.0) - maxOffset; // Random north/east/south/west direction int dir = RandomInt() & 3; dir *= 90; Quaternion rotation(0, dir, 0); Node@ enemyNode = SpawnObject(rotation * Vector3(offset, 10, -120), rotation, "Ninja"); // Initialize variables Ninja@ enemyNinja = cast(enemyNode.scriptObject); enemyNinja.side = SIDE_ENEMY; @enemyNinja.controller = AIController(); RigidBody@ enemyBody = enemyNode.GetComponent("RigidBody"); enemyBody.linearVelocity = rotation * Vector3(0, 10, 30); } } } void CheckEndAndRestart() { // Only check end of game if singleplayer or client if (runServer) return; // Check if player node has vanished Node@ playerNode = FindOwnNode(); if (gameOn && playerNode is null) { gameOn = false; SetMessage("Press Fire or Jump to restart!"); return; } // Check for restart (singleplayer only) if (!gameOn && singlePlayer && playerControls.IsPressed(CTRL_FIRE | CTRL_JUMP, prevPlayerControls)) StartGame(null); } void UpdateControls() { if (singlePlayer || runClient) { prevPlayerControls = playerControls; playerControls.Set(CTRL_ALL, false); if (touchEnabled) { for (uint i = 0; i < input.numTouches; ++i) { TouchState@ touch = input.touches[i]; if (touch.touchedElement is null) { // Touch on empty space playerControls.yaw += touchSensitivity * gameCamera.fov / graphics.height * touch.delta.x; playerControls.pitch += touchSensitivity * gameCamera.fov / graphics.height * touch.delta.y; } } } if (input.numJoysticks > 0) { JoystickState@ joystick = touchEnabled ? input.joysticks[screenJoystickID] : input.joysticksByIndex[0]; if (joystick.numButtons > 0) { if (joystick.buttonDown[0]) playerControls.Set(CTRL_JUMP, true); if (joystick.buttonDown[1]) playerControls.Set(CTRL_FIRE, true); if (joystick.numButtons >= 6) { if (joystick.buttonDown[4]) playerControls.Set(CTRL_JUMP, true); if (joystick.buttonDown[5]) playerControls.Set(CTRL_FIRE, true); } if (joystick.numHats > 0) { if (joystick.hatPosition[0] & HAT_LEFT != 0) playerControls.Set(CTRL_LEFT, true); if (joystick.hatPosition[0] & HAT_RIGHT != 0) playerControls.Set(CTRL_RIGHT, true); if (joystick.hatPosition[0] & HAT_UP != 0) playerControls.Set(CTRL_UP, true); if (joystick.hatPosition[0] & HAT_DOWN != 0) playerControls.Set(CTRL_DOWN, true); } if (joystick.numAxes >= 2) { if (joystick.axisPosition[0] < -joyMoveDeadZone) playerControls.Set(CTRL_LEFT, true); if (joystick.axisPosition[0] > joyMoveDeadZone) playerControls.Set(CTRL_RIGHT, true); if (joystick.axisPosition[1] < -joyMoveDeadZone) playerControls.Set(CTRL_UP, true); if (joystick.axisPosition[1] > joyMoveDeadZone) playerControls.Set(CTRL_DOWN, true); } if (joystick.numAxes >= 4) { float lookX = joystick.axisPosition[2]; float lookY = joystick.axisPosition[3]; if (lookX < -joyLookDeadZone) playerControls.yaw -= joySensitivity * lookX * lookX; if (lookX > joyLookDeadZone) playerControls.yaw += joySensitivity * lookX * lookX; if (lookY < -joyLookDeadZone) playerControls.pitch -= joySensitivity * lookY * lookY; if (lookY > joyLookDeadZone) playerControls.pitch += joySensitivity * lookY * lookY; } } } // For the triggered actions (fire & jump) check also for press, in case the FPS is low // and the key was already released if (console is null || !console.visible) { if (input.keyDown['W']) playerControls.Set(CTRL_UP, true); if (input.keyDown['S']) playerControls.Set(CTRL_DOWN, true); if (input.keyDown['A']) playerControls.Set(CTRL_LEFT, true); if (input.keyDown['D']) playerControls.Set(CTRL_RIGHT, true); if (input.keyDown[KEY_LCTRL] || input.keyPress[KEY_LCTRL]) playerControls.Set(CTRL_FIRE, true); if (input.keyDown[' '] || input.keyPress[' ']) playerControls.Set(CTRL_JUMP, true); if (input.mouseButtonDown[MOUSEB_LEFT] || input.mouseButtonPress[MOUSEB_LEFT]) playerControls.Set(CTRL_FIRE, true); if (input.mouseButtonDown[MOUSEB_RIGHT] || input.mouseButtonPress[MOUSEB_RIGHT]) playerControls.Set(CTRL_JUMP, true); playerControls.yaw += mouseSensitivity * input.mouseMoveX; playerControls.pitch += mouseSensitivity * input.mouseMoveY; playerControls.pitch = Clamp(playerControls.pitch, -60.0, 60.0); } // In singleplayer, set controls directly on the player's ninja. In multiplayer, transmit to server if (singlePlayer) { Node@ playerNode = gameScene.GetChild("Player", true); if (playerNode !is null) { Ninja@ playerNinja = cast(playerNode.scriptObject); playerNinja.controls = playerControls; } } else if (network.serverConnection !is null) { // Set the latest yaw & pitch to server controls, and accumulate the buttons so that we do not miss any presses network.serverConnection.controls.yaw = playerControls.yaw; network.serverConnection.controls.pitch = playerControls.pitch; network.serverConnection.controls.buttons |= playerControls.buttons; // Tell the camera position to server for interest management network.serverConnection.position = gameCameraNode.worldPosition; } } if (runServer) { // Apply each connection's controls to the ninja they control for (uint i = 0; i < players.length; ++i) { Node@ playerNode = FindPlayerNode(i); if (playerNode !is null) { Ninja@ playerNinja = cast(playerNode.scriptObject); playerNinja.controls = players[i].connection.controls; } else { // If player has no ninja, respawn if fire/jump is pressed if (players[i].connection.controls.IsPressed(CTRL_FIRE | CTRL_JUMP, players[i].lastControls)) SpawnPlayer(players[i].connection); } players[i].lastControls = players[i].connection.controls; } } } void UpdateCamera() { if (engine.headless) return; // On the server, use a simple freelook camera if (runServer) { UpdateFreelookCamera(); return; } Node@ playerNode = FindOwnNode(); if (playerNode is null) return; Vector3 pos = playerNode.position; Quaternion dir; // Make controls seem more immediate by forcing the current mouse yaw to player ninja's Y-axis rotation if (playerNode.vars["Health"].GetInt() > 0) playerNode.rotation = Quaternion(0, playerControls.yaw, 0); dir = dir * Quaternion(playerNode.rotation.yaw, Vector3(0, 1, 0)); dir = dir * Quaternion(playerControls.pitch, Vector3(1, 0, 0)); Vector3 aimPoint = pos + Vector3(0, 1, 0); Vector3 minDist = aimPoint + dir * Vector3(0, 0, -cameraMinDist); Vector3 maxDist = aimPoint + dir * Vector3(0, 0, -cameraMaxDist); // Collide camera ray with static objects (collision mask 2) Vector3 rayDir = (maxDist - minDist).Normalized(); float rayDistance = cameraMaxDist - cameraMinDist + cameraSafetyDist; PhysicsRaycastResult result = gameScene.physicsWorld.RaycastSingle(Ray(minDist, rayDir), rayDistance, 2); if (result.body !is null) rayDistance = Min(rayDistance, result.distance - cameraSafetyDist); gameCameraNode.position = minDist + rayDir * rayDistance; gameCameraNode.rotation = dir; } void UpdateFreelookCamera() { if (console is null || !console.visible) { float timeStep = time.timeStep; float speedMultiplier = 1.0; if (input.keyDown[KEY_LSHIFT]) speedMultiplier = 5.0; if (input.keyDown[KEY_LCTRL]) speedMultiplier = 0.1; if (input.keyDown['W']) gameCameraNode.Translate(Vector3(0, 0, 10) * timeStep * speedMultiplier); if (input.keyDown['S']) gameCameraNode.Translate(Vector3(0, 0, -10) * timeStep * speedMultiplier); if (input.keyDown['A']) gameCameraNode.Translate(Vector3(-10, 0, 0) * timeStep * speedMultiplier); if (input.keyDown['D']) gameCameraNode.Translate(Vector3(10, 0, 0) * timeStep * speedMultiplier); playerControls.yaw += mouseSensitivity * input.mouseMoveX; playerControls.pitch += mouseSensitivity * input.mouseMoveY; playerControls.pitch = Clamp(playerControls.pitch, -90.0, 90.0); gameCameraNode.rotation = Quaternion(playerControls.pitch, playerControls.yaw, 0); } } void UpdateStatus() { if (engine.headless || runServer) return; if (singlePlayer) { if (players.length > 0) scoreText.text = "Score " + players[0].score; if (hiscores.length > 0) hiscoreText.text = "Hiscore " + hiscores[0].score; } Node@ playerNode = FindOwnNode(); if (playerNode !is null) { int health = 0; if (singlePlayer) { GameObject@ object = cast(playerNode.scriptObject); health = object.health; } else { // In multiplayer the client does not have script logic components, but health is replicated via node user variables health = playerNode.vars["Health"].GetInt(); } healthBar.width = 116 * health / playerHealth; } } void HandleScreenMode() { int height = graphics.height / 22; if (height > 64) height = 64; sight.SetSize(height, height); messageText.SetPosition(0, -height * 2); }