1134 lines
37 KiB
ActionScript
1134 lines
37 KiB
ActionScript
// 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<Player> players;
|
|
Array<HiscoreEntry> 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 for better performance, adjust the cascaded shadows
|
|
String platform = GetPlatform();
|
|
if (platform == "Android" || platform == "iOS" || platform == "Raspberry Pi")
|
|
{
|
|
renderer.reuseShadowMaps = false;
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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<Node@> 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<Ninja>(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_ESCAPE)
|
|
{
|
|
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;
|
|
|
|
if (key == KEY_F5)
|
|
debugHud.Toggle(DEBUGHUD_SHOW_EVENTPROFILER);
|
|
|
|
// Take screenshot
|
|
if (key == KEY_F6)
|
|
{
|
|
Image@ screenshot = Image();
|
|
graphics.TakeScreenShot(screenshot);
|
|
// Here we save in the Data folder with date and time appended
|
|
screenshot.SavePNG(fileSystem.programDir + "Data/Screenshot_" +
|
|
time.timeStamp.Replaced(':', '_').Replaced('.', '_').Replaced(' ', '_') + ".png");
|
|
}
|
|
// Allow pause only in singleplayer
|
|
if (key == KEY_P && singlePlayer && !console.visible && gameOn)
|
|
{
|
|
gameScene.updateEnabled = !gameScene.updateEnabled;
|
|
if (!gameScene.updateEnabled)
|
|
{
|
|
SetMessage("PAUSED");
|
|
audio.PauseSoundType(SOUND_EFFECT);
|
|
|
|
// 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("");
|
|
audio.ResumeSoundType(SOUND_EFFECT);
|
|
|
|
// 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<Ninja>(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<GameObject>(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<GameObject>(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<Node@> ninjaNodes = gameScene.GetChildrenWithScript("Ninja", true);
|
|
for (uint i = 0; i < ninjaNodes.length; ++i)
|
|
{
|
|
Ninja@ ninja = cast<Ninja>(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<Ninja>(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[KEY_W])
|
|
playerControls.Set(CTRL_UP, true);
|
|
if (input.keyDown[KEY_S])
|
|
playerControls.Set(CTRL_DOWN, true);
|
|
if (input.keyDown[KEY_A])
|
|
playerControls.Set(CTRL_LEFT, true);
|
|
if (input.keyDown[KEY_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<Ninja>(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<Ninja>(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[KEY_W])
|
|
gameCameraNode.Translate(Vector3(0, 0, 10) * timeStep * speedMultiplier);
|
|
if (input.keyDown[KEY_S])
|
|
gameCameraNode.Translate(Vector3(0, 0, -10) * timeStep * speedMultiplier);
|
|
if (input.keyDown[KEY_A])
|
|
gameCameraNode.Translate(Vector3(-10, 0, 0) * timeStep * speedMultiplier);
|
|
if (input.keyDown[KEY_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<GameObject>(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);
|
|
}
|