diff --git a/Content/Scenes/MainScene.scene b/Content/Scenes/MainScene.scene index 8a2bd58..620420d 100644 --- a/Content/Scenes/MainScene.scene +++ b/Content/Scenes/MainScene.scene @@ -1,7 +1,7 @@ { "ID": "194e05f445ece24ec5448d886e1334df", "TypeName": "FlaxEngine.SceneAsset", - "EngineBuild": 6331, + "EngineBuild": 6332, "Data": [ { "ID": "194e05f445ece24ec5448d886e1334df", @@ -76,7 +76,7 @@ }, "Control": "FlaxEngine.GUI.Label", "Data": { - "Text": "eFPS: 121 uTime: 33.4012628\nuFPS: 117 uTime: 0.00833330024033785\nrFPS: 120 rTime: 0\npFPS: 30 pTime: 0", + "Text": "eFPS: 120 uTime: 0.1533353\nuFPS: -2147483648 uTime: 0.00833330024033785\nrFPS: -2147483648 rTime: 0\npFPS: -2147483648 pTime: 0", "TextColor": { "R": 1.0, "G": 1.0, @@ -123,7 +123,7 @@ }, "Offsets": { "Left": 0.0, - "Right": 231.0, + "Right": 284.0, "Top": -97.0, "Bottom": 64.0 }, diff --git a/Source/Game/AudioManager.cs b/Source/Game/AudioManager.cs index f558a6e..16e7c24 100644 --- a/Source/Game/AudioManager.cs +++ b/Source/Game/AudioManager.cs @@ -14,32 +14,16 @@ namespace Game None = 0, /// Avoid replacing the existing playing audio source in this channel. - ContinuePlayingExistingSource = 1, + ContinuePlayingExistingSource = 1 } public static class AudioManager { - private class AudioInfo - { - public AudioClip[] AudioClips; - public int lastAudioPlayed; - } + private static readonly Random random = new Random(); - private class ActorAudioChannels - { - public Dictionary channelSources; + private static readonly Dictionary cachedAudioInfos = new Dictionary(); - public ActorAudioChannels() - { - channelSources = new Dictionary(); - } - } - - private static Random random = new Random(); - - private static Dictionary cachedAudioInfos = new Dictionary(); - - private static Dictionary actorAudioChannels = + private static readonly Dictionary actorAudioChannels = new Dictionary(); public static void PlaySound(string soundName, Actor actor, Vector3 position, float volume = 1f) @@ -57,12 +41,14 @@ namespace Game PlaySound(soundName, actor, channel, AudioFlags.None, position, volume, Vector2.One); } - public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Vector3 position, float volume = 1f) + public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Vector3 position, + float volume = 1f) { PlaySound(soundName, actor, channel, flags, position, volume, Vector2.One); } - public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Vector3 position, float volume, Vector2 pitchRange) + public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Vector3 position, + float volume, Vector2 pitchRange) { AudioInfo audio = GetSound(soundName); if (audio.AudioClips.Length == 0) @@ -109,15 +95,17 @@ namespace Game audio.lastAudioPlayed = randomIndex; } else + { audioClip = audio.AudioClips[0]; + } float pitch; if (pitchRange[0] < pitchRange[1]) // Randomized pitch - pitch = (float)(pitchRange[0] + (random.NextDouble() * (pitchRange[1] - pitchRange[0]))); + pitch = (float)(pitchRange[0] + random.NextDouble() * (pitchRange[1] - pitchRange[0])); else pitch = pitchRange[0]; - var audioSource = new AudioSource(); + AudioSource audioSource = new AudioSource(); audioSource.Clip = audioClip; audioSource.Position = position; audioSource.Parent = actor.Parent; @@ -126,24 +114,23 @@ namespace Game audioSource.Volume = volume; if (volume != 1f) - audioSource.Name += ", vol: " + volume.ToString(); + audioSource.Name += ", vol: " + volume; if (pitch != 1f) - audioSource.Name += ", pitch: " + pitch.ToString(); + audioSource.Name += ", pitch: " + pitch; audioSource.Play(); Object.Destroy(audioSource, audioClip.Length); if (channel > 0) - { actorChannels.channelSources[channel] = audioSource; - } } - public static async void PlaySoundDelayed(Vector2 delayRange, string soundName, Actor actor, int channel, Vector3 position, float volume = 1f) + public static async void PlaySoundDelayed(Vector2 delayRange, string soundName, Actor actor, int channel, + Vector3 position, float volume = 1f) { float randomDelay; if (delayRange[0] < delayRange[1]) - randomDelay = (float)(delayRange[0] + (random.NextDouble() * (delayRange[1] - delayRange[0]))); + randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0])); else randomDelay = delayRange[0]; @@ -155,11 +142,12 @@ namespace Game PlaySound(soundName, actor, channel, AudioFlags.None, position, volume, Vector2.One); } - public static async void PlaySoundDelayed(Vector2 delayRange, string soundName, Actor actor, int channel, Vector3 position, float volume, Vector2 pitchRange) + public static async void PlaySoundDelayed(Vector2 delayRange, string soundName, Actor actor, int channel, + Vector3 position, float volume, Vector2 pitchRange) { float randomDelay; if (delayRange[0] < delayRange[1]) - randomDelay = (float)(delayRange[0] + (random.NextDouble() * (delayRange[1] - delayRange[0]))); + randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0])); else randomDelay = delayRange[0]; @@ -171,11 +159,12 @@ namespace Game PlaySound(soundName, actor, channel, AudioFlags.None, position, volume, pitchRange); } - public static async void PlaySoundDelayed(Vector2 delayRange, string soundName, Actor actor, int channel, AudioFlags flags, Vector3 position, float volume, Vector2 pitchRange) + public static async void PlaySoundDelayed(Vector2 delayRange, string soundName, Actor actor, int channel, + AudioFlags flags, Vector3 position, float volume, Vector2 pitchRange) { float randomDelay; if (delayRange[0] < delayRange[1]) - randomDelay = (float)(delayRange[0] + (random.NextDouble() * (delayRange[1] - delayRange[0]))); + randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0])); else randomDelay = delayRange[0]; @@ -219,9 +208,7 @@ namespace Game if (existingAudioSource && existingAudioSource != null && existingAudioSource.State == AudioSource.States.Playing) - { return true; - } return false; } @@ -233,19 +220,22 @@ namespace Game AudioInfo audio = new AudioInfo(); - var workDir = Directory.GetCurrentDirectory(); - var audioBasePath = Path.Combine(workDir, "Content", "Audio"); + string workDir = Directory.GetCurrentDirectory(); + string audioBasePath = Path.Combine(workDir, "Content", "Audio"); AudioClip audioClip = Content.Load(Path.Combine(audioBasePath, soundName + ".flax")); if (audioClip != null) - audio.AudioClips = new AudioClip[] { audioClip }; + { + audio.AudioClips = new[] { audioClip }; + } else { // Check if this audio has multiple variations - List audioClips = new List(); - for (int i = 1; i<50; i++) + var audioClips = new List(); + for (int i = 1; i < 50; i++) { // TODO: make this more efficient, maybe get a list of assets and filter by name? - AudioClip audioClipVariation = Content.Load(Path.Combine(audioBasePath, soundName + "_var" + i + ".flax")); + AudioClip audioClipVariation = + Content.Load(Path.Combine(audioBasePath, soundName + "_var" + i + ".flax")); if (audioClipVariation == null) break; @@ -263,5 +253,21 @@ namespace Game cachedAudioInfos.Add(soundName, audio); return audio; } + + private class AudioInfo + { + public AudioClip[] AudioClips; + public int lastAudioPlayed; + } + + private class ActorAudioChannels + { + public readonly Dictionary channelSources; + + public ActorAudioChannels() + { + channelSources = new Dictionary(); + } + } } } \ No newline at end of file diff --git a/Source/Game/Camera/CameraMovement.cs b/Source/Game/Camera/CameraMovement.cs index 2a3d203..9cdc398 100644 --- a/Source/Game/Camera/CameraMovement.cs +++ b/Source/Game/Camera/CameraMovement.cs @@ -1,124 +1,120 @@ -using FlaxEngine; -using Cabrito; -using System.Diagnostics; -using System.IO; +using Cabrito; +using FlaxEngine; namespace Game { - public class CameraMovement : Script - { - [Limit(0, 9000), Tooltip("Camera speed")] - public float MoveSpeed { get; set; } = 400; + public class CameraMovement : Script + { + private readonly InputEvent onExit = new InputEvent("Exit"); - private float viewPitch; - private float viewYaw; - private float viewRoll; + private float viewPitch; + private float viewRoll; + private float viewYaw; - private InputEvent onExit = new InputEvent("Exit"); + [Limit(0, 9000)] + [Tooltip("Camera speed")] + public float MoveSpeed { get; set; } = 400; - public override void OnAwake() - { - base.OnAwake(); + public override void OnAwake() + { + base.OnAwake(); - onExit.Triggered += () => - { - if (Console.IsSafeToQuit) - Engine.RequestExit(); - }; - } + onExit.Triggered += () => + { + if (Console.IsSafeToQuit) + Engine.RequestExit(); + }; + } - public override void OnDestroy() - { - base.OnDestroy(); + public override void OnDestroy() + { + base.OnDestroy(); - onExit.Dispose(); - } + onExit.Dispose(); + } - public override void OnStart() - { - var initialEulerAngles = Actor.Orientation.EulerAngles; - viewPitch = initialEulerAngles.X; - viewYaw = initialEulerAngles.Y; - viewRoll = initialEulerAngles.Z; - } + public override void OnStart() + { + Vector3 initialEulerAngles = Actor.Orientation.EulerAngles; + viewPitch = initialEulerAngles.X; + viewYaw = initialEulerAngles.Y; + viewRoll = initialEulerAngles.Z; + } - public override void OnUpdate() - { - var camTrans = Actor.Transform; - var rootActor = Actor.GetChild(0); - var camera = rootActor.GetChild(); + public override void OnUpdate() + { + Transform camTrans = Actor.Transform; + Actor rootActor = Actor.GetChild(0); + Camera camera = rootActor.GetChild(); - float xAxis = InputManager.GetAxisRaw("Mouse X"); - float yAxis = InputManager.GetAxisRaw("Mouse Y"); - if (xAxis != 0.0f || yAxis != 0.0f) - { - viewPitch += yAxis; - viewYaw += xAxis; + float xAxis = InputManager.GetAxisRaw("Mouse X"); + float yAxis = InputManager.GetAxisRaw("Mouse Y"); + if (xAxis != 0.0f || yAxis != 0.0f) + { + viewPitch += yAxis; + viewYaw += xAxis; - viewPitch = Mathf.Clamp(viewPitch, -90.0f, 90.0f); + viewPitch = Mathf.Clamp(viewPitch, -90.0f, 90.0f); - // root orientation must be set first - rootActor.Orientation = Quaternion.Euler(0, viewYaw, 0); - camera.Orientation = Quaternion.Euler(viewPitch, viewYaw, viewRoll); - } + // root orientation must be set first + rootActor.Orientation = Quaternion.Euler(0, viewYaw, 0); + camera.Orientation = Quaternion.Euler(viewPitch, viewYaw, viewRoll); + } - float inputH = InputManager.GetAxis("Horizontal"); - float inputV = InputManager.GetAxis("Vertical"); - var move = new Vector3(inputH, 0.0f, inputV); + float inputH = InputManager.GetAxis("Horizontal"); + float inputV = InputManager.GetAxis("Vertical"); + Vector3 move = new Vector3(inputH, 0.0f, inputV); - if (!move.IsZero) - { - move.Normalize(); - move = camera.Transform.TransformDirection(move) * MoveSpeed; + if (!move.IsZero) + { + move.Normalize(); + move = camera.Transform.TransformDirection(move) * MoveSpeed; - { - Vector3 delta = move * Time.UnscaledDeltaTime; - float movementLeft = delta.Length; + { + Vector3 delta = move * Time.UnscaledDeltaTime; + float movementLeft = delta.Length; - // TODO: check multiple times in case we get stuck in walls + // TODO: check multiple times in case we get stuck in walls - float sphereRadius = 10.0f; // TODO: use collider radius - RayCastHit[] hitInfos; - float moveDist = delta.Length; - Physics.SphereCastAll(Actor.Transform.Translation, sphereRadius, move.Normalized, out hitInfos, - moveDist); + float sphereRadius = 10.0f; // TODO: use collider radius + RayCastHit[] hitInfos; + float moveDist = delta.Length; + Physics.SphereCastAll(Actor.Transform.Translation, sphereRadius, move.Normalized, out hitInfos, + moveDist); - //bool nohit = true; - float hitDistance = moveDist; - Vector3 hitNormal = move.Normalized; - foreach (RayCastHit hitInfo in hitInfos) - { - if (hitInfo.Collider.Parent == Parent) - continue; + //bool nohit = true; + float hitDistance = moveDist; + Vector3 hitNormal = move.Normalized; + foreach (RayCastHit hitInfo in hitInfos) + { + if (hitInfo.Collider.Parent == Parent) + continue; - if (hitInfo.Distance < hitDistance) - { - hitDistance = hitInfo.Distance; - hitNormal = hitInfo.Normal; - } - //nohit = false; - //break; - } + if (hitInfo.Distance < hitDistance) + { + hitDistance = hitInfo.Distance; + hitNormal = hitInfo.Normal; + } + //nohit = false; + //break; + } - if (hitDistance != moveDist) - { - //camTrans.Translation = Vector3.Lerp(Actor.Transform.Translation, camTrans.Translation, hitDistance); + if (hitDistance != moveDist) + //camTrans.Translation = Vector3.Lerp(Actor.Transform.Translation, camTrans.Translation, hitDistance); - //projected = normal * dot(direction, normal); - //direction = direction - projected + //projected = normal * dot(direction, normal); + //direction = direction - projected + //camTrans.Translation += hitNormal * (moveDist - hitDistance); // correct? + //camTrans.Translation = hitNormal * (move * hitNormal); // correct? + //camTrans.Translation = Actor.Transform.Translation; + delta += -Vector3.Dot(delta, hitNormal) * hitNormal; // correct? - //camTrans.Translation += hitNormal * (moveDist - hitDistance); // correct? - //camTrans.Translation = hitNormal * (move * hitNormal); // correct? - //camTrans.Translation = Actor.Transform.Translation; - delta += -Vector3.Dot(delta, hitNormal) * hitNormal; // correct? - } + camTrans.Translation += delta; + } + } - camTrans.Translation += delta; - } - } - - Actor.Transform = camTrans; - } - } + Actor.Transform = camTrans; + } + } } \ No newline at end of file diff --git a/Source/Game/Camera/CameraRender.cs b/Source/Game/Camera/CameraRender.cs index 7c59f0f..0983109 100644 --- a/Source/Game/Camera/CameraRender.cs +++ b/Source/Game/Camera/CameraRender.cs @@ -1,30 +1,34 @@ -using System; -using System.Runtime; +using Cabrito; using FlaxEditor.Content.Settings; using FlaxEngine; -using Console = Cabrito.Console; namespace Game { /// - /// CameraRender Script. + /// CameraRender Script. /// [ExecuteInEditMode] public class CameraRender : PostProcessEffect { public Camera camera; - public Material material; - private GPUTexture texture; - private GPUTexture texture2; + private bool lastEnabled; + public Material material; private MaterialInstance materialInstance; private SceneRenderTask sceneTask; private SceneRenderTask sceneTask2; + private GPUTexture texture; + private GPUTexture texture2; + + public override PostProcessEffectLocation Location => PostProcessEffectLocation.Default; + public override int Order => 110; + public override bool CanRender => camera.IsActive; + private void CreateTextures(int width, int height) { - var textureDesc = GPUTextureDescription.New2D(width, height, PixelFormat.R8G8B8A8_UNorm); + GPUTextureDescription textureDesc = GPUTextureDescription.New2D(width, height, PixelFormat.R8G8B8A8_UNorm); // Prepare texture and SceneRenderTask for viewmodel camera if (texture == null) @@ -82,11 +86,8 @@ namespace Game Destroy(ref texture2); } - public override PostProcessEffectLocation Location => PostProcessEffectLocation.Default; - public override int Order => 110; - public override bool CanRender => camera.IsActive; - - public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, GPUTexture output) + public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, + GPUTexture output) { if (texture == null || texture2 == null) return; @@ -94,13 +95,12 @@ namespace Game Renderer.DrawPostFxMaterial(context, ref renderContext, materialInstance, output, input.View()); } - private bool lastEnabled; public override void OnUpdate() { #if FLAX_EDITOR if (Input.GetKeyDown(KeyboardKeys.F7)) { - var physicsSettings = GameSettings.Load(); + PhysicsSettings physicsSettings = GameSettings.Load(); physicsSettings.EnableSubstepping = !physicsSettings.EnableSubstepping; GameSettings.Save(physicsSettings); //GameSettings.Apply(); @@ -130,4 +130,4 @@ namespace Game //OnAwake(); } } -} +} \ No newline at end of file diff --git a/Source/Game/Camera/CameraSpring.cs b/Source/Game/Camera/CameraSpring.cs index e7ca0a2..416049b 100644 --- a/Source/Game/Camera/CameraSpring.cs +++ b/Source/Game/Camera/CameraSpring.cs @@ -1,25 +1,19 @@ -using System; -using System.Runtime; -using FlaxEditor.Content.Settings; -using FlaxEngine; -using Console = Cabrito.Console; - +using FlaxEngine; namespace Game { public class CameraSpring : Script { - private Vector3 targetOffset; + private bool lastGround; private Vector3 lastPosition; - - public float speed = 240f; public float percY; private Actor playerActor; - private Actor viewModelHolder; private PlayerMovement playerMovement; - private bool lastGround; + public float speed = 240f; + private Vector3 targetOffset; + private Actor viewModelHolder; public override void OnStart() { @@ -62,7 +56,9 @@ namespace Game } } else + { UpdatePosition(position); + } lastPosition = position; lastGround = playerMovement.onGround; diff --git a/Source/Game/Camera/WeaponSway.cs b/Source/Game/Camera/WeaponSway.cs index d4744a2..040dcf2 100644 --- a/Source/Game/Camera/WeaponSway.cs +++ b/Source/Game/Camera/WeaponSway.cs @@ -1,16 +1,17 @@ using System; -using System.Collections.Generic; using FlaxEngine; -using Console = Cabrito.Console; namespace Game { public class WeaponSway : Script { - public float swaySpeed = 3000f; + private Actor cameraHolder; private Actor rootActor; - private Actor cameraHolder; + public float swaySpeed = 3000f; + + private float timeRemainder; + public override void OnStart() { rootActor = Actor.Parent.GetChild("RootActor"); @@ -25,7 +26,6 @@ namespace Game return yawRoll * pitch; } - private float timeRemainder = 0f; public override void OnLateUpdate() { Quaternion rotation = GetRotation(); @@ -65,7 +65,7 @@ namespace Game float percZ = Mathf.Abs(deltaZ) / maxAngle; float minSpeed = swaySpeedScaled * 0.00001f * 0f; - Func fun = (f) => Mathf.Pow(f, 1.3f); + Func fun = f => Mathf.Pow(f, 1.3f); angles.X = Mathf.MoveTowardsAngle(angles.X, targetAngles.X, Math.Max(swaySpeedScaled * fun(percX), minSpeed)); @@ -74,9 +74,10 @@ namespace Game angles.Z = Mathf.MoveTowardsAngle(angles.Z, targetAngles.Z, Math.Max(swaySpeedScaled * fun(percZ), minSpeed)); } while (remaining > minTime); + timeRemainder -= remaining; Actor.LocalOrientation = Quaternion.Euler(angles); } } -} +} \ No newline at end of file diff --git a/Source/Game/Console/CommonCommands.cs b/Source/Game/Console/CommonCommands.cs index f74d70a..05fe675 100644 --- a/Source/Game/Console/CommonCommands.cs +++ b/Source/Game/Console/CommonCommands.cs @@ -1,49 +1,42 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Cabrito { - // Holds common miscellaneous Console variables and commands - public static class CommonCommands - { - [ConsoleCommand("")] - public static void NullCommand() - { - } + // Holds common miscellaneous Console variables and commands + public static class CommonCommands + { + [ConsoleVariable("developer")] + public static string Developer + { + get => Console.DebugVerbosity.ToString(); + set + { + if (int.TryParse(value, out int intValue) && intValue >= 0) + Console.DebugVerbosity = intValue; + } + } - [ConsoleCommand("echo")] - public static void EchoCommand() - { - Console.Print("nothing"); - } + [ConsoleCommand("")] + public static void NullCommand() + { + } - [ConsoleCommand("echo")] - public static void EchoCommand(string[] text) - { - Console.Print(string.Join(" ", text)); - } + [ConsoleCommand("echo")] + public static void EchoCommand() + { + Console.Print("nothing"); + } - [ConsoleCommand("debugthrow")] - public static void DebugThrowCommand(string[] text) - { - throw new Exception(string.Join(" ", text)); - } + [ConsoleCommand("echo")] + public static void EchoCommand(string[] text) + { + Console.Print(string.Join(" ", text)); + } - [ConsoleVariable("developer")] - public static string Developer - { - get - { - return Console.DebugVerbosity.ToString(); - } - set - { - if (int.TryParse(value, out int intValue) && intValue >= 0) - Console.DebugVerbosity = intValue; - } - } - } + [ConsoleCommand("debugthrow")] + public static void DebugThrowCommand(string[] text) + { + throw new Exception(string.Join(" ", text)); + } + } } \ No newline at end of file diff --git a/Source/Game/Console/Console.cs b/Source/Game/Console/Console.cs index 94894b9..57ae28b 100644 --- a/Source/Game/Console/Console.cs +++ b/Source/Game/Console/Console.cs @@ -3,472 +3,507 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using FlaxEngine; -using Debug = System.Diagnostics.Debug; +using FlaxEditor; namespace Cabrito { - public class ConsoleLine - { - public string content; + public class ConsoleLine + { + public string content; - internal ConsoleLine(string line) - { - content = line; - } + internal ConsoleLine(string line) + { + content = line; + } - public static implicit operator string(ConsoleLine line) => line.content; - public static explicit operator ConsoleLine(string line) => new ConsoleLine(line); + public static implicit operator string(ConsoleLine line) + { + return line.content; + } - public override string ToString() => content.ToString(); - } + public static explicit operator ConsoleLine(string line) + { + return new ConsoleLine(line); + } - public static class Console - { - private static ConsoleInstance instance; + public override string ToString() + { + return content; + } + } - public static void Init() - { - if (instance != null) - return; + public static class Console + { + private static ConsoleInstance instance; - Destroy(); - instance = new ConsoleInstance(); - instance.InitConsoleSubsystems(); + // Returns if Console window open right now. + public static bool IsOpen => instance.IsOpen; + + // For debugging only: Returns true when Console was not closed during the same frame. + // Needed when Escape-key both closes the console and exits the game. + public static bool IsSafeToQuit => instance.IsSafeToQuit; + + // Called when Console is opened. + public static Action OnOpen + { + get => instance.OnOpen; + set => instance.OnOpen = value; + } + + // Called when Console is closed. + public static Action OnClose + { + get => instance.OnClose; + set => instance.OnClose = value; + } + + // Called when a line of text was printed in Console. + public static Action OnPrint + { + get => instance.OnPrint; + set => instance.OnPrint = value; + } + + public static bool ShowExecutedLines => instance.ShowExecutedLines; + + public static int DebugVerbosity + { + get => instance.DebugVerbosity; + set => instance.DebugVerbosity = value; + } + + public static string LinePrefix => instance.LinePrefix; + + public static IReadOnlyCollection Lines => instance.Lines; + + public static void Init() + { + if (instance != null) + return; + + Destroy(); + instance = new ConsoleInstance(); + instance.InitConsoleSubsystems(); #if FLAX_EDITOR - FlaxEditor.ScriptsBuilder.ScriptsReload += Destroy; - FlaxEditor.Editor.Instance.StateMachine.StateChanged += OnEditorStateChanged; + ScriptsBuilder.ScriptsReload += Destroy; + Editor.Instance.StateMachine.StateChanged += OnEditorStateChanged; #endif - } + } - public static void Destroy() - { - if (instance != null) - { - instance.Dispose(); - instance = null; + public static void Destroy() + { + if (instance != null) + { + instance.Dispose(); + instance = null; #if FLAX_EDITOR - FlaxEditor.ScriptsBuilder.ScriptsReload -= Destroy; - FlaxEditor.Editor.Instance.StateMachine.StateChanged -= OnEditorStateChanged; + ScriptsBuilder.ScriptsReload -= Destroy; + Editor.Instance.StateMachine.StateChanged -= OnEditorStateChanged; #endif - } - } + } + } #if FLAX_EDITOR - private static void OnEditorStateChanged() - { - if (!FlaxEditor.Editor.Instance.StateMachine.IsPlayMode) - { - // Clear console buffer when leaving play mode - if (instance != null) - Console.Clear(); - } - } + private static void OnEditorStateChanged() + { + if (!Editor.Instance.StateMachine.IsPlayMode) + // Clear console buffer when leaving play mode + if (instance != null) + Clear(); + } #endif - // Returns if Console window open right now. - public static bool IsOpen => instance.IsOpen; - - // For debugging only: Returns true when Console was not closed during the same frame. - // Needed when Escape-key both closes the console and exits the game. - public static bool IsSafeToQuit => instance.IsSafeToQuit; - - // Called when Console is opened. - public static Action OnOpen - { - get { return instance.OnOpen; } - set { instance.OnOpen = value; } - } - - // Called when Console is closed. - public static Action OnClose - { - get { return instance.OnClose; } - set { instance.OnClose = value; } - } - - // Called when a line of text was printed in Console. - public static Action OnPrint - { - get { return instance.OnPrint; } - set { instance.OnPrint = value; } - } - - public static bool ShowExecutedLines => instance.ShowExecutedLines; - public static int DebugVerbosity - { - get => instance.DebugVerbosity; - set => instance.DebugVerbosity = value; - } - - public static string LinePrefix => instance.LinePrefix; - - public static IReadOnlyCollection Lines => instance.Lines; - - public static string GetBufferHistory(int index) => instance.GetBufferHistory(index); - - // Echoes text to Console - public static void Print(string text) => instance.Print(text); - - // Echoes warning text to Console - public static void PrintWarning(string text) => instance.PrintWarning(text); - - // Echoes error text to Console - public static void PrintError(string text) => instance.PrintError(text); - - // Echoes developer/debug text to Console - public static void PrintDebug(string text) => instance.PrintDebug(1, false, text); - - // Echoes developer/debug text to Console - public static void PrintDebug(int verbosity, string text) => instance.PrintDebug(verbosity, false, text); - - // Echoes developer/debug text to Console - public static void PrintDebug(int verbosity, bool noRepeat, string text) => instance.PrintDebug(verbosity, noRepeat, text); - - // Opens the Console - public static void Open() => instance.Open(); - - // Closes the Console - public static void Close() => instance.Close(); - - // Clears the content of the Console - public static void Clear() => instance.Clear(); - - public static void Execute(string str, bool bufferInput = false) => instance.Execute(str, bufferInput); - - public static string GetVariable(string variableName) => instance.GetVariable(variableName); - } - - public class ConsoleInstance : IDisposable - { - public bool IsOpen { get; internal set; } = true; - - public bool IsSafeToQuit - { - get { return stopwatch.Elapsed.TotalSeconds > 0.1; } - } - - private Stopwatch stopwatch = Stopwatch.StartNew(); - - public Action OnOpen; - public Action OnClose; - public Action OnPrint; - - public bool ShowExecutedLines = true; - public int DebugVerbosity { get; set; } = 1; - public string LinePrefix { get; internal set; } = "]"; - - //private static List consoleLines = new List(); - private List consoleLines = new List(); - private Dictionary consoleCommands = new Dictionary(); - private Dictionary consoleVariables = new Dictionary(); - private List consoleBufferHistory = new List(); - - internal ConsoleInstance() - { - } - - // Initializes the Console system. - internal void InitConsoleSubsystems() - { - AppDomain currentDomain = AppDomain.CurrentDomain; - Assembly[] assemblies = currentDomain.GetAssemblies(); - - foreach (var assembly in assemblies) - { - // Skip common assemblies - var assemblyName = assembly.GetName().Name; - if (assemblyName == "System" || - assemblyName.StartsWith("System.") || - assemblyName.StartsWith("Mono.") || - assemblyName == "mscorlib" || - assemblyName == "Newtonsoft.Json" || - assemblyName.StartsWith("FlaxEngine.") || - assemblyName.StartsWith("JetBrains.") || - assemblyName.StartsWith("Microsoft.") || - assemblyName.StartsWith("nunit.")) - { - continue; - } - - foreach (var type in assembly.GetTypes()) - { - Dictionary cmdParsed = new Dictionary(); - Dictionary> cmdMethods = new Dictionary>(); - MethodInfo cmdInitializer = null; - - foreach (MethodInfo method in type.GetMethods()) - { - if (!method.IsStatic) - continue; - - Attribute[] attributes = Attribute.GetCustomAttributes(method); - - foreach (Attribute attr in attributes) - { - if (attr is ConsoleCommandAttribute cmdAttribute) - { - //Console.Print("found cmd '" + cmdAttribute.name + "' bound to field '" + method.Name + "'"); - - // Defer constructing the command until we have parsed all the methods for it in this assembly. - - List methods; - if (!cmdMethods.TryGetValue(cmdAttribute.name, out methods)) - { - methods = new List(); - cmdMethods.Add(cmdAttribute.name, methods); - } - - methods.Add(method); - - ConsoleCommand cmd = new ConsoleCommand(cmdAttribute.name, null); - if (!cmdParsed.ContainsKey(cmdAttribute.name)) - cmdParsed.Add(cmdAttribute.name, cmd); - foreach (var alias in cmdAttribute.aliases) - { - if (!cmdParsed.ContainsKey(alias)) - cmdParsed.Add(alias, cmd); - - List aliasMethods; - if (!cmdMethods.TryGetValue(alias, out aliasMethods)) - { - aliasMethods = new List(); - cmdMethods.Add(alias, aliasMethods); - } - - aliasMethods.Add(method); - } - } - else if (attr is ConsoleSubsystemInitializer) - { - cmdInitializer = method; - } - } - } - - foreach (var kv in cmdParsed) - { - var methods = cmdMethods[kv.Key]; - var definition = kv.Value; - - ConsoleCommand cmd = new ConsoleCommand(definition.name, methods.ToArray()); - consoleCommands.Add(kv.Key, cmd); - } - - foreach (FieldInfo field in type.GetFields()) - { - if (!field.IsStatic) - continue; - - Attribute[] attributes = Attribute.GetCustomAttributes(field); - - foreach (Attribute attr in attributes) - { - if (attr is ConsoleVariableAttribute cvarAttribute) - { - //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); - consoleVariables.Add(cvarAttribute.name, - new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, field)); - foreach (var alias in cvarAttribute.aliases) - consoleVariables.Add(alias, - new ConsoleVariable(cvarAttribute.name, - cvarAttribute.flags | ConsoleFlags.NoSerialize, field)); - } - } - } - - foreach (PropertyInfo prop in type.GetProperties()) - { - MethodInfo getter = prop.GetGetMethod(); - MethodInfo setter = prop.GetSetMethod(); - if (getter == null || setter == null || !getter.IsStatic || !setter.IsStatic) - continue; - - Attribute[] attributes = Attribute.GetCustomAttributes(prop); - - foreach (Attribute attr in attributes) - { - if (attr is ConsoleVariableAttribute cvarAttribute) - { - //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); - consoleVariables.Add(cvarAttribute.name, - new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, getter, setter)); - foreach (var alias in cvarAttribute.aliases) - consoleVariables.Add(alias, - new ConsoleVariable(cvarAttribute.name, - cvarAttribute.flags | ConsoleFlags.NoSerialize, getter, setter)); - } - } - } - - if (cmdInitializer != null) - { - Console.PrintDebug(2, "Initializing " + type.Name); - cmdInitializer.Invoke(null, null); - } - } - } - } - - public void Dispose() - { - } - - public IReadOnlyCollection Lines - { - get => consoleLines.AsReadOnly(); - } - - public string GetBufferHistory(int index) - { - if (consoleBufferHistory.Count == 0) - return null; - - if (index > consoleBufferHistory.Count - 1 || index < 0) - return null; - - return consoleBufferHistory[index]; - } - - // Echoes text to Console - public void Print(string text) - { - debugLastLine = text; - - foreach (var line in text.Split(new[] { '\n' })) - { - ConsoleLine lineEntry = new ConsoleLine(line); - consoleLines.Add(lineEntry); - OnPrint?.Invoke(text); - } - } - - // Echoes warning text to Console - public void PrintWarning(string text) - { - foreach (var line in text.Split(new[] { '\n' })) - { - ConsoleLine lineEntry = new ConsoleLine(line); - consoleLines.Add(lineEntry); - OnPrint?.Invoke(text); - } - } - - // Echoes error text to Console - public void PrintError(string text) - { - foreach (var line in text.Split(new[] { '\n' })) - { - ConsoleLine lineEntry = new ConsoleLine(line); - consoleLines.Add(lineEntry); - OnPrint?.Invoke(text); - } - - if (Debugger.IsAttached) - Debugger.Break(); - else - throw new Exception(text); - } - - // Echoes developer/debug text to Console - private string debugLastLine = ""; - public void PrintDebug(int verbosity, bool noRepeat, string text) - { - if (DebugVerbosity < verbosity) - return; - - if (noRepeat) - { - if (debugLastLine.Length == text.Length && debugLastLine == text) - return; - } - - debugLastLine = text; - - foreach (var line in text.Split(new[] { '\n' })) - { - ConsoleLine lineEntry = new ConsoleLine(line); - consoleLines.Add(lineEntry); - OnPrint?.Invoke(text); - } - } - - // Opens the Console - public void Open() - { - if (IsOpen) - return; - - IsOpen = true; - OnOpen?.Invoke(); - } - - // Closes the Console - public void Close() - { - if (!IsOpen) - return; - - IsOpen = false; - OnClose?.Invoke(); - - stopwatch.Restart(); - } - - // Clears the content of the Console - public void Clear() - { - consoleLines.Clear(); - } - - public void Execute(string str, bool bufferInput = false) - { - if (bufferInput) - consoleBufferHistory.Insert(0, str); - - str = str.Trim(); - - if (ShowExecutedLines) - Console.Print(LinePrefix + str); - - string[] strs = str.Split(' '); - string execute = strs[0]; - string executeLower = execute.ToLowerInvariant(); - string value = strs.Length > 1 ? str.Substring(execute.Length + 1) : null; - - //Console.PrintDebug("Executed '" + execute + "' with params: '" + value + "'"); - - if (consoleCommands.TryGetValue(executeLower, out ConsoleCommand cmd)) - { - string[] values = strs.Skip(1).ToArray(); - if (values.Length > 0) - cmd.Invoke(values); - else - cmd.Invoke(); - //Console.Print("Command bound to '" + execute + "' is '" + cmd.method.Name + "'"); - } - else if (consoleVariables.TryGetValue(executeLower, out ConsoleVariable cvar)) - { - if (value != null) - cvar.SetValue(value); - - Console.Print("'" + execute + "' is '" + cvar.GetValueString() + "'"); - } - else - Console.Print("Unknown command '" + execute + "'"); - } - - public string GetVariable(string variableName) - { - if (consoleVariables.TryGetValue(variableName, out ConsoleVariable cvar)) - { - string value = cvar.GetValueString(); - return value; - } - - return null; - } - } + public static string GetBufferHistory(int index) + { + return instance.GetBufferHistory(index); + } + + // Echoes text to Console + public static void Print(string text) + { + instance.Print(text); + } + + // Echoes warning text to Console + public static void PrintWarning(string text) + { + instance.PrintWarning(text); + } + + // Echoes error text to Console + public static void PrintError(string text) + { + instance.PrintError(text); + } + + // Echoes developer/debug text to Console + public static void PrintDebug(string text) + { + instance.PrintDebug(1, false, text); + } + + // Echoes developer/debug text to Console + public static void PrintDebug(int verbosity, string text) + { + instance.PrintDebug(verbosity, false, text); + } + + // Echoes developer/debug text to Console + public static void PrintDebug(int verbosity, bool noRepeat, string text) + { + instance.PrintDebug(verbosity, noRepeat, text); + } + + // Opens the Console + public static void Open() + { + instance.Open(); + } + + // Closes the Console + public static void Close() + { + instance.Close(); + } + + // Clears the content of the Console + public static void Clear() + { + instance.Clear(); + } + + public static void Execute(string str, bool bufferInput = false) + { + instance.Execute(str, bufferInput); + } + + public static string GetVariable(string variableName) + { + return instance.GetVariable(variableName); + } + } + + public class ConsoleInstance : IDisposable + { + private readonly List consoleBufferHistory = new List(); + private readonly Dictionary consoleCommands = new Dictionary(); + + //private static List consoleLines = new List(); + private readonly List consoleLines = new List(); + + private readonly Dictionary consoleVariables = + new Dictionary(); + + private readonly Stopwatch stopwatch = Stopwatch.StartNew(); + + // Echoes developer/debug text to Console + private string debugLastLine = ""; + public Action OnClose; + + public Action OnOpen; + public Action OnPrint; + + public bool ShowExecutedLines = true; + + internal ConsoleInstance() + { + } + + public bool IsOpen { get; internal set; } = true; + + public bool IsSafeToQuit => stopwatch.Elapsed.TotalSeconds > 0.1; + + public int DebugVerbosity { get; set; } = 1; + public string LinePrefix { get; internal set; } = "]"; + + public IReadOnlyCollection Lines => consoleLines.AsReadOnly(); + + public void Dispose() + { + } + + // Initializes the Console system. + internal void InitConsoleSubsystems() + { + AppDomain currentDomain = AppDomain.CurrentDomain; + var assemblies = currentDomain.GetAssemblies(); + + foreach (Assembly assembly in assemblies) + { + // Skip common assemblies + string assemblyName = assembly.GetName().Name; + if (assemblyName == "System" || + assemblyName.StartsWith("System.") || + assemblyName.StartsWith("Mono.") || + assemblyName == "mscorlib" || + assemblyName == "Newtonsoft.Json" || + assemblyName.StartsWith("FlaxEngine.") || + assemblyName.StartsWith("JetBrains.") || + assemblyName.StartsWith("Microsoft.") || + assemblyName.StartsWith("nunit.")) + continue; + + foreach (Type type in assembly.GetTypes()) + { + var cmdParsed = new Dictionary(); + var cmdMethods = new Dictionary>(); + MethodInfo cmdInitializer = null; + + foreach (MethodInfo method in type.GetMethods()) + { + if (!method.IsStatic) + continue; + + var attributes = Attribute.GetCustomAttributes(method); + + foreach (Attribute attr in attributes) + if (attr is ConsoleCommandAttribute cmdAttribute) + { + //Console.Print("found cmd '" + cmdAttribute.name + "' bound to field '" + method.Name + "'"); + + // Defer constructing the command until we have parsed all the methods for it in this assembly. + + List methods; + if (!cmdMethods.TryGetValue(cmdAttribute.name, out methods)) + { + methods = new List(); + cmdMethods.Add(cmdAttribute.name, methods); + } + + methods.Add(method); + + ConsoleCommand cmd = new ConsoleCommand(cmdAttribute.name, null); + if (!cmdParsed.ContainsKey(cmdAttribute.name)) + cmdParsed.Add(cmdAttribute.name, cmd); + foreach (string alias in cmdAttribute.aliases) + { + if (!cmdParsed.ContainsKey(alias)) + cmdParsed.Add(alias, cmd); + + List aliasMethods; + if (!cmdMethods.TryGetValue(alias, out aliasMethods)) + { + aliasMethods = new List(); + cmdMethods.Add(alias, aliasMethods); + } + + aliasMethods.Add(method); + } + } + else if (attr is ConsoleSubsystemInitializer) + { + cmdInitializer = method; + } + } + + foreach (var kv in cmdParsed) + { + var methods = cmdMethods[kv.Key]; + ConsoleCommand definition = kv.Value; + + ConsoleCommand cmd = new ConsoleCommand(definition.name, methods.ToArray()); + consoleCommands.Add(kv.Key, cmd); + } + + foreach (FieldInfo field in type.GetFields()) + { + if (!field.IsStatic) + continue; + + var attributes = Attribute.GetCustomAttributes(field); + + foreach (Attribute attr in attributes) + if (attr is ConsoleVariableAttribute cvarAttribute) + { + //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); + consoleVariables.Add(cvarAttribute.name, + new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, field)); + foreach (string alias in cvarAttribute.aliases) + consoleVariables.Add(alias, + new ConsoleVariable(cvarAttribute.name, + cvarAttribute.flags | ConsoleFlags.NoSerialize, field)); + } + } + + foreach (PropertyInfo prop in type.GetProperties()) + { + MethodInfo getter = prop.GetGetMethod(); + MethodInfo setter = prop.GetSetMethod(); + if (getter == null || setter == null || !getter.IsStatic || !setter.IsStatic) + continue; + + var attributes = Attribute.GetCustomAttributes(prop); + + foreach (Attribute attr in attributes) + if (attr is ConsoleVariableAttribute cvarAttribute) + { + //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); + consoleVariables.Add(cvarAttribute.name, + new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, getter, setter)); + foreach (string alias in cvarAttribute.aliases) + consoleVariables.Add(alias, + new ConsoleVariable(cvarAttribute.name, + cvarAttribute.flags | ConsoleFlags.NoSerialize, getter, setter)); + } + } + + if (cmdInitializer != null) + { + Console.PrintDebug(2, "Initializing " + type.Name); + cmdInitializer.Invoke(null, null); + } + } + } + } + + public string GetBufferHistory(int index) + { + if (consoleBufferHistory.Count == 0) + return null; + + if (index > consoleBufferHistory.Count - 1 || index < 0) + return null; + + return consoleBufferHistory[index]; + } + + // Echoes text to Console + public void Print(string text) + { + debugLastLine = text; + + foreach (string line in text.Split('\n')) + { + ConsoleLine lineEntry = new ConsoleLine(line); + consoleLines.Add(lineEntry); + OnPrint?.Invoke(text); + } + } + + // Echoes warning text to Console + public void PrintWarning(string text) + { + foreach (string line in text.Split('\n')) + { + ConsoleLine lineEntry = new ConsoleLine(line); + consoleLines.Add(lineEntry); + OnPrint?.Invoke(text); + } + } + + // Echoes error text to Console + public void PrintError(string text) + { + foreach (string line in text.Split('\n')) + { + ConsoleLine lineEntry = new ConsoleLine(line); + consoleLines.Add(lineEntry); + OnPrint?.Invoke(text); + } + + if (Debugger.IsAttached) + Debugger.Break(); + else + throw new Exception(text); + } + + public void PrintDebug(int verbosity, bool noRepeat, string text) + { + if (DebugVerbosity < verbosity) + return; + + if (noRepeat) + if (debugLastLine.Length == text.Length && debugLastLine == text) + return; + + debugLastLine = text; + + foreach (string line in text.Split('\n')) + { + ConsoleLine lineEntry = new ConsoleLine(line); + consoleLines.Add(lineEntry); + OnPrint?.Invoke(text); + } + } + + // Opens the Console + public void Open() + { + if (IsOpen) + return; + + IsOpen = true; + OnOpen?.Invoke(); + } + + // Closes the Console + public void Close() + { + if (!IsOpen) + return; + + IsOpen = false; + OnClose?.Invoke(); + + stopwatch.Restart(); + } + + // Clears the content of the Console + public void Clear() + { + consoleLines.Clear(); + } + + public void Execute(string str, bool bufferInput = false) + { + if (bufferInput) + consoleBufferHistory.Insert(0, str); + + str = str.Trim(); + + if (ShowExecutedLines) + Console.Print(LinePrefix + str); + + string[] strs = str.Split(' '); + string execute = strs[0]; + string executeLower = execute.ToLowerInvariant(); + string value = strs.Length > 1 ? str.Substring(execute.Length + 1) : null; + + //Console.PrintDebug("Executed '" + execute + "' with params: '" + value + "'"); + + if (consoleCommands.TryGetValue(executeLower, out ConsoleCommand cmd)) + { + string[] values = strs.Skip(1).ToArray(); + if (values.Length > 0) + cmd.Invoke(values); + else + cmd.Invoke(); + //Console.Print("Command bound to '" + execute + "' is '" + cmd.method.Name + "'"); + } + else if (consoleVariables.TryGetValue(executeLower, out ConsoleVariable cvar)) + { + if (value != null) + cvar.SetValue(value); + + Console.Print("'" + execute + "' is '" + cvar.GetValueString() + "'"); + } + else + { + Console.Print("Unknown command '" + execute + "'"); + } + } + + public string GetVariable(string variableName) + { + if (consoleVariables.TryGetValue(variableName, out ConsoleVariable cvar)) + { + string value = cvar.GetValueString(); + return value; + } + + return null; + } + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleAttributes.cs b/Source/Game/Console/ConsoleAttributes.cs index c7516ea..4a5cb21 100644 --- a/Source/Game/Console/ConsoleAttributes.cs +++ b/Source/Game/Console/ConsoleAttributes.cs @@ -1,68 +1,67 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Cabrito { - [AttributeUsage(AttributeTargets.All)] - public abstract class ConsoleBaseAttribute : Attribute - { - internal string name; + [AttributeUsage(AttributeTargets.All)] + public abstract class ConsoleBaseAttribute : Attribute + { + // Additional aliases for this command, these should only be used with user interaction. + // Commands such as 'cvarlist' should not list these in order to avoid clutter. + internal string[] aliases = new string[0]; + internal string name; - // Additional aliases for this command, these should only be used with user interaction. - // Commands such as 'cvarlist' should not list these in order to avoid clutter. - internal string[] aliases = new string[0]; + public ConsoleBaseAttribute(string name) + { + this.name = name.ToLowerInvariant(); + } - public ConsoleFlags flags { get; private set; } + public ConsoleBaseAttribute(params string[] names) + { + name = names[0].ToLowerInvariant(); + aliases = new List(names).Skip(1).Select(x => x.ToLowerInvariant()).ToArray(); + } - public ConsoleBaseAttribute(string name) - { - this.name = name.ToLowerInvariant(); - } + public ConsoleFlags flags { get; private set; } + } - public ConsoleBaseAttribute(params string[] names) - { - this.name = names[0].ToLowerInvariant(); - aliases = new List(names).Skip(1).Select(x => x.ToLowerInvariant()).ToArray(); - } - } + [AttributeUsage(AttributeTargets.All)] + public class ConsoleVariableAttribute : ConsoleBaseAttribute + { + public ConsoleVariableAttribute(string name) : base(name) + { + } + } - [AttributeUsage(AttributeTargets.All)] - public class ConsoleVariableAttribute : ConsoleBaseAttribute - { - public ConsoleVariableAttribute(string name) : base(name) - { - } - } + [AttributeUsage(AttributeTargets.All)] + public class ConsoleCommandAttribute : ConsoleBaseAttribute + { + /// + /// Registers a command to Console system. + /// + /// Name used for calling this command. + public ConsoleCommandAttribute(string name) : base(name) + { + } - [AttributeUsage(AttributeTargets.All)] - public class ConsoleCommandAttribute : ConsoleBaseAttribute - { - /// - /// Registers a command to Console system. - /// - /// Name used for calling this command. - public ConsoleCommandAttribute(string name) : base(name) - { - } + /// + /// Registers a command to Console system. + /// + /// + /// Names used for calling this command. First name is the main name for this command, rest of the + /// names are aliases. + /// + public ConsoleCommandAttribute(params string[] names) : base(names) + { + } + } - /// - /// Registers a command to Console system. - /// - /// Names used for calling this command. First name is the main name for this command, rest of the names are aliases. - public ConsoleCommandAttribute(params string[] names) : base(names) - { - } - } - - /// - /// Constructor for the subsystem, must be called first before registering console commands. - /// - [AttributeUsage(AttributeTargets.All)] - public class ConsoleSubsystemInitializer : Attribute - { - - } + /// + /// Constructor for the subsystem, must be called first before registering console commands. + /// + [AttributeUsage(AttributeTargets.All)] + public class ConsoleSubsystemInitializer : Attribute + { + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleCommand.cs b/Source/Game/Console/ConsoleCommand.cs index 05adf60..e2cc608 100644 --- a/Source/Game/Console/ConsoleCommand.cs +++ b/Source/Game/Console/ConsoleCommand.cs @@ -1,81 +1,76 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Cabrito { - internal struct ConsoleCommand - { - public string name { get; private set; } + internal struct ConsoleCommand + { + public string name { get; } - private MethodInfo[] methods; + private readonly MethodInfo[] methods; - public ConsoleCommand(string name, MethodInfo[] method) - { - this.name = name; - this.methods = method; - } + public ConsoleCommand(string name, MethodInfo[] method) + { + this.name = name; + methods = method; + } - public void Invoke() - { - foreach (var method in methods) - { - var methodParameters = method.GetParameters(); - if (methodParameters.Length != 0) - continue; + public void Invoke() + { + foreach (MethodInfo method in methods) + { + var methodParameters = method.GetParameters(); + if (methodParameters.Length != 0) + continue; - method.Invoke(null, null); - return; - } + method.Invoke(null, null); + return; + } - throw new Exception("Unexpected number of parameters."); - } + throw new Exception("Unexpected number of parameters."); + } - public void Invoke(string[] parameters) - { - MethodInfo match = null; - foreach (var method in methods) - { - var methodParameters = method.GetParameters(); - if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[])) - { - match = method; - continue; - } - else if (methodParameters.Length != parameters.Length) - continue; + public void Invoke(string[] parameters) + { + MethodInfo match = null; + foreach (MethodInfo method in methods) + { + var methodParameters = method.GetParameters(); + if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[])) + { + match = method; + continue; + } - // TODO: try to parse string parameters to needed types first, - // may require finding the exact match first instead of first matching one. - for (int i = 0; i < methodParameters.Length; i++) - //if (methodParameters[i].ParameterType != parameters[i].GetType()) - if (methodParameters[i].ParameterType != typeof(string)) - continue; + if (methodParameters.Length != parameters.Length) + continue; - if (match != null) - { - // Prefer exact number of parameters over string[] match - if (methodParameters.Length != parameters.Length) - continue; - } + // TODO: try to parse string parameters to needed types first, + // may require finding the exact match first instead of first matching one. + for (int i = 0; i < methodParameters.Length; i++) + //if (methodParameters[i].ParameterType != parameters[i].GetType()) + if (methodParameters[i].ParameterType != typeof(string)) + continue; - match = method; - } + if (match != null) + // Prefer exact number of parameters over string[] match + if (methodParameters.Length != parameters.Length) + continue; - if (match != null) - { - if (match.GetParameters().Length == 1 && match.GetParameters()[0].ParameterType == typeof(string[])) - match.Invoke(null, new object[] {parameters}); - else - match.Invoke(null, parameters); + match = method; + } - return; - } + if (match != null) + { + if (match.GetParameters().Length == 1 && match.GetParameters()[0].ParameterType == typeof(string[])) + match.Invoke(null, new object[] { parameters }); + else + match.Invoke(null, parameters); - throw new Exception("Unexpected number of parameters."); - } - } + return; + } + + throw new Exception("Unexpected number of parameters."); + } + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleContentTextBox.cs b/Source/Game/Console/ConsoleContentTextBox.cs index 3fef8a6..3f1de9a 100644 --- a/Source/Game/Console/ConsoleContentTextBox.cs +++ b/Source/Game/Console/ConsoleContentTextBox.cs @@ -3,656 +3,647 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; -using FlaxEditor; using FlaxEngine; using FlaxEngine.GUI; namespace Cabrito { - public class ConsoleContentTextBox : Control - { - struct LineInfo - { - public int lineIndex; - public int lineOffset; - public int lineLength; - } - - [HideInEditor] public ConsoleInputTextBox inputBox; - - protected TextLayoutOptions _layout; - - private FontReference Font; - - public int FontHeight - { - get - { - return Font.GetFont().Height; - } - } - - public float LineSpacing = 1.0f; - - public TextWrapping Wrapping; - - public Color SelectionColor = new Color(0x00, 0x7A, 0xCC, 0xFF); - public Color BackgroundSelectedColor = Color.Transparent; - public float BackgroundSelectedFlashSpeed = 0; - public Color BorderSelectedColor = Color.Transparent; - public float CaretFlashSpeed = 0; - public Color BorderColor; - public Color TextColor = Color.White; - - public int DefaultMargin = 1; - public int ScrollOffset = 0; - public int ScrollMouseLines = 3; - - private float heightMultiplier = 1.0f; - - public float HeightMultiplier - { - get => heightMultiplier; - set - { - heightMultiplier = value; - UpdateHeight(); - } - } - - private int selectionStartLine = 0; - private int selectionStartChar = 0; - private int selectionEndLine = 0; - private int selectionEndChar = 0; - - - private bool selectionActive; - public bool HasSelection => !(selectionStartLine == selectionEndLine && selectionStartChar == selectionEndChar); - - public ConsoleContentTextBox() : base() - { - } - - public ConsoleContentTextBox(FontReference font, ConsoleInputTextBox inputBox, float x, float y, float width, float height) : base( - x, y, width, height) - { - this.inputBox = inputBox; - Height = height; - - Font = font; - - _layout = TextLayoutOptions.Default; - _layout.VerticalAlignment = TextAlignment.Near; - _layout.TextWrapping = TextWrapping.WrapChars; - _layout.Bounds = new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); - - - //IsMultiline = true; - //IsReadOnly = true; - //CaretColor = new Color(0f, 0f, 0f, 0f); - AutoFocus = false; - } - - private int GetFontCharacterWidth() - { - var font = Font.GetFont(); - if (!font) - return 0; - return (int) font.MeasureText("a").X; // hacky, but works for fixed-size fonts... - } - - public int GetFontHeight() - { - var font = Font.GetFont(); - if (font == null) - return (int) Height; - - return (int) Mathf.Round(LineSpacing * (font.Height / Platform.DpiScale) * Scale.Y); - } - - private int GetHeightInLines() - { - var font = Font.GetFont(); - if (!font) - return 0; - return (int) (Height / (font.Height / Platform.DpiScale)); // number of fully visible lines - } - - protected override void OnParentChangedInternal() - { - base.OnParentChangedInternal(); - - if (Parent != null) - OnParentResized(); - } - - public override void OnParentResized() - { - UpdateHeight(); - base.OnParentResized(); - } - - private void UpdateHeight() - { - if (Parent != null && Parent.Parent != null) - Height = (Parent.Parent.Size.Y * HeightMultiplier) - GetFontHeight(); - } - - - private void CalculateVisibleLines(IReadOnlyCollection lines, out int firstVisibleLine, - out int lastVisibleLine, out LineInfo[] wrappedLines) - { - wrappedLines = null; - firstVisibleLine = 0; - lastVisibleLine = 0; - - var font = Font.GetFont(); - if (!font) - return; - - float fontWidth = GetFontCharacterWidth(); - int lineMaxChars = (int) (Width / fontWidth); - int lineMaxLines = GetHeightInLines(); - int numLines = 0; - int lineIndex = lines.Count; - List lineInfos = new List(lineMaxLines + 1); - int linesSkipped = 0; - foreach (string line in lines.Reverse()) - { - lineIndex--; - if (linesSkipped < ScrollOffset) - { - linesSkipped++; - continue; - } - - int numChars = 0; - int startIndex = lineInfos.Count; - while (numChars < line.Length) - { - LineInfo li = new LineInfo(); - li.lineIndex = lineIndex; - li.lineOffset = numChars; - li.lineLength = Math.Min(line.Length - numChars, lineMaxChars); - lineInfos.Add(li); - - numChars += lineMaxChars; - } - - if (lineInfos.Count - startIndex > 1) - lineInfos.Reverse(startIndex, lineInfos.Count - startIndex); - numLines++; - - if (lineInfos.Count > lineMaxLines) - break; - } - - lineInfos.Reverse(); - wrappedLines = lineInfos.ToArray(); - - //return lines[lines.Count - numLines .. lines.Count]; // C# 8.0... - lastVisibleLine = lineIndex; - firstVisibleLine = lastVisibleLine - numLines; - } - - public static double accumDrawTime = 0.0; - public static long accumDrawTimes = 0; - - public override void Draw() - { - // Cache data - var rect = new Rectangle(Vector2.Zero, Size); - var font = Font.GetFont(); - if (!font) - return; - - Stopwatch sw = Stopwatch.StartNew(); - - // Background - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Background"); - Color backColor = BackgroundColor; - if (IsMouseOver) - backColor = BackgroundSelectedColor; - if (backColor.A > 0.0f) - Render2D.FillRectangle(rect, backColor); - - Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; - if (borderColor.A > 0.0f) - Render2D.DrawRectangle(rect, borderColor); - Profiler.EndEvent(); - - Profiler.BeginEvent("ConsoleContentTextBoxDraw_FetchLines"); - var lines = Console.Lines; - Profiler.EndEvent(); - if (lines.Count > 0) - { - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines"); - - // Apply view offset and clip mask - var textClipRectangle = new Rectangle(1, 1, Width - 2, Height - 2); - Render2D.PushClip(textClipRectangle); - - Profiler.BeginEvent("ConsoleContentTextBoxDraw_CalcVisLines"); - // Make sure lengthy lines are split - CalculateVisibleLines(lines, out int startLine, out int lastLine, out LineInfo[] wrappedLines); - Profiler.EndEvent(); - - float lineHeight = font.Height / Platform.DpiScale; - float accumHeight = wrappedLines.Length * lineHeight; - - // selection in line-space, wrapping ignored - int selectionLeftLine = selectionStartLine; - int selectionLeftChar = selectionStartChar; - int selectionRightLine = selectionEndLine; - int selectionRightChar = selectionEndChar; - - if (selectionLeftLine > selectionRightLine || - (selectionLeftLine == selectionRightLine && selectionLeftChar > selectionRightChar)) - { - selectionLeftLine = selectionEndLine; - selectionLeftChar = selectionEndChar; - selectionRightLine = selectionStartLine; - selectionRightChar = selectionStartChar; - } - - // render selection - if (selectionActive) - { - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Selection"); - //float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); - //alpha = alpha * alpha; - Color selectionColor = SelectionColor; // * alpha; - - TextLayoutOptions layout = _layout; - layout.Bounds = rect; - layout.Bounds.Y -= accumHeight - Height; - //for (int i = startLine; i < lastLine; i++) - foreach (LineInfo li in wrappedLines) - { - var lineIndex = li.lineIndex; - string fullLine = lines.ElementAt(lineIndex); - string line = fullLine.Substring(li.lineOffset, li.lineLength); - - int leftChar = selectionLeftChar; - int rightChar = selectionRightChar; - - Rectangle selectionRect = new Rectangle(layout.Bounds.X, layout.Bounds.Y, 0f, 0f); - - // apply selection - if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) - { - if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) - { - // whole line is selected - Vector2 lineSize = font.MeasureText(line); - selectionRect.Width = lineSize.X; - selectionRect.Height = lineSize.Y; - } - else if (lineIndex == selectionLeftLine) - { - if (lineIndex < selectionRightLine) - { - // right side of the line is selected - Vector2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); - Vector2 rightSize = font.MeasureText(fullLine.Substring(leftChar)); - selectionRect.X += leftSize.X; - selectionRect.Width = rightSize.X; - selectionRect.Height = rightSize.Y; - - //int diff = line.Length - selectionLeftChar; - //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : ""); - } - else if (lineIndex == selectionRightLine && leftChar != rightChar) - { - // selecting middle of the one line - Vector2 lineSize = font.MeasureText(fullLine); - Vector2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); - Vector2 midSize = - font.MeasureText(fullLine.Substring(leftChar, rightChar - leftChar)); - - selectionRect.X += leftSize.X; - selectionRect.Width = midSize.X; - selectionRect.Height = lineSize.Y; - - //int diff = selectionRightChar - selectionLeftChar; - //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : "") + line.Substring(selectionRightChar); - } - } - else if (lineIndex == selectionRightLine) - { - // left side of the line is selected - Vector2 leftSize = font.MeasureText(fullLine.Substring(0, rightChar)); - selectionRect.Width = leftSize.X; - selectionRect.Height = leftSize.Y; - - //line = (selectionRightChar > 0 ? new string('X', selectionRightChar) : "") + line.Substring(selectionRightChar); - } - } - - - Render2D.FillRectangle(selectionRect, selectionColor); - - layout.Bounds.Y += lineHeight; - } - - Profiler.EndEvent(); - } - - // render lines - { - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines_Render"); - TextLayoutOptions layout = _layout; - layout.Bounds = rect; - layout.Bounds.Y -= accumHeight - Height; - foreach (LineInfo li in wrappedLines) - { - var lineIndex = li.lineIndex; - string line = lines.ElementAt(lineIndex).content.Substring(li.lineOffset, li.lineLength); - Render2D.DrawText(font, line, TextColor, ref layout); - layout.Bounds.Y += lineHeight; - } - Profiler.EndEvent(); - } - - /*if (CaretPosition > -1) - { - var prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Vector2(); - var caretBounds = CaretBounds; - caretBounds.X += prefixSize.X; - - float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); - alpha = alpha * alpha * alpha * alpha * alpha * alpha; - Render2D.FillRectangle(caretBounds, CaretColor * alpha); - }*/ - - Render2D.PopClip(); - - Profiler.EndEvent(); - } - - sw.Stop(); - accumDrawTime += sw.Elapsed.TotalSeconds; - accumDrawTimes++; - } - - - private void OnSelectingBegin() - { - if (selectionActive) - return; - - selectionActive = true; - StartMouseCapture(); - } - - private void OnSelectingEnd() - { - if (!selectionActive) - return; - - selectionActive = false; - EndMouseCapture(); - } - - public bool HitTestText(Vector2 location, out int hitLine, out int hitChar) - { - hitLine = 0; - hitChar = 0; - var font = Font.GetFont(); - if (font == null) - return false; - - var lines = Console.Lines; - CalculateVisibleLines(lines, out int startLine, out int lastLine, out LineInfo[] wrappedLines); - - TextLayoutOptions layout = _layout; - layout.Bounds = new Rectangle(0, 0, Size); - float lineHeightNormalized = font.Height; - float lineHeight = lineHeightNormalized / Platform.DpiScale; - float visibleHeight = wrappedLines.Length * lineHeight; - float top = (layout.Bounds.Bottom - visibleHeight) / - Platform.DpiScale; // UI coordinate space remains normalized - int lineMaxLines = (int) (Height / lineHeight); - - int hiddenLines = 0; - if (wrappedLines.Length > lineMaxLines) - hiddenLines = wrappedLines.Length - lineMaxLines; - //if (top < layout.Bounds.Top) - // hiddenLines = (int)Math.Ceiling((layout.Bounds.Top - top) / (float)lineHeight); - - int hitWrappedLine = (int) ((location.Y - top) / lineHeight); //+ hiddenLines; - if (hitWrappedLine < 0 || hitWrappedLine >= wrappedLines.Length) - return false; - - hitLine = wrappedLines[hitWrappedLine].lineIndex; - string line = lines.ElementAt(hitLine).content.Substring(wrappedLines[hitWrappedLine].lineOffset, - wrappedLines[hitWrappedLine].lineLength); - - layout.Bounds.Y = top + ((hitWrappedLine) * lineHeight); - layout.Bounds.Height = top + 9999; //(visibleHeight / Platform.DpiScale); - /*if (layout.Bounds.Y < 0) - { - layout.Bounds.Y = 1; - }*/ - - hitChar = font.HitTestText(line, location, ref layout); - hitChar += wrappedLines[hitWrappedLine].lineOffset; - - //FlaxEngine.Debug.Log(string.Format("hit line {0}/{1}, char {2}", hitWrappedLine, wrappedLines.Length, hitChar)); - return true; - } - - /*public int CharIndexAtPoint(ref Vector2 location) - { - return HitTestText(location + _viewOffset); - }*/ - - public override bool OnKeyDown(KeyboardKeys key) - { - bool shiftDown = Root.GetKey(KeyboardKeys.Shift); - bool ctrlDown = Root.GetKey(KeyboardKeys.Control); - - if ((shiftDown && key == KeyboardKeys.Delete) || (ctrlDown && key == KeyboardKeys.Insert) || - (ctrlDown && key == KeyboardKeys.C) || (ctrlDown && key == KeyboardKeys.X)) - { - Copy(); - return true; - } - else if (key == KeyboardKeys.PageUp) - { - ScrollOffset += GetHeightInLines() / 2; - // should count the wrapped line count here over Console.Lines.Count - //var maxOffset = Console.Lines.Count - GetHeightInLines(); - var maxOffset = Console.Lines.Count - 1; - if (ScrollOffset > maxOffset) - ScrollOffset = maxOffset; - } - else if (key == KeyboardKeys.PageDown) - { - ScrollOffset -= GetHeightInLines() / 2; - if (ScrollOffset < 0) - ScrollOffset = 0; - } - - //else if (ctrlDown && key == KeyboardKeys.A) - // SelectAll(); - return base.OnKeyDown(key); - } - - public override bool OnMouseWheel(Vector2 location, float delta) - { - if (delta < 0) - { - ScrollOffset -= ScrollMouseLines; - if (ScrollOffset < 0) - ScrollOffset = 0; - } - else if (delta > 0) - { - ScrollOffset += ScrollMouseLines; - var maxOffset = Console.Lines.Count - GetHeightInLines(); - if (ScrollOffset > maxOffset) - ScrollOffset = maxOffset; - } - - return false; - } - - public override bool OnMouseDown(Vector2 location, MouseButton button) - { - bool ret = false; - if (button == MouseButton.Left && !IsFocused) - { - Focus(); - ret = true; - } - - if (button == MouseButton.Left && Console.Lines.Count > 0) - { - bool selectionStarted = !selectionActive; - Focus(); - OnSelectingBegin(); - - //FlaxEngine.Debug.Log("mousedown, started: " + selectionStarted.ToString()); - - if (HitTestText(location, out int hitLine, out int hitChar)) - { - selectionStartLine = hitLine; - selectionStartChar = hitChar; - selectionEndLine = hitLine; - selectionEndChar = hitChar; - //FlaxEngine.Debug.Log(string.Format("start line {0} char {1}", hitLine, hitChar)); - } - - - // Select range with shift - /*if (_selectionStart != -1 && RootWindow.GetKey(KeyboardKeys.Shift) && SelectionLength == 0) - { - if (hitPos < _selectionStart) - SetSelection(hitPos, _selectionStart); - else - SetSelection(_selectionStart, hitPos); - } - else - { - SetSelection(hitPos); - }*/ - - return true; - } - - return ret; - } - - public override void OnMouseMove(Vector2 location) - { - if (selectionActive) - { - if (HitTestText(location, out int hitLine, out int hitChar)) - { - selectionEndLine = hitLine; - selectionEndChar = hitChar; - //FlaxEngine.Debug.Log(string.Format("end line {0} char {1}", hitLine, hitChar)); - } - } - } - - /// - public override bool OnMouseUp(Vector2 location, MouseButton button) - { - if (button == MouseButton.Left) - { - OnSelectingEnd(); - Focus(inputBox); - return true; - } - - return false; - } - - public override void OnMouseLeave() - { - base.OnMouseLeave(); - - if (selectionActive) - { - OnSelectingEnd(); - Focus(inputBox); - } - } - - protected void Copy() - { - if (!selectionActive) - return; - - // selection in line-space, wrapping ignored - int selectionLeftLine = selectionStartLine; - int selectionLeftChar = selectionStartChar; - int selectionRightLine = selectionEndLine; - int selectionRightChar = selectionEndChar; - - if (selectionLeftLine > selectionRightLine || - (selectionLeftLine == selectionRightLine && selectionLeftChar > selectionRightChar)) - { - selectionLeftLine = selectionEndLine; - selectionLeftChar = selectionEndChar; - selectionRightLine = selectionStartLine; - selectionRightChar = selectionStartChar; - } - - var lines = Console.Lines; - CalculateVisibleLines(lines, out int startLine, out int lastLine, out LineInfo[] wrappedLines); - - StringBuilder selectedText = new StringBuilder(); - int lastLineIndex = -1; - - foreach (LineInfo li in wrappedLines) - { - var lineIndex = li.lineIndex; - if (lineIndex < selectionLeftLine || lineIndex > selectionRightLine) - continue; - - if (lastLineIndex != lineIndex && lastLineIndex != -1) - selectedText.AppendLine(); - lastLineIndex = lineIndex; - - string fullLine = lines.ElementAt(lineIndex); - string line = fullLine.Substring(li.lineOffset, li.lineLength); - - int leftChar = selectionLeftChar; - int rightChar = selectionRightChar; - - if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) - { - if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) - { - // whole line is selected - selectedText.Append(line); - } - else if (lineIndex == selectionLeftLine) - { - if (lineIndex < selectionRightLine) - { - // right side of the line is selected - selectedText.Append(fullLine.Substring(leftChar)); - } - else if (lineIndex == selectionRightLine && leftChar != rightChar) - { - // selecting middle of the one line - selectedText.Append(fullLine.Substring(leftChar, rightChar - leftChar)); - } - } - else if (lineIndex == selectionRightLine) - { - // left side of the line is selected - selectedText.Append(fullLine.Substring(0, rightChar)); - } - } - } - - if (selectedText.Length > 0) - Clipboard.Text = selectedText.ToString(); - } - } + public class ConsoleContentTextBox : Control + { + public static double accumDrawTime; + public static long accumDrawTimes; + + private readonly FontReference Font; + + protected TextLayoutOptions _layout; + public Color BackgroundSelectedColor = Color.Transparent; + public float BackgroundSelectedFlashSpeed = 0; + public Color BorderColor; + public Color BorderSelectedColor = Color.Transparent; + public float CaretFlashSpeed = 0; + + public int DefaultMargin = 1; + + private float heightMultiplier = 1.0f; + + [HideInEditor] public ConsoleInputTextBox inputBox; + + public float LineSpacing = 1.0f; + public int ScrollMouseLines = 3; + public int ScrollOffset; + + + private bool selectionActive; + + public Color SelectionColor = new Color(0x00, 0x7A, 0xCC); + private int selectionEndChar; + private int selectionEndLine; + private int selectionStartChar; + + private int selectionStartLine; + public Color TextColor = Color.White; + + public TextWrapping Wrapping; + + public ConsoleContentTextBox() + { + } + + public ConsoleContentTextBox(FontReference font, ConsoleInputTextBox inputBox, float x, float y, float width, + float height) : base( + x, y, width, height) + { + this.inputBox = inputBox; + Height = height; + + Font = font; + + _layout = TextLayoutOptions.Default; + _layout.VerticalAlignment = TextAlignment.Near; + _layout.TextWrapping = TextWrapping.WrapChars; + _layout.Bounds = new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); + + + //IsMultiline = true; + //IsReadOnly = true; + //CaretColor = new Color(0f, 0f, 0f, 0f); + AutoFocus = false; + } + + public int FontHeight => Font.GetFont().Height; + + public float HeightMultiplier + { + get => heightMultiplier; + set + { + heightMultiplier = value; + UpdateHeight(); + } + } + + public bool HasSelection => !(selectionStartLine == selectionEndLine && selectionStartChar == selectionEndChar); + + private int GetFontCharacterWidth() + { + Font font = Font.GetFont(); + if (!font) + return 0; + return (int)font.MeasureText("a").X; // hacky, but works for fixed-size fonts... + } + + public int GetFontHeight() + { + Font font = Font.GetFont(); + if (font == null) + return (int)Height; + + return (int)Mathf.Round(LineSpacing * (font.Height / Platform.DpiScale) * Scale.Y); + } + + private int GetHeightInLines() + { + Font font = Font.GetFont(); + if (!font) + return 0; + return (int)(Height / (font.Height / Platform.DpiScale)); // number of fully visible lines + } + + protected override void OnParentChangedInternal() + { + base.OnParentChangedInternal(); + + if (Parent != null) + OnParentResized(); + } + + public override void OnParentResized() + { + UpdateHeight(); + base.OnParentResized(); + } + + private void UpdateHeight() + { + if (Parent != null && Parent.Parent != null) + Height = Parent.Parent.Size.Y * HeightMultiplier - GetFontHeight(); + } + + + private void CalculateVisibleLines(IReadOnlyCollection lines, out int firstVisibleLine, + out int lastVisibleLine, out LineInfo[] wrappedLines) + { + wrappedLines = null; + firstVisibleLine = 0; + lastVisibleLine = 0; + + Font font = Font.GetFont(); + if (!font) + return; + + float fontWidth = GetFontCharacterWidth(); + int lineMaxChars = (int)(Width / fontWidth); + int lineMaxLines = GetHeightInLines(); + int numLines = 0; + int lineIndex = lines.Count; + var lineInfos = new List(lineMaxLines + 1); + int linesSkipped = 0; + foreach (string line in lines.Reverse()) + { + lineIndex--; + if (linesSkipped < ScrollOffset) + { + linesSkipped++; + continue; + } + + int numChars = 0; + int startIndex = lineInfos.Count; + while (numChars < line.Length) + { + LineInfo li = new LineInfo(); + li.lineIndex = lineIndex; + li.lineOffset = numChars; + li.lineLength = Math.Min(line.Length - numChars, lineMaxChars); + lineInfos.Add(li); + + numChars += lineMaxChars; + } + + if (lineInfos.Count - startIndex > 1) + lineInfos.Reverse(startIndex, lineInfos.Count - startIndex); + numLines++; + + if (lineInfos.Count > lineMaxLines) + break; + } + + lineInfos.Reverse(); + wrappedLines = lineInfos.ToArray(); + + //return lines[lines.Count - numLines .. lines.Count]; // C# 8.0... + lastVisibleLine = lineIndex; + firstVisibleLine = lastVisibleLine - numLines; + } + + public override void Draw() + { + // Cache data + Rectangle rect = new Rectangle(Vector2.Zero, Size); + Font font = Font.GetFont(); + if (!font) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + // Background + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Background"); + Color backColor = BackgroundColor; + if (IsMouseOver) + backColor = BackgroundSelectedColor; + if (backColor.A > 0.0f) + Render2D.FillRectangle(rect, backColor); + + Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; + if (borderColor.A > 0.0f) + Render2D.DrawRectangle(rect, borderColor); + Profiler.EndEvent(); + + Profiler.BeginEvent("ConsoleContentTextBoxDraw_FetchLines"); + var lines = Console.Lines; + Profiler.EndEvent(); + if (lines.Count > 0) + { + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines"); + + // Apply view offset and clip mask + Rectangle textClipRectangle = new Rectangle(1, 1, Width - 2, Height - 2); + Render2D.PushClip(textClipRectangle); + + Profiler.BeginEvent("ConsoleContentTextBoxDraw_CalcVisLines"); + // Make sure lengthy lines are split + CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); + Profiler.EndEvent(); + + float lineHeight = font.Height / Platform.DpiScale; + float accumHeight = wrappedLines.Length * lineHeight; + + // selection in line-space, wrapping ignored + int selectionLeftLine = selectionStartLine; + int selectionLeftChar = selectionStartChar; + int selectionRightLine = selectionEndLine; + int selectionRightChar = selectionEndChar; + + if (selectionLeftLine > selectionRightLine || + (selectionLeftLine == selectionRightLine && selectionLeftChar > selectionRightChar)) + { + selectionLeftLine = selectionEndLine; + selectionLeftChar = selectionEndChar; + selectionRightLine = selectionStartLine; + selectionRightChar = selectionStartChar; + } + + // render selection + if (selectionActive) + { + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Selection"); + //float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); + //alpha = alpha * alpha; + Color selectionColor = SelectionColor; // * alpha; + + TextLayoutOptions layout = _layout; + layout.Bounds = rect; + layout.Bounds.Y -= accumHeight - Height; + //for (int i = startLine; i < lastLine; i++) + foreach (LineInfo li in wrappedLines) + { + int lineIndex = li.lineIndex; + string fullLine = lines.ElementAt(lineIndex); + string line = fullLine.Substring(li.lineOffset, li.lineLength); + + int leftChar = selectionLeftChar; + int rightChar = selectionRightChar; + + Rectangle selectionRect = new Rectangle(layout.Bounds.X, layout.Bounds.Y, 0f, 0f); + + // apply selection + if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) + { + if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) + { + // whole line is selected + Vector2 lineSize = font.MeasureText(line); + selectionRect.Width = lineSize.X; + selectionRect.Height = lineSize.Y; + } + else if (lineIndex == selectionLeftLine) + { + if (lineIndex < selectionRightLine) + { + // right side of the line is selected + Vector2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); + Vector2 rightSize = font.MeasureText(fullLine.Substring(leftChar)); + selectionRect.X += leftSize.X; + selectionRect.Width = rightSize.X; + selectionRect.Height = rightSize.Y; + + //int diff = line.Length - selectionLeftChar; + //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : ""); + } + else if (lineIndex == selectionRightLine && leftChar != rightChar) + { + // selecting middle of the one line + Vector2 lineSize = font.MeasureText(fullLine); + Vector2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); + Vector2 midSize = + font.MeasureText(fullLine.Substring(leftChar, rightChar - leftChar)); + + selectionRect.X += leftSize.X; + selectionRect.Width = midSize.X; + selectionRect.Height = lineSize.Y; + + //int diff = selectionRightChar - selectionLeftChar; + //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : "") + line.Substring(selectionRightChar); + } + } + else if (lineIndex == selectionRightLine) + { + // left side of the line is selected + Vector2 leftSize = font.MeasureText(fullLine.Substring(0, rightChar)); + selectionRect.Width = leftSize.X; + selectionRect.Height = leftSize.Y; + + //line = (selectionRightChar > 0 ? new string('X', selectionRightChar) : "") + line.Substring(selectionRightChar); + } + } + + + Render2D.FillRectangle(selectionRect, selectionColor); + + layout.Bounds.Y += lineHeight; + } + + Profiler.EndEvent(); + } + + // render lines + { + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines_Render"); + TextLayoutOptions layout = _layout; + layout.Bounds = rect; + layout.Bounds.Y -= accumHeight - Height; + foreach (LineInfo li in wrappedLines) + { + int lineIndex = li.lineIndex; + string line = lines.ElementAt(lineIndex).content.Substring(li.lineOffset, li.lineLength); + Render2D.DrawText(font, line, TextColor, ref layout); + layout.Bounds.Y += lineHeight; + } + + Profiler.EndEvent(); + } + + /*if (CaretPosition > -1) + { + var prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Vector2(); + var caretBounds = CaretBounds; + caretBounds.X += prefixSize.X; + + float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); + alpha = alpha * alpha * alpha * alpha * alpha * alpha; + Render2D.FillRectangle(caretBounds, CaretColor * alpha); + }*/ + + Render2D.PopClip(); + + Profiler.EndEvent(); + } + + sw.Stop(); + accumDrawTime += sw.Elapsed.TotalSeconds; + accumDrawTimes++; + } + + + private void OnSelectingBegin() + { + if (selectionActive) + return; + + selectionActive = true; + StartMouseCapture(); + } + + private void OnSelectingEnd() + { + if (!selectionActive) + return; + + selectionActive = false; + EndMouseCapture(); + } + + public bool HitTestText(Vector2 location, out int hitLine, out int hitChar) + { + hitLine = 0; + hitChar = 0; + Font font = Font.GetFont(); + if (font == null) + return false; + + var lines = Console.Lines; + CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); + + TextLayoutOptions layout = _layout; + layout.Bounds = new Rectangle(0, 0, Size); + float lineHeightNormalized = font.Height; + float lineHeight = lineHeightNormalized / Platform.DpiScale; + float visibleHeight = wrappedLines.Length * lineHeight; + float top = (layout.Bounds.Bottom - visibleHeight) / + Platform.DpiScale; // UI coordinate space remains normalized + int lineMaxLines = (int)(Height / lineHeight); + + int hiddenLines = 0; + if (wrappedLines.Length > lineMaxLines) + hiddenLines = wrappedLines.Length - lineMaxLines; + //if (top < layout.Bounds.Top) + // hiddenLines = (int)Math.Ceiling((layout.Bounds.Top - top) / (float)lineHeight); + + int hitWrappedLine = (int)((location.Y - top) / lineHeight); //+ hiddenLines; + if (hitWrappedLine < 0 || hitWrappedLine >= wrappedLines.Length) + return false; + + hitLine = wrappedLines[hitWrappedLine].lineIndex; + string line = lines.ElementAt(hitLine).content.Substring(wrappedLines[hitWrappedLine].lineOffset, + wrappedLines[hitWrappedLine].lineLength); + + layout.Bounds.Y = top + hitWrappedLine * lineHeight; + layout.Bounds.Height = top + 9999; //(visibleHeight / Platform.DpiScale); + /*if (layout.Bounds.Y < 0) + { + layout.Bounds.Y = 1; + }*/ + + hitChar = font.HitTestText(line, location, ref layout); + hitChar += wrappedLines[hitWrappedLine].lineOffset; + + //FlaxEngine.Debug.Log(string.Format("hit line {0}/{1}, char {2}", hitWrappedLine, wrappedLines.Length, hitChar)); + return true; + } + + /*public int CharIndexAtPoint(ref Vector2 location) + { + return HitTestText(location + _viewOffset); + }*/ + + public override bool OnKeyDown(KeyboardKeys key) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + bool ctrlDown = Root.GetKey(KeyboardKeys.Control); + + if ((shiftDown && key == KeyboardKeys.Delete) || (ctrlDown && key == KeyboardKeys.Insert) || + (ctrlDown && key == KeyboardKeys.C) || (ctrlDown && key == KeyboardKeys.X)) + { + Copy(); + return true; + } + + if (key == KeyboardKeys.PageUp) + { + ScrollOffset += GetHeightInLines() / 2; + // should count the wrapped line count here over Console.Lines.Count + //var maxOffset = Console.Lines.Count - GetHeightInLines(); + int maxOffset = Console.Lines.Count - 1; + if (ScrollOffset > maxOffset) + ScrollOffset = maxOffset; + } + else if (key == KeyboardKeys.PageDown) + { + ScrollOffset -= GetHeightInLines() / 2; + if (ScrollOffset < 0) + ScrollOffset = 0; + } + + //else if (ctrlDown && key == KeyboardKeys.A) + // SelectAll(); + return base.OnKeyDown(key); + } + + public override bool OnMouseWheel(Vector2 location, float delta) + { + if (delta < 0) + { + ScrollOffset -= ScrollMouseLines; + if (ScrollOffset < 0) + ScrollOffset = 0; + } + else if (delta > 0) + { + ScrollOffset += ScrollMouseLines; + int maxOffset = Console.Lines.Count - GetHeightInLines(); + if (ScrollOffset > maxOffset) + ScrollOffset = maxOffset; + } + + return false; + } + + public override bool OnMouseDown(Vector2 location, MouseButton button) + { + bool ret = false; + if (button == MouseButton.Left && !IsFocused) + { + Focus(); + ret = true; + } + + if (button == MouseButton.Left && Console.Lines.Count > 0) + { + bool selectionStarted = !selectionActive; + Focus(); + OnSelectingBegin(); + + //FlaxEngine.Debug.Log("mousedown, started: " + selectionStarted.ToString()); + + if (HitTestText(location, out int hitLine, out int hitChar)) + { + selectionStartLine = hitLine; + selectionStartChar = hitChar; + selectionEndLine = hitLine; + selectionEndChar = hitChar; + //FlaxEngine.Debug.Log(string.Format("start line {0} char {1}", hitLine, hitChar)); + } + + + // Select range with shift + /*if (_selectionStart != -1 && RootWindow.GetKey(KeyboardKeys.Shift) && SelectionLength == 0) + { + if (hitPos < _selectionStart) + SetSelection(hitPos, _selectionStart); + else + SetSelection(_selectionStart, hitPos); + } + else + { + SetSelection(hitPos); + }*/ + + return true; + } + + return ret; + } + + public override void OnMouseMove(Vector2 location) + { + if (selectionActive) + if (HitTestText(location, out int hitLine, out int hitChar)) + { + selectionEndLine = hitLine; + selectionEndChar = hitChar; + //FlaxEngine.Debug.Log(string.Format("end line {0} char {1}", hitLine, hitChar)); + } + } + + /// + public override bool OnMouseUp(Vector2 location, MouseButton button) + { + if (button == MouseButton.Left) + { + OnSelectingEnd(); + Focus(inputBox); + return true; + } + + return false; + } + + public override void OnMouseLeave() + { + base.OnMouseLeave(); + + if (selectionActive) + { + OnSelectingEnd(); + Focus(inputBox); + } + } + + protected void Copy() + { + if (!selectionActive) + return; + + // selection in line-space, wrapping ignored + int selectionLeftLine = selectionStartLine; + int selectionLeftChar = selectionStartChar; + int selectionRightLine = selectionEndLine; + int selectionRightChar = selectionEndChar; + + if (selectionLeftLine > selectionRightLine || + (selectionLeftLine == selectionRightLine && selectionLeftChar > selectionRightChar)) + { + selectionLeftLine = selectionEndLine; + selectionLeftChar = selectionEndChar; + selectionRightLine = selectionStartLine; + selectionRightChar = selectionStartChar; + } + + var lines = Console.Lines; + CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); + + StringBuilder selectedText = new StringBuilder(); + int lastLineIndex = -1; + + foreach (LineInfo li in wrappedLines) + { + int lineIndex = li.lineIndex; + if (lineIndex < selectionLeftLine || lineIndex > selectionRightLine) + continue; + + if (lastLineIndex != lineIndex && lastLineIndex != -1) + selectedText.AppendLine(); + lastLineIndex = lineIndex; + + string fullLine = lines.ElementAt(lineIndex); + string line = fullLine.Substring(li.lineOffset, li.lineLength); + + int leftChar = selectionLeftChar; + int rightChar = selectionRightChar; + + if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) + { + if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) + { + // whole line is selected + selectedText.Append(line); + } + else if (lineIndex == selectionLeftLine) + { + if (lineIndex < selectionRightLine) + // right side of the line is selected + selectedText.Append(fullLine.Substring(leftChar)); + else if (lineIndex == selectionRightLine && leftChar != rightChar) + // selecting middle of the one line + selectedText.Append(fullLine.Substring(leftChar, rightChar - leftChar)); + } + else if (lineIndex == selectionRightLine) + { + // left side of the line is selected + selectedText.Append(fullLine.Substring(0, rightChar)); + } + } + } + + if (selectedText.Length > 0) + Clipboard.Text = selectedText.ToString(); + } + + private struct LineInfo + { + public int lineIndex; + public int lineOffset; + public int lineLength; + } + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleInputTextBox.cs b/Source/Game/Console/ConsoleInputTextBox.cs index 45e4133..c93458e 100644 --- a/Source/Game/Console/ConsoleInputTextBox.cs +++ b/Source/Game/Console/ConsoleInputTextBox.cs @@ -1,173 +1,167 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using FlaxEditor; using FlaxEngine; -using FlaxEngine.GUI; namespace Cabrito { - public class ConsoleInputTextBox : ConsoleTextBoxBase - { - public override string TextPrefix - { - get => Console.LinePrefix; - } + public class ConsoleInputTextBox : ConsoleTextBoxBase + { + private readonly ConsoleContentTextBox contentBox; - private ConsoleContentTextBox contentBox; + private int inputHistoryIndex = -1; - protected override Rectangle TextRectangle => new Rectangle(0, 0, Width, Height); - protected override Rectangle TextClipRectangle => new Rectangle(0, 0, Width, Height); + public ConsoleInputTextBox() + { + } - public ConsoleInputTextBox() : base() - { - } + public ConsoleInputTextBox(ConsoleContentTextBox contentBox, float x, float y, float width, float height) : + base(x, y, width, height) + { + this.contentBox = contentBox; + IsMultiline = true; // Not really but behaves better than single-line box + } - public ConsoleInputTextBox(ConsoleContentTextBox contentBox, float x, float y, float width, float height) : - base(x, y, width, height) - { - this.contentBox = contentBox; - IsMultiline = true; // Not really but behaves better than single-line box - } + public override string TextPrefix => Console.LinePrefix; - private bool IsConsoleKeyPressed(KeyboardKeys key = KeyboardKeys.None) - { - // Ignore any characters generated by the key which opens the console - string inputTextLower = Input.InputText.ToLowerInvariant(); + protected override Rectangle TextRectangle => new Rectangle(0, 0, Width, Height); + protected override Rectangle TextClipRectangle => new Rectangle(0, 0, Width, Height); - IEnumerable consoleKeyMappings; - if (key == KeyboardKeys.None) - consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); - else - consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key == key); - foreach (var mapping in consoleKeyMappings) - { - if (inputTextLower.Length > 0) - { - if ((mapping.Key == KeyboardKeys.Backslash || mapping.Key == KeyboardKeys.BackQuote) && - (inputTextLower.Contains('ö') || - inputTextLower.Contains('æ') || - inputTextLower.Contains('ø'))) - { - continue; // Scandinavian keyboard layouts - } - else if (mapping.Key == KeyboardKeys.BackQuote && inputTextLower.Contains('\'')) - continue; - else if (mapping.Key == KeyboardKeys.Backslash && - (inputTextLower.Contains('\\') || inputTextLower.Contains('|'))) - { - continue; - } - } + private bool IsConsoleKeyPressed(KeyboardKeys key = KeyboardKeys.None) + { + // Ignore any characters generated by the key which opens the console + string inputTextLower = Input.InputText.ToLowerInvariant(); - if (Input.GetKey(mapping.Key)) - return true; - } + IEnumerable consoleKeyMappings; + if (key == KeyboardKeys.None) + consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); + else + consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key == key); + foreach (ActionConfig mapping in consoleKeyMappings) + { + if (inputTextLower.Length > 0) + { + if ((mapping.Key == KeyboardKeys.Backslash || mapping.Key == KeyboardKeys.BackQuote) && + (inputTextLower.Contains('ö') || + inputTextLower.Contains('æ') || + inputTextLower.Contains('ø'))) + continue; // Scandinavian keyboard layouts + if (mapping.Key == KeyboardKeys.BackQuote && inputTextLower.Contains('\'')) + continue; + if (mapping.Key == KeyboardKeys.Backslash && + (inputTextLower.Contains('\\') || inputTextLower.Contains('|'))) + continue; + } - return false; - } + if (Input.GetKey(mapping.Key)) + return true; + } - public override bool OnCharInput(char c) - { - if (IsConsoleKeyPressed()) - return true; + return false; + } - return base.OnCharInput(c); - } + public override bool OnCharInput(char c) + { + if (IsConsoleKeyPressed()) + return true; - private int inputHistoryIndex = -1; - public override bool OnKeyDown(KeyboardKeys key) - { - bool shiftDown = Root.GetKey(KeyboardKeys.Shift); - bool ctrlDown = Root.GetKey(KeyboardKeys.Control); + return base.OnCharInput(c); + } - if (IsConsoleKeyPressed(key)) - { - Clear(); - return true; - } - else if (key == KeyboardKeys.Escape) - { - Console.Close(); - Clear(); - return true; - } - else if (key == KeyboardKeys.Return) - { - try - { - Console.Execute(Text, true); - inputHistoryIndex = -1; - } - finally - { - Clear(); - } + public override bool OnKeyDown(KeyboardKeys key) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + bool ctrlDown = Root.GetKey(KeyboardKeys.Control); - contentBox.ScrollOffset = 0; - return true; - } - else if (key == KeyboardKeys.ArrowUp) - { - inputHistoryIndex++; + if (IsConsoleKeyPressed(key)) + { + Clear(); + return true; + } - string line = Console.GetBufferHistory(inputHistoryIndex); - if (line == null) - { - inputHistoryIndex--; - return true; - } + if (key == KeyboardKeys.Escape) + { + Console.Close(); + Clear(); + return true; + } - SetText(line); - SetSelection(TextLength); - return true; - } - else if (key == KeyboardKeys.ArrowDown) - { - if (inputHistoryIndex > 0) - inputHistoryIndex--; + if (key == KeyboardKeys.Return) + { + try + { + Console.Execute(Text, true); + inputHistoryIndex = -1; + } + finally + { + Clear(); + } - string line = Console.GetBufferHistory(inputHistoryIndex); - if (line == null) - return true; + contentBox.ScrollOffset = 0; + return true; + } - SetText(line); - SetSelection(TextLength); - return true; - } - else if (key == KeyboardKeys.PageUp || key == KeyboardKeys.PageDown) - { - return contentBox.OnKeyDown(key); - } + if (key == KeyboardKeys.ArrowUp) + { + inputHistoryIndex++; - return base.OnKeyDown(key); - } + string line = Console.GetBufferHistory(inputHistoryIndex); + if (line == null) + { + inputHistoryIndex--; + return true; + } - public override void OnLostFocus() - { - // Avoids reseting the caret location - var oldEditing = _isEditing; - _isEditing = false; - base.OnLostFocus(); - _isEditing = oldEditing; - } + SetText(line); + SetSelection(TextLength); + return true; + } - public override bool OnMouseDown(Vector2 location, MouseButton button) - { - base.OnMouseDown(location, button); - return true; - } + if (key == KeyboardKeys.ArrowDown) + { + if (inputHistoryIndex > 0) + inputHistoryIndex--; - public override bool OnMouseWheel(Vector2 location, float delta) - { - return contentBox.OnMouseWheel(location, delta); - } + string line = Console.GetBufferHistory(inputHistoryIndex); + if (line == null) + return true; - public override void Draw() - { - Profiler.BeginEvent("ConsoleInputTextBoxDraw"); - base.Draw(); - Profiler.EndEvent(); - } - } + SetText(line); + SetSelection(TextLength); + return true; + } + + if (key == KeyboardKeys.PageUp || key == KeyboardKeys.PageDown) + return contentBox.OnKeyDown(key); + + return base.OnKeyDown(key); + } + + public override void OnLostFocus() + { + // Avoids reseting the caret location + bool oldEditing = _isEditing; + _isEditing = false; + base.OnLostFocus(); + _isEditing = oldEditing; + } + + public override bool OnMouseDown(Vector2 location, MouseButton button) + { + base.OnMouseDown(location, button); + return true; + } + + public override bool OnMouseWheel(Vector2 location, float delta) + { + return contentBox.OnMouseWheel(location, delta); + } + + public override void Draw() + { + Profiler.BeginEvent("ConsoleInputTextBoxDraw"); + base.Draw(); + Profiler.EndEvent(); + } + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsolePlugin.cs b/Source/Game/Console/ConsolePlugin.cs index ac342a0..391caee 100644 --- a/Source/Game/Console/ConsolePlugin.cs +++ b/Source/Game/Console/ConsolePlugin.cs @@ -1,59 +1,56 @@ using System; -using System.Collections.Generic; -using System.Runtime.Serialization; using FlaxEngine; using Console = Cabrito.Console; - #if FLAX_EDITOR using FlaxEditor; #endif namespace Game { - public class ConsolePlugin : GamePlugin - { - public override void Initialize() - { - base.Initialize(); - Debug.Log("ConsolePlugin Initialize"); + public class ConsolePlugin : GamePlugin + { + public static PluginDescription DescriptionInternal = new PluginDescription + { + Author = "Ari Vuollet", + Name = "Console", + Description = "Quake-like console", + Version = Version.Parse("0.1.0"), + IsAlpha = true, + Category = "Game" + }; - Console.Init(); - } + public override void Initialize() + { + base.Initialize(); + Debug.Log("ConsolePlugin Initialize"); - public override void Deinitialize() - { - base.Deinitialize(); - Debug.Log("ConsolePlugin Deinitialize"); - } + Console.Init(); + } - public static PluginDescription DescriptionInternal = new PluginDescription() - { - Author = "Ari Vuollet", - Name = "Console", - Description = "Quake-like console", - Version = Version.Parse("0.1.0"), - IsAlpha = true, - Category = "Game", - }; - } + public override void Deinitialize() + { + base.Deinitialize(); + Debug.Log("ConsolePlugin Deinitialize"); + } + } #if FLAX_EDITOR - public class ConsoleEditorPlugin : EditorPlugin - { - public override void Initialize() - { - Debug.Log("ConsoleEditorPlugin Initialize"); - Console.Init(); - } + public class ConsoleEditorPlugin : EditorPlugin + { + public override PluginDescription Description => ConsolePlugin.DescriptionInternal; - public override void Deinitialize() - { - Debug.Log("ConsoleEditorPlugin Deinitialize"); - } + public override Type GamePluginType => typeof(ConsolePlugin); - public override PluginDescription Description => ConsolePlugin.DescriptionInternal; + public override void Initialize() + { + Debug.Log("ConsoleEditorPlugin Initialize"); + Console.Init(); + } - public override Type GamePluginType => typeof(ConsolePlugin); - } + public override void Deinitialize() + { + Debug.Log("ConsoleEditorPlugin Deinitialize"); + } + } #endif } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleScript.cs b/Source/Game/Console/ConsoleScript.cs index 8068b53..5749a57 100644 --- a/Source/Game/Console/ConsoleScript.cs +++ b/Source/Game/Console/ConsoleScript.cs @@ -1,151 +1,149 @@ using System; -using System.Collections.Generic; using System.Linq; using FlaxEditor; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; +using Object = FlaxEngine.Object; namespace Cabrito { - public class ConsoleScript : Script - { - [Limit(5, 720, 1)] public int ConsoleFontSize = 16; + public class ConsoleScript : Script + { + public Color BackgroundColor; - [Limit(0.05f, 1.0f, 1)] public float ConsoleHeight = 0.65f; + public Texture BackgroundTexture; + private ConsoleContentTextBox consoleBox; - [Limit(0)] public int ConsoleNotifyLines = 3; + public FontAsset ConsoleFont; + [Limit(5, 720)] public int ConsoleFontSize = 16; - [Limit(0f)] public float ConsoleSpeed = 3500f; + [Limit(0.05f, 1.0f)] public float ConsoleHeight = 0.65f; + private ConsoleInputTextBox consoleInputBox; - public FontAsset ConsoleFont; + internal InputEvent consoleInputEvent; + private ConsoleContentTextBox consoleNotifyBox; - public Texture BackgroundTexture; + [Limit(0)] public int ConsoleNotifyLines = 3; - public Color BackgroundColor; + [Limit(0f)] public float ConsoleSpeed = 3500f; - private UIControl rootControl; - private ConsoleContentTextBox consoleBox; - private ConsoleInputTextBox consoleInputBox; - private ConsoleContentTextBox consoleNotifyBox; + private UIControl rootControl; - internal InputEvent consoleInputEvent; + public override void OnStart() + { + consoleInputEvent = new InputEvent("Console"); + consoleInputEvent.Triggered += OnConsoleInputEvent; - public override void OnStart() - { - consoleInputEvent = new InputEvent("Console"); - consoleInputEvent.Triggered += OnConsoleInputEvent; + FontReference fontReference = new FontReference(ConsoleFont, ConsoleFontSize); + Font fontRaw = fontReference.GetFont(); + int fontHeight = (int)(fontRaw.Height / Platform.DpiScale); - FontReference fontReference = new FontReference(ConsoleFont, ConsoleFontSize); - Font fontRaw = fontReference.GetFont(); - int fontHeight = (int) (fontRaw.Height / Platform.DpiScale); + // root actor which holds all the elements + //var rootContainerControl = new ContainerControl(new Rectangle(0, 0, screenSize.X, screenSize.Y)); + ContainerControl rootContainerControl = new ContainerControl(new Rectangle()); + rootContainerControl.SetAnchorPreset(AnchorPresets.StretchAll, false); - // root actor which holds all the elements - //var rootContainerControl = new ContainerControl(new Rectangle(0, 0, screenSize.X, screenSize.Y)); - var rootContainerControl = new ContainerControl(new Rectangle()); - rootContainerControl.SetAnchorPreset(AnchorPresets.StretchAll, false); + rootControl = Actor.AddChild(); + rootControl.Name = "ConsoleRoot"; + rootControl.Control = rootContainerControl; - rootControl = Actor.AddChild(); - rootControl.Name = "ConsoleRoot"; - rootControl.Control = rootContainerControl; + VerticalPanel contentContainer = new VerticalPanel + { + AutoSize = true, + Margin = Margin.Zero, + Spacing = 0, + Bounds = new Rectangle(), + BackgroundColor = BackgroundColor + }; + contentContainer.SetAnchorPreset(AnchorPresets.StretchAll, true); - var contentContainer = new VerticalPanel() - { - AutoSize = true, - Margin = Margin.Zero, - Spacing = 0, - Bounds = new Rectangle(), - BackgroundColor = BackgroundColor - }; - contentContainer.SetAnchorPreset(AnchorPresets.StretchAll, true); + UIControl contentContainerControl = rootControl.AddChild(); + contentContainerControl.Name = "ContentContainer"; + contentContainerControl.Control = contentContainer; - var contentContainerControl = rootControl.AddChild(); - contentContainerControl.Name = "ContentContainer"; - contentContainerControl.Control = contentContainer; - - { - if (consoleBox == null) - { - //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); - consoleBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); + { + if (consoleBox == null) + { + //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); + consoleBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); - consoleBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); - //consoleBox.AnchorMax = new Vector2(1.0f, ConsoleHeight); - //consoleBox.Height = consoleSize.Y - fontHeight; + consoleBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); + //consoleBox.AnchorMax = new Vector2(1.0f, ConsoleHeight); + //consoleBox.Height = consoleSize.Y - fontHeight; - //consoleBox.HorizontalAlignment = TextAlignment.Near; - //consoleBox.VerticalAlignment = TextAlignment.Near; - consoleBox.HeightMultiplier = ConsoleHeight; - consoleBox.Wrapping = TextWrapping.WrapWords; - consoleBox.BackgroundColor = Color.Transparent; - consoleBox.BackgroundSelectedColor = Color.Transparent; - consoleBox.BackgroundSelectedFlashSpeed = 0; - consoleBox.BorderSelectedColor = Color.Transparent; - consoleBox.CaretFlashSpeed = 0; - } + //consoleBox.HorizontalAlignment = TextAlignment.Near; + //consoleBox.VerticalAlignment = TextAlignment.Near; + consoleBox.HeightMultiplier = ConsoleHeight; + consoleBox.Wrapping = TextWrapping.WrapWords; + consoleBox.BackgroundColor = Color.Transparent; + consoleBox.BackgroundSelectedColor = Color.Transparent; + consoleBox.BackgroundSelectedFlashSpeed = 0; + consoleBox.BorderSelectedColor = Color.Transparent; + consoleBox.CaretFlashSpeed = 0; + } - var locationFix = consoleBox.Location; - var parentControl = contentContainerControl.AddChild(); - parentControl.Name = "ConsoleContent"; - parentControl.Control = consoleBox; - consoleBox.Location = locationFix; // workaround to UIControl.Control overriding the old position + Vector2 locationFix = consoleBox.Location; + UIControl parentControl = contentContainerControl.AddChild(); + parentControl.Name = "ConsoleContent"; + parentControl.Control = consoleBox; + consoleBox.Location = locationFix; // workaround to UIControl.Control overriding the old position - if (consoleNotifyBox == null) - { - //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); - consoleNotifyBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); - consoleNotifyBox.HeightMultiplier = 0; - consoleNotifyBox.Height = ConsoleNotifyLines * fontHeight; - consoleNotifyBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); - //consoleBox.AnchorMax = new Vector2(1.0f, ConsoleHeight); + if (consoleNotifyBox == null) + { + //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); + consoleNotifyBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); + consoleNotifyBox.HeightMultiplier = 0; + consoleNotifyBox.Height = ConsoleNotifyLines * fontHeight; + consoleNotifyBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); + //consoleBox.AnchorMax = new Vector2(1.0f, ConsoleHeight); - //consoleBox.HorizontalAlignment = TextAlignment.Near; - //consoleBox.VerticalAlignment = TextAlignment.Near; - //consoleNotifyBox.HeightMultiplier = ConsoleHeight; - consoleNotifyBox.Wrapping = TextWrapping.WrapWords; - consoleNotifyBox.BackgroundColor = Color.Transparent; - consoleNotifyBox.BackgroundSelectedColor = Color.Transparent; - consoleNotifyBox.BackgroundSelectedFlashSpeed = 0; - consoleNotifyBox.BorderSelectedColor = Color.Transparent; - consoleNotifyBox.CaretFlashSpeed = 0; - } + //consoleBox.HorizontalAlignment = TextAlignment.Near; + //consoleBox.VerticalAlignment = TextAlignment.Near; + //consoleNotifyBox.HeightMultiplier = ConsoleHeight; + consoleNotifyBox.Wrapping = TextWrapping.WrapWords; + consoleNotifyBox.BackgroundColor = Color.Transparent; + consoleNotifyBox.BackgroundSelectedColor = Color.Transparent; + consoleNotifyBox.BackgroundSelectedFlashSpeed = 0; + consoleNotifyBox.BorderSelectedColor = Color.Transparent; + consoleNotifyBox.CaretFlashSpeed = 0; + } - var locationFix2 = consoleNotifyBox.Location; - var parentControl2 = Actor.AddChild(); - parentControl2.Name = "ConsoleNotifyContent"; - parentControl2.Control = consoleNotifyBox; - consoleNotifyBox.Location = locationFix2; // workaround to UIControl.Control overriding the old position - } - { - if (consoleInputBox == null) - { - //consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, consoleSize.Y - fontHeight, consoleSize.X, fontHeight); - consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, 0, 0, 0); - consoleInputBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); - //consoleInputBox.Location = new Vector2(0, consoleSize.Y - fontHeight); - consoleInputBox.Height = fontHeight; + Vector2 locationFix2 = consoleNotifyBox.Location; + UIControl parentControl2 = Actor.AddChild(); + parentControl2.Name = "ConsoleNotifyContent"; + parentControl2.Control = consoleNotifyBox; + consoleNotifyBox.Location = locationFix2; // workaround to UIControl.Control overriding the old position + } + { + if (consoleInputBox == null) + { + //consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, consoleSize.Y - fontHeight, consoleSize.X, fontHeight); + consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, 0, 0, 0); + consoleInputBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); + //consoleInputBox.Location = new Vector2(0, consoleSize.Y - fontHeight); + consoleInputBox.Height = fontHeight; - consoleInputBox.Font = fontReference; - consoleBox.inputBox = consoleInputBox; + consoleInputBox.Font = fontReference; + consoleBox.inputBox = consoleInputBox; - consoleInputBox.Wrapping = TextWrapping.WrapWords; - consoleInputBox.BackgroundColor = Color.Transparent; - consoleInputBox.BackgroundSelectedColor = Color.Transparent; - consoleInputBox.BackgroundSelectedFlashSpeed = 0; - consoleInputBox.BorderSelectedColor = Color.Transparent; - consoleInputBox.CaretFlashSpeed = 0; - } + consoleInputBox.Wrapping = TextWrapping.WrapWords; + consoleInputBox.BackgroundColor = Color.Transparent; + consoleInputBox.BackgroundSelectedColor = Color.Transparent; + consoleInputBox.BackgroundSelectedFlashSpeed = 0; + consoleInputBox.BorderSelectedColor = Color.Transparent; + consoleInputBox.CaretFlashSpeed = 0; + } + Vector2 locationFix = consoleInputBox.Location; + UIControl parentControl = contentContainerControl.AddChild(); + parentControl.Name = "ConsoleInput"; + parentControl.Control = consoleInputBox; - var locationFix = consoleInputBox.Location; - var parentControl = contentContainerControl.AddChild(); - parentControl.Name = "ConsoleInput"; - parentControl.Control = consoleInputBox; - - consoleInputBox.Location = locationFix; // workaround to UIControl.Control overriding the old position - } + consoleInputBox.Location = locationFix; // workaround to UIControl.Control overriding the old position + } #if false //for (int i = 0; i < 10; i++) @@ -208,191 +206,191 @@ namespace Cabrito Console.Print(l); } #endif - /*FlaxEditor.Editor.Options.OptionsChanged += (FlaxEditor.Options.EditorOptions options) => - { + /*FlaxEditor.Editor.Options.OptionsChanged += (FlaxEditor.Options.EditorOptions options) => + { - };*/ + };*/ - /*Console.Print("normal line"); - Console.Print( - "a very very very long long long line in repeat a very very very long long long line in repeat 1 a very very ver" - + "y long long long line in repeat a very very very 2 long long long line in repeat a very very very 3 long long" - + " long line in repeat a very very very long long long 4 line in repeat"); - Console.Print("another normal line");*/ + /*Console.Print("normal line"); + Console.Print( + "a very very very long long long line in repeat a very very very long long long line in repeat 1 a very very ver" + + "y long long long line in repeat a very very very 2 long long long line in repeat a very very very 3 long long" + + " long line in repeat a very very very long long long 4 line in repeat"); + Console.Print("another normal line");*/ - Debug.Logger.LogHandler.SendLog += OnSendLog; - Debug.Logger.LogHandler.SendExceptionLog += OnSendExceptionLog; + Debug.Logger.LogHandler.SendLog += OnSendLog; + Debug.Logger.LogHandler.SendExceptionLog += OnSendExceptionLog; - Console.OnOpen += OnConsoleOpen; - Console.OnClose += OnConsoleClose; - Console.OnPrint += OnPrint; + Console.OnOpen += OnConsoleOpen; + Console.OnClose += OnConsoleClose; + Console.OnPrint += OnPrint; - // hide console by default, and close it instantly - Console.Close(); - var rootlocation = rootControl.Control.Location; - rootlocation.Y = -rootControl.Control.Height; - rootControl.Control.Location = rootlocation; + // hide console by default, and close it instantly + Console.Close(); + Vector2 rootlocation = rootControl.Control.Location; + rootlocation.Y = -rootControl.Control.Height; + rootControl.Control.Location = rootlocation; - Console.Print("Renderer: " + GPUDevice.Instance.RendererType); - } + Console.Print("Renderer: " + GPUDevice.Instance.RendererType); + } - private void OnSendLog(LogType level, string msg, FlaxEngine.Object obj, string stackTrace) - { - Console.Print("[DEBUG] " + msg); - } + private void OnSendLog(LogType level, string msg, Object obj, string stackTrace) + { + Console.Print("[DEBUG] " + msg); + } - private void OnSendExceptionLog(Exception exception, FlaxEngine.Object obj) - { - AssertionException assert = exception as AssertionException; - if (assert != null) - { - var assertLines = assert.Message.Split('\n'); - if (assertLines.Length > 2) - Console.Print("Assert Failure: " + assertLines[2]); - else - Console.Print("Assert Failure: " + assert.Message); - } - else - Console.Print("[EXCEP] " + exception.Message); - } + private void OnSendExceptionLog(Exception exception, Object obj) + { + AssertionException assert = exception as AssertionException; + if (assert != null) + { + string[] assertLines = assert.Message.Split('\n'); + if (assertLines.Length > 2) + Console.Print("Assert Failure: " + assertLines[2]); + else + Console.Print("Assert Failure: " + assert.Message); + } + else + { + Console.Print("[EXCEP] " + exception.Message); + } + } - public override void OnDestroy() - { - base.OnDestroy(); + public override void OnDestroy() + { + base.OnDestroy(); - //consoleInputEvent.Triggered -= OnConsoleInputEvent; - consoleInputEvent?.Dispose(); - consoleBox?.Dispose(); - consoleNotifyBox?.Dispose(); + //consoleInputEvent.Triggered -= OnConsoleInputEvent; + consoleInputEvent?.Dispose(); + consoleBox?.Dispose(); + consoleNotifyBox?.Dispose(); - Console.OnOpen -= OnConsoleOpen; - Console.OnClose -= OnConsoleClose; - Console.OnPrint -= OnPrint; + Console.OnOpen -= OnConsoleOpen; + Console.OnClose -= OnConsoleClose; + Console.OnPrint -= OnPrint; - Debug.Logger.LogHandler.SendLog -= OnSendLog; - Debug.Logger.LogHandler.SendExceptionLog -= OnSendExceptionLog; - } + Debug.Logger.LogHandler.SendLog -= OnSendLog; + Debug.Logger.LogHandler.SendExceptionLog -= OnSendExceptionLog; + } - private void OnConsoleInputEvent() - { - string currentInput = Input.InputText; + private void OnConsoleInputEvent() + { + string currentInput = Input.InputText; - if (Input.InputText.Length > 0) - { - // Really need rawinput support with separate ActionConfig.RawKey values, bound to physical keys/scancode instead of virtual ones - var consoleKeys = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); - bool backslash = consoleKeys.Any(x => x.Key == KeyboardKeys.Backslash); - bool backquote = consoleKeys.Any(x => x.Key == KeyboardKeys.BackQuote); + if (Input.InputText.Length > 0) + { + // Really need rawinput support with separate ActionConfig.RawKey values, bound to physical keys/scancode instead of virtual ones + var consoleKeys = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); + bool backslash = consoleKeys.Any(x => x.Key == KeyboardKeys.Backslash); + bool backquote = consoleKeys.Any(x => x.Key == KeyboardKeys.BackQuote); - // Workaround to only trigger Console key from key bound to left side of 1 (tilde/backquote/backslash key) - if ((backslash || backquote) && - (Input.InputText.ToLowerInvariant().Contains('ö') || - Input.InputText.ToLowerInvariant().Contains('æ') || - Input.InputText.ToLowerInvariant().Contains('ø'))) // Scandinavian keyboard layouts - { - return; - } - else if (backquote && Input.InputText.ToLowerInvariant().Contains('\'')) // UK keyboard layouts - return; - else if (backslash && (Input.InputText.ToLowerInvariant().Contains('\\') || - Input.InputText.ToLowerInvariant() - .Contains('|'))) // US/International keyboard layouts - return; - } + // Workaround to only trigger Console key from key bound to left side of 1 (tilde/backquote/backslash key) + if ((backslash || backquote) && + (Input.InputText.ToLowerInvariant().Contains('ö') || + Input.InputText.ToLowerInvariant().Contains('æ') || + Input.InputText.ToLowerInvariant().Contains('ø'))) // Scandinavian keyboard layouts + return; + if (backquote && Input.InputText.ToLowerInvariant().Contains('\'')) // UK keyboard layouts + return; + if (backslash && (Input.InputText.ToLowerInvariant().Contains('\\') || + Input.InputText.ToLowerInvariant() + .Contains('|'))) // US/International keyboard layouts + return; + } - if (!consoleInputBox.IsFocused) - Console.Open(); - else - Console.Close(); - } + if (!consoleInputBox.IsFocused) + Console.Open(); + else + Console.Close(); + } - public void OnConsoleOpen() - { - Screen.CursorVisible = true; - Screen.CursorLock = CursorLockMode.None; + public void OnConsoleOpen() + { + Screen.CursorVisible = true; + Screen.CursorLock = CursorLockMode.None; - consoleInputBox.Focus(); - Parent.As().ReceivesEvents = true; - } + consoleInputBox.Focus(); + Parent.As().ReceivesEvents = true; + } - public void OnConsoleClose() - { - Screen.CursorVisible = false; - Screen.CursorLock = CursorLockMode.Locked; + public void OnConsoleClose() + { + Screen.CursorVisible = false; + Screen.CursorLock = CursorLockMode.Locked; - consoleInputBox.Defocus(); + consoleInputBox.Defocus(); #if FLAX_EDITOR - Editor.Instance.Windows.GameWin.Focus(); + Editor.Instance.Windows.GameWin.Focus(); #endif - Parent.As().ReceivesEvents = false; - } + Parent.As().ReceivesEvents = false; + } - public override void OnUpdate() - { - base.OnUpdate(); + public override void OnUpdate() + { + base.OnUpdate(); - if (!Console.IsOpen && Input.GetAction("ClearConsole")) - Console.Clear(); + if (!Console.IsOpen && Input.GetAction("ClearConsole")) + Console.Clear(); - float targetY; - float conHeight = rootControl.Control.Height /*/ Platform.DpiScale*/; - if (!Console.IsOpen) - targetY = -conHeight; - else - targetY = 0.0f; + float targetY; + float conHeight = rootControl.Control.Height /*/ Platform.DpiScale*/; + if (!Console.IsOpen) + targetY = -conHeight; + else + targetY = 0.0f; - Vector2 location = rootControl.Control.Location; - if (location.Y != targetY) - { - if (location.Y > targetY) - { - // closing - location.Y -= Time.UnscaledDeltaTime * ConsoleSpeed; - if (location.Y < targetY) - location.Y = targetY; + Vector2 location = rootControl.Control.Location; + if (location.Y != targetY) + { + if (location.Y > targetY) + { + // closing + location.Y -= Time.UnscaledDeltaTime * ConsoleSpeed; + if (location.Y < targetY) + location.Y = targetY; - if (location.Y < targetY * ConsoleHeight) - location.Y = targetY; - } - else if (location.Y < targetY) - { - // opening - if (location.Y < -conHeight * ConsoleHeight) - location.Y = -conHeight * ConsoleHeight; + if (location.Y < targetY * ConsoleHeight) + location.Y = targetY; + } + else if (location.Y < targetY) + { + // opening + if (location.Y < -conHeight * ConsoleHeight) + location.Y = -conHeight * ConsoleHeight; - location.Y += Time.UnscaledDeltaTime * ConsoleSpeed; - if (location.Y > targetY) - location.Y = targetY; - } + location.Y += Time.UnscaledDeltaTime * ConsoleSpeed; + if (location.Y > targetY) + location.Y = targetY; + } - rootControl.Control.Location = location; + rootControl.Control.Location = location; - if (Console.IsOpen) - { - consoleNotifyBox.Visible = false; - consoleInputBox.Visible = true; - } - else if (!Console.IsOpen) - { - int fontHeight = (int) (consoleNotifyBox.FontHeight / Platform.DpiScale); - if (location.Y < (-conHeight * ConsoleHeight) + fontHeight) - { - consoleNotifyBox.Visible = true; - consoleInputBox.Visible = false; - } - } - } - } + if (Console.IsOpen) + { + consoleNotifyBox.Visible = false; + consoleInputBox.Visible = true; + } + else if (!Console.IsOpen) + { + int fontHeight = (int)(consoleNotifyBox.FontHeight / Platform.DpiScale); + if (location.Y < -conHeight * ConsoleHeight + fontHeight) + { + consoleNotifyBox.Visible = true; + consoleInputBox.Visible = false; + } + } + } + } - public void OnPrint(string text) - { - int fontHeight = (int) (consoleNotifyBox.FontHeight / Platform.DpiScale); - consoleNotifyBox.Height = Math.Min(ConsoleNotifyLines, Console.Lines.Count) * fontHeight; - } + public void OnPrint(string text) + { + int fontHeight = (int)(consoleNotifyBox.FontHeight / Platform.DpiScale); + consoleNotifyBox.Height = Math.Min(ConsoleNotifyLines, Console.Lines.Count) * fontHeight; + } - public void SetInput(string text) - { - consoleInputBox.Text = text; - } - } + public void SetInput(string text) + { + consoleInputBox.Text = text; + } + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleTextBoxBase.cs b/Source/Game/Console/ConsoleTextBoxBase.cs index aa89728..fe61609 100644 --- a/Source/Game/Console/ConsoleTextBoxBase.cs +++ b/Source/Game/Console/ConsoleTextBoxBase.cs @@ -1,390 +1,389 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FlaxEditor; +using System.Diagnostics; using FlaxEngine; using FlaxEngine.GUI; namespace Cabrito { - // Mostly based on TextBox - public class ConsoleTextBoxBase : TextBoxBase - { - protected TextLayoutOptions _layout; - - /// - /// Gets or sets the text wrapping within the control bounds. - /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The text wrapping within the control bounds.")] - public TextWrapping Wrapping - { - get => _layout.TextWrapping; - set => _layout.TextWrapping = value; - } - - /// - /// Gets or sets the font. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public FontReference Font { get; set; } - - /// - /// Gets or sets the color of the text. - /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the text.")] - public Color TextColor = Color.White; - - /// - /// Gets or sets the color of the selection (Transparent if not used). - /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the selection (Transparent if not used).")] - public Color SelectionColor = new Color(0x00, 0x7A, 0xCC, 0xFF); - - [HideInEditor] public virtual string TextPrefix { get; set; } = ""; - - //[HideInEditor] - //public override string Text => _text; - - public ConsoleTextBoxBase() : base() - { - } - - public ConsoleTextBoxBase(float x, float y, float width, float height) : base(false, x, y, width) - { - Height = height; - - IsReadOnly = false; - CaretColor = new Color(1f, 1f, 1f, 1f); - AutoFocus = true; - - _layout = TextLayoutOptions.Default; - _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; - _layout.TextWrapping = TextWrapping.NoWrap; - _layout.Bounds = - new Rectangle(0, 0, Width, - Height); //new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); - } - - /*protected override void SetText(string value) - { - // Prevent from null problems - if (value == null) - value = string.Empty; - - // Filter text - if (value.IndexOf('\r') != -1) - value = value.Replace("\r", ""); - - // Clamp length - if (value.Length > MaxLength) - value = value.Substring(0, MaxLength); - - // Ensure to use only single line - if (_isMultiline == false && value.Length > 0) - { - // Extract only the first line - value = value.GetLines()[0]; - } - - if (Text != value) - { - Deselect(); - ResetViewOffset(); - - Text = value; - - OnTextChanged(); - } - }*/ - - public int GetFontHeight() - { - var font = Font.GetFont(); - if (font == null) - return (int) Height; - - return (int) Mathf.Round(font.Height * Scale.Y); - } - - public override void Clear() - { - // Can't clear the text while user is editing it... - var oldEditing = _isEditing; - _isEditing = false; - base.Clear(); - _isEditing = oldEditing; - } - - public override void ResetViewOffset() - { - TargetViewOffset = new Vector2(0, 0); - } - - /*public void ScrollToEnd() - { - float maxY = TextSize.Y - Height; - float spacing = GetRealLineSpacing(); - maxY += spacing; - - TargetViewOffset = new Vector2(0, Math.Max(0, maxY)); - }*/ - - public override void ScrollToCaret() - { - if (Text.Length == 0) - return; - - Rectangle caretBounds = CaretBounds; - - float maxY = TextSize.Y - Height; - - Vector2 newLocation = CaretBounds.Location; - TargetViewOffset = Vector2.Clamp(newLocation, new Vector2(0, 0), new Vector2(_targetViewOffset.X, maxY)); - } - - /*const bool smoothScrolling = false; - - public override bool OnMouseWheel(Vector2 location, float delta) - { - if (!IsMultiline || Text.Length == 0) - return false; - - if (!smoothScrolling) - delta = GetFontHeight() * Math.Sign(delta) * 3; - else - delta *= 30; - - float maxY = TextSize.Y - Height; - float offset = GetRealLineSpacing(); - maxY += offset; - TargetViewOffset = Vector2.Clamp(_targetViewOffset - new Vector2(0, delta), new Vector2(0, offset), new Vector2(_targetViewOffset.X, maxY)); - return true; - }*/ - - public override Vector2 GetTextSize() - { - var font = Font.GetFont(); - if (font == null) - return Vector2.Zero; - - return font.MeasureText(Text, ref _layout); - } - - public override Vector2 GetCharPosition(int index, out float height) - { - var font = Font.GetFont(); - if (font == null) - { - height = Height; - return Vector2.Zero; - } - - height = GetFontHeight(); - return font.GetCharPosition(Text, index, ref _layout); - } - - public override int HitTestText(Vector2 location) - { - var font = Font.GetFont(); - if (font == null) - return 0; - - if (TextPrefix != "") - { - var prefixSize = font.MeasureText(TextPrefix); - location.X -= prefixSize.X; - } - - return font.HitTestText(Text, location, ref _layout); - } - - protected override void OnIsMultilineChanged() - { - base.OnIsMultilineChanged(); - - _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; - } - - public override bool OnKeyDown(KeyboardKeys key) - { - bool shiftDown = Root.GetKey(KeyboardKeys.Shift); - bool ctrlDown = Root.GetKey(KeyboardKeys.Control); - - if (shiftDown && key == KeyboardKeys.Delete) - Cut(); - else if (ctrlDown && key == KeyboardKeys.Insert) - Copy(); - else if (shiftDown && key == KeyboardKeys.Insert) - Paste(); - if (shiftDown && key == KeyboardKeys.Home) - { - if (!IsReadOnly) - SetSelection(_selectionStart, 0); - return true; - } - else if (shiftDown && key == KeyboardKeys.End) - { - if (!IsReadOnly) - SetSelection(_selectionStart, TextLength); - return true; - } - - return base.OnKeyDown(key); - } - - bool doubleClicked = false; - System.Diagnostics.Stopwatch lastDoubleClick = new System.Diagnostics.Stopwatch(); - Vector2 lastDoubleClickLocation = new Vector2(0, 0); - - public override bool OnMouseDown(Vector2 location, MouseButton button) - { - if (doubleClicked && lastDoubleClick.Elapsed.TotalSeconds < 0.5 && - location == lastDoubleClickLocation) // Windows defaults to 500ms window - { - doubleClicked = false; - if (OnMouseTripleClick(location, button)) - return true; - } - - return base.OnMouseDown(location, button); - } - - - public override bool OnMouseDoubleClick(Vector2 location, MouseButton button) - { - doubleClicked = true; - lastDoubleClick.Restart(); - lastDoubleClickLocation = location; - - return base.OnMouseDoubleClick(location, button); - } - - public bool OnMouseTripleClick(Vector2 location, MouseButton button) - { - if (!IsMultiline) - SelectAll(); - else - { - // TODO: select the line - SelectAll(); - } - - return true; - } - - protected override void OnSizeChanged() - { - base.OnSizeChanged(); - - _layout.Bounds = TextRectangle; - } - - public override void Draw() - { - // Cache data - var rect = new Rectangle(Vector2.Zero, Size); - var font = Font.GetFont(); - if (!font) - return; - - // Background - Color backColor = BackgroundColor; - if (IsMouseOver) - backColor = BackgroundSelectedColor; - if (backColor.A > 0.0f) - Render2D.FillRectangle(rect, backColor); - - Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; - if (borderColor.A > 0.0f) - Render2D.DrawRectangle(rect, borderColor); - - //string text = TextPrefix + Text; - string text = TextPrefix + Text; - - if (text.Length == 0) - return; - - // Apply view offset and clip mask - Render2D.PushClip(TextClipRectangle); - bool useViewOffset = !_viewOffset.IsZero; - if (useViewOffset) - Render2D.PushTransform(Matrix3x3.Translation2D(-_viewOffset)); - - // Check if any text is selected to draw selection - if (HasSelection) - { - Vector2 leftEdge = font.GetCharPosition(text, SelectionLeft + TextPrefix.Length, ref _layout); - Vector2 rightEdge = font.GetCharPosition(text, SelectionRight + TextPrefix.Length, ref _layout); - float fontHeight = GetFontHeight(); - - // Draw selection background - float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); - alpha = alpha * alpha; - Color selectionColor = SelectionColor * alpha; - - int selectedLinesCount = 1 + Mathf.FloorToInt((rightEdge.Y - leftEdge.Y) / fontHeight); - if (selectedLinesCount == 1) // Selected is part of single line - { - Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight); - Render2D.FillRectangle(r1, selectionColor); - } - else // Selected is more than one line - { - float leftMargin = _layout.Bounds.Location.X; - Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight); - Render2D.FillRectangle(r1, selectionColor); - - for (int i = 3; i <= selectedLinesCount; i++) - { - leftEdge.Y += fontHeight; - Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight); - Render2D.FillRectangle(r, selectionColor); - } - - Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight); - Render2D.FillRectangle(r2, selectionColor); - } - } - - Render2D.DrawText(font, text, TextColor, ref _layout); - - if (CaretPosition > -1) - { - var prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Vector2(); - var caretBounds = CaretBounds; - caretBounds.X += prefixSize.X; - - float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); - alpha = alpha * alpha * alpha * alpha * alpha * alpha; - Render2D.FillRectangle(caretBounds, CaretColor * alpha); - } - - // Restore rendering state - if (useViewOffset) - Render2D.PopTransform(); - Render2D.PopClip(); - } - - public override void Paste() - { - if (IsReadOnly) - return; - - var clipboardText = Clipboard.Text; - // Handle newlines in clipboard text - if (!string.IsNullOrEmpty(clipboardText)) - { - // TODO: probably better to just split these lines and parse each line separately - clipboardText = clipboardText.Replace("\r\n", ""); - clipboardText = clipboardText.Replace("\n", ""); - } - - if (!string.IsNullOrEmpty(clipboardText)) - { - var right = SelectionRight; - Insert(clipboardText); - SetSelection(Mathf.Max(right, 0) + clipboardText.Length); - } - } - } + // Mostly based on TextBox + public class ConsoleTextBoxBase : TextBoxBase + { + private readonly Stopwatch lastDoubleClick = new Stopwatch(); + protected TextLayoutOptions _layout; + + private bool doubleClicked; + private Vector2 lastDoubleClickLocation = new Vector2(0, 0); + + /// + /// Gets or sets the color of the selection (Transparent if not used). + /// + [EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the selection (Transparent if not used).")] + public Color SelectionColor = new Color(0x00, 0x7A, 0xCC); + + /// + /// Gets or sets the color of the text. + /// + [EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the text.")] + public Color TextColor = Color.White; + + //[HideInEditor] + //public override string Text => _text; + + public ConsoleTextBoxBase() + { + } + + public ConsoleTextBoxBase(float x, float y, float width, float height) : base(false, x, y, width) + { + Height = height; + + IsReadOnly = false; + CaretColor = new Color(1f, 1f, 1f, 1f); + AutoFocus = true; + + _layout = TextLayoutOptions.Default; + _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; + _layout.TextWrapping = TextWrapping.NoWrap; + _layout.Bounds = + new Rectangle(0, 0, Width, + Height); //new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); + } + + /// + /// Gets or sets the text wrapping within the control bounds. + /// + [EditorDisplay("Style")] + [EditorOrder(2000)] + [Tooltip("The text wrapping within the control bounds.")] + public TextWrapping Wrapping + { + get => _layout.TextWrapping; + set => _layout.TextWrapping = value; + } + + /// + /// Gets or sets the font. + /// + [EditorDisplay("Style")] + [EditorOrder(2000)] + public FontReference Font { get; set; } + + [HideInEditor] public virtual string TextPrefix { get; set; } = ""; + + /*protected override void SetText(string value) + { + // Prevent from null problems + if (value == null) + value = string.Empty; + + // Filter text + if (value.IndexOf('\r') != -1) + value = value.Replace("\r", ""); + + // Clamp length + if (value.Length > MaxLength) + value = value.Substring(0, MaxLength); + + // Ensure to use only single line + if (_isMultiline == false && value.Length > 0) + { + // Extract only the first line + value = value.GetLines()[0]; + } + + if (Text != value) + { + Deselect(); + ResetViewOffset(); + + Text = value; + + OnTextChanged(); + } + }*/ + + public int GetFontHeight() + { + Font font = Font.GetFont(); + if (font == null) + return (int)Height; + + return (int)Mathf.Round(font.Height * Scale.Y); + } + + public override void Clear() + { + // Can't clear the text while user is editing it... + bool oldEditing = _isEditing; + _isEditing = false; + base.Clear(); + _isEditing = oldEditing; + } + + public override void ResetViewOffset() + { + TargetViewOffset = new Vector2(0, 0); + } + + /*public void ScrollToEnd() + { + float maxY = TextSize.Y - Height; + float spacing = GetRealLineSpacing(); + maxY += spacing; + + TargetViewOffset = new Vector2(0, Math.Max(0, maxY)); + }*/ + + public override void ScrollToCaret() + { + if (Text.Length == 0) + return; + + Rectangle caretBounds = CaretBounds; + + float maxY = TextSize.Y - Height; + + Vector2 newLocation = CaretBounds.Location; + TargetViewOffset = Vector2.Clamp(newLocation, new Vector2(0, 0), new Vector2(_targetViewOffset.X, maxY)); + } + + /*const bool smoothScrolling = false; + + public override bool OnMouseWheel(Vector2 location, float delta) + { + if (!IsMultiline || Text.Length == 0) + return false; + + if (!smoothScrolling) + delta = GetFontHeight() * Math.Sign(delta) * 3; + else + delta *= 30; + + float maxY = TextSize.Y - Height; + float offset = GetRealLineSpacing(); + maxY += offset; + TargetViewOffset = Vector2.Clamp(_targetViewOffset - new Vector2(0, delta), new Vector2(0, offset), new Vector2(_targetViewOffset.X, maxY)); + return true; + }*/ + + public override Vector2 GetTextSize() + { + Font font = Font.GetFont(); + if (font == null) + return Vector2.Zero; + + return font.MeasureText(Text, ref _layout); + } + + public override Vector2 GetCharPosition(int index, out float height) + { + Font font = Font.GetFont(); + if (font == null) + { + height = Height; + return Vector2.Zero; + } + + height = GetFontHeight(); + return font.GetCharPosition(Text, index, ref _layout); + } + + public override int HitTestText(Vector2 location) + { + Font font = Font.GetFont(); + if (font == null) + return 0; + + if (TextPrefix != "") + { + Vector2 prefixSize = font.MeasureText(TextPrefix); + location.X -= prefixSize.X; + } + + return font.HitTestText(Text, location, ref _layout); + } + + protected override void OnIsMultilineChanged() + { + base.OnIsMultilineChanged(); + + _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; + } + + public override bool OnKeyDown(KeyboardKeys key) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + bool ctrlDown = Root.GetKey(KeyboardKeys.Control); + + if (shiftDown && key == KeyboardKeys.Delete) + Cut(); + else if (ctrlDown && key == KeyboardKeys.Insert) + Copy(); + else if (shiftDown && key == KeyboardKeys.Insert) + Paste(); + if (shiftDown && key == KeyboardKeys.Home) + { + if (!IsReadOnly) + SetSelection(_selectionStart, 0); + return true; + } + + if (shiftDown && key == KeyboardKeys.End) + { + if (!IsReadOnly) + SetSelection(_selectionStart, TextLength); + return true; + } + + return base.OnKeyDown(key); + } + + public override bool OnMouseDown(Vector2 location, MouseButton button) + { + if (doubleClicked && lastDoubleClick.Elapsed.TotalSeconds < 0.5 && + location == lastDoubleClickLocation) // Windows defaults to 500ms window + { + doubleClicked = false; + if (OnMouseTripleClick(location, button)) + return true; + } + + return base.OnMouseDown(location, button); + } + + + public override bool OnMouseDoubleClick(Vector2 location, MouseButton button) + { + doubleClicked = true; + lastDoubleClick.Restart(); + lastDoubleClickLocation = location; + + return base.OnMouseDoubleClick(location, button); + } + + public bool OnMouseTripleClick(Vector2 location, MouseButton button) + { + if (!IsMultiline) + SelectAll(); + else + // TODO: select the line + SelectAll(); + + return true; + } + + protected override void OnSizeChanged() + { + base.OnSizeChanged(); + + _layout.Bounds = TextRectangle; + } + + public override void Draw() + { + // Cache data + Rectangle rect = new Rectangle(Vector2.Zero, Size); + Font font = Font.GetFont(); + if (!font) + return; + + // Background + Color backColor = BackgroundColor; + if (IsMouseOver) + backColor = BackgroundSelectedColor; + if (backColor.A > 0.0f) + Render2D.FillRectangle(rect, backColor); + + Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; + if (borderColor.A > 0.0f) + Render2D.DrawRectangle(rect, borderColor); + + //string text = TextPrefix + Text; + string text = TextPrefix + Text; + + if (text.Length == 0) + return; + + // Apply view offset and clip mask + Render2D.PushClip(TextClipRectangle); + bool useViewOffset = !_viewOffset.IsZero; + if (useViewOffset) + Render2D.PushTransform(Matrix3x3.Translation2D(-_viewOffset)); + + // Check if any text is selected to draw selection + if (HasSelection) + { + Vector2 leftEdge = font.GetCharPosition(text, SelectionLeft + TextPrefix.Length, ref _layout); + Vector2 rightEdge = font.GetCharPosition(text, SelectionRight + TextPrefix.Length, ref _layout); + float fontHeight = GetFontHeight(); + + // Draw selection background + float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); + alpha = alpha * alpha; + Color selectionColor = SelectionColor * alpha; + + int selectedLinesCount = 1 + Mathf.FloorToInt((rightEdge.Y - leftEdge.Y) / fontHeight); + if (selectedLinesCount == 1) // Selected is part of single line + { + Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight); + Render2D.FillRectangle(r1, selectionColor); + } + else // Selected is more than one line + { + float leftMargin = _layout.Bounds.Location.X; + Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight); + Render2D.FillRectangle(r1, selectionColor); + + for (int i = 3; i <= selectedLinesCount; i++) + { + leftEdge.Y += fontHeight; + Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight); + Render2D.FillRectangle(r, selectionColor); + } + + Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight); + Render2D.FillRectangle(r2, selectionColor); + } + } + + Render2D.DrawText(font, text, TextColor, ref _layout); + + if (CaretPosition > -1) + { + Vector2 prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Vector2(); + Rectangle caretBounds = CaretBounds; + caretBounds.X += prefixSize.X; + + float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); + alpha = alpha * alpha * alpha * alpha * alpha * alpha; + Render2D.FillRectangle(caretBounds, CaretColor * alpha); + } + + // Restore rendering state + if (useViewOffset) + Render2D.PopTransform(); + Render2D.PopClip(); + } + + public override void Paste() + { + if (IsReadOnly) + return; + + string clipboardText = Clipboard.Text; + // Handle newlines in clipboard text + if (!string.IsNullOrEmpty(clipboardText)) + { + // TODO: probably better to just split these lines and parse each line separately + clipboardText = clipboardText.Replace("\r\n", ""); + clipboardText = clipboardText.Replace("\n", ""); + } + + if (!string.IsNullOrEmpty(clipboardText)) + { + int right = SelectionRight; + Insert(clipboardText); + SetSelection(Mathf.Max(right, 0) + clipboardText.Length); + } + } + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleVariable.cs b/Source/Game/Console/ConsoleVariable.cs index 96db87c..22a5085 100644 --- a/Source/Game/Console/ConsoleVariable.cs +++ b/Source/Game/Console/ConsoleVariable.cs @@ -1,75 +1,75 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Cabrito { - [Flags] - public enum ConsoleFlags - { - NoSerialize = 1, // Value does not persist + [Flags] + public enum ConsoleFlags + { + NoSerialize = 1, // Value does not persist - Alias = NoSerialize, - } + Alias = NoSerialize + } - internal struct ConsoleVariable - { - public string name { get; private set; } - public ConsoleFlags flags { get; private set; } + internal struct ConsoleVariable + { + public string name { get; } + public ConsoleFlags flags { get; } - private FieldInfo field; - private MethodInfo getter; - private MethodInfo setter; + private readonly FieldInfo field; + private readonly MethodInfo getter; + private readonly MethodInfo setter; - public ConsoleVariable(string name, ConsoleFlags flags, FieldInfo field) - { - this.name = name; - this.flags = flags; - this.field = field; - this.getter = null; - this.setter = null; - } + public ConsoleVariable(string name, ConsoleFlags flags, FieldInfo field) + { + this.name = name; + this.flags = flags; + this.field = field; + getter = null; + setter = null; + } - public ConsoleVariable(string name, ConsoleFlags flags, MethodInfo getter, MethodInfo setter) - { - this.name = name; - this.flags = flags; - this.field = null; - this.getter = getter; - this.setter = setter; - } + public ConsoleVariable(string name, ConsoleFlags flags, MethodInfo getter, MethodInfo setter) + { + this.name = name; + this.flags = flags; + field = null; + this.getter = getter; + this.setter = setter; + } - public string GetValueString() - { - var type = field != null ? field.FieldType : getter.ReturnType; - if (type == typeof(string)) - { - if (field != null) - return (string) field.GetValue(null); - else if (setter != null) - return (string) getter.Invoke(null, null); - } - else - throw new Exception("cvar is not type of string"); + public string GetValueString() + { + Type type = field != null ? field.FieldType : getter.ReturnType; + if (type == typeof(string)) + { + if (field != null) + return (string)field.GetValue(null); + if (setter != null) + return (string)getter.Invoke(null, null); + } + else + { + throw new Exception("cvar is not type of string"); + } - throw new Exception("GetValueString no field or getter specified"); - } + throw new Exception("GetValueString no field or getter specified"); + } - public void SetValue(string value) - { - var type = field != null ? field.FieldType : getter.ReturnType; - if (type == typeof(string)) - { - if (field != null) - field.SetValue(null, value); - else if (setter != null) - setter.Invoke(null, new object[] {value}); - } - else - throw new Exception("Unsupported type for SetValue: " + type.Name); - } - } + public void SetValue(string value) + { + Type type = field != null ? field.FieldType : getter.ReturnType; + if (type == typeof(string)) + { + if (field != null) + field.SetValue(null, value); + else if (setter != null) + setter.Invoke(null, new object[] { value }); + } + else + { + throw new Exception("Unsupported type for SetValue: " + type.Name); + } + } + } } \ No newline at end of file diff --git a/Source/Game/Console/EngineSubsystem.cs b/Source/Game/Console/EngineSubsystem.cs index 7b3bb04..2f99cf3 100644 --- a/Source/Game/Console/EngineSubsystem.cs +++ b/Source/Game/Console/EngineSubsystem.cs @@ -1,217 +1,209 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FlaxEngine; using FidelityFX; +using FlaxEngine; namespace Cabrito { - // Holds Console variables and commands to control engine behaviour - public static class EngineSubsystem - { - [ConsoleSubsystemInitializer] - public static void Initialize() - { - } + // Holds Console variables and commands to control engine behaviour + public static class EngineSubsystem + { + private static FSR _fsrPlugin; - [ConsoleCommand("quit", "exit")] - public static void ExitCommand() - { - Engine.RequestExit(); - } + // TODO: this should manually set all postprocessing values to 0 or disabled + /*[ConsoleVariable("r_postprocessing")] + public static string PostProcessing + { + get + { + PostFxVolume postFx = Level.FindActor(); + if (postFx != null) + return postFx.CameraArtifacts.OverrideFlags.ToString(); + return ""; + } + set + { + bool valueBoolean = false; + if (int.TryParse(value, out int valueInt)) + valueBoolean = valueInt != 0; + else + return; - [ConsoleCommand("debuglog")] - public static void DebugLogCommand(string[] text) - { - Debug.Log(string.Join(" ", text)); - } + PostFxVolume postFx = Level.FindActor(); + if (postFx != null) + { + var cameraArtifacts = postFx.CameraArtifacts; + cameraArtifacts.OverrideFlags = valueBoolean ? CameraArtifactsSettingsOverride.None : CameraArtifactsSettingsOverride.All; + postFx.CameraArtifacts = cameraArtifacts; + } + } + }*/ - // TODO: this should manually set all postprocessing values to 0 or disabled - /*[ConsoleVariable("r_postprocessing")] - public static string PostProcessing - { - get - { - PostFxVolume postFx = Level.FindActor(); - if (postFx != null) - return postFx.CameraArtifacts.OverrideFlags.ToString(); - return ""; - } - set - { - bool valueBoolean = false; - if (int.TryParse(value, out int valueInt)) - valueBoolean = valueInt != 0; - else - return; + [ConsoleVariable("r_vignette")] + public static string Vignette + { + get + { + PostFxVolume postFx = Level.FindActor(); + if (postFx != null) + return postFx.CameraArtifacts.VignetteIntensity.ToString(); + return ""; + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + PostFxVolume postFx = Level.FindActor(); + if (postFx != null) + { + valueFloat = Mathf.Clamp(valueFloat, 0.0f, 2.0f); - PostFxVolume postFx = Level.FindActor(); - if (postFx != null) - { - var cameraArtifacts = postFx.CameraArtifacts; - cameraArtifacts.OverrideFlags = valueBoolean ? CameraArtifactsSettingsOverride.None : CameraArtifactsSettingsOverride.All; - postFx.CameraArtifacts = cameraArtifacts; - } - } - }*/ + CameraArtifactsSettings cameraArtifacts = postFx.CameraArtifacts; + cameraArtifacts.VignetteIntensity = valueFloat; + postFx.CameraArtifacts = cameraArtifacts; + } + } + } + } - [ConsoleVariable("r_vignette")] - public static string Vignette - { - get - { - PostFxVolume postFx = Level.FindActor(); - if (postFx != null) - return postFx.CameraArtifacts.VignetteIntensity.ToString(); - return ""; - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - PostFxVolume postFx = Level.FindActor(); - if (postFx != null) - { - valueFloat = Mathf.Clamp(valueFloat, 0.0f, 2.0f); + [ConsoleVariable("cl_maxfps")] + public static string MaxFps + { + get => Time.UpdateFPS.ToString(); + set + { + if (float.TryParse(value, out float valueFloat)) + { + if (valueFloat <= 0.0f) + valueFloat = 0.0f; + else + valueFloat = Mathf.Clamp(valueFloat, 10f, 99999999999.0f); - var cameraArtifacts = postFx.CameraArtifacts; - cameraArtifacts.VignetteIntensity = valueFloat; - postFx.CameraArtifacts = cameraArtifacts; - } - } - } - } + if (Time.UpdateFPS != valueFloat) + Time.UpdateFPS = valueFloat; + if (Time.DrawFPS != valueFloat) + Time.DrawFPS = valueFloat; + } + } + } - [ConsoleVariable("cl_maxfps")] - public static string MaxFps - { - get { return Time.UpdateFPS.ToString(); } - set - { - if (float.TryParse(value, out float valueFloat)) - { - if (valueFloat <= 0.0f) - valueFloat = 0.0f; - else - valueFloat = Mathf.Clamp(valueFloat, 10f, 99999999999.0f); + [ConsoleVariable("r_renderscale")] + public static string RenderScale + { + get => MainRenderTask.Instance.RenderingPercentage.ToString(); + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0.00001f, 4.0f); + MainRenderTask.Instance.RenderingPercentage = valueFloat; + } + } + } - if (Time.UpdateFPS != valueFloat) - Time.UpdateFPS = valueFloat; - if (Time.DrawFPS != valueFloat) - Time.DrawFPS = valueFloat; - } - } - } + public static FSR FsrPlugin + { + get + { + if (_fsrPlugin == null) + _fsrPlugin = PluginManager.GetPlugin(); + return _fsrPlugin; + } + } - [ConsoleVariable("r_renderscale")] - public static string RenderScale - { - get - { - return MainRenderTask.Instance.RenderingPercentage.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.00001f, 4.0f); - MainRenderTask.Instance.RenderingPercentage = valueFloat; - } - } - } + // TODO: r_upscaling + [ConsoleVariable("r_fsr_enabled")] + public static string FsrEnabled + { + get => FsrPlugin.PostFx.Enabled ? "1" : "0"; + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; - private static FSR _fsrPlugin; - public static FSR FsrPlugin - { - get - { - if (_fsrPlugin == null) - _fsrPlugin = PluginManager.GetPlugin(); - return _fsrPlugin; - } - } + FsrPlugin.PostFx.Enabled = boolValue; + } + } - // TODO: r_upscaling - [ConsoleVariable("r_fsr_enabled")] - public static string FsrEnabled - { - get - { - return FsrPlugin.PostFx.Enabled ? "1" : "0"; - } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; + [ConsoleVariable("r_fsr_sharpness")] + public static string FsrSharpness + { + get + { + // In shader, the value of 0 is the max sharpness... + float sharpness = 2.0f - FsrPlugin.PostFx.Sharpness; + return sharpness.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0f, 2.0f); + FsrPlugin.PostFx.Sharpness = 2.0f - valueFloat; + } + } + } - FsrPlugin.PostFx.Enabled = boolValue; - } - } + [ConsoleVariable("cl_showweapon")] + public static string ShowWeapon + { + get => Level.FindActor("ViewModelCamera").IsActive ? "1" : "0"; + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; - [ConsoleVariable("r_fsr_sharpness")] - public static string FsrSharpness - { - get - { - // In shader, the value of 0 is the max sharpness... - float sharpness = 2.0f - FsrPlugin.PostFx.Sharpness; - return sharpness.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0f, 2.0f); - FsrPlugin.PostFx.Sharpness = 2.0f - valueFloat; - } - } - } - - [ConsoleVariable("cl_showweapon")] - public static string ShowWeapon - { - get - { - return Level.FindActor("ViewModelCamera").IsActive ? "1" : "0"; - } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; - - Level.FindActor("ViewModelCamera").IsActive = boolValue; - } - } + Level.FindActor("ViewModelCamera").IsActive = boolValue; + } + } - // Horizontal field of view of the Camera - [ConsoleVariable("fov")] - public static string CameraFov - { - get - { - float valueFloat = Level.FindActor("PlayerCamera").As().FieldOfView; - float horizontalFov = (float)(180.0f / Math.PI * (2*Math.Atan((4f/3f) * Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); - return horizontalFov.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.01f, 360.0f); + // Horizontal field of view of the Camera + [ConsoleVariable("fov")] + public static string CameraFov + { + get + { + float valueFloat = Level.FindActor("PlayerCamera").As().FieldOfView; + float horizontalFov = (float)(180.0f / Math.PI * + (2 * Math.Atan(4f / 3f * + Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); + return horizontalFov.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0.01f, 360.0f); - float verticalFov = (float)(180.0f / Math.PI * (2*Math.Atan((3f/4f) * Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); - Level.FindActor("PlayerCamera").As().FieldOfView = verticalFov; - } - } - } - } + float verticalFov = (float)(180.0f / Math.PI * + (2 * Math.Atan(3f / 4f * + Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); + Level.FindActor("PlayerCamera").As().FieldOfView = verticalFov; + } + } + } + + [ConsoleSubsystemInitializer] + public static void Initialize() + { + } + + [ConsoleCommand("quit", "exit")] + public static void ExitCommand() + { + Engine.RequestExit(); + } + + [ConsoleCommand("debuglog")] + public static void DebugLogCommand(string[] text) + { + Debug.Log(string.Join(" ", text)); + } + } } \ No newline at end of file diff --git a/Source/Game/EngineUtilities.cs b/Source/Game/EngineUtilities.cs index 456a18b..e1bfda4 100644 --- a/Source/Game/EngineUtilities.cs +++ b/Source/Game/EngineUtilities.cs @@ -5,6 +5,11 @@ namespace Game { public static class Utilities { + public static ScopeProfiler ProfileScope(string eventName) + { + return new ScopeProfiler(eventName); + } + public class ScopeProfiler : IDisposable { public ScopeProfiler(string eventName) @@ -17,10 +22,5 @@ namespace Game Profiler.EndEvent(); } } - - public static ScopeProfiler ProfileScope(string eventName) - { - return new ScopeProfiler(eventName); - } } } \ No newline at end of file diff --git a/Source/Game/GameMode/GameMode.cs b/Source/Game/GameMode/GameMode.cs index 7416f8d..fb504e5 100644 --- a/Source/Game/GameMode/GameMode.cs +++ b/Source/Game/GameMode/GameMode.cs @@ -2,6 +2,5 @@ { public abstract class GameMode { - } } \ No newline at end of file diff --git a/Source/Game/GameMode/GameModeManager.cs b/Source/Game/GameMode/GameModeManager.cs index 4f1d70c..6ec4caa 100644 --- a/Source/Game/GameMode/GameModeManager.cs +++ b/Source/Game/GameMode/GameModeManager.cs @@ -1,5 +1,7 @@ -using Cabrito; +using System; +using FlaxEngine; using FlaxEngine.Networking; +using Console = Cabrito.Console; namespace Game { @@ -8,6 +10,19 @@ namespace Game public static void Init() { NetworkManager.OnMessage += OnClientConnected; + Level.SceneLoaded += OnLevelLoaded; + Console.Print("level was loaded: " + Level.IsAnySceneLoaded); + } + + public static void Cleanup() + { + NetworkManager.OnMessage -= OnClientConnected; + Level.SceneLoaded -= OnLevelLoaded; + } + + public static void OnLevelLoaded(Scene scene, Guid assetGuid) + { + Console.Print("level loaded"); } public static bool OnClientConnected(NetworkMessage message) diff --git a/Source/Game/Hud/FpsScript.cs b/Source/Game/Hud/FpsScript.cs index 9e2f684..c4bdbb3 100644 --- a/Source/Game/Hud/FpsScript.cs +++ b/Source/Game/Hud/FpsScript.cs @@ -1,108 +1,104 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Reflection; -using System.Runtime.InteropServices; using System.Text; using FlaxEditor.Content.Settings; using FlaxEngine; using FlaxEngine.GUI; - namespace Cabrito { - [ExecuteInEditMode] - public class FpsScript : Script - { - public UIControl control; - Label label; + [ExecuteInEditMode] + public class FpsScript : Script + { + private const double updateInterval = 0.25; + private const double drawInterval = 0.25; + private const double physicsInterval = 0.25; - Stopwatch sw; - double updateTimeAvg = 0.0; - ulong updateTimeCount; - const double updateInterval = 0.25; - double updateAccumTime = 0.0; + private double conTime; + public UIControl control; - Stopwatch sw2; - double drawTimeAvg = 0.0; - ulong drawTimeCount; - const double drawInterval = 0.25; - double drawAccumTime = 0.0; + private string currentRenderer = "Unknown"; + private double drawAccumTime; + private double drawTimeAvg; + private ulong drawTimeCount; + private Label label; + private double physicsAccumTime; + private double physicsTimeAvg; + private ulong physicsTimeCount; - Stopwatch sw3; - double physicsTimeAvg = 0.0; - ulong physicsTimeCount; - const double physicsInterval = 0.25; - double physicsAccumTime = 0.0; + private Stopwatch sw; - Stopwatch sw0; + private Stopwatch sw0; - string currentRenderer = "Unknown"; + private Stopwatch sw2; - RenderTask t; + private Stopwatch sw3; - TimeSettings timeSettings; + private RenderTask t; - public override void OnAwake() - { - label = (Label) control.Control; + private TimeSettings timeSettings; + private double updateAccumTime; + private double updateTimeAvg; + private ulong updateTimeCount; - sw = Stopwatch.StartNew(); - sw2 = Stopwatch.StartNew(); - sw3 = Stopwatch.StartNew(); - sw0 = Stopwatch.StartNew(); + public override void OnAwake() + { + label = (Label)control.Control; - currentRenderer = GPUDevice.Instance.RendererType.ToString(); + sw = Stopwatch.StartNew(); + sw2 = Stopwatch.StartNew(); + sw3 = Stopwatch.StartNew(); + sw0 = Stopwatch.StartNew(); - if (t == null) - { - //Destroy(t); - t = new RenderTask(); - t.Render += OnDraw; - } + currentRenderer = GPUDevice.Instance.RendererType.ToString(); - var settings = FlaxEditor.Content.Settings.GameSettings.Load(); - timeSettings = settings.Time.CreateInstance(); - } + if (t == null) + { + //Destroy(t); + t = new RenderTask(); + t.Render += OnDraw; + } - public override void OnDestroy() - { - Destroy(t); - t = null; - } + GameSettings settings = GameSettings.Load(); + timeSettings = settings.Time.CreateInstance(); + } - double conTime = 0.0; + public override void OnDestroy() + { + Destroy(t); + t = null; + } - public override void OnUpdate() - { - updateAccumTime += Time.DeltaTime; - updateTimeCount++; - double elapsed = sw.Elapsed.TotalSeconds; - if (elapsed >= updateInterval) - { - sw.Restart(); - updateTimeAvg = elapsed / updateTimeCount; - updateTimeCount = 0; + public override void OnUpdate() + { + updateAccumTime += Time.DeltaTime; + updateTimeCount++; + double elapsed = sw.Elapsed.TotalSeconds; + if (elapsed >= updateInterval) + { + sw.Restart(); + updateTimeAvg = elapsed / updateTimeCount; + updateTimeCount = 0; - conTime = ((ConsoleContentTextBox.accumDrawTime / ConsoleContentTextBox.accumDrawTimes) * 1000.0); - ConsoleContentTextBox.accumDrawTime = 0.0; - ConsoleContentTextBox.accumDrawTimes = 0; - } + conTime = ConsoleContentTextBox.accumDrawTime / ConsoleContentTextBox.accumDrawTimes * 1000.0; + ConsoleContentTextBox.accumDrawTime = 0.0; + ConsoleContentTextBox.accumDrawTimes = 0; + } - StringBuilder sb = new StringBuilder(); - sb.Append("eFPS: " + Engine.FramesPerSecond); - sb.Append(" uTime: " + sw0.Elapsed.TotalSeconds); - sb.Append("\nuFPS: " + ((int) Math.Round(1.0f / updateTimeAvg))); - sb.Append(" uTime: " + updateAccumTime); - sb.Append("\nrFPS: " + ((int) Math.Round(1.0f / drawTimeAvg))); - sb.Append(" rTime: " + drawAccumTime); - sb.Append("\npFPS: " + ((int) Math.Round(1.0f / physicsTimeAvg))); - sb.Append(" pTime: " + physicsAccumTime); - //sb.Append("\nCon: " + conTime.ToString() + "ms"); - //sb.Append("\nGC memory: " + (GC.GetTotalMemory(false) / 1000000.0f).ToString() + "MB"); - //sb.Append("\nUpdate profiler: " + updateProfTime.ToString() + "ms"); + StringBuilder sb = new StringBuilder(); + sb.Append("eFPS: " + Engine.FramesPerSecond); + sb.Append(" uTime: " + sw0.Elapsed.TotalSeconds); + sb.Append("\nuFPS: " + (int)Math.Round(1.0f / updateTimeAvg)); + sb.Append(" uTime: " + updateAccumTime); + sb.Append("\nrFPS: " + (int)Math.Round(1.0f / drawTimeAvg)); + sb.Append(" rTime: " + drawAccumTime); + sb.Append("\npFPS: " + (int)Math.Round(1.0f / physicsTimeAvg)); + sb.Append(" pTime: " + physicsAccumTime); + //sb.Append("\nCon: " + conTime.ToString() + "ms"); + //sb.Append("\nGC memory: " + (GC.GetTotalMemory(false) / 1000000.0f).ToString() + "MB"); + //sb.Append("\nUpdate profiler: " + updateProfTime.ToString() + "ms"); - #if false +#if false #if BUILD_DEVELOPMENT var nameOffset = Marshal.OffsetOf(typeof(ProfilerCPU.Event), "Name0"); foreach (var eventsCpu in FlaxEngine.ProfilingTools.EventsCPU) @@ -132,54 +128,54 @@ namespace Cabrito #endif #endif - ((Label) control.Control).Text = sb.ToString(); - /*if (!Platform.HasFocus) - { - Time.UpdateFPS = 15; - Time.DrawFPS = 15; - Time.PhysicsFPS = 15; - } + ((Label)control.Control).Text = sb.ToString(); + /*if (!Platform.HasFocus) + { + Time.UpdateFPS = 15; + Time.DrawFPS = 15; + Time.PhysicsFPS = 15; + } #if FLAX_EDITOR - else if (!FlaxEditor.Editor.IsPlayMode) - { - var editorFPS = FlaxEditor.Editor.Instance.Options.Options.General.EditorFPS; - Time.UpdateFPS = editorFPS; - Time.DrawFPS = editorFPS; - Time.PhysicsFPS = timeSettings.PhysicsFPS; - } + else if (!FlaxEditor.Editor.IsPlayMode) + { + var editorFPS = FlaxEditor.Editor.Instance.Options.Options.General.EditorFPS; + Time.UpdateFPS = editorFPS; + Time.DrawFPS = editorFPS; + Time.PhysicsFPS = timeSettings.PhysicsFPS; + } #endif - else - { - Time.UpdateFPS = timeSettings.UpdateFPS; - Time.DrawFPS = timeSettings.DrawFPS; - Time.PhysicsFPS = timeSettings.PhysicsFPS; - }*/ - } + else + { + Time.UpdateFPS = timeSettings.UpdateFPS; + Time.DrawFPS = timeSettings.DrawFPS; + Time.PhysicsFPS = timeSettings.PhysicsFPS; + }*/ + } - public override void OnFixedUpdate() - { - physicsAccumTime += Time.DeltaTime; - physicsTimeCount++; - double elapsed = sw3.Elapsed.TotalSeconds; - if (elapsed >= physicsInterval) - { - sw3.Restart(); - physicsTimeAvg = elapsed / physicsTimeCount; - physicsTimeCount = 0; - } - } + public override void OnFixedUpdate() + { + physicsAccumTime += Time.DeltaTime; + physicsTimeCount++; + double elapsed = sw3.Elapsed.TotalSeconds; + if (elapsed >= physicsInterval) + { + sw3.Restart(); + physicsTimeAvg = elapsed / physicsTimeCount; + physicsTimeCount = 0; + } + } - void OnDraw(RenderTask tt, GPUContext context) - { - drawAccumTime += Time.DeltaTime; - drawTimeCount++; - double elapsed = sw2.Elapsed.TotalSeconds; - if (elapsed >= drawInterval) - { - sw2.Restart(); - drawTimeAvg = elapsed / drawTimeCount; - drawTimeCount = 0; - } - } - } + private void OnDraw(RenderTask tt, GPUContext context) + { + drawAccumTime += Time.DeltaTime; + drawTimeCount++; + double elapsed = sw2.Elapsed.TotalSeconds; + if (elapsed >= drawInterval) + { + sw2.Restart(); + drawTimeAvg = elapsed / drawTimeCount; + drawTimeCount = 0; + } + } + } } \ No newline at end of file diff --git a/Source/Game/Level/BrushMaterialList.cs b/Source/Game/Level/BrushMaterialList.cs index 2c47b72..c6ee75a 100644 --- a/Source/Game/Level/BrushMaterialList.cs +++ b/Source/Game/Level/BrushMaterialList.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using FlaxEngine; +using FlaxEngine; namespace Game { /// - /// List of supported materials for loaded levels. - /// Maps the given texture/shader name to Flax Material/MaterialInstance. + /// List of supported materials for loaded levels. + /// Maps the given texture/shader name to Flax Material/MaterialInstance. /// public class BrushMaterialList { @@ -16,12 +14,10 @@ namespace Game public struct BrushMaterialListEntry { - [EditorOrder(1)] - [EditorDisplay(name: "Name")] + [EditorOrder(1)] [EditorDisplay(name: "Name")] public string name; - [EditorOrder(2)] - [EditorDisplay(name: "Material")] + [EditorOrder(2)] [EditorDisplay(name: "Material")] public MaterialBase asset; } -} +} \ No newline at end of file diff --git a/Source/Game/Level/MapParser.cs b/Source/Game/Level/MapParser.cs index 21cdd15..5f27ec8 100644 --- a/Source/Game/Level/MapParser.cs +++ b/Source/Game/Level/MapParser.cs @@ -9,533 +9,520 @@ using FlaxEngine; namespace Game { - public struct MapFacePlane - { - public Vector3 v1, v2, v3; - public Plane plane; - public string texture; - public Vector2 offset; - public float rotation; - public Vector2 scale; - public int contentFlags, surfaceFlags, surfaceValue; - } - - public class MapBrush - { - public MapFacePlane[] planes; - } - - public class MapPatch - { - public string name; - } - - public struct PatchVertex - { - public Vector3 v; - public Vector2 uv; - } - - public class MapEntity - { - public Dictionary properties = new Dictionary(); - public List entities = new List(); - public List brushes = new List(); - public List patches = new List(); - } - - public static class MapParser - { - static public MapEntity Parse(byte[] data) - { - if (data.Length == 0) - return null; - - MapEntity rootEntity = new MapEntity(); - MapEntity currentEntity = rootEntity; - - int level = 0; - int index = 0; - //for (int i=0; i 2) - // throw new Exception("Failed to parse .map file: unexpected entity found at line " + lineNumber.ToString()); - } while (++index < data.Length); - - return rootEntity; - } - - static void ParseComment(byte[] data, ref int index) - { - for (; index < data.Length; index++) - { - if (data[index] == '\n') - break; - } - } - - static void ParseEntity(MapEntity currentEntity, byte[] data, ref int index) - { - bool entityParsed = false; - do - { - char c = (char) data[index]; - char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; - - switch (c) - { - case '\n': - case '\r': - break; - - case '/': - if (c1 == '/') - ParseComment(data, ref index); - else - throw new Exception("unexpected character: '" + c + "'"); - break; - - // "name" "value" - case '"': - { - string propName = ParseQuotedString(data, ref index); - string propValue = ParseQuotedString(data, ref index); - - if (currentEntity.properties.ContainsKey(propName)) - throw new Exception("Failed to parse .map file: multiple properties defined for " + - propName + - " at line ?");// + lineNumber.ToString()); - currentEntity.properties.Add(propName, propValue); - break; - } - - // brush - case '{': - { - index++; - for (; index < data.Length; index++) - { - if (data[index] != ' ' && data[index] != '\r' && data[index] != '\n') - break; - index++; - } - if (index >= data.Length) - break; - - if (data[index] == '(') - currentEntity.brushes.Add(ParseBrush(data, ref index)); - else if (char.IsLetter((char)data[index]) || char.IsNumber((char)data[index])) - currentEntity.patches.Add(ParsePatch(data, ref index)); - break; - } - case '}': - { - entityParsed = true; - break; - } - - default: - throw new Exception("unsupported character: '" + c + "'"); - } - } while (index++ < data.Length && !entityParsed); - } - - static string ParseQuotedString(byte[] data, ref int index) - { - StringBuilder sb = new StringBuilder(); - index++; - - for (; index < data.Length; index++) - { - if (data[index] == '"') - break; - sb.Append((char)data[index]); - } - index++; - - while (index < data.Length) - { - if (data[index] != ' ' && data[index] != '\t') - break; - index++; - } - - return sb.ToString(); - } - - static string ParseString(byte[] data, ref int index) - { - StringBuilder sb = new StringBuilder(); - - for (; index < data.Length; index++) - { - if (data[index] == ' ' || data[index] == '\r' || data[index] == '\n') - break; - sb.Append((char)data[index]); - } - index++; - - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - return sb.ToString(); - } - - static float ParseFloat(byte[] data, ref int index) - { - string fs = ParseString(data, ref index); - - if (float.TryParse(fs, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) - return value; - //else if (float.TryParse(fs, CultureInfo.InvariantCulture, out intValue)) - // return intValue; - else - throw new Exception("failed to ParseFloat: " + fs); - } - - static int ParseInt(byte[] data, ref int index) - { - string fs = ParseString(data, ref index); - - if (int.TryParse(fs, out int value)) - return value; - else - throw new Exception("failed to ParseInt: " + fs); - } - - static Vector3 ParseVector3(byte[] data, ref int index) - { - return new Vector3( - ParseFloat(data, ref index), - ParseFloat(data, ref index), - ParseFloat(data, ref index) - ); - } - - static Vector2 ParseVector2(byte[] data, ref int index) - { - return new Vector2( - ParseFloat(data, ref index), - ParseFloat(data, ref index) - ); - } - - static Vector3 ParsePlaneVector3(byte[] data, ref int index) - { - index++; - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - Vector3 vector = ParseVector3(data, ref index); - // rounding - /*float temp = vector.Z; - vector.Z = vector.Y; - vector.Y = temp;*/ - /*vector.X = (float)Math.Round(vector.X, 1); - vector.Y = (float)Math.Round(vector.Y, 1); - vector.Z = (float)Math.Round(vector.Z, 1);*/ - - while (index < data.Length) - { - if (data[index] == ')') - break; - index++; - } - - index++; - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - return vector; - } - - static MapBrush ParseBrush(byte[] data, ref int index) - { - MapBrush brush = new MapBrush(); - - List planes = new List(6); - - bool brushParsed = false; - do - { - char c = (char) data[index]; - //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; - - switch (c) - { - case '\r': - case '\n': - break; - - // brush face (quake format): - // ( ) ( ) ( ) - - // brush face (quake3 format): - // ( ) ( ) ( ) - - // brush face (valve format): - // ( ) ( ) ( ) [ ] [ ] - case '(': - { - MapFacePlane plane = new MapFacePlane(); - plane.v1 = ParsePlaneVector3(data, ref index); - plane.v2 = ParsePlaneVector3(data, ref index); - plane.v3 = ParsePlaneVector3(data, ref index); - plane.texture = ParseString(data, ref index); - - if (true) // quake or quake3 format - { - plane.offset = ParseVector2(data, ref index); - plane.rotation = ParseFloat(data, ref index); - plane.scale = ParseVector2(data, ref index); - - var peekChar = (char) data[index]; - if (peekChar != '\n') // quake3 format - { - plane.contentFlags = ParseInt(data, ref index); - plane.surfaceFlags = ParseInt(data, ref index); - plane.surfaceValue = ParseInt(data, ref index); - } - } - else // valve format - { - /* - // " [ " - plane.textureRight = ParseVector3(data, ref index); - plane.offset.X = ParseFloat(data, ref index); - // " ] [ " - plane.textureUp = ParseVector3(data, ref index); - plane.offset.Y = ParseFloat(data, ref index); - // " ] " - plane.rotation = ParseFloat(data, ref index); - plane.scale = ParseVector2(data, ref index); - */ - } - - // Flip Y and Z - plane.v1 = new Vector3(plane.v1.X, plane.v1.Z, plane.v1.Y); - plane.v2 = new Vector3(plane.v2.X, plane.v2.Z, plane.v2.Y); - plane.v3 = new Vector3(plane.v3.X, plane.v3.Z, plane.v3.Y); - - plane.plane = new Plane(plane.v1, plane.v2, plane.v3); - - planes.Add(plane); - break; - } - - case '}': - { - brushParsed = true; - break; - } - - default: - if (char.IsLetter(c) || char.IsNumber(c)) - { - // patch name - - } - throw new Exception("unsupported character: '" + c + "'"); - } - } while (index++ < data.Length && !brushParsed); - - brush.planes = planes.ToArray(); - - return brush; - } - - static MapPatch ParsePatch(byte[] data, ref int index) - { - MapPatch patch = new MapPatch(); - - patch.name = ParseString(data, ref index); - - bool patchParsed = false; - do - { - char c = (char) data[index]; - //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; - - switch (c) - { - case '\r': - case '\n': - break; - - case '{': - { - index++; - ParsePatchInner(patch, data, ref index); - break; - } - - case '}': - { - patchParsed = true; - break; - } - - default: - throw new Exception("unsupported character: '" + c + "'"); - } - } while (index++ < data.Length && !patchParsed); - - return patch; - } - - // unfinished and untested - static void ParsePatchInner(MapPatch patch, byte[] data, ref int index) - { - string shaderName = ParseString(data, ref index); - - while (index < data.Length) - { - if (data[index] == '(') - break; - index++; - } - index++; - - int width = ParseInt(data, ref index); - int height = ParseInt(data, ref index); - int dummy1 = ParseInt(data, ref index); - int dummy2 = ParseInt(data, ref index); - int dummy3 = ParseInt(data, ref index); - - while (index < data.Length) - { - if (data[index] == ')') - break; - index++; - } - index++; - - while (index < data.Length) - { - if (data[index] == '(') - break; - index++; - } - index++; - - PatchVertex[] vertices = new PatchVertex[width * height]; - - bool verticesParsed = false; - int vertexIndex = 0; - do - { - char c = (char) data[index]; - //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; - - switch (c) - { - case '\r': - case '\n': - break; - - case '(': - { - index++; - - for (int iw = 0; iw < width; iw++) - { - while (index < data.Length) - { - if (data[index] == '(') - break; - index++; - } - - index++; - - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - vertices[vertexIndex].v = ParseVector3(data, ref index); - vertices[vertexIndex].uv = new Vector2(ParseFloat(data, ref index), ParseFloat(data, ref index)); - vertexIndex++; - - while (index < data.Length) - { - if (data[index] == ')') - break; - index++; - } - index++; - } - - - break; - } - - case '}': - { - verticesParsed = true; - break; - } - - default: - throw new Exception("unsupported character: '" + c + "'"); - } - } while (index++ < data.Length && !verticesParsed); - } - - } + public struct MapFacePlane + { + public Vector3 v1, v2, v3; + public Plane plane; + public string texture; + public Vector2 offset; + public float rotation; + public Vector2 scale; + public int contentFlags, surfaceFlags, surfaceValue; + } + + public class MapBrush + { + public MapFacePlane[] planes; + } + + public class MapPatch + { + public string name; + } + + public struct PatchVertex + { + public Vector3 v; + public Vector2 uv; + } + + public class MapEntity + { + public List brushes = new List(); + public List entities = new List(); + public List patches = new List(); + public Dictionary properties = new Dictionary(); + } + + public static class MapParser + { + public static MapEntity Parse(byte[] data) + { + if (data.Length == 0) + return null; + + MapEntity rootEntity = new MapEntity(); + MapEntity currentEntity = rootEntity; + + int level = 0; + int index = 0; + //for (int i=0; i 2) + // throw new Exception("Failed to parse .map file: unexpected entity found at line " + lineNumber.ToString()); + } while (++index < data.Length); + + return rootEntity; + } + + private static void ParseComment(byte[] data, ref int index) + { + for (; index < data.Length; index++) + if (data[index] == '\n') + break; + } + + private static void ParseEntity(MapEntity currentEntity, byte[] data, ref int index) + { + bool entityParsed = false; + do + { + char c = (char)data[index]; + char c1 = index + 1 < data.Length ? (char)data[index + 1] : (char)0; + + switch (c) + { + case '\n': + case '\r': + break; + + case '/': + if (c1 == '/') + ParseComment(data, ref index); + else + throw new Exception("unexpected character: '" + c + "'"); + break; + + // "name" "value" + case '"': + { + string propName = ParseQuotedString(data, ref index); + string propValue = ParseQuotedString(data, ref index); + + if (currentEntity.properties.ContainsKey(propName)) + throw new Exception("Failed to parse .map file: multiple properties defined for " + + propName + + " at line ?"); // + lineNumber.ToString()); + currentEntity.properties.Add(propName, propValue); + break; + } + + // brush + case '{': + { + index++; + for (; index < data.Length; index++) + { + if (data[index] != ' ' && data[index] != '\r' && data[index] != '\n') + break; + index++; + } + + if (index >= data.Length) + break; + + if (data[index] == '(') + currentEntity.brushes.Add(ParseBrush(data, ref index)); + else if (char.IsLetter((char)data[index]) || char.IsNumber((char)data[index])) + currentEntity.patches.Add(ParsePatch(data, ref index)); + break; + } + case '}': + { + entityParsed = true; + break; + } + + default: + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !entityParsed); + } + + private static string ParseQuotedString(byte[] data, ref int index) + { + StringBuilder sb = new StringBuilder(); + index++; + + for (; index < data.Length; index++) + { + if (data[index] == '"') + break; + sb.Append((char)data[index]); + } + + index++; + + while (index < data.Length) + { + if (data[index] != ' ' && data[index] != '\t') + break; + index++; + } + + return sb.ToString(); + } + + private static string ParseString(byte[] data, ref int index) + { + StringBuilder sb = new StringBuilder(); + + for (; index < data.Length; index++) + { + if (data[index] == ' ' || data[index] == '\r' || data[index] == '\n') + break; + sb.Append((char)data[index]); + } + + index++; + + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + return sb.ToString(); + } + + private static float ParseFloat(byte[] data, ref int index) + { + string fs = ParseString(data, ref index); + + if (float.TryParse(fs, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) + return value; + //else if (float.TryParse(fs, CultureInfo.InvariantCulture, out intValue)) + // return intValue; + throw new Exception("failed to ParseFloat: " + fs); + } + + private static int ParseInt(byte[] data, ref int index) + { + string fs = ParseString(data, ref index); + + if (int.TryParse(fs, out int value)) + return value; + throw new Exception("failed to ParseInt: " + fs); + } + + private static Vector3 ParseVector3(byte[] data, ref int index) + { + return new Vector3( + ParseFloat(data, ref index), + ParseFloat(data, ref index), + ParseFloat(data, ref index) + ); + } + + private static Vector2 ParseVector2(byte[] data, ref int index) + { + return new Vector2( + ParseFloat(data, ref index), + ParseFloat(data, ref index) + ); + } + + private static Vector3 ParsePlaneVector3(byte[] data, ref int index) + { + index++; + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + Vector3 vector = ParseVector3(data, ref index); + // rounding + /*float temp = vector.Z; + vector.Z = vector.Y; + vector.Y = temp;*/ + /*vector.X = (float)Math.Round(vector.X, 1); + vector.Y = (float)Math.Round(vector.Y, 1); + vector.Z = (float)Math.Round(vector.Z, 1);*/ + + while (index < data.Length) + { + if (data[index] == ')') + break; + index++; + } + + index++; + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + return vector; + } + + private static MapBrush ParseBrush(byte[] data, ref int index) + { + MapBrush brush = new MapBrush(); + + var planes = new List(6); + + bool brushParsed = false; + do + { + char c = (char)data[index]; + //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; + + switch (c) + { + case '\r': + case '\n': + break; + + // brush face (quake format): + // ( ) ( ) ( ) + + // brush face (quake3 format): + // ( ) ( ) ( ) + + // brush face (valve format): + // ( ) ( ) ( ) [ ] [ ] + case '(': + { + MapFacePlane plane = new MapFacePlane(); + plane.v1 = ParsePlaneVector3(data, ref index); + plane.v2 = ParsePlaneVector3(data, ref index); + plane.v3 = ParsePlaneVector3(data, ref index); + plane.texture = ParseString(data, ref index); + + if (true) // quake or quake3 format + { + plane.offset = ParseVector2(data, ref index); + plane.rotation = ParseFloat(data, ref index); + plane.scale = ParseVector2(data, ref index); + + char peekChar = (char)data[index]; + if (peekChar != '\n') // quake3 format + { + plane.contentFlags = ParseInt(data, ref index); + plane.surfaceFlags = ParseInt(data, ref index); + plane.surfaceValue = ParseInt(data, ref index); + } + } + + // Flip Y and Z + plane.v1 = new Vector3(plane.v1.X, plane.v1.Z, plane.v1.Y); + plane.v2 = new Vector3(plane.v2.X, plane.v2.Z, plane.v2.Y); + plane.v3 = new Vector3(plane.v3.X, plane.v3.Z, plane.v3.Y); + + plane.plane = new Plane(plane.v1, plane.v2, plane.v3); + + planes.Add(plane); + break; + } + + case '}': + { + brushParsed = true; + break; + } + + default: + if (char.IsLetter(c) || char.IsNumber(c)) + { + // patch name + } + + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !brushParsed); + + brush.planes = planes.ToArray(); + + return brush; + } + + private static MapPatch ParsePatch(byte[] data, ref int index) + { + MapPatch patch = new MapPatch(); + + patch.name = ParseString(data, ref index); + + bool patchParsed = false; + do + { + char c = (char)data[index]; + //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; + + switch (c) + { + case '\r': + case '\n': + break; + + case '{': + { + index++; + ParsePatchInner(patch, data, ref index); + break; + } + + case '}': + { + patchParsed = true; + break; + } + + default: + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !patchParsed); + + return patch; + } + + // unfinished and untested + private static void ParsePatchInner(MapPatch patch, byte[] data, ref int index) + { + string shaderName = ParseString(data, ref index); + + while (index < data.Length) + { + if (data[index] == '(') + break; + index++; + } + + index++; + + int width = ParseInt(data, ref index); + int height = ParseInt(data, ref index); + int dummy1 = ParseInt(data, ref index); + int dummy2 = ParseInt(data, ref index); + int dummy3 = ParseInt(data, ref index); + + while (index < data.Length) + { + if (data[index] == ')') + break; + index++; + } + + index++; + + while (index < data.Length) + { + if (data[index] == '(') + break; + index++; + } + + index++; + + var vertices = new PatchVertex[width * height]; + + bool verticesParsed = false; + int vertexIndex = 0; + do + { + char c = (char)data[index]; + //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; + + switch (c) + { + case '\r': + case '\n': + break; + + case '(': + { + index++; + + for (int iw = 0; iw < width; iw++) + { + while (index < data.Length) + { + if (data[index] == '(') + break; + index++; + } + + index++; + + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + vertices[vertexIndex].v = ParseVector3(data, ref index); + vertices[vertexIndex].uv = + new Vector2(ParseFloat(data, ref index), ParseFloat(data, ref index)); + vertexIndex++; + + while (index < data.Length) + { + if (data[index] == ')') + break; + index++; + } + + index++; + } + + + break; + } + + case '}': + { + verticesParsed = true; + break; + } + + default: + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !verticesParsed); + } + } } \ No newline at end of file diff --git a/Source/Game/Level/Q3MapImporter.cs b/Source/Game/Level/Q3MapImporter.cs index bc95606..05f719c 100644 --- a/Source/Game/Level/Q3MapImporter.cs +++ b/Source/Game/Level/Q3MapImporter.cs @@ -1,197 +1,192 @@ using System; using System.Collections.Generic; -using FlaxEngine; +using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.Serialization; -using FlaxEditor; +using FlaxEngine; using FlaxEngine.Assertions; using Console = Cabrito.Console; -using Stopwatch = System.Diagnostics.Stopwatch; namespace Game { - public class BrushGeometryMesh - { - public List vertices = new List(); - public List indices = new List(); - public List uvs = new List(); - public List normals = new List(); - public MaterialBase material; - } - public class BrushGeometry - { - public MapBrush brush; - public Vector3[] vertices; // all vertices - public Vector3 offset; - public BrushGeometryMesh[] meshes; - public Model model; - public Dictionary brushMaterials; - } + public class BrushGeometryMesh + { + public List indices = new List(); + public MaterialBase material; + public List normals = new List(); + public List uvs = new List(); + public List vertices = new List(); + } - [ExecuteInEditMode] - public class Q3MapImporter : Script - { - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q1.map"; - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q3.map"; - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_valve.map"; - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\dm4.map"; + public class BrushGeometry + { + public MapBrush brush; + public Dictionary brushMaterials; + public BrushGeometryMesh[] meshes; + public Model model; + public Vector3 offset; + public Vector3[] vertices; // all vertices + } - public string mapPath = @"C:\dev\Goake\maps\aerowalk\aerowalk.map"; - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\problematic.map"; + [ExecuteInEditMode] + public class Q3MapImporter : Script + { + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q1.map"; + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q3.map"; + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_valve.map"; + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\dm4.map"; - public bool importLights = false; + public string mapPath = @"C:\dev\Goake\maps\aerowalk\aerowalk.map"; + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\problematic.map"; - Model model; - private MaterialBase missingMaterial; + public bool importLights = false; - static void QuickHull(Vector3[] points, out Vector3[] outVertices) - { - var verts = new List(); - var tris = new List(); - var normals = new List(); + private Model model; + private MaterialBase missingMaterial; - var calc = new ConvexHullCalculator(); - calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals); + private static void QuickHull(Vector3[] points, out Vector3[] outVertices) + { + var verts = new List(); + var tris = new List(); + var normals = new List(); - var finalPoints = new List(); + ConvexHullCalculator calc = new ConvexHullCalculator(); + calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals); - foreach (var tri in tris) - { - finalPoints.Add(verts[tri]); - } + var finalPoints = new List(); - outVertices = finalPoints.ToArray(); + foreach (int tri in tris) + finalPoints.Add(verts[tri]); - //verts = new QuickHull().QuickHull2(points); - //outVertices = verts.ToArray(); - } + outVertices = finalPoints.ToArray(); - private MapEntity root; + //verts = new QuickHull().QuickHull2(points); + //outVertices = verts.ToArray(); + } - private static IEnumerable> DifferentCombinations(IEnumerable elements, int k) - { - return k == 0 ? new[] { new T[0] } : - elements.SelectMany((e, i) => - DifferentCombinations(elements.Skip(i + 1), (k - 1)).Select(c => (new[] {e}).Concat(c))); - } + private MapEntity root; - /// - /// Triangulates the brush by calculating intersection points between triplets of planes. - /// Does not work well with off-axis aligned planes. - /// - public static void TriangulateBrush(MapBrush brush, out Vector3[] vertices) - { - HashSet planePoints = new HashSet(); + private static IEnumerable> DifferentCombinations(IEnumerable elements, int k) + { + return k == 0 + ? new[] { new T[0] } + : elements.SelectMany((e, i) => + DifferentCombinations(elements.Skip(i + 1), k - 1).Select(c => new[] { e }.Concat(c))); + } - List planes = new List(); - float maxDist = 0f; - foreach (var brushPlane in brush.planes) - { - if (Mathf.Abs(brushPlane.plane.D) > maxDist) - maxDist = Mathf.Abs(brushPlane.plane.D); - planes.Add(brushPlane.plane); - } - maxDist *= Mathf.Sqrt(3); + /// + /// Triangulates the brush by calculating intersection points between triplets of planes. + /// Does not work well with off-axis aligned planes. + /// + public static void TriangulateBrush(MapBrush brush, out Vector3[] vertices) + { + var planePoints = new HashSet(); - var combinations = DifferentCombinations(planes, 3).ToList(); + var planes = new List(); + float maxDist = 0f; + foreach (MapFacePlane brushPlane in brush.planes) + { + if (Mathf.Abs(brushPlane.plane.D) > maxDist) + maxDist = Mathf.Abs(brushPlane.plane.D); + planes.Add(brushPlane.plane); + } - // pass 1: get all intersection points - foreach (var comb in combinations) - { - var p1 = comb.Skip(0).First(); - var p2 = comb.Skip(1).First(); - var p3 = comb.Skip(2).First(); + maxDist *= Mathf.Sqrt(3); - //var maxDist = Math.Abs(p1.D * p2.D * p3.D);//Math.Max(p1.D, Math.Max(p2.D, p3.D)); + var combinations = DifferentCombinations(planes, 3).ToList(); - // intersection of three planes - double denom = Vector3.Dot(p1.Normal, Vector3.Cross(p2.Normal, p3.Normal)); - //if (denom < 0.0000000001) - // continue; + // pass 1: get all intersection points + foreach (var comb in combinations) + { + Plane p1 = comb.Skip(0).First(); + Plane p2 = comb.Skip(1).First(); + Plane p3 = comb.Skip(2).First(); + + //var maxDist = Math.Abs(p1.D * p2.D * p3.D);//Math.Max(p1.D, Math.Max(p2.D, p3.D)); + + // intersection of three planes + double denom = Vector3.Dot(p1.Normal, Vector3.Cross(p2.Normal, p3.Normal)); + //if (denom < 0.0000000001) + // continue; - var intersection = (Vector3.Cross(p2.Normal, p3.Normal) * -p1.D + - Vector3.Cross(p3.Normal, p1.Normal) * -p2.D + - Vector3.Cross(p1.Normal, p2.Normal) * -p3.D) / (float)denom; + Vector3 intersection = (Vector3.Cross(p2.Normal, p3.Normal) * -p1.D + + Vector3.Cross(p3.Normal, p1.Normal) * -p2.D + + Vector3.Cross(p1.Normal, p2.Normal) * -p3.D) / (float)denom; - if (Mathf.Abs(intersection.X) > maxDist * 1f || Mathf.Abs(intersection.Y) > maxDist * 1f || - Mathf.Abs(intersection.Z) > maxDist * 1f) - { - continue; - } + if (Mathf.Abs(intersection.X) > maxDist * 1f || Mathf.Abs(intersection.Y) > maxDist * 1f || + Mathf.Abs(intersection.Z) > maxDist * 1f) + continue; - if (Math.Abs(denom) < 0.0000000001) - continue; - //if (intersection.Length > maxDist*2f) - // continue; + if (Math.Abs(denom) < 0.0000000001) + continue; + //if (intersection.Length > maxDist*2f) + // continue; - // Flip Y and Z - /*var temp = intersection.Y; - intersection.Y = intersection.Z; - intersection.Z = temp;*/ + // Flip Y and Z + /*var temp = intersection.Y; + intersection.Y = intersection.Z; + intersection.Z = temp;*/ - //if (intersection.Length >= maxDist) - // temp = temp; + //if (intersection.Length >= maxDist) + // temp = temp; - planePoints.Add(intersection); - } + planePoints.Add(intersection); + } - // remove duplicate points - var planePoints3 = planePoints; - planePoints = new HashSet(); + // remove duplicate points + var planePoints3 = planePoints; + planePoints = new HashSet(); - foreach (var p1 in planePoints3) - { - bool found = false; - foreach (var p2 in planePoints) - { - if (Mathf.Abs((p1 - p2).Length) < 0.00001f) - { - found = true; - break; - } - } + foreach (Vector3 p1 in planePoints3) + { + bool found = false; + foreach (Vector3 p2 in planePoints) + if (Mathf.Abs((p1 - p2).Length) < 0.00001f) + { + found = true; + break; + } - if (!found) - planePoints.Add(p1); - } + if (!found) + planePoints.Add(p1); + } - if (planePoints.Count != planePoints3.Count) - Console.Print("culled " + (planePoints3.Count - planePoints.Count) + " points while triangulation"); + if (planePoints.Count != planePoints3.Count) + Console.Print("culled " + (planePoints3.Count - planePoints.Count) + " points while triangulation"); - // pass 2: cull points behind clipping planes - var planePoints2 = planePoints; - planePoints = new HashSet(); + // pass 2: cull points behind clipping planes + var planePoints2 = planePoints; + planePoints = new HashSet(); - foreach (var p in planePoints2) - { - bool front = true; - foreach (var brushPlane in brush.planes) - { - var dot = -Plane.DotCoordinate(brushPlane.plane, p); + foreach (Vector3 p in planePoints2) + { + bool front = true; + foreach (MapFacePlane brushPlane in brush.planes) + { + float dot = -Plane.DotCoordinate(brushPlane.plane, p); - if (dot < -0.01f) - { - front = false; - break; - } - } + if (dot < -0.01f) + { + front = false; + break; + } + } - if (front) - planePoints.Add(p); - } + if (front) + planePoints.Add(p); + } - if (planePoints.Count > 0) - { - QuickHull(planePoints.ToArray(), out vertices); - return; - } + if (planePoints.Count > 0) + { + QuickHull(planePoints.ToArray(), out vertices); + return; + } - vertices = new Vector3[0]; - } + vertices = new Vector3[0]; + } -#if false//FLAX_EDITOR +#if false //FLAX_EDITOR [OnSerializing] internal void OnSerializing(StreamingContext context) { @@ -216,8 +211,8 @@ namespace Game Debug.Log("OnDeserialized: " + Editor.IsPlayMode); } #endif - public override void OnStart() - { + public override void OnStart() + { #if false Action onScriptsReloadBegin = null; onScriptsReloadBegin = () => @@ -248,525 +243,554 @@ namespace Game ScriptsBuilder.ScriptsReloadEnd += onScriptsReloadEnd; #endif - //Debug.Log("LoadMap"); - LoadMap(false); - } - - private void LoadMap(bool forceLoad) - { - Actor worldSpawnActor = Actor.GetChild("WorldSpawn"); - if (worldSpawnActor != null) - { - if (forceLoad) - { - worldSpawnActor.DestroyChildren(); - } - else - { - Console.Print("Map already loaded in the scene"); - return; - } - } - - { - var workDir = Directory.GetCurrentDirectory(); - var matBasePath = Path.Combine(workDir, "Content", "Materials"); - var assetPath = Path.Combine(matBasePath, "missing.flax"); - missingMaterial = Content.Load(assetPath); - } - - Stopwatch sw = Stopwatch.StartNew(); - byte[] mapChars = File.ReadAllBytes(mapPath); - root = MapParser.Parse(mapChars); - sw.Stop(); - - //Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); - - bool oneMesh = false; - bool convexMesh = true; - - if (!oneMesh) - { - Dictionary materials = new Dictionary(); - { - BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy") - ?.CreateInstance(); - if (brushMaterialList != null) - { - foreach (var m in brushMaterialList.materialAssets) - materials.Add(m.name, m.asset); - } - } - - if (worldSpawnActor == null) - { - worldSpawnActor = Actor.AddChild(); - worldSpawnActor.Name = "WorldSpawn"; - worldSpawnActor.HideFlags |= HideFlags.DontSave; - //worldSpawnActor.HideFlags |= HideFlags.DontSelect; - } - - List brushGeometries = new List(root.entities[0].brushes.Count); - - // pass 1: triangulation - sw.Restart(); - int brushIndex = 0; - int totalverts = 0; - foreach (var brush in root.entities[0].brushes) - { - try - { - BrushGeometry geom = new BrushGeometry(); - - TriangulateBrush(brush, out geom.vertices); - geom.brush = brush; - - brushGeometries.Add(geom); - totalverts += geom.vertices.Length; - - Assert.IsTrue(geom.vertices.Length > 0); - - foreach (var vert in geom.vertices) - geom.offset += vert; - geom.offset /= geom.vertices.Length; - - for (int i=0; i brushMaterials = new Dictionary(); - foreach (var brushPlane in geom.brush.planes) - { - var textureName = brushPlane.texture; - if (brushMaterials.ContainsKey(textureName)) - continue; - - if (!materials.TryGetValue(textureName, out var brushMaterial)) - { - var workDir = Directory.GetCurrentDirectory(); - var matBasePath = Path.Combine(workDir, "Content", "Materials"); - var assetPath = Path.Combine(matBasePath, textureName + ".flax"); - brushMaterial = Content.Load(assetPath); - if (brushMaterial != null) - materials.Add(textureName, brushMaterial); - else - { - // TODO: engine doesn't seem to always load the asset even though it exists, bug? seems to happen at low framerate - //Console.Print("Material '" + textureName + "' not found for brush, assetPath: " + assetPath); - materials.Add(textureName, missingMaterial); - brushMaterial = missingMaterial; - } - } - brushMaterials.Add(textureName, brushMaterial); - } - geom.brushMaterials = brushMaterials; - geom.meshes = new BrushGeometryMesh[brushMaterials.Count]; - for (int i = 0; i < geom.meshes.Length; i++) - { - geom.meshes[i] = new BrushGeometryMesh(); - geom.meshes[i].material = geom.brushMaterials[geom.brushMaterials.Keys.ToList()[i]]; - } - } - catch (Exception e) - { - Console.Print("Failed to triangulate brush " + brushIndex.ToString() + ": " + e.Message); - //FlaxEngine.Engine.RequestExit(); - } - brushIndex++; - } - sw.Stop(); - //Console.Print("Pass 1: triangulation: " + sw.Elapsed.TotalMilliseconds + "ms"); - - // pass 2: texturing - brushIndex = 0; - sw.Restart(); - foreach (var geom in brushGeometries) - { - var brushVertices = geom.vertices; - for (int i = 0; i < brushVertices.Length; i += 3) - { - Vector3 v1 = brushVertices[i + 0]; - Vector3 v2 = brushVertices[i + 1]; - Vector3 v3 = brushVertices[i + 2]; - - Vector3 normal = -Vector3.Cross(v3 - v1, v2 - v1).Normalized; - - // fetch the texture parameters from the plane with matching normal - Vector2 uvScale = new Vector2(0f); - float uvRotation = 0f; - Vector2 uvOffset = new Vector2(0f); - bool found = false; - int meshIndex = 0; - foreach (var brushPlane in geom.brush.planes) - { - if ((brushPlane.plane.Normal - normal).Length < 0.01f) - { - normal = brushPlane.plane.Normal; // for consistency - uvScale = 1f / brushPlane.scale; - uvRotation = brushPlane.rotation; - uvOffset = brushPlane.offset * brushPlane.scale; - found = true; - meshIndex = geom.brushMaterials.Keys.ToList().IndexOf(brushPlane.texture); // ugh? - break; - } - } - - if (!found) - Console.Print("no matching plane found for brush " + brushIndex + ", bad geometry?"); - - Vector2 uv1, uv2, uv3; - // if quake format - { - // The texture is projected to the surface from three angles, the axis with least - // distortion is chosen here. - - // Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis. - // This behaviour is seemingly random in different engines and editors, so let's not bother. - - Vector3 textureNormal = new Vector3((float)Math.Round(normal.X, 4), - (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); - - var dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right)); - var dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up)); - var dotZ = Math.Abs(Vector3.Dot(textureNormal, Vector3.Forward)); - - Vector3 axis; - if (dotY >= dotX && dotY >= dotZ) - axis = -Vector3.Up; - else if (dotX >= dotY && dotX >= dotZ) - axis = Vector3.Right; - else if (dotZ >= dotX && dotZ >= dotY) - axis = -Vector3.Forward; - else - axis = Vector3.Right; - - var axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f - ? -Vector3.Forward - : Vector3.Up; - var axisForward2 = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f - ? Vector3.Up - : -Vector3.Forward; - - Quaternion rot = Quaternion.Identity; - rot = rot * Quaternion.LookRotation(axis, axisForward); - rot = rot * Quaternion.RotationAxis(-Vector3.Forward, - 180f * Mathf.DegreesToRadians); - rot = rot * Quaternion.RotationAxis( - Mathf.Abs(Vector3.Dot(axis, Vector3.Right)) > 0.01f - ? Vector3.Right - : axisForward2, - uvRotation * Mathf.DegreesToRadians); - - uv1 = ((Vector2)((v1 + geom.offset) * rot) + uvOffset) * uvScale; - uv2 = ((Vector2)((v2 + geom.offset) * rot) + uvOffset) * uvScale; - uv3 = ((Vector2)((v3 + geom.offset) * rot) + uvOffset) * uvScale; - } - var mesh = geom.meshes[meshIndex]; - - mesh.indices.Add((uint)mesh.vertices.Count+0); - mesh.indices.Add((uint)mesh.vertices.Count+1); - mesh.indices.Add((uint)mesh.vertices.Count+2); - - mesh.vertices.Add(v1); - mesh.vertices.Add(v2); - mesh.vertices.Add(v3); - - mesh.uvs.Add(uv1); - mesh.uvs.Add(uv2); - mesh.uvs.Add(uv3); - - mesh.normals.Add(normal); - mesh.normals.Add(normal); - mesh.normals.Add(normal); - } - - geom.model = Content.CreateVirtualAsset(); - geom.model.SetupLODs(new int[] { geom.meshes.Length }); - geom.model.SetupMaterialSlots(geom.meshes.Length); - - for (int i=0; i(); - childModel.Name = "Brush_" + brushIndex; - childModel.Model = geom.model; - childModel.Position = geom.offset; - - for (int i = 0; i < geom.meshes.Length; i++) - childModel.SetMaterial(i, geom.meshes[i].material); - - uint[] indices = new uint[geom.vertices.Length]; - for (uint i = 0; i < indices.Length; i++) - indices[i] = i; - - if (geom.meshes.Length == 1) - { - var info = geom.meshes[0].material.GetParameter("IsClipMaterial"); - if (info != null && (bool)info.Value) - { - var entries = childModel.Entries; - entries[0].Visible = false; - childModel.Entries = entries; - } - } - - CollisionData collisionData = Content.CreateVirtualAsset(); - if (collisionData.CookCollision(convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, geom.vertices, - indices)) - { - bool failed = true; - if (convexMesh) - { - // fallback to triangle mesh - failed = collisionData.CookCollision(CollisionDataType.TriangleMesh, - geom.vertices, - indices); - if (!failed) - Console.PrintWarning("Hull brush " + brushIndex.ToString() + " is not convex"); - } - if (failed) - throw new Exception("failed to cook final collision"); - } - - var meshCollider = childModel.AddChild(); - meshCollider.CollisionData = collisionData; - brushIndex++; - } - sw.Stop(); - //Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms"); - } - else - { - List vertices = new List(); - List uvs = new List(); - List normals = new List(); - - sw.Restart(); - int brushIndex = 0; - foreach (var brush in root.entities[0].brushes) - { - try - { - TriangulateBrush(brush, out Vector3[] brushVertices); - Vector2[] brushUvs = new Vector2[brushVertices.Length]; - Vector3[] brushNormals = new Vector3[brushVertices.Length]; - - for (int i = 0; i < brushVertices.Length; i += 3) - { - Vector3 v1 = brushVertices[i + 0]; - Vector3 v2 = brushVertices[i + 1]; - Vector3 v3 = brushVertices[i + 2]; - - Vector3 normal = -Vector3.Cross(v3 - v1, v2 - v1).Normalized; - - // fetch the texture parameters from the plane with matching normal - Vector2 uvScale = new Vector2(0f); - float uvRotation = 0f; - Vector2 uvOffset = new Vector2(0f); - bool found = false; - foreach (var brushPlane in brush.planes) - { - if ((brushPlane.plane.Normal - normal).Length < 0.01f) - { - normal = brushPlane.plane.Normal; // for consistency - uvScale = 1f / brushPlane.scale; - uvRotation = brushPlane.rotation; - uvOffset = brushPlane.offset * brushPlane.scale; - found = true; - break; - } - } - - if (!found) - Console.Print("no matching plane found, bad geometry?"); - - Vector2 uv1, uv2, uv3; - // if quake format - { - // The texture is projected to the surface from three angles, the axis with least - // distortion is chosen here. - - // Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis. - // This behaviour is seemingly random in different engines and editors, so let's not bother. - Vector3 textureNormal = new Vector3((float)Math.Round(normal.X, 4), - (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); - - var dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right)); - var dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up)); - var dotZ = Math.Abs(Vector3.Dot(textureNormal, Vector3.Forward)); - - Vector3 axis; - if (dotY >= dotX && dotY >= dotZ) - axis = -Vector3.Up; - else if (dotX >= dotY && dotX >= dotZ) - axis = Vector3.Right; - else if (dotZ >= dotX && dotZ >= dotY) - axis = -Vector3.Forward; - else - axis = Vector3.Right; - - var axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f - ? -Vector3.Forward - : Vector3.Up; - var axisForward2 = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f - ? Vector3.Up - : -Vector3.Forward; - - Quaternion rot = Quaternion.Identity; - rot = rot * Quaternion.LookRotation(axis, axisForward); - rot = rot * Quaternion.RotationAxis(-Vector3.Forward, - 180f * Mathf.DegreesToRadians); - rot = rot * Quaternion.RotationAxis( - Mathf.Abs(Vector3.Dot(axis, Vector3.Right)) > 0.01f - ? Vector3.Right - : axisForward2, - uvRotation * Mathf.DegreesToRadians); - - uv1 = ((Vector2)(v1 * rot) + uvOffset) * uvScale; - uv2 = ((Vector2)(v2 * rot) + uvOffset) * uvScale; - uv3 = ((Vector2)(v3 * rot) + uvOffset) * uvScale; - } - - brushUvs[i + 0] = uv1; - brushUvs[i + 1] = uv2; - brushUvs[i + 2] = uv3; - - brushNormals[i + 0] = normal; - brushNormals[i + 1] = normal; - brushNormals[i + 2] = normal; - } - - vertices.AddRange(brushVertices); - uvs.AddRange(brushUvs); - normals.AddRange(brushNormals); - } - catch (Exception e) - { - Console.Print("Failed to hull brush " + brushIndex.ToString() + ": " + e.Message); - } - - brushIndex++; - } - sw.Stop(); - Console.Print("Pass 1: triangulation and texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); - - sw.Restart(); - if (vertices.Count > 0) - { - uint[] triangles = new uint[vertices.Count]; - for (uint i = 0; i < vertices.Count; i++) - triangles[i] = i; - - model = Content.CreateVirtualAsset(); - model.SetupLODs(new int[] { 1 }); - model.LODs[0].Meshes[0].UpdateMesh(vertices.ToArray(), (int[])(object)triangles, normals.ToArray(), - null, uvs.ToArray()); - - StaticModel childModel = Actor.AddChild(); - childModel.Name = "MapModel"; - childModel.Model = model; - childModel.SetMaterial(0, missingMaterial); - - CollisionData collisionData = Content.CreateVirtualAsset(); - if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(), - triangles.ToArray())) - throw new Exception("failed to cook final collision"); - var meshCollider = childModel.AddChild(); - meshCollider.CollisionData = collisionData; - } - sw.Stop(); - Console.Print("Pass 2: model and collision: " + sw.Elapsed.TotalMilliseconds + "ms"); - } - - // Handle entities - if (importLights) - { - sw.Restart(); - int lightIndex = 0; - foreach (var lightEntity in root.entities.Where(x => - x.properties.ContainsKey("classname") && x.properties["classname"] == "light")) - { - - Vector3 ParseOrigin(string origin) - { - string[] points = origin.Split(new char[] { ' ' }); - return new Vector3(float.Parse(points[0]), float.Parse(points[2]), float.Parse(points[1])); - } - - Color ParseColor(string origin) - { - string[] points = origin.Split(new char[] { ' ' }); - return new Color(float.Parse(points[0]), float.Parse(points[1]), float.Parse(points[2])); - } - - //Console.Print("light"); - PointLight light = worldSpawnActor.AddChild(); - light.Name = "Light_" + lightIndex; - light.LocalPosition = ParseOrigin(lightEntity.properties["origin"]); - - //"_color" "0.752941 0.752941 0" - //"light" "200" - - if (lightEntity.properties.TryGetValue("_color", out string colorStr)) - { - light.Color = ParseColor(colorStr); - } - - float lightamm = 200f; - if (lightEntity.properties.TryGetValue("light", out string lightStr)) - { - lightamm = float.Parse(lightStr); - } - - - - light.Brightness = lightamm / 128f; - light.Layer = 1; - light.Radius = 1000f * 0.5f; - light.UseInverseSquaredFalloff = false; - light.FallOffExponent = 8; - light.ShadowsDistance = 2000f * 1f; - - - - if (true) - { - // match FTEQW dynamic only light values - } - - //Console.Print("light pos: " + light.Position); - - lightIndex++; - } - - //Console.Print("test: " + (new Vector2(1f,0f) == Vector2.Zero)); - - Console.Print("light parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); - } - } - - public override void OnDestroy() - { - Destroy(ref model); - base.OnDestroy(); - } - } + //Debug.Log("LoadMap"); + LoadMap(false); + } + + private void LoadMap(bool forceLoad) + { + Actor worldSpawnActor = Actor.GetChild("WorldSpawn"); + if (worldSpawnActor != null) + { + if (forceLoad) + { + worldSpawnActor.DestroyChildren(); + } + else + { + Console.Print("Map already loaded in the scene"); + return; + } + } + + { + string workDir = Directory.GetCurrentDirectory(); + string matBasePath = Path.Combine(workDir, "Content", "Materials"); + string assetPath = Path.Combine(matBasePath, "missing.flax"); + missingMaterial = Content.Load(assetPath); + } + + Stopwatch sw = Stopwatch.StartNew(); + byte[] mapChars = File.ReadAllBytes(mapPath); + root = MapParser.Parse(mapChars); + sw.Stop(); + + //Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); + + bool oneMesh = false; + bool convexMesh = true; + + if (!oneMesh) + { + var materials = new Dictionary(); + { + BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy") + ?.CreateInstance(); + if (brushMaterialList != null) + foreach (BrushMaterialListEntry m in brushMaterialList.materialAssets) + materials.Add(m.name, m.asset); + } + + if (worldSpawnActor == null) + { + worldSpawnActor = Actor.AddChild(); + worldSpawnActor.Name = "WorldSpawn"; + worldSpawnActor.HideFlags |= HideFlags.DontSave; + //worldSpawnActor.HideFlags |= HideFlags.DontSelect; + } + + var brushGeometries = new List(root.entities[0].brushes.Count); + + // pass 1: triangulation + sw.Restart(); + int brushIndex = 0; + int totalverts = 0; + foreach (MapBrush brush in root.entities[0].brushes) + { + try + { + BrushGeometry geom = new BrushGeometry(); + + TriangulateBrush(brush, out geom.vertices); + geom.brush = brush; + + brushGeometries.Add(geom); + totalverts += geom.vertices.Length; + + Assert.IsTrue(geom.vertices.Length > 0); + + foreach (Vector3 vert in geom.vertices) + geom.offset += vert; + geom.offset /= geom.vertices.Length; + + for (int i = 0; i < geom.vertices.Length; i++) + geom.vertices[i] -= geom.offset; + + var brushMaterials = new Dictionary(); + foreach (MapFacePlane brushPlane in geom.brush.planes) + { + string textureName = brushPlane.texture; + if (brushMaterials.ContainsKey(textureName)) + continue; + + if (!materials.TryGetValue(textureName, out MaterialBase brushMaterial)) + { + string workDir = Directory.GetCurrentDirectory(); + string matBasePath = Path.Combine(workDir, "Content", "Materials"); + string assetPath = Path.Combine(matBasePath, textureName + ".flax"); + brushMaterial = Content.Load(assetPath); + if (brushMaterial != null) + { + materials.Add(textureName, brushMaterial); + } + else + { + // TODO: engine doesn't seem to always load the asset even though it exists, bug? seems to happen at low framerate + //Console.Print("Material '" + textureName + "' not found for brush, assetPath: " + assetPath); + materials.Add(textureName, missingMaterial); + brushMaterial = missingMaterial; + } + } + + brushMaterials.Add(textureName, brushMaterial); + } + + geom.brushMaterials = brushMaterials; + geom.meshes = new BrushGeometryMesh[brushMaterials.Count]; + for (int i = 0; i < geom.meshes.Length; i++) + { + geom.meshes[i] = new BrushGeometryMesh(); + geom.meshes[i].material = geom.brushMaterials[geom.brushMaterials.Keys.ToList()[i]]; + } + } + catch (Exception e) + { + Console.Print("Failed to triangulate brush " + brushIndex + ": " + e.Message); + //FlaxEngine.Engine.RequestExit(); + } + + brushIndex++; + } + + sw.Stop(); + //Console.Print("Pass 1: triangulation: " + sw.Elapsed.TotalMilliseconds + "ms"); + + // pass 2: texturing + brushIndex = 0; + sw.Restart(); + foreach (BrushGeometry geom in brushGeometries) + { + var brushVertices = geom.vertices; + for (int i = 0; i < brushVertices.Length; i += 3) + { + Vector3 v1 = brushVertices[i + 0]; + Vector3 v2 = brushVertices[i + 1]; + Vector3 v3 = brushVertices[i + 2]; + + Vector3 normal = -Vector3.Cross(v3 - v1, v2 - v1).Normalized; + + // fetch the texture parameters from the plane with matching normal + Vector2 uvScale = new Vector2(0f); + float uvRotation = 0f; + Vector2 uvOffset = new Vector2(0f); + bool found = false; + int meshIndex = 0; + foreach (MapFacePlane brushPlane in geom.brush.planes) + if ((brushPlane.plane.Normal - normal).Length < 0.01f) + { + normal = brushPlane.plane.Normal; // for consistency + uvScale = 1f / brushPlane.scale; + uvRotation = brushPlane.rotation; + uvOffset = brushPlane.offset * brushPlane.scale; + found = true; + meshIndex = geom.brushMaterials.Keys.ToList().IndexOf(brushPlane.texture); // ugh? + break; + } + + if (!found) + Console.Print("no matching plane found for brush " + brushIndex + ", bad geometry?"); + + Vector2 uv1, uv2, uv3; + // if quake format + { + // The texture is projected to the surface from three angles, the axis with least + // distortion is chosen here. + + // Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis. + // This behaviour is seemingly random in different engines and editors, so let's not bother. + + Vector3 textureNormal = new Vector3((float)Math.Round(normal.X, 4), + (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); + + float dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right)); + float dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up)); + float dotZ = Math.Abs(Vector3.Dot(textureNormal, Vector3.Forward)); + + Vector3 axis; + if (dotY >= dotX && dotY >= dotZ) + axis = -Vector3.Up; + else if (dotX >= dotY && dotX >= dotZ) + axis = Vector3.Right; + else if (dotZ >= dotX && dotZ >= dotY) + axis = -Vector3.Forward; + else + axis = Vector3.Right; + + Vector3 axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f + ? -Vector3.Forward + : Vector3.Up; + Vector3 axisForward2 = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f + ? Vector3.Up + : -Vector3.Forward; + + Quaternion rot = Quaternion.Identity; + rot = rot * Quaternion.LookRotation(axis, axisForward); + rot = rot * Quaternion.RotationAxis(-Vector3.Forward, + 180f * Mathf.DegreesToRadians); + rot = rot * Quaternion.RotationAxis( + Mathf.Abs(Vector3.Dot(axis, Vector3.Right)) > 0.01f + ? Vector3.Right + : axisForward2, + uvRotation * Mathf.DegreesToRadians); + + uv1 = ((Vector2)((v1 + geom.offset) * rot) + uvOffset) * uvScale; + uv2 = ((Vector2)((v2 + geom.offset) * rot) + uvOffset) * uvScale; + uv3 = ((Vector2)((v3 + geom.offset) * rot) + uvOffset) * uvScale; + } + BrushGeometryMesh mesh = geom.meshes[meshIndex]; + + mesh.indices.Add((uint)mesh.vertices.Count + 0); + mesh.indices.Add((uint)mesh.vertices.Count + 1); + mesh.indices.Add((uint)mesh.vertices.Count + 2); + + mesh.vertices.Add(v1); + mesh.vertices.Add(v2); + mesh.vertices.Add(v3); + + mesh.uvs.Add(uv1); + mesh.uvs.Add(uv2); + mesh.uvs.Add(uv3); + + mesh.normals.Add(normal); + mesh.normals.Add(normal); + mesh.normals.Add(normal); + } + + geom.model = Content.CreateVirtualAsset(); + geom.model.SetupLODs(new[] { geom.meshes.Length }); + geom.model.SetupMaterialSlots(geom.meshes.Length); + + for (int i = 0; i < geom.meshes.Length; i++) + { + BrushGeometryMesh mesh = geom.meshes[i]; + if (mesh.vertices.Count == 0) + continue; + + geom.model.LODs[0].Meshes[i].UpdateMesh(mesh.vertices, mesh.indices, mesh.normals, + null, mesh.uvs); + geom.model.LODs[0].Meshes[i].MaterialSlotIndex = i; + geom.model.MaterialSlots[i].Material = geom.meshes[i].material; + } + + brushIndex++; + } + + sw.Stop(); + //Console.Print("Pass 2: texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); + + // pass 3: collision + sw.Restart(); + brushIndex = 0; + foreach (BrushGeometry geom in brushGeometries) + { + StaticModel childModel = worldSpawnActor.AddChild(); + childModel.Name = "Brush_" + brushIndex; + childModel.Model = geom.model; + childModel.Position = geom.offset; + + for (int i = 0; i < geom.meshes.Length; i++) + childModel.SetMaterial(i, geom.meshes[i].material); + + uint[] indices = new uint[geom.vertices.Length]; + for (uint i = 0; i < indices.Length; i++) + indices[i] = i; + + if (geom.meshes.Length == 1) + { + MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); + if (info != null && (bool)info.Value) + { + var entries = childModel.Entries; + entries[0].Visible = false; + childModel.Entries = entries; + } + } + + CollisionData collisionData = Content.CreateVirtualAsset(); + if (collisionData.CookCollision( + convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, geom.vertices, + indices)) + { + bool failed = true; + if (convexMesh) + { + // fallback to triangle mesh + failed = collisionData.CookCollision(CollisionDataType.TriangleMesh, + geom.vertices, + indices); + if (!failed) + Console.PrintWarning("Hull brush " + brushIndex + " is not convex"); + } + + if (failed) + throw new Exception("failed to cook final collision"); + } + + MeshCollider meshCollider = childModel.AddChild(); + meshCollider.CollisionData = collisionData; + brushIndex++; + } + + sw.Stop(); + //Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms"); + } + else + { + var vertices = new List(); + var uvs = new List(); + var normals = new List(); + + sw.Restart(); + int brushIndex = 0; + foreach (MapBrush brush in root.entities[0].brushes) + { + try + { + TriangulateBrush(brush, out var brushVertices); + var brushUvs = new Vector2[brushVertices.Length]; + var brushNormals = new Vector3[brushVertices.Length]; + + for (int i = 0; i < brushVertices.Length; i += 3) + { + Vector3 v1 = brushVertices[i + 0]; + Vector3 v2 = brushVertices[i + 1]; + Vector3 v3 = brushVertices[i + 2]; + + Vector3 normal = -Vector3.Cross(v3 - v1, v2 - v1).Normalized; + + // fetch the texture parameters from the plane with matching normal + Vector2 uvScale = new Vector2(0f); + float uvRotation = 0f; + Vector2 uvOffset = new Vector2(0f); + bool found = false; + foreach (MapFacePlane brushPlane in brush.planes) + if ((brushPlane.plane.Normal - normal).Length < 0.01f) + { + normal = brushPlane.plane.Normal; // for consistency + uvScale = 1f / brushPlane.scale; + uvRotation = brushPlane.rotation; + uvOffset = brushPlane.offset * brushPlane.scale; + found = true; + break; + } + + if (!found) + Console.Print("no matching plane found, bad geometry?"); + + Vector2 uv1, uv2, uv3; + // if quake format + { + // The texture is projected to the surface from three angles, the axis with least + // distortion is chosen here. + + // Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis. + // This behaviour is seemingly random in different engines and editors, so let's not bother. + Vector3 textureNormal = new Vector3((float)Math.Round(normal.X, 4), + (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); + + float dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right)); + float dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up)); + float dotZ = Math.Abs(Vector3.Dot(textureNormal, Vector3.Forward)); + + Vector3 axis; + if (dotY >= dotX && dotY >= dotZ) + axis = -Vector3.Up; + else if (dotX >= dotY && dotX >= dotZ) + axis = Vector3.Right; + else if (dotZ >= dotX && dotZ >= dotY) + axis = -Vector3.Forward; + else + axis = Vector3.Right; + + Vector3 axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f + ? -Vector3.Forward + : Vector3.Up; + Vector3 axisForward2 = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f + ? Vector3.Up + : -Vector3.Forward; + + Quaternion rot = Quaternion.Identity; + rot = rot * Quaternion.LookRotation(axis, axisForward); + rot = rot * Quaternion.RotationAxis(-Vector3.Forward, + 180f * Mathf.DegreesToRadians); + rot = rot * Quaternion.RotationAxis( + Mathf.Abs(Vector3.Dot(axis, Vector3.Right)) > 0.01f + ? Vector3.Right + : axisForward2, + uvRotation * Mathf.DegreesToRadians); + + uv1 = ((Vector2)(v1 * rot) + uvOffset) * uvScale; + uv2 = ((Vector2)(v2 * rot) + uvOffset) * uvScale; + uv3 = ((Vector2)(v3 * rot) + uvOffset) * uvScale; + } + + brushUvs[i + 0] = uv1; + brushUvs[i + 1] = uv2; + brushUvs[i + 2] = uv3; + + brushNormals[i + 0] = normal; + brushNormals[i + 1] = normal; + brushNormals[i + 2] = normal; + } + + vertices.AddRange(brushVertices); + uvs.AddRange(brushUvs); + normals.AddRange(brushNormals); + } + catch (Exception e) + { + Console.Print("Failed to hull brush " + brushIndex + ": " + e.Message); + } + + brushIndex++; + } + + sw.Stop(); + Console.Print("Pass 1: triangulation and texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); + + sw.Restart(); + if (vertices.Count > 0) + { + uint[] triangles = new uint[vertices.Count]; + for (uint i = 0; i < vertices.Count; i++) + triangles[i] = i; + + model = Content.CreateVirtualAsset(); + model.SetupLODs(new[] { 1 }); + model.LODs[0].Meshes[0].UpdateMesh(vertices.ToArray(), (int[])(object)triangles, normals.ToArray(), + null, uvs.ToArray()); + + StaticModel childModel = Actor.AddChild(); + childModel.Name = "MapModel"; + childModel.Model = model; + childModel.SetMaterial(0, missingMaterial); + + CollisionData collisionData = Content.CreateVirtualAsset(); + if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(), + triangles.ToArray())) + throw new Exception("failed to cook final collision"); + MeshCollider meshCollider = childModel.AddChild(); + meshCollider.CollisionData = collisionData; + } + + sw.Stop(); + Console.Print("Pass 2: model and collision: " + sw.Elapsed.TotalMilliseconds + "ms"); + } + + // Handle entities + + { + sw.Restart(); + + int lightIndex = 0; + int playerSpawnIndex = 0; + foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname"))) + switch (entity.properties["classname"]) + { + case "light": + if (importLights) + ParseLight(entity, worldSpawnActor, ref lightIndex); + break; + case "info_player_deathmatch": + ParsePlayerSpawn(entity, worldSpawnActor, ref playerSpawnIndex); + break; + } + + Console.Print("entity parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); + } + } + + private void ParseLight(MapEntity entity, Actor worldSpawnActor, ref int lightIndex) + { + //Console.Print("light"); + PointLight light = worldSpawnActor.AddChild(); + light.Name = "Light_" + lightIndex; + light.LocalPosition = ParseOrigin(entity.properties["origin"]); + + //"_color" "0.752941 0.752941 0" + //"light" "200" + + if (entity.properties.TryGetValue("_color", out string colorStr)) + light.Color = ParseColor(colorStr); + + float lightamm = 200f; + if (entity.properties.TryGetValue("light", out string lightStr)) + lightamm = float.Parse(lightStr); + + + light.Brightness = lightamm / 128f; + light.Layer = 1; + light.Radius = 1000f * 0.5f; + light.UseInverseSquaredFalloff = false; + light.FallOffExponent = 8; + light.ShadowsDistance = 2000f * 1f; + + + if (true) + { + // match FTEQW dynamic only light values + } + + //Console.Print("light pos: " + light.Position); + + lightIndex++; + } + + private void ParsePlayerSpawn(MapEntity entity, Actor worldSpawnActor, ref int playerSpawnIndex) + { + Actor spawn = worldSpawnActor.AddChild(); + spawn.Name = "PlayerSpawn_" + playerSpawnIndex; + spawn.LocalPosition = ParseOrigin(entity.properties["origin"]); + if (entity.properties.ContainsKey("angle")) + spawn.Orientation = ParseAngle(entity.properties["angle"]); + + spawn.Tag = "PlayerSpawn"; + + playerSpawnIndex++; + } + + private static Vector3 ParseOrigin(string origin) + { + string[] points = origin.Split(' '); + return new Vector3(float.Parse(points[0]), float.Parse(points[2]), float.Parse(points[1])); + } + + private static Color ParseColor(string origin) + { + string[] points = origin.Split(' '); + return new Color(float.Parse(points[0]), float.Parse(points[1]), float.Parse(points[2])); + } + + private static Quaternion ParseAngle(string origin) + { + string[] angles = origin.Split(' '); + return Quaternion.Euler(new Vector3(0f, float.Parse(angles[0]), 0f)); + } + + public override void OnDestroy() + { + Destroy(ref model); + base.OnDestroy(); + } + } } \ No newline at end of file diff --git a/Source/Game/Level/QuickHull.cs b/Source/Game/Level/QuickHull.cs index 593ccac..bb860e0 100644 --- a/Source/Game/Level/QuickHull.cs +++ b/Source/Game/Level/QuickHull.cs @@ -1,5 +1,4 @@ #if false - using System; using System.Collections.Generic; using System.Linq; @@ -855,7 +854,8 @@ namespace Game Vector3 nextVertex = qhe.next.tail; Vector3 edgeVector = (nextVertex - vertex).Normalized; - Vector3 outVector = Vector3.Cross(-(new Plane(mergedFace.v1, mergedFace.v2, mergedFace.v3).Normal), edgeVector); + Vector3 outVector = + Vector3.Cross(-(new Plane(mergedFace.v1, mergedFace.v2, mergedFace.v3).Normal), edgeVector); HalfEdge testHe = qhe.next; do @@ -1125,7 +1125,7 @@ namespace Game foreach (var dface in discardedFaces) { - for (int i=0; i - /// An implementation of the quickhull algorithm for generating 3d convex - /// hulls. - /// - /// The algorithm works like this: you start with an initial "seed" hull, - /// that is just a simple tetrahedron made up of four points in the point - /// cloud. This seed hull is then grown until it all the points in the - /// point cloud is inside of it, at which point it will be the convex hull - /// for the entire set. - /// - /// All of the points in the point cloud is divided into two parts, the - /// "open set" and the "closed set". The open set consists of all the - /// points outside of the tetrahedron, and the closed set is all of the - /// points inside the tetrahedron. After each iteration of the algorithm, - /// the closed set gets bigger and the open set get smaller. When the open - /// set is empty, the algorithm is finished. - /// - /// Each point in the open set is assigned to a face that it lies outside - /// of. To grow the hull, the point in the open set which is farthest from - /// it's face is chosen. All faces which are facing that point (I call - /// them "lit faces" in the code, because if you imagine the point as a - /// point light, it's the set of points which would be lit by that point - /// light) are removed, and a "horizon" of edges is found from where the - /// faces were removed. From this horizon, new faces are constructed in a - /// "cone" like fashion connecting the point to the edges. - /// - /// To keep track of the faces, I use a struct for each face which - /// contains the three vertices of the face in CCW order, as well as the - /// three triangles which share an edge. I was considering doing a - /// half-edge structure to store the mesh, but it's not needed. Using a - /// struct for each face and neighbors simplify the algorithm and makes it - /// easy to export it as a mesh. - /// - /// The most subtle part of the algorithm is finding the horizon. In order - /// to properly construct the cone so that all neighbors are kept - /// consistent, you can do a depth-first search from the first lit face. - /// If the depth-first search always proceeeds in a counter-clockwise - /// fashion, it guarantees that the horizon will be found in a - /// counter-clockwise order, which makes it easy to construct the cone of - /// new faces. - /// - /// A note: the code uses a right-handed coordinate system, where the - /// cross-product uses the right-hand rule and the faces are in CCW order. - /// At the end of the algorithm, the hull is exported in a Unity-friendly - /// fashion, with a left-handed mesh. + /// An implementation of the quickhull algorithm for generating 3d convex + /// hulls. + /// The algorithm works like this: you start with an initial "seed" hull, + /// that is just a simple tetrahedron made up of four points in the point + /// cloud. This seed hull is then grown until it all the points in the + /// point cloud is inside of it, at which point it will be the convex hull + /// for the entire set. + /// All of the points in the point cloud is divided into two parts, the + /// "open set" and the "closed set". The open set consists of all the + /// points outside of the tetrahedron, and the closed set is all of the + /// points inside the tetrahedron. After each iteration of the algorithm, + /// the closed set gets bigger and the open set get smaller. When the open + /// set is empty, the algorithm is finished. + /// Each point in the open set is assigned to a face that it lies outside + /// of. To grow the hull, the point in the open set which is farthest from + /// it's face is chosen. All faces which are facing that point (I call + /// them "lit faces" in the code, because if you imagine the point as a + /// point light, it's the set of points which would be lit by that point + /// light) are removed, and a "horizon" of edges is found from where the + /// faces were removed. From this horizon, new faces are constructed in a + /// "cone" like fashion connecting the point to the edges. + /// To keep track of the faces, I use a struct for each face which + /// contains the three vertices of the face in CCW order, as well as the + /// three triangles which share an edge. I was considering doing a + /// half-edge structure to store the mesh, but it's not needed. Using a + /// struct for each face and neighbors simplify the algorithm and makes it + /// easy to export it as a mesh. + /// The most subtle part of the algorithm is finding the horizon. In order + /// to properly construct the cone so that all neighbors are kept + /// consistent, you can do a depth-first search from the first lit face. + /// If the depth-first search always proceeeds in a counter-clockwise + /// fashion, it guarantees that the horizon will be found in a + /// counter-clockwise order, which makes it easy to construct the cone of + /// new faces. + /// A note: the code uses a right-handed coordinate system, where the + /// cross-product uses the right-hand rule and the faces are in CCW order. + /// At the end of the algorithm, the hull is exported in a Unity-friendly + /// fashion, with a left-handed mesh. /// - public class ConvexHullCalculator { - - /// - /// Constant representing a point that has yet to be assigned to a - /// face. It's only used immediately after constructing the seed hull. - /// - const int UNASSIGNED = -2; - - /// - /// Constant representing a point that is inside the convex hull, and - /// thus is behind all faces. In the openSet array, all points with - /// INSIDE are at the end of the array, with indexes larger - /// openSetTail. - /// - const int INSIDE = -1; - - /// - /// Epsilon value. If the coordinates of the point space are - /// exceptionally close to each other, this value might need to be - /// adjusted. - /// - const float EPSILON = 0.001f; - - /// - /// Struct representing a single face. - /// - /// Vertex0, Vertex1 and Vertex2 are the vertices in CCW order. They - /// acutal points are stored in the points array, these are just - /// indexes into that array. - /// - /// Opposite0, Opposite1 and Opposite2 are the keys to the faces which - /// share an edge with this face. Opposite0 is the face opposite - /// Vertex0 (so it has an edge with Vertex2 and Vertex1), etc. - /// - /// Normal is (unsurprisingly) the normal of the triangle. - /// - struct Face { - public int Vertex0; - public int Vertex1; - public int Vertex2; - - public int Opposite0; - public int Opposite1; - public int Opposite2; - - public Vector3 Normal; - - public Face(int v0, int v1, int v2, int o0, int o1, int o2, Vector3 normal) { - Vertex0 = v0; - Vertex1 = v1; - Vertex2 = v2; - Opposite0 = o0; - Opposite1 = o1; - Opposite2 = o2; - Normal = normal; - } - - public bool Equals(Face other) { - return (this.Vertex0 == other.Vertex0) - && (this.Vertex1 == other.Vertex1) - && (this.Vertex2 == other.Vertex2) - && (this.Opposite0 == other.Opposite0) - && (this.Opposite1 == other.Opposite1) - && (this.Opposite2 == other.Opposite2) - && (this.Normal == other.Normal); - } - } - - /// - /// Struct representing a mapping between a point and a face. These - /// are used in the openSet array. - /// - /// Point is the index of the point in the points array, Face is the - /// key of the face in the Key dictionary, Distance is the distance - /// from the face to the point. - /// - struct PointFace { - public int Point; - public int Face; - public float Distance; - - public PointFace(int p, int f, float d) { - Point = p; - Face = f; - Distance = d; - } - } - - /// - /// Struct representing a single edge in the horizon. - /// - /// Edge0 and Edge1 are the vertexes of edge in CCW order, Face is the - /// face on the other side of the horizon. - /// - /// TODO Edge1 isn't actually needed, you can just index the next item - /// in the horizon array. - /// - struct HorizonEdge { - public int Face; - public int Edge0; - public int Edge1; - } - - /// - /// A dictionary storing the faces of the currently generated convex - /// hull. The key is the id of the face, used in the Face, PointFace - /// and HorizonEdge struct. - /// - /// This is a Dictionary, because we need both random access to it, - /// the ability to loop through it, and ability to quickly delete - /// faces (in the ConstructCone method), and Dictionary is the obvious - /// candidate that can do all of those things. - /// - /// I'm wondering if using a Dictionary is best idea, though. It might - /// be better to just have them in a ]]> and mark a face as - /// deleted by adding a field to the Face struct. The downside is that - /// we would need an extra field in the Face struct, and when we're - /// looping through the points in openSet, we would have to loop - /// through all the Faces EVER created in the algorithm, and skip the - /// ones that have been marked as deleted. However, looping through a - /// list is fairly fast, and it might be worth it to avoid Dictionary - /// overhead. - /// - /// TODO test converting to a ]]> instead. - /// - Dictionary faces; - - /// - /// The set of points to be processed. "openSet" is a misleading name, - /// because it's both the open set (points which are still outside the - /// convex hull) and the closed set (points that are inside the convex - /// hull). The first part of the array (with ) - /// is the openSet, the last part of the array (with - /// openSetTail]]>) are the closed set, with Face set to INSIDE. The - /// closed set is largely irrelevant to the algorithm, the open set is - /// what matters. - /// - /// Storing the entire open set in one big list has a downside: when - /// we're reassigning points after ConstructCone, we only need to - /// reassign points that belong to the faces that have been removed, - /// but storing it in one array, we have to loop through the entire - /// list, and checking litFaces to determine which we can skip and - /// which need to be reassigned. - /// - /// The alternative here is to give each face in Face array it's own - /// openSet. I don't like that solution, because then you have to - /// juggle so many more heap-allocated 's]]>, we'd have to use - /// object pools and such. It would do a lot more allocation, and it - /// would have worse locality. I should maybe test that solution, but - /// it probably wont be faster enough (if at all) to justify the extra - /// allocations. - /// - List openSet; - - /// - /// Set of faces which are "lit" by the current point in the set. This - /// is used in the FindHorizon() DFS search to keep track of which - /// faces we've already visited, and in the ReassignPoints() method to - /// know which points need to be reassigned. - /// - HashSet litFaces; - - /// - /// The current horizon. Generated by the FindHorizon() DFS search, - /// and used in ConstructCone to construct new faces. The list of - /// edges are in CCW order. - /// - List horizon; - - /// - /// If SplitVerts is false, this Dictionary is used to keep track of - /// which points we've added to the final mesh. - /// - Dictionary hullVerts; - - /// - /// The "tail" of the openSet, the last index of a vertex that has - /// been assigned to a face. - /// - int openSetTail = -1; - - /// - /// When adding a new face to the faces Dictionary, use this for the - /// key and then increment it. - /// - int faceCount = 0; - - /// - /// Generate a convex hull from points in points array, and store the - /// mesh in Unity-friendly format in verts and tris. If splitVerts is - /// true, the the verts will be split, if false, the same vert will be - /// used for more than one triangle. - /// - public void GenerateHull( - List points, - bool splitVerts, - ref List verts, - ref List tris, - ref List normals) - { - if (points.Count < 4) { - throw new System.ArgumentException("Need at least 4 points to generate a convex hull"); - } - - Initialize(points, splitVerts); - - GenerateInitialHull(points); - - while (openSetTail >= 0) { - GrowHull(points); - } - - ExportMesh(points, splitVerts, ref verts, ref tris, ref normals); - //VerifyMesh(points, ref verts, ref tris); - } - - /// - /// Make sure all the buffers and variables needed for the algorithm - /// are initialized. - /// - void Initialize(List points, bool splitVerts) { - faceCount = 0; - openSetTail = -1; - - if (faces == null) { - faces = new Dictionary(); - litFaces = new HashSet(); - horizon = new List(); - openSet = new List(points.Count); - } else { - faces.Clear(); - litFaces.Clear(); - horizon.Clear(); - openSet.Clear(); - - if (openSet.Capacity < points.Count) { - // i wonder if this is a good idea... if you call - // GenerateHull over and over with slightly increasing - // points counts, it's going to reallocate every time. Maybe - // i should just use .Add(), and let the List manage the - // capacity, increasing it geometrically every time we need - // to reallocate. - - // maybe do - // openSet.Capacity = Mathf.NextPowerOfTwo(points.Count) - // instead? - - openSet.Capacity = points.Count; - } - } - - if (!splitVerts) { - if (hullVerts == null) { - hullVerts = new Dictionary(); - } else { - hullVerts.Clear(); - } - } - } - - /// - /// Create initial seed hull. - /// - void GenerateInitialHull(List points) { - // Find points suitable for use as the seed hull. Some varieties of - // this algorithm pick extreme points here, but I'm not convinced - // you gain all that much from that. Currently what it does is just - // find the first four points that are not coplanar. - int b0, b1, b2, b3; - FindInitialHullIndices(points, out b0, out b1, out b2, out b3); - - var v0 = points[b0]; - var v1 = points[b1]; - var v2 = points[b2]; - var v3 = points[b3]; - - var above = Dot(v3 - v1, Cross(v1 - v0, v2 - v0)) > 0.0f; - - // Create the faces of the seed hull. You need to draw a diagram - // here, otherwise it's impossible to know what's going on :) - - // Basically: there are two different possible start-tetrahedrons, - // depending on whether the fourth point is above or below the base - // triangle. If you draw a tetrahedron with these coordinates (in a - // right-handed coordinate-system): - - // b0 = (0,0,0) - // b1 = (1,0,0) - // b2 = (0,1,0) - // b3 = (0,0,1) - - // you can see the first case (set b3 = (0,0,-1) for the second - // case). The faces are added with the proper references to the - // faces opposite each vertex - - faceCount = 0; - if (above) { - faces[faceCount++] = new Face(b0, b2, b1, 3, 1, 2, Normal(points[b0], points[b2], points[b1])); - faces[faceCount++] = new Face(b0, b1, b3, 3, 2, 0, Normal(points[b0], points[b1], points[b3])); - faces[faceCount++] = new Face(b0, b3, b2, 3, 0, 1, Normal(points[b0], points[b3], points[b2])); - faces[faceCount++] = new Face(b1, b2, b3, 2, 1, 0, Normal(points[b1], points[b2], points[b3])); - } else { - faces[faceCount++] = new Face(b0, b1, b2, 3, 2, 1, Normal(points[b0], points[b1], points[b2])); - faces[faceCount++] = new Face(b0, b3, b1, 3, 0, 2, Normal(points[b0], points[b3], points[b1])); - faces[faceCount++] = new Face(b0, b2, b3, 3, 1, 0, Normal(points[b0], points[b2], points[b3])); - faces[faceCount++] = new Face(b1, b3, b2, 2, 0, 1, Normal(points[b1], points[b3], points[b2])); - } - - VerifyFaces(points); - - // Create the openSet. Add all points except the points of the seed - // hull. - for (int i = 0; i < points.Count; i++) { - if (i == b0 || i == b1 || i == b2 || i == b3) continue; - - openSet.Add(new PointFace(i, UNASSIGNED, 0.0f)); - } - - // Add the seed hull verts to the tail of the list. - openSet.Add(new PointFace(b0, INSIDE, float.NaN)); - openSet.Add(new PointFace(b1, INSIDE, float.NaN)); - openSet.Add(new PointFace(b2, INSIDE, float.NaN)); - openSet.Add(new PointFace(b3, INSIDE, float.NaN)); - - // Set the openSetTail value. Last item in the array is - // openSet.Count - 1, but four of the points (the verts of the seed - // hull) are part of the closed set, so move openSetTail to just - // before those. - openSetTail = openSet.Count - 5; - - Assert.IsTrue(openSet.Count == points.Count); - - // Assign all points of the open set. This does basically the same - // thing as ReassignPoints() - for (int i = 0; i <= openSetTail; i++) { - Assert.IsTrue(openSet[i].Face == UNASSIGNED); - Assert.IsTrue(openSet[openSetTail].Face == UNASSIGNED); - Assert.IsTrue(openSet[openSetTail + 1].Face == INSIDE); - - var assigned = false; - var fp = openSet[i]; - - Assert.IsTrue(faces.Count == 4); - Assert.IsTrue(faces.Count == faceCount); - for (int j = 0; j < 4; j++) { - Assert.IsTrue(faces.ContainsKey(j)); - - var face = faces[j]; - - var dist = PointFaceDistance(points[fp.Point], points[face.Vertex0], face); - - if (dist > 0) { - fp.Face = j; - fp.Distance = dist; - openSet[i] = fp; - - assigned = true; - break; - } - } - - if (!assigned) { - // Point is inside - fp.Face = INSIDE; - fp.Distance = float.NaN; - - // Point is inside seed hull: swap point with tail, and move - // openSetTail back. We also have to decrement i, because - // there's a new item at openSet[i], and we need to process - // it next iteration - openSet[i] = openSet[openSetTail]; - openSet[openSetTail] = fp; - - openSetTail -= 1; - i -= 1; - } - } - - VerifyOpenSet(points); - } - - /// - /// Find four points in the point cloud that are not coplanar for the - /// seed hull - /// - void FindInitialHullIndices(List points, out int b0, out int b1, out int b2, out int b3) { - var count = points.Count; - - for (int i0 = 0; i0 < count - 3; i0++) { - for (int i1 = i0 + 1; i1 < count - 2; i1++) { - var p0 = points[i0]; - var p1 = points[i1]; - - if (AreCoincident(p0, p1)) continue; - - for (int i2 = i1 + 1; i2 < count - 1; i2++) { - var p2 = points[i2]; - - if (AreCollinear(p0, p1, p2)) continue; - - for (int i3 = i2 + 1; i3 < count - 0; i3++) { - var p3 = points[i3]; - - if(AreCoplanar(p0, p1, p2, p3)) continue; - - b0 = i0; - b1 = i1; - b2 = i2; - b3 = i3; - return; - } - } - } - } - - throw new System.ArgumentException("Can't generate hull, points are coplanar"); - } - - /// - /// Grow the hull. This method takes the current hull, and expands it - /// to encompass the point in openSet with the point furthest away - /// from its face. - /// - void GrowHull(List points) { - Assert.IsTrue(openSetTail >= 0); - Assert.IsTrue(openSet[0].Face != INSIDE); - - // Find farthest point and first lit face. - var farthestPoint = 0; - var dist = openSet[0].Distance; - - for (int i = 1; i <= openSetTail; i++) { - if (openSet[i].Distance > dist) { - farthestPoint = i; - dist = openSet[i].Distance; - } - } - - // Use lit face to find horizon and the rest of the lit - // faces. - FindHorizon( - points, - points[openSet[farthestPoint].Point], - openSet[farthestPoint].Face, - faces[openSet[farthestPoint].Face]); - - VerifyHorizon(); - - // Construct new cone from horizon - ConstructCone(points, openSet[farthestPoint].Point); - - VerifyFaces(points); - - // Reassign points - ReassignPoints(points); - } - - /// - /// Start the search for the horizon. - /// - /// The search is a DFS search that searches neighboring triangles in - /// a counter-clockwise fashion. When it find a neighbor which is not - /// lit, that edge will be a line on the horizon. If the search always - /// proceeds counter-clockwise, the edges of the horizon will be found - /// in counter-clockwise order. - /// - /// The heart of the search can be found in the recursive - /// SearchHorizon() method, but the the first iteration of the search - /// is special, because it has to visit three neighbors (all the - /// neighbors of the initial triangle), while the rest of the search - /// only has to visit two (because one of them has already been - /// visited, the one you came from). - /// - void FindHorizon(List points, Vector3 point, int fi, Face face) { - // TODO should I use epsilon in the PointFaceDistance comparisons? - - litFaces.Clear(); - horizon.Clear(); - - litFaces.Add(fi); - - Assert.IsTrue(PointFaceDistance(point, points[face.Vertex0], face) > 0.0f); - - // For the rest of the recursive search calls, we first check if the - // triangle has already been visited and is part of litFaces. - // However, in this first call we can skip that because we know it - // can't possibly have been visited yet, since the only thing in - // litFaces is the current triangle. - { - var oppositeFace = faces[face.Opposite0]; - - var dist = PointFaceDistance( - point, - points[oppositeFace.Vertex0], - oppositeFace); - - if (dist <= 0.0f) { - horizon.Add(new HorizonEdge { - Face = face.Opposite0, - Edge0 = face.Vertex1, - Edge1 = face.Vertex2, - }); - } else { - SearchHorizon(points, point, fi, face.Opposite0, oppositeFace); - } - } - - if (!litFaces.Contains(face.Opposite1)) { - var oppositeFace = faces[face.Opposite1]; - - var dist = PointFaceDistance( - point, - points[oppositeFace.Vertex0], - oppositeFace); - - if (dist <= 0.0f) { - horizon.Add(new HorizonEdge { - Face = face.Opposite1, - Edge0 = face.Vertex2, - Edge1 = face.Vertex0, - }); - } else { - SearchHorizon(points, point, fi, face.Opposite1, oppositeFace); - } - } - - if (!litFaces.Contains(face.Opposite2)) { - var oppositeFace = faces[face.Opposite2]; - - var dist = PointFaceDistance( - point, - points[oppositeFace.Vertex0], - oppositeFace); - - if (dist <= 0.0f) { - horizon.Add(new HorizonEdge { - Face = face.Opposite2, - Edge0 = face.Vertex0, - Edge1 = face.Vertex1, - }); - } else { - SearchHorizon(points, point, fi, face.Opposite2, oppositeFace); - } - } - } - - /// - /// Recursively search to find the horizon or lit set. - /// - void SearchHorizon(List points, Vector3 point, int prevFaceIndex, int faceCount, Face face) { - Assert.IsTrue(prevFaceIndex >= 0); - Assert.IsTrue(litFaces.Contains(prevFaceIndex)); - Assert.IsTrue(!litFaces.Contains(faceCount)); - Assert.IsTrue(faces[faceCount].Equals(face)); - - litFaces.Add(faceCount); - - // Use prevFaceIndex to determine what the next face to search will - // be, and what edges we need to cross to get there. It's important - // that the search proceeds in counter-clockwise order from the - // previous face. - int nextFaceIndex0; - int nextFaceIndex1; - int edge0; - int edge1; - int edge2; - - if (prevFaceIndex == face.Opposite0) { - nextFaceIndex0 = face.Opposite1; - nextFaceIndex1 = face.Opposite2; - - edge0 = face.Vertex2; - edge1 = face.Vertex0; - edge2 = face.Vertex1; - } else if (prevFaceIndex == face.Opposite1) { - nextFaceIndex0 = face.Opposite2; - nextFaceIndex1 = face.Opposite0; - - edge0 = face.Vertex0; - edge1 = face.Vertex1; - edge2 = face.Vertex2; - } else { - Assert.IsTrue(prevFaceIndex == face.Opposite2); - - nextFaceIndex0 = face.Opposite0; - nextFaceIndex1 = face.Opposite1; - - edge0 = face.Vertex1; - edge1 = face.Vertex2; - edge2 = face.Vertex0; - } - - if (!litFaces.Contains(nextFaceIndex0)) { - var oppositeFace = faces[nextFaceIndex0]; - - var dist = PointFaceDistance( - point, - points[oppositeFace.Vertex0], - oppositeFace); - - if (dist <= 0.0f) { - horizon.Add(new HorizonEdge { - Face = nextFaceIndex0, - Edge0 = edge0, - Edge1 = edge1, - }); - } else { - SearchHorizon(points, point, faceCount, nextFaceIndex0, oppositeFace); - } - } - - if (!litFaces.Contains(nextFaceIndex1)) { - var oppositeFace = faces[nextFaceIndex1]; - - var dist = PointFaceDistance( - point, - points[oppositeFace.Vertex0], - oppositeFace); - - if (dist <= 0.0f) { - horizon.Add(new HorizonEdge { - Face = nextFaceIndex1, - Edge0 = edge1, - Edge1 = edge2, - }); - } else { - SearchHorizon(points, point, faceCount, nextFaceIndex1, oppositeFace); - } - } - } - - /// - /// Remove all lit faces and construct new faces from the horizon in a - /// "cone-like" fashion. - /// - /// This is a relatively straight-forward procedure, given that the - /// horizon is handed to it in already sorted counter-clockwise. The - /// neighbors of the new faces are easy to find: they're the previous - /// and next faces to be constructed in the cone, as well as the face - /// on the other side of the horizon. We also have to update the face - /// on the other side of the horizon to reflect it's new neighbor from - /// the cone. - /// - void ConstructCone(List points, int farthestPoint) { - foreach (var fi in litFaces) { - Assert.IsTrue(faces.ContainsKey(fi)); - faces.Remove(fi); - } - - var firstNewFace = faceCount; - - // Check for coplanar faces - /*var firstNormal = new Vector3(0, 0, 0); - int sameNormals = 1; - for (int i = 0; i < horizon.Count; i++) - { - var v0 = farthestPoint; - var v1 = horizon[i].Edge0; - var v2 = horizon[i].Edge1; - var norm = Normal(points[v0], points[v1], points[v2]); - if (firstNormal.IsZero) - { - firstNormal = norm; - continue; - } - - const float tol = 0.1f; - if ((firstNormal - norm).Length < tol || (firstNormal - norm).Length > 2.0f-tol) - { - sameNormals++; - } - } - - if (sameNormals == horizon.Count) - sameNormals = sameNormals;*/ - - for (int i = 0; i < horizon.Count; i++) { - // Vertices of the new face, the farthest point as well as the - // edge on the horizon. Horizon edge is CCW, so the triangle - // should be as well. - var v0 = farthestPoint; - var v1 = horizon[i].Edge0; - var v2 = horizon[i].Edge1; - - // Opposite faces of the triangle. First, the edge on the other - // side of the horizon, then the next/prev faces on the new cone - var o0 = horizon[i].Face; - var o1 = (i == horizon.Count - 1) ? firstNewFace : firstNewFace + i + 1; - var o2 = (i == 0) ? (firstNewFace + horizon.Count - 1) : firstNewFace + i - 1; - - var fi = faceCount++; - - faces[fi] = new Face( - v0, v1, v2, - o0, o1, o2, - Normal(points[v0], points[v1], points[v2])); - - var horizonFace = faces[horizon[i].Face]; - - if (horizonFace.Vertex0 == v1) { - Assert.IsTrue(v2 == horizonFace.Vertex2); - horizonFace.Opposite1 = fi; - } else if (horizonFace.Vertex1 == v1) { - Assert.IsTrue(v2 == horizonFace.Vertex0); - horizonFace.Opposite2 = fi; - } else { - Assert.IsTrue(v1 == horizonFace.Vertex2); - Assert.IsTrue(v2 == horizonFace.Vertex1); - horizonFace.Opposite0 = fi; - } - - faces[horizon[i].Face] = horizonFace; - } - } - - /// - /// Reassign points based on the new faces added by ConstructCone(). - /// - /// Only points that were previous assigned to a removed face need to - /// be updated, so check litFaces while looping through the open set. - /// - /// There is a potential optimization here: there's no reason to loop - /// through the entire openSet here. If each face had it's own - /// openSet, we could just loop through the openSets in the removed - /// faces. That would make the loop here shorter. - /// - /// However, to do that, we would have to juggle A LOT more 's]]>, - /// and we would need an object pool to manage them all without - /// generating a whole bunch of garbage. I don't think it's worth - /// doing that to make this loop shorter, a straight for-loop through - /// a list is pretty darn fast. Still, it might be worth trying - /// - void ReassignPoints(List points) { - for (int i = 0; i <= openSetTail; i++) { - var fp = openSet[i]; - - if (litFaces.Contains(fp.Face)) { - var assigned = false; - var point = points[fp.Point]; - - foreach (var kvp in faces) { - var fi = kvp.Key; - var face = kvp.Value; - - var dist = PointFaceDistance( - point, - points[face.Vertex0], - face); - - if (dist > EPSILON) { - assigned = true; - - fp.Face = fi; - fp.Distance = dist; - - openSet[i] = fp; - break; - } - } - - if (!assigned) { - // If point hasn't been assigned, then it's inside the - // convex hull. Swap it with openSetTail, and decrement - // openSetTail. We also have to decrement i, because - // there's now a new thing in openSet[i], so we need i - // to remain the same the next iteration of the loop. - fp.Face = INSIDE; - fp.Distance = float.NaN; - - openSet[i] = openSet[openSetTail]; - openSet[openSetTail] = fp; - - i--; - openSetTail--; - } - } - } - } - - /// - /// Final step in algorithm, export the faces of the convex hull in a - /// mesh-friendly format. - /// - /// TODO normals calculation for non-split vertices. Right now it just - /// leaves the normal array empty. - /// - void ExportMesh( - List points, - bool splitVerts, - ref List verts, - ref List tris, - ref List normals) - { - if (verts == null) { - verts = new List(); - } else { - verts.Clear(); - } - - if (tris == null) { - tris = new List(); - } else { - tris.Clear(); - } - - if (normals == null) { - normals = new List(); - } else { - normals.Clear(); - } - - foreach (var face in faces.Values) { - int vi0, vi1, vi2; - - if (splitVerts) { - vi0 = verts.Count; verts.Add(points[face.Vertex0]); - vi1 = verts.Count; verts.Add(points[face.Vertex1]); - vi2 = verts.Count; verts.Add(points[face.Vertex2]); - - normals.Add(face.Normal); - normals.Add(face.Normal); - normals.Add(face.Normal); - } else { - if (!hullVerts.TryGetValue(face.Vertex0, out vi0)) { - vi0 = verts.Count; - hullVerts[face.Vertex0] = vi0; - verts.Add(points[face.Vertex0]); - } - - if (!hullVerts.TryGetValue(face.Vertex1, out vi1)) { - vi1 = verts.Count; - hullVerts[face.Vertex1] = vi1; - verts.Add(points[face.Vertex1]); - } - - if (!hullVerts.TryGetValue(face.Vertex2, out vi2)) { - vi2 = verts.Count; - hullVerts[face.Vertex2] = vi2; - verts.Add(points[face.Vertex2]); - } - } - - tris.Add(vi0); - tris.Add(vi1); - tris.Add(vi2); - } - } - - /// - /// Signed distance from face to point (a positive number means that - /// the point is above the face) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - float PointFaceDistance(Vector3 point, Vector3 pointOnFace, Face face) { - return Dot(face.Normal, point - pointOnFace); - } - - /// - /// Calculate normal for triangle - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - Vector3 Normal(Vector3 v0, Vector3 v1, Vector3 v2) { - return Cross(v1 - v0, v2 - v0).Normalized; - } - - /// - /// Dot product, for convenience. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static float Dot(Vector3 a, Vector3 b) { - return a.X*b.X + a.Y*b.Y + a.Z*b.Z; - } - - /// - /// Vector3.Cross i left-handed, the algorithm is right-handed. Also, - /// i wanna test to see if using aggressive inlining makes any - /// difference here. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static Vector3 Cross(Vector3 a, Vector3 b) { - return new Vector3( - a.Y*b.Z - a.Z*b.Y, - a.Z*b.X - a.X*b.Z, - a.X*b.Y - a.Y*b.X); - } - - /// - /// Check if two points are coincident - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - bool AreCoincident(Vector3 a, Vector3 b) { - return (a - b).Length <= EPSILON; - } - - /// - /// Check if three points are collinear - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - bool AreCollinear(Vector3 a, Vector3 b, Vector3 c) { - return Cross(c - a, c - b).Length <= EPSILON; - } - - /// - /// Check if four points are coplanar - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - bool AreCoplanar(Vector3 a, Vector3 b, Vector3 c, Vector3 d) { - var n1 = Cross(c - a, c - b); - var n2 = Cross(d - a, d - b); - - var m1 = n1.Length; - var m2 = n2.Length; - - return m1 <= EPSILON - || m2 <= EPSILON - || AreCollinear(Vector3.Zero, - (1.0f / m1) * n1, - (1.0f / m2) * n2); - } - - /// - /// Method used for debugging, verifies that the openSet is in a - /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if - /// defined. - /// - void VerifyOpenSet(List points) { - for (int i = 0; i < openSet.Count; i++) { - if (i > openSetTail) { - Assert.IsTrue(openSet[i].Face == INSIDE); - } else { - Assert.IsTrue(openSet[i].Face != INSIDE); - Assert.IsTrue(openSet[i].Face != UNASSIGNED); - - Assert.IsTrue(PointFaceDistance( - points[openSet[i].Point], - points[faces[openSet[i].Face].Vertex0], - faces[openSet[i].Face]) > 0.0f); - } - } - } - - /// - /// Method used for debugging, verifies that the horizon is in a - /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if - /// defined. - /// - void VerifyHorizon() { - for (int i = 0; i < horizon.Count; i++) { - var prev = i == 0 ? horizon.Count - 1 : i - 1; - - Assert.IsTrue(horizon[prev].Edge1 == horizon[i].Edge0); - Assert.IsTrue(HasEdge(faces[horizon[i].Face], horizon[i].Edge1, horizon[i].Edge0)); - } - } - - /// - /// Method used for debugging, verifies that the faces array is in a - /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if - /// defined. - /// - void VerifyFaces(List points) { - foreach (var kvp in faces) { - var fi = kvp.Key; - var face = kvp.Value; - - Assert.IsTrue(faces.ContainsKey(face.Opposite0)); - Assert.IsTrue(faces.ContainsKey(face.Opposite1)); - Assert.IsTrue(faces.ContainsKey(face.Opposite2)); - - Assert.IsTrue(face.Opposite0 != fi); - Assert.IsTrue(face.Opposite1 != fi); - Assert.IsTrue(face.Opposite2 != fi); - - Assert.IsTrue(face.Vertex0 != face.Vertex1); - Assert.IsTrue(face.Vertex0 != face.Vertex2); - Assert.IsTrue(face.Vertex1 != face.Vertex2); - - Assert.IsTrue(HasEdge(faces[face.Opposite0], face.Vertex2, face.Vertex1)); - Assert.IsTrue(HasEdge(faces[face.Opposite1], face.Vertex0, face.Vertex2)); - Assert.IsTrue(HasEdge(faces[face.Opposite2], face.Vertex1, face.Vertex0)); - - Assert.IsTrue((face.Normal - Normal( - points[face.Vertex0], - points[face.Vertex1], - points[face.Vertex2])).Length < EPSILON); - } - } - - /// - /// Method used for debugging, verifies that the final mesh is - /// actually a convex hull of all the points. Conditionally compiled - /// if DEBUG_QUICKHULL if defined. - /// - void VerifyMesh(List points, ref List verts, ref List tris) { - Assert.IsTrue(tris.Count % 3 == 0); - - for (int i = 0; i < points.Count; i++) { - for (int j = 0; j < tris.Count; j+=3) { - var t0 = verts[tris[j]]; - var t1 = verts[tris[j + 1]]; - var t2 = verts[tris[j + 2]]; - - var dot = Dot(points[i] - t0, Vector3.Cross(t1 - t0, t2 - t0)); - //Assert.IsTrue(dot <= EPSILON, $"not convex hull: {dot} > {EPSILON}"); - if (!(dot <= EPSILON)) - Cabrito.Console.PrintError($"not convex hull: {dot} > {EPSILON}"); - } - - } - } - - /// - /// Does face f have a face with vertexes e0 and e1? Used only for - /// debugging. - /// - bool HasEdge(Face f, int e0, int e1) { - return (f.Vertex0 == e0 && f.Vertex1 == e1) - || (f.Vertex1 == e0 && f.Vertex2 == e1) - || (f.Vertex2 == e0 && f.Vertex0 == e1); - } - } - + public class ConvexHullCalculator + { + /// + /// Constant representing a point that has yet to be assigned to a + /// face. It's only used immediately after constructing the seed hull. + /// + private const int UNASSIGNED = -2; + + /// + /// Constant representing a point that is inside the convex hull, and + /// thus is behind all faces. In the openSet array, all points with + /// INSIDE are at the end of the array, with indexes larger + /// openSetTail. + /// + private const int INSIDE = -1; + + /// + /// Epsilon value. If the coordinates of the point space are + /// exceptionally close to each other, this value might need to be + /// adjusted. + /// + private const float EPSILON = 0.001f; + + /// + /// When adding a new face to the faces Dictionary, use this for the + /// key and then increment it. + /// + private int faceCount; + + /// + /// A dictionary storing the faces of the currently generated convex + /// hull. The key is the id of the face, used in the Face, PointFace + /// and HorizonEdge struct. + /// This is a Dictionary, because we need both random access to it, + /// the ability to loop through it, and ability to quickly delete + /// faces (in the ConstructCone method), and Dictionary is the obvious + /// candidate that can do all of those things. + /// I'm wondering if using a Dictionary is best idea, though. It might + /// be better to just have them in a ]]> and mark a face as + /// deleted by adding a field to the Face struct. The downside is that + /// we would need an extra field in the Face struct, and when we're + /// looping through the points in openSet, we would have to loop + /// through all the Faces EVER created in the algorithm, and skip the + /// ones that have been marked as deleted. However, looping through a + /// list is fairly fast, and it might be worth it to avoid Dictionary + /// overhead. + /// TODO test converting to a ]]> instead. + /// + private Dictionary faces; + + /// + /// The current horizon. Generated by the FindHorizon() DFS search, + /// and used in ConstructCone to construct new faces. The list of + /// edges are in CCW order. + /// + private List horizon; + + /// + /// If SplitVerts is false, this Dictionary is used to keep track of + /// which points we've added to the final mesh. + /// + private Dictionary hullVerts; + + /// + /// Set of faces which are "lit" by the current point in the set. This + /// is used in the FindHorizon() DFS search to keep track of which + /// faces we've already visited, and in the ReassignPoints() method to + /// know which points need to be reassigned. + /// + private HashSet litFaces; + + /// + /// The set of points to be processed. "openSet" is a misleading name, + /// because it's both the open set (points which are still outside the + /// convex hull) and the closed set (points that are inside the convex + /// hull). The first part of the array (with ) + /// is the openSet, the last part of the array (with + /// openSetTail]]>) are the closed set, with + /// Face set to INSIDE. The + /// closed set is largely irrelevant to the algorithm, the open set is + /// what matters. + /// Storing the entire open set in one big list has a downside: when + /// we're reassigning points after ConstructCone, we only need to + /// reassign points that belong to the faces that have been removed, + /// but storing it in one array, we have to loop through the entire + /// list, and checking litFaces to determine which we can skip and + /// which need to be reassigned. + /// The alternative here is to give each face in Face array it's own + /// openSet. I don't like that solution, because then you have to + /// juggle so many more heap-allocated 's]]>, we'd have to use + /// object pools and such. It would do a lot more allocation, and it + /// would have worse locality. I should maybe test that solution, but + /// it probably wont be faster enough (if at all) to justify the extra + /// allocations. + /// + private List openSet; + + /// + /// The "tail" of the openSet, the last index of a vertex that has + /// been assigned to a face. + /// + private int openSetTail = -1; + + /// + /// Generate a convex hull from points in points array, and store the + /// mesh in Unity-friendly format in verts and tris. If splitVerts is + /// true, the the verts will be split, if false, the same vert will be + /// used for more than one triangle. + /// + public void GenerateHull( + List points, + bool splitVerts, + ref List verts, + ref List tris, + ref List normals) + { + if (points.Count < 4) + throw new ArgumentException("Need at least 4 points to generate a convex hull"); + + Initialize(points, splitVerts); + + GenerateInitialHull(points); + + while (openSetTail >= 0) + GrowHull(points); + + ExportMesh(points, splitVerts, ref verts, ref tris, ref normals); + //VerifyMesh(points, ref verts, ref tris); + } + + /// + /// Make sure all the buffers and variables needed for the algorithm + /// are initialized. + /// + private void Initialize(List points, bool splitVerts) + { + faceCount = 0; + openSetTail = -1; + + if (faces == null) + { + faces = new Dictionary(); + litFaces = new HashSet(); + horizon = new List(); + openSet = new List(points.Count); + } + else + { + faces.Clear(); + litFaces.Clear(); + horizon.Clear(); + openSet.Clear(); + + if (openSet.Capacity < points.Count) // i wonder if this is a good idea... if you call + // GenerateHull over and over with slightly increasing + // points counts, it's going to reallocate every time. Maybe + // i should just use .Add(), and let the List manage the + // capacity, increasing it geometrically every time we need + // to reallocate. + + // maybe do + // openSet.Capacity = Mathf.NextPowerOfTwo(points.Count) + // instead? + + openSet.Capacity = points.Count; + } + + if (!splitVerts) + { + if (hullVerts == null) + hullVerts = new Dictionary(); + else + hullVerts.Clear(); + } + } + + /// + /// Create initial seed hull. + /// + private void GenerateInitialHull(List points) + { + // Find points suitable for use as the seed hull. Some varieties of + // this algorithm pick extreme points here, but I'm not convinced + // you gain all that much from that. Currently what it does is just + // find the first four points that are not coplanar. + int b0, b1, b2, b3; + FindInitialHullIndices(points, out b0, out b1, out b2, out b3); + + Vector3 v0 = points[b0]; + Vector3 v1 = points[b1]; + Vector3 v2 = points[b2]; + Vector3 v3 = points[b3]; + + bool above = Dot(v3 - v1, Cross(v1 - v0, v2 - v0)) > 0.0f; + + // Create the faces of the seed hull. You need to draw a diagram + // here, otherwise it's impossible to know what's going on :) + + // Basically: there are two different possible start-tetrahedrons, + // depending on whether the fourth point is above or below the base + // triangle. If you draw a tetrahedron with these coordinates (in a + // right-handed coordinate-system): + + // b0 = (0,0,0) + // b1 = (1,0,0) + // b2 = (0,1,0) + // b3 = (0,0,1) + + // you can see the first case (set b3 = (0,0,-1) for the second + // case). The faces are added with the proper references to the + // faces opposite each vertex + + faceCount = 0; + if (above) + { + faces[faceCount++] = new Face(b0, b2, b1, 3, 1, 2, Normal(points[b0], points[b2], points[b1])); + faces[faceCount++] = new Face(b0, b1, b3, 3, 2, 0, Normal(points[b0], points[b1], points[b3])); + faces[faceCount++] = new Face(b0, b3, b2, 3, 0, 1, Normal(points[b0], points[b3], points[b2])); + faces[faceCount++] = new Face(b1, b2, b3, 2, 1, 0, Normal(points[b1], points[b2], points[b3])); + } + else + { + faces[faceCount++] = new Face(b0, b1, b2, 3, 2, 1, Normal(points[b0], points[b1], points[b2])); + faces[faceCount++] = new Face(b0, b3, b1, 3, 0, 2, Normal(points[b0], points[b3], points[b1])); + faces[faceCount++] = new Face(b0, b2, b3, 3, 1, 0, Normal(points[b0], points[b2], points[b3])); + faces[faceCount++] = new Face(b1, b3, b2, 2, 0, 1, Normal(points[b1], points[b3], points[b2])); + } + + VerifyFaces(points); + + // Create the openSet. Add all points except the points of the seed + // hull. + for (int i = 0; i < points.Count; i++) + { + if (i == b0 || i == b1 || i == b2 || i == b3) + continue; + + openSet.Add(new PointFace(i, UNASSIGNED, 0.0f)); + } + + // Add the seed hull verts to the tail of the list. + openSet.Add(new PointFace(b0, INSIDE, float.NaN)); + openSet.Add(new PointFace(b1, INSIDE, float.NaN)); + openSet.Add(new PointFace(b2, INSIDE, float.NaN)); + openSet.Add(new PointFace(b3, INSIDE, float.NaN)); + + // Set the openSetTail value. Last item in the array is + // openSet.Count - 1, but four of the points (the verts of the seed + // hull) are part of the closed set, so move openSetTail to just + // before those. + openSetTail = openSet.Count - 5; + + Assert.IsTrue(openSet.Count == points.Count); + + // Assign all points of the open set. This does basically the same + // thing as ReassignPoints() + for (int i = 0; i <= openSetTail; i++) + { + Assert.IsTrue(openSet[i].Face == UNASSIGNED); + Assert.IsTrue(openSet[openSetTail].Face == UNASSIGNED); + Assert.IsTrue(openSet[openSetTail + 1].Face == INSIDE); + + bool assigned = false; + PointFace fp = openSet[i]; + + Assert.IsTrue(faces.Count == 4); + Assert.IsTrue(faces.Count == faceCount); + for (int j = 0; j < 4; j++) + { + Assert.IsTrue(faces.ContainsKey(j)); + + Face face = faces[j]; + + float dist = PointFaceDistance(points[fp.Point], points[face.Vertex0], face); + + if (dist > 0) + { + fp.Face = j; + fp.Distance = dist; + openSet[i] = fp; + + assigned = true; + break; + } + } + + if (!assigned) + { + // Point is inside + fp.Face = INSIDE; + fp.Distance = float.NaN; + + // Point is inside seed hull: swap point with tail, and move + // openSetTail back. We also have to decrement i, because + // there's a new item at openSet[i], and we need to process + // it next iteration + openSet[i] = openSet[openSetTail]; + openSet[openSetTail] = fp; + + openSetTail -= 1; + i -= 1; + } + } + + VerifyOpenSet(points); + } + + /// + /// Find four points in the point cloud that are not coplanar for the + /// seed hull + /// + private void FindInitialHullIndices(List points, out int b0, out int b1, out int b2, out int b3) + { + int count = points.Count; + + for (int i0 = 0; i0 < count - 3; i0++) + for (int i1 = i0 + 1; i1 < count - 2; i1++) + { + Vector3 p0 = points[i0]; + Vector3 p1 = points[i1]; + + if (AreCoincident(p0, p1)) + continue; + + for (int i2 = i1 + 1; i2 < count - 1; i2++) + { + Vector3 p2 = points[i2]; + + if (AreCollinear(p0, p1, p2)) + continue; + + for (int i3 = i2 + 1; i3 < count - 0; i3++) + { + Vector3 p3 = points[i3]; + + if (AreCoplanar(p0, p1, p2, p3)) + continue; + + b0 = i0; + b1 = i1; + b2 = i2; + b3 = i3; + return; + } + } + } + + throw new ArgumentException("Can't generate hull, points are coplanar"); + } + + /// + /// Grow the hull. This method takes the current hull, and expands it + /// to encompass the point in openSet with the point furthest away + /// from its face. + /// + private void GrowHull(List points) + { + Assert.IsTrue(openSetTail >= 0); + Assert.IsTrue(openSet[0].Face != INSIDE); + + // Find farthest point and first lit face. + int farthestPoint = 0; + float dist = openSet[0].Distance; + + for (int i = 1; i <= openSetTail; i++) + if (openSet[i].Distance > dist) + { + farthestPoint = i; + dist = openSet[i].Distance; + } + + // Use lit face to find horizon and the rest of the lit + // faces. + FindHorizon( + points, + points[openSet[farthestPoint].Point], + openSet[farthestPoint].Face, + faces[openSet[farthestPoint].Face]); + + VerifyHorizon(); + + // Construct new cone from horizon + ConstructCone(points, openSet[farthestPoint].Point); + + VerifyFaces(points); + + // Reassign points + ReassignPoints(points); + } + + /// + /// Start the search for the horizon. + /// The search is a DFS search that searches neighboring triangles in + /// a counter-clockwise fashion. When it find a neighbor which is not + /// lit, that edge will be a line on the horizon. If the search always + /// proceeds counter-clockwise, the edges of the horizon will be found + /// in counter-clockwise order. + /// The heart of the search can be found in the recursive + /// SearchHorizon() method, but the the first iteration of the search + /// is special, because it has to visit three neighbors (all the + /// neighbors of the initial triangle), while the rest of the search + /// only has to visit two (because one of them has already been + /// visited, the one you came from). + /// + private void FindHorizon(List points, Vector3 point, int fi, Face face) + { + // TODO should I use epsilon in the PointFaceDistance comparisons? + + litFaces.Clear(); + horizon.Clear(); + + litFaces.Add(fi); + + Assert.IsTrue(PointFaceDistance(point, points[face.Vertex0], face) > 0.0f); + + // For the rest of the recursive search calls, we first check if the + // triangle has already been visited and is part of litFaces. + // However, in this first call we can skip that because we know it + // can't possibly have been visited yet, since the only thing in + // litFaces is the current triangle. + { + Face oppositeFace = faces[face.Opposite0]; + + float dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) + horizon.Add(new HorizonEdge + { + Face = face.Opposite0, + Edge0 = face.Vertex1, + Edge1 = face.Vertex2 + }); + else + SearchHorizon(points, point, fi, face.Opposite0, oppositeFace); + } + + if (!litFaces.Contains(face.Opposite1)) + { + Face oppositeFace = faces[face.Opposite1]; + + float dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) + horizon.Add(new HorizonEdge + { + Face = face.Opposite1, + Edge0 = face.Vertex2, + Edge1 = face.Vertex0 + }); + else + SearchHorizon(points, point, fi, face.Opposite1, oppositeFace); + } + + if (!litFaces.Contains(face.Opposite2)) + { + Face oppositeFace = faces[face.Opposite2]; + + float dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) + horizon.Add(new HorizonEdge + { + Face = face.Opposite2, + Edge0 = face.Vertex0, + Edge1 = face.Vertex1 + }); + else + SearchHorizon(points, point, fi, face.Opposite2, oppositeFace); + } + } + + /// + /// Recursively search to find the horizon or lit set. + /// + private void SearchHorizon(List points, Vector3 point, int prevFaceIndex, int faceCount, Face face) + { + Assert.IsTrue(prevFaceIndex >= 0); + Assert.IsTrue(litFaces.Contains(prevFaceIndex)); + Assert.IsTrue(!litFaces.Contains(faceCount)); + Assert.IsTrue(faces[faceCount].Equals(face)); + + litFaces.Add(faceCount); + + // Use prevFaceIndex to determine what the next face to search will + // be, and what edges we need to cross to get there. It's important + // that the search proceeds in counter-clockwise order from the + // previous face. + int nextFaceIndex0; + int nextFaceIndex1; + int edge0; + int edge1; + int edge2; + + if (prevFaceIndex == face.Opposite0) + { + nextFaceIndex0 = face.Opposite1; + nextFaceIndex1 = face.Opposite2; + + edge0 = face.Vertex2; + edge1 = face.Vertex0; + edge2 = face.Vertex1; + } + else if (prevFaceIndex == face.Opposite1) + { + nextFaceIndex0 = face.Opposite2; + nextFaceIndex1 = face.Opposite0; + + edge0 = face.Vertex0; + edge1 = face.Vertex1; + edge2 = face.Vertex2; + } + else + { + Assert.IsTrue(prevFaceIndex == face.Opposite2); + + nextFaceIndex0 = face.Opposite0; + nextFaceIndex1 = face.Opposite1; + + edge0 = face.Vertex1; + edge1 = face.Vertex2; + edge2 = face.Vertex0; + } + + if (!litFaces.Contains(nextFaceIndex0)) + { + Face oppositeFace = faces[nextFaceIndex0]; + + float dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) + horizon.Add(new HorizonEdge + { + Face = nextFaceIndex0, + Edge0 = edge0, + Edge1 = edge1 + }); + else + SearchHorizon(points, point, faceCount, nextFaceIndex0, oppositeFace); + } + + if (!litFaces.Contains(nextFaceIndex1)) + { + Face oppositeFace = faces[nextFaceIndex1]; + + float dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) + horizon.Add(new HorizonEdge + { + Face = nextFaceIndex1, + Edge0 = edge1, + Edge1 = edge2 + }); + else + SearchHorizon(points, point, faceCount, nextFaceIndex1, oppositeFace); + } + } + + /// + /// Remove all lit faces and construct new faces from the horizon in a + /// "cone-like" fashion. + /// This is a relatively straight-forward procedure, given that the + /// horizon is handed to it in already sorted counter-clockwise. The + /// neighbors of the new faces are easy to find: they're the previous + /// and next faces to be constructed in the cone, as well as the face + /// on the other side of the horizon. We also have to update the face + /// on the other side of the horizon to reflect it's new neighbor from + /// the cone. + /// + private void ConstructCone(List points, int farthestPoint) + { + foreach (int fi in litFaces) + { + Assert.IsTrue(faces.ContainsKey(fi)); + faces.Remove(fi); + } + + int firstNewFace = faceCount; + + // Check for coplanar faces + /*var firstNormal = new Vector3(0, 0, 0); + int sameNormals = 1; + for (int i = 0; i < horizon.Count; i++) + { + var v0 = farthestPoint; + var v1 = horizon[i].Edge0; + var v2 = horizon[i].Edge1; + var norm = Normal(points[v0], points[v1], points[v2]); + if (firstNormal.IsZero) + { + firstNormal = norm; + continue; + } + + const float tol = 0.1f; + if ((firstNormal - norm).Length < tol || (firstNormal - norm).Length > 2.0f-tol) + { + sameNormals++; + } + } + + if (sameNormals == horizon.Count) + sameNormals = sameNormals;*/ + + for (int i = 0; i < horizon.Count; i++) + { + // Vertices of the new face, the farthest point as well as the + // edge on the horizon. Horizon edge is CCW, so the triangle + // should be as well. + int v0 = farthestPoint; + int v1 = horizon[i].Edge0; + int v2 = horizon[i].Edge1; + + // Opposite faces of the triangle. First, the edge on the other + // side of the horizon, then the next/prev faces on the new cone + int o0 = horizon[i].Face; + int o1 = i == horizon.Count - 1 ? firstNewFace : firstNewFace + i + 1; + int o2 = i == 0 ? firstNewFace + horizon.Count - 1 : firstNewFace + i - 1; + + int fi = faceCount++; + + faces[fi] = new Face( + v0, v1, v2, + o0, o1, o2, + Normal(points[v0], points[v1], points[v2])); + + Face horizonFace = faces[horizon[i].Face]; + + if (horizonFace.Vertex0 == v1) + { + Assert.IsTrue(v2 == horizonFace.Vertex2); + horizonFace.Opposite1 = fi; + } + else if (horizonFace.Vertex1 == v1) + { + Assert.IsTrue(v2 == horizonFace.Vertex0); + horizonFace.Opposite2 = fi; + } + else + { + Assert.IsTrue(v1 == horizonFace.Vertex2); + Assert.IsTrue(v2 == horizonFace.Vertex1); + horizonFace.Opposite0 = fi; + } + + faces[horizon[i].Face] = horizonFace; + } + } + + /// + /// Reassign points based on the new faces added by ConstructCone(). + /// Only points that were previous assigned to a removed face need to + /// be updated, so check litFaces while looping through the open set. + /// There is a potential optimization here: there's no reason to loop + /// through the entire openSet here. If each face had it's own + /// openSet, we could just loop through the openSets in the removed + /// faces. That would make the loop here shorter. + /// However, to do that, we would have to juggle A LOT more 's]]>, + /// and we would need an object pool to manage them all without + /// generating a whole bunch of garbage. I don't think it's worth + /// doing that to make this loop shorter, a straight for-loop through + /// a list is pretty darn fast. Still, it might be worth trying + /// + private void ReassignPoints(List points) + { + for (int i = 0; i <= openSetTail; i++) + { + PointFace fp = openSet[i]; + + if (litFaces.Contains(fp.Face)) + { + bool assigned = false; + Vector3 point = points[fp.Point]; + + foreach (var kvp in faces) + { + int fi = kvp.Key; + Face face = kvp.Value; + + float dist = PointFaceDistance( + point, + points[face.Vertex0], + face); + + if (dist > EPSILON) + { + assigned = true; + + fp.Face = fi; + fp.Distance = dist; + + openSet[i] = fp; + break; + } + } + + if (!assigned) + { + // If point hasn't been assigned, then it's inside the + // convex hull. Swap it with openSetTail, and decrement + // openSetTail. We also have to decrement i, because + // there's now a new thing in openSet[i], so we need i + // to remain the same the next iteration of the loop. + fp.Face = INSIDE; + fp.Distance = float.NaN; + + openSet[i] = openSet[openSetTail]; + openSet[openSetTail] = fp; + + i--; + openSetTail--; + } + } + } + } + + /// + /// Final step in algorithm, export the faces of the convex hull in a + /// mesh-friendly format. + /// TODO normals calculation for non-split vertices. Right now it just + /// leaves the normal array empty. + /// + private void ExportMesh( + List points, + bool splitVerts, + ref List verts, + ref List tris, + ref List normals) + { + if (verts == null) + verts = new List(); + else + verts.Clear(); + + if (tris == null) + tris = new List(); + else + tris.Clear(); + + if (normals == null) + normals = new List(); + else + normals.Clear(); + + foreach (Face face in faces.Values) + { + int vi0, vi1, vi2; + + if (splitVerts) + { + vi0 = verts.Count; + verts.Add(points[face.Vertex0]); + vi1 = verts.Count; + verts.Add(points[face.Vertex1]); + vi2 = verts.Count; + verts.Add(points[face.Vertex2]); + + normals.Add(face.Normal); + normals.Add(face.Normal); + normals.Add(face.Normal); + } + else + { + if (!hullVerts.TryGetValue(face.Vertex0, out vi0)) + { + vi0 = verts.Count; + hullVerts[face.Vertex0] = vi0; + verts.Add(points[face.Vertex0]); + } + + if (!hullVerts.TryGetValue(face.Vertex1, out vi1)) + { + vi1 = verts.Count; + hullVerts[face.Vertex1] = vi1; + verts.Add(points[face.Vertex1]); + } + + if (!hullVerts.TryGetValue(face.Vertex2, out vi2)) + { + vi2 = verts.Count; + hullVerts[face.Vertex2] = vi2; + verts.Add(points[face.Vertex2]); + } + } + + tris.Add(vi0); + tris.Add(vi1); + tris.Add(vi2); + } + } + + /// + /// Signed distance from face to point (a positive number means that + /// the point is above the face) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float PointFaceDistance(Vector3 point, Vector3 pointOnFace, Face face) + { + return Dot(face.Normal, point - pointOnFace); + } + + /// + /// Calculate normal for triangle + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector3 Normal(Vector3 v0, Vector3 v1, Vector3 v2) + { + return Cross(v1 - v0, v2 - v0).Normalized; + } + + /// + /// Dot product, for convenience. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Dot(Vector3 a, Vector3 b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z; + } + + /// + /// Vector3.Cross i left-handed, the algorithm is right-handed. Also, + /// i wanna test to see if using aggressive inlining makes any + /// difference here. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 Cross(Vector3 a, Vector3 b) + { + return new Vector3( + a.Y * b.Z - a.Z * b.Y, + a.Z * b.X - a.X * b.Z, + a.X * b.Y - a.Y * b.X); + } + + /// + /// Check if two points are coincident + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool AreCoincident(Vector3 a, Vector3 b) + { + return (a - b).Length <= EPSILON; + } + + /// + /// Check if three points are collinear + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool AreCollinear(Vector3 a, Vector3 b, Vector3 c) + { + return Cross(c - a, c - b).Length <= EPSILON; + } + + /// + /// Check if four points are coplanar + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool AreCoplanar(Vector3 a, Vector3 b, Vector3 c, Vector3 d) + { + Vector3 n1 = Cross(c - a, c - b); + Vector3 n2 = Cross(d - a, d - b); + + float m1 = n1.Length; + float m2 = n2.Length; + + return m1 <= EPSILON + || m2 <= EPSILON + || AreCollinear(Vector3.Zero, + 1.0f / m1 * n1, + 1.0f / m2 * n2); + } + + /// + /// Method used for debugging, verifies that the openSet is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + private void VerifyOpenSet(List points) + { + for (int i = 0; i < openSet.Count; i++) + if (i > openSetTail) + { + Assert.IsTrue(openSet[i].Face == INSIDE); + } + else + { + Assert.IsTrue(openSet[i].Face != INSIDE); + Assert.IsTrue(openSet[i].Face != UNASSIGNED); + + Assert.IsTrue(PointFaceDistance( + points[openSet[i].Point], + points[faces[openSet[i].Face].Vertex0], + faces[openSet[i].Face]) > 0.0f); + } + } + + /// + /// Method used for debugging, verifies that the horizon is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + private void VerifyHorizon() + { + for (int i = 0; i < horizon.Count; i++) + { + int prev = i == 0 ? horizon.Count - 1 : i - 1; + + Assert.IsTrue(horizon[prev].Edge1 == horizon[i].Edge0); + Assert.IsTrue(HasEdge(faces[horizon[i].Face], horizon[i].Edge1, horizon[i].Edge0)); + } + } + + /// + /// Method used for debugging, verifies that the faces array is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + private void VerifyFaces(List points) + { + foreach (var kvp in faces) + { + int fi = kvp.Key; + Face face = kvp.Value; + + Assert.IsTrue(faces.ContainsKey(face.Opposite0)); + Assert.IsTrue(faces.ContainsKey(face.Opposite1)); + Assert.IsTrue(faces.ContainsKey(face.Opposite2)); + + Assert.IsTrue(face.Opposite0 != fi); + Assert.IsTrue(face.Opposite1 != fi); + Assert.IsTrue(face.Opposite2 != fi); + + Assert.IsTrue(face.Vertex0 != face.Vertex1); + Assert.IsTrue(face.Vertex0 != face.Vertex2); + Assert.IsTrue(face.Vertex1 != face.Vertex2); + + Assert.IsTrue(HasEdge(faces[face.Opposite0], face.Vertex2, face.Vertex1)); + Assert.IsTrue(HasEdge(faces[face.Opposite1], face.Vertex0, face.Vertex2)); + Assert.IsTrue(HasEdge(faces[face.Opposite2], face.Vertex1, face.Vertex0)); + + Assert.IsTrue((face.Normal - Normal( + points[face.Vertex0], + points[face.Vertex1], + points[face.Vertex2])).Length < EPSILON); + } + } + + /// + /// Method used for debugging, verifies that the final mesh is + /// actually a convex hull of all the points. Conditionally compiled + /// if DEBUG_QUICKHULL if defined. + /// + private void VerifyMesh(List points, ref List verts, ref List tris) + { + Assert.IsTrue(tris.Count % 3 == 0); + + for (int i = 0; i < points.Count; i++) + for (int j = 0; j < tris.Count; j += 3) + { + Vector3 t0 = verts[tris[j]]; + Vector3 t1 = verts[tris[j + 1]]; + Vector3 t2 = verts[tris[j + 2]]; + + float dot = Dot(points[i] - t0, Vector3.Cross(t1 - t0, t2 - t0)); + //Assert.IsTrue(dot <= EPSILON, $"not convex hull: {dot} > {EPSILON}"); + if (!(dot <= EPSILON)) + Console.PrintError($"not convex hull: {dot} > {EPSILON}"); + } + } + + /// + /// Does face f have a face with vertexes e0 and e1? Used only for + /// debugging. + /// + private bool HasEdge(Face f, int e0, int e1) + { + return (f.Vertex0 == e0 && f.Vertex1 == e1) + || (f.Vertex1 == e0 && f.Vertex2 == e1) + || (f.Vertex2 == e0 && f.Vertex0 == e1); + } + + /// + /// Struct representing a single face. + /// Vertex0, Vertex1 and Vertex2 are the vertices in CCW order. They + /// acutal points are stored in the points array, these are just + /// indexes into that array. + /// Opposite0, Opposite1 and Opposite2 are the keys to the faces which + /// share an edge with this face. Opposite0 is the face opposite + /// Vertex0 (so it has an edge with Vertex2 and Vertex1), etc. + /// Normal is (unsurprisingly) the normal of the triangle. + /// + private struct Face + { + public readonly int Vertex0; + public readonly int Vertex1; + public readonly int Vertex2; + + public int Opposite0; + public int Opposite1; + public int Opposite2; + + public readonly Vector3 Normal; + + public Face(int v0, int v1, int v2, int o0, int o1, int o2, Vector3 normal) + { + Vertex0 = v0; + Vertex1 = v1; + Vertex2 = v2; + Opposite0 = o0; + Opposite1 = o1; + Opposite2 = o2; + Normal = normal; + } + + public bool Equals(Face other) + { + return Vertex0 == other.Vertex0 + && Vertex1 == other.Vertex1 + && Vertex2 == other.Vertex2 + && Opposite0 == other.Opposite0 + && Opposite1 == other.Opposite1 + && Opposite2 == other.Opposite2 + && Normal == other.Normal; + } + } + + /// + /// Struct representing a mapping between a point and a face. These + /// are used in the openSet array. + /// Point is the index of the point in the points array, Face is the + /// key of the face in the Key dictionary, Distance is the distance + /// from the face to the point. + /// + private struct PointFace + { + public readonly int Point; + public int Face; + public float Distance; + + public PointFace(int p, int f, float d) + { + Point = p; + Face = f; + Distance = d; + } + } + + /// + /// Struct representing a single edge in the horizon. + /// Edge0 and Edge1 are the vertexes of edge in CCW order, Face is the + /// face on the other side of the horizon. + /// TODO Edge1 isn't actually needed, you can just index the next item + /// in the horizon array. + /// + private struct HorizonEdge + { + public int Face; + public int Edge0; + public int Edge1; + } + } } \ No newline at end of file diff --git a/Source/Game/Network/NetworkManager.cs b/Source/Game/Network/NetworkManager.cs index 0dce22e..8beb52f 100644 --- a/Source/Game/Network/NetworkManager.cs +++ b/Source/Game/Network/NetworkManager.cs @@ -1,22 +1,15 @@ using System; -using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading; using FlaxEditor; using FlaxEngine; using FlaxEngine.Networking; using Console = Cabrito.Console; -using Debug = FlaxEngine.Debug; namespace Game { [AttributeUsage(AttributeTargets.Class)] public class NetworkPredictedAttribute : Attribute { - public NetworkPredictedAttribute() - { - } } // TODO: insert code to update variables with this attribute? @@ -24,9 +17,6 @@ namespace Game [AttributeUsage(AttributeTargets.Class)] public class NetworkedAttribute : Attribute { - public NetworkedAttribute() - { - } } // NetworkMulticastAttribute: calls methods marked with this in all clients @@ -34,24 +24,23 @@ namespace Game public enum NetworkMessageType : byte { Handshake = 1, - Message, + Message } - public static partial class NetworkManager { - private static bool initialized = false; + public delegate bool OnMessageDecl(NetworkMessage message); + + private static bool initialized; private static NetworkPeer server; private static NetworkPeer client; - private static ushort ServerPort = 59183; - private static string ServerAddress = null; - private static ushort MTU = 1500; - private static ushort MaximumClients = 32; - - public delegate bool OnMessageDecl(NetworkMessage message); + private static readonly ushort ServerPort = 59183; + private static string ServerAddress; + private static readonly ushort MTU = 1500; + private static readonly ushort MaximumClients = 32; public static OnMessageDecl OnMessage; public static void Init() @@ -79,14 +68,18 @@ namespace Game initialized = true; GameModeManager.Init(); // FIXME + +#if FLAX_EDITOR + Editor.Instance.PlayModeEnd += Cleanup; +#endif } - public static void Deinitialize() + public static void Cleanup() { if (server != null) { Scripting.FixedUpdate -= OnServerUpdate; - Scripting.Exit -= Deinitialize; + Scripting.Exit -= Cleanup; Level.ActorSpawned -= OnServerActorSpawned; NetworkPeer.ShutdownPeer(server); server = null; @@ -95,11 +88,16 @@ namespace Game if (client != null) { Scripting.FixedUpdate -= OnClientUpdate; - Scripting.Exit -= Deinitialize; + Scripting.Exit -= Cleanup; Level.ActorSpawned -= OnClientActorSpawned; NetworkPeer.ShutdownPeer(client); client = null; } + +#if FLAX_EDITOR + Editor.Instance.PlayModeEnd -= Cleanup; + GameModeManager.Cleanup(); +#endif } private static void OnNetworkMessage(NetworkEvent networkEvent) @@ -117,14 +115,13 @@ namespace Game { case NetworkMessageType.Handshake: { - var message = networkEvent.Message.ReadString(); + string message = networkEvent.Message.ReadString(); Console.Print($"Received handshake from {networkEvent.Sender.ConnectionId}, msg: " + message); break; } case NetworkMessageType.Message: { if (OnMessage != null) - { foreach (OnMessageDecl func in OnMessage.GetInvocationList() .Cast().ToArray()) { @@ -132,7 +129,7 @@ namespace Game if (ret) break; } - } + break; } default: diff --git a/Source/Game/Network/NetworkManagerPlugin.cs b/Source/Game/Network/NetworkManagerPlugin.cs index 31034b4..c30fbf4 100644 --- a/Source/Game/Network/NetworkManagerPlugin.cs +++ b/Source/Game/Network/NetworkManagerPlugin.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using FlaxEngine; using Console = Cabrito.Console; - #if FLAX_EDITOR using FlaxEditor; #endif @@ -11,51 +9,51 @@ namespace Game { public class NetworkManagerPlugin : GamePlugin { - public override void Initialize() - { - base.Initialize(); - Debug.Log("NetworkManagerPlugin Initialize"); - Console.Init(); - - NetworkManager.Init(); - - Level.SceneLoaded += (scene, guid) => Console.Print("scene loaded: " + scene.Name); - } - - public override void Deinitialize() - { - base.Deinitialize(); - Debug.Log("NetworkManagerPlugin Deinitialize"); - } - - public static PluginDescription DescriptionInternal = new PluginDescription() + public static PluginDescription DescriptionInternal = new PluginDescription { Author = "Ari Vuollet", Name = "NetworkManager", Description = "NetworkManager for Goake", Version = Version.Parse("0.1.0"), IsAlpha = true, - Category = "Game", + Category = "Game" }; + + public override void Initialize() + { + base.Initialize(); + Debug.Log("NetworkManagerPlugin Initialize"); + Console.Init(); + + NetworkManager.Init(); + } + + public override void Deinitialize() + { + base.Deinitialize(); + Debug.Log("NetworkManagerPlugin Deinitialize"); + } } #if FLAX_EDITOR - public class NetworkManagerEditorPlugin : EditorPlugin - { - public override void Initialize() - { - Debug.Log("NetworkManagerEditorPlugin Initialize"); - Console.Init(); - } + public class NetworkManagerEditorPlugin : EditorPlugin + { + public override PluginDescription Description => NetworkManagerPlugin.DescriptionInternal; - public override void Deinitialize() - { - Debug.Log("NetworkManagerEditorPlugin Deinitialize"); - } + public override Type GamePluginType => typeof(NetworkManagerPlugin); - public override PluginDescription Description => NetworkManagerPlugin.DescriptionInternal; + public override void Initialize() + { + Debug.Log("NetworkManagerEditorPlugin Initialize"); + Console.Init(); - public override Type GamePluginType => typeof(NetworkManagerPlugin); - } + NetworkManager.Init(); + } + + public override void Deinitialize() + { + Debug.Log("NetworkManagerEditorPlugin Deinitialize"); + } + } #endif } \ No newline at end of file diff --git a/Source/Game/Network/NetworkManager_Client.cs b/Source/Game/Network/NetworkManager_Client.cs index 0b1e0b6..d714018 100644 --- a/Source/Game/Network/NetworkManager_Client.cs +++ b/Source/Game/Network/NetworkManager_Client.cs @@ -1,29 +1,25 @@ using System; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading; using FlaxEngine; using FlaxEngine.Networking; using Console = Cabrito.Console; -using Debug = FlaxEngine.Debug; +using Object = FlaxEngine.Object; namespace Game { public static partial class NetworkManager { - public static uint LocalPlayerClientId { get; private set; } = 0; + public static uint LocalPlayerClientId { get; private set; } public static bool ConnectServer() { client = NetworkPeer.CreatePeer(new NetworkConfig { - NetworkDriver = FlaxEngine.Object.New(typeof(ENetDriver)), + NetworkDriver = Object.New(typeof(ENetDriver)), ConnectionsLimit = MaximumClients, MessagePoolSize = 2048, MessageSize = MTU, Address = ServerAddress == "localhost" ? "127.0.0.1" : ServerAddress, - Port = ServerPort, + Port = ServerPort }); if (!client.Connect()) { @@ -32,17 +28,16 @@ namespace Game } Scripting.FixedUpdate += OnClientUpdate; - Scripting.Exit += Deinitialize; + Scripting.Exit += Cleanup; Level.ActorSpawned += OnClientActorSpawned; return true; } private static void OnClientUpdate() { - using var _ = Utilities.ProfileScope("NetworkManager_OnClientUpdate"); + using Utilities.ScopeProfiler _ = Utilities.ProfileScope("NetworkManager_OnClientUpdate"); while (client.PopEvent(out NetworkEvent networkEvent)) - { switch (networkEvent.EventType) { case NetworkEventType.Connected: @@ -66,18 +61,17 @@ namespace Game case NetworkEventType.Message: { OnNetworkMessage(networkEvent); + client.RecycleMessage(networkEvent.Message); break; } default: throw new ArgumentOutOfRangeException(); } - } } private static void OnClientActorSpawned(Actor actor) { - } } } \ No newline at end of file diff --git a/Source/Game/Network/NetworkManager_Server.cs b/Source/Game/Network/NetworkManager_Server.cs index 61c6520..a3e348f 100644 --- a/Source/Game/Network/NetworkManager_Server.cs +++ b/Source/Game/Network/NetworkManager_Server.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading; using FlaxEngine; using FlaxEngine.Networking; using Console = Cabrito.Console; -using Debug = FlaxEngine.Debug; +using Object = FlaxEngine.Object; namespace Game { @@ -23,7 +20,7 @@ namespace Game ConnectedClients = new List(MaximumClients); server = NetworkPeer.CreatePeer(new NetworkConfig { - NetworkDriver = FlaxEngine.Object.New(typeof(ENetDriver)), + NetworkDriver = Object.New(typeof(ENetDriver)), ConnectionsLimit = MaximumClients, MessagePoolSize = 2048, MessageSize = MTU, @@ -37,18 +34,18 @@ namespace Game } Scripting.FixedUpdate += OnServerUpdate; - Scripting.Exit += Deinitialize; - Level.ActorSpawned += OnClientActorSpawned; + Scripting.Exit += Cleanup; + Level.ActorSpawned += OnServerActorSpawned; NetworkedTypes = new List(); AppDomain currentDomain = AppDomain.CurrentDomain; - Assembly[] assemblies = currentDomain.GetAssemblies(); + var assemblies = currentDomain.GetAssemblies(); - foreach (var assembly in assemblies) + foreach (Assembly assembly in assemblies) { // Skip common assemblies - var assemblyName = assembly.GetName().Name; + string assemblyName = assembly.GetName().Name; if (assemblyName == "System" || assemblyName.StartsWith("System.") || assemblyName.StartsWith("Mono.") || @@ -58,31 +55,24 @@ namespace Game assemblyName.StartsWith("JetBrains.") || assemblyName.StartsWith("Microsoft.") || assemblyName.StartsWith("nunit.")) - { continue; - } - foreach (var type in assembly.GetTypes()) - { + foreach (Type type in assembly.GetTypes()) if (type.GetCustomAttributes().Any(x => x is NetworkedAttribute)) NetworkedTypes.Add(type); - } } foreach (Type type in NetworkedTypes) - { Console.Print("tracking networked type: " + type.Name); - } return true; } private static void OnServerUpdate() { - using var _ = Utilities.ProfileScope("NetworkManager_OnServerUpdate"); + using Utilities.ScopeProfiler _ = Utilities.ProfileScope("NetworkManager_OnServerUpdate"); while (server.PopEvent(out NetworkEvent networkEvent)) - { switch (networkEvent.EventType) { case NetworkEventType.Connected: @@ -119,12 +109,11 @@ namespace Game default: throw new ArgumentOutOfRangeException(); } - } } private static void OnServerActorSpawned(Actor actor) { - Console.Print($"actor spawned: {actor.Name} ({actor.TypeName})"); + //Console.Print($"actor spawned: {actor.Name} ({actor.TypeName})"); } } } \ No newline at end of file diff --git a/Source/Game/Player/CustomCharacterController.cs b/Source/Game/Player/CustomCharacterController.cs index 73d4f1e..0706bdc 100644 --- a/Source/Game/Player/CustomCharacterController.cs +++ b/Source/Game/Player/CustomCharacterController.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using FlaxEngine; +using FlaxEngine; namespace Game { - public class CustomCharacterController : CharacterController - { - } + public class CustomCharacterController : CharacterController + { + } } \ No newline at end of file diff --git a/Source/Game/Player/InputManager.cs b/Source/Game/Player/InputManager.cs index a7e311b..62597cc 100644 --- a/Source/Game/Player/InputManager.cs +++ b/Source/Game/Player/InputManager.cs @@ -1,30 +1,29 @@ -using System.Collections.Generic; +using Cabrito; using FlaxEngine; -using Cabrito; namespace Game { - public static class InputManager - { - public static bool GetAction(string name) - { - if (Console.IsOpen) - return false; - return Input.GetAction(name); - } + public static class InputManager + { + public static bool GetAction(string name) + { + if (Console.IsOpen) + return false; + return Input.GetAction(name); + } - public static float GetAxis(string name) - { - if (Console.IsOpen) - return 0.0f; - return Input.GetAxis(name); - } + public static float GetAxis(string name) + { + if (Console.IsOpen) + return 0.0f; + return Input.GetAxis(name); + } - public static float GetAxisRaw(string name) - { - if (Console.IsOpen) - return 0.0f; - return Input.GetAxisRaw(name); - } - } + public static float GetAxisRaw(string name) + { + if (Console.IsOpen) + return 0.0f; + return Input.GetAxisRaw(name); + } + } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerActor.cs b/Source/Game/Player/PlayerActor.cs index 7267df9..38e986e 100644 --- a/Source/Game/Player/PlayerActor.cs +++ b/Source/Game/Player/PlayerActor.cs @@ -1,12 +1,9 @@ using System.Collections.Generic; -using System.Linq; using FlaxEngine; -using Cabrito; #if FLAX_EDITOR using FlaxEditor.CustomEditors.Dedicated; using FlaxEditor.Scripting; #endif -using FlaxEngine.GUI; namespace Game { @@ -16,16 +13,16 @@ namespace Game { protected override List GetItemsForType(ScriptType type) { - List items = GetItemsForType(type, type.IsClass, true); + var items = GetItemsForType(type, type.IsClass, true); // Remove all Rigid Body options items.RemoveAll(x => x.Display.Group == "Rigid Body"); // Inject scripts editor - var scriptsMember = type.GetProperty("Scripts"); + ScriptMemberInfo scriptsMember = type.GetProperty("Scripts"); if (scriptsMember != ScriptMemberInfo.Null) { - var item = new ItemInfo(scriptsMember) + ItemInfo item = new ItemInfo(scriptsMember) { CustomEditor = new CustomEditorAttribute(typeof(ScriptsEditor)) }; diff --git a/Source/Game/Player/PlayerInput.cs b/Source/Game/Player/PlayerInput.cs index 807a94b..f1305d9 100644 --- a/Source/Game/Player/PlayerInput.cs +++ b/Source/Game/Player/PlayerInput.cs @@ -1,75 +1,70 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using FlaxEngine; namespace Game { - [StructLayout(LayoutKind.Sequential)] - public struct PlayerInputState - { - public ulong frame; - public float viewDeltaX, viewDeltaY; - public float moveForward; - public float moveRight; - public bool attacking; - public bool jumping; + [StructLayout(LayoutKind.Sequential)] + public struct PlayerInputState + { + public ulong frame; + public float viewDeltaX, viewDeltaY; + public float moveForward; + public float moveRight; + public bool attacking; + public bool jumping; - public Vector3 verificationPosition; - public Vector3 verificationVelocity; - public Vector3 verificationViewAngles; - public Quaternion verificationOrientation; - } + public Vector3 verificationPosition; + public Vector3 verificationVelocity; + public Vector3 verificationViewAngles; + public Quaternion verificationOrientation; + } - [StructLayout(LayoutKind.Sequential)] - public struct PlayerActorState - { - public Vector3 position; - public Vector3 velocity; - public Quaternion orientation; - public Vector3 viewAngles; // yaw, pitch, roll - } + [StructLayout(LayoutKind.Sequential)] + public struct PlayerActorState + { + public Vector3 position; + public Vector3 velocity; + public Quaternion orientation; + public Vector3 viewAngles; // yaw, pitch, roll + } - [StructLayout(LayoutKind.Sequential)] - public struct PlayerState - { - public PlayerInputState input; - public PlayerActorState actor; - } + [StructLayout(LayoutKind.Sequential)] + public struct PlayerState + { + public PlayerInputState input; + public PlayerActorState actor; + } - public class PlayerInput - { - public PlayerState currentState; - public ulong frame; + public class PlayerInput + { + public const byte DemoVer = 1; + public PlayerState currentState; + public ulong frame; - public const byte DemoVer = 1; + public virtual void OnUpdate() + { + } - public virtual void OnUpdate() - { - } + public virtual void OnFixedUpdate() + { + } - public virtual void OnFixedUpdate() - { - } + public virtual void OnEndFrame() + { + } - public virtual void OnEndFrame() - { - } + public virtual void RecordCurrentActorState(PlayerActorState actorState) + { + } - public virtual void RecordCurrentActorState(PlayerActorState actorState) - { - } + public PlayerInputState GetCurrentInputState() + { + return currentState.input; + } - public PlayerInputState GetCurrentInputState() - { - return currentState.input; - } - - public PlayerActorState GetCurrentActorState() - { - return currentState.actor; - } - } + public PlayerActorState GetCurrentActorState() + { + return currentState.actor; + } + } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerInputDemo.cs b/Source/Game/Player/PlayerInputDemo.cs index 3e8be83..b617685 100644 --- a/Source/Game/Player/PlayerInputDemo.cs +++ b/Source/Game/Player/PlayerInputDemo.cs @@ -2,88 +2,89 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Runtime.InteropServices; -using FlaxEngine; using Console = Cabrito.Console; namespace Game { - public class PlayerInputDemo : PlayerInput - { - protected List buffer = new List(); - protected IEnumerator bufferEnumerable; + public class PlayerInputDemo : PlayerInput + { + protected List buffer = new List(); + protected IEnumerator bufferEnumerable; - public PlayerInputDemo(string demoPath) - { - if (!File.Exists(demoPath)) - return; + public PlayerInputDemo(string demoPath) + { + if (!File.Exists(demoPath)) + return; - var expectedPlayerInputStateSize = Marshal.SizeOf(typeof(PlayerInputState)); + int expectedPlayerInputStateSize = Marshal.SizeOf(typeof(PlayerInputState)); - var stream = File.OpenRead(demoPath); - var ver = (int)stream.ReadByte(); - var inputStateSize = (int)stream.ReadByte(); - if (ver != DemoVer && inputStateSize != expectedPlayerInputStateSize) - { - Console.Print("demover mismatch: version " + ver + " != " + DemoVer + ", inputStateSize " + inputStateSize + " != " + Marshal.SizeOf(typeof(PlayerInputState))); - stream.Close(); - return; - } + FileStream stream = File.OpenRead(demoPath); + int ver = stream.ReadByte(); + int inputStateSize = stream.ReadByte(); + if (ver != DemoVer && inputStateSize != expectedPlayerInputStateSize) + { + Console.Print("demover mismatch: version " + ver + " != " + DemoVer + ", inputStateSize " + + inputStateSize + " != " + Marshal.SizeOf(typeof(PlayerInputState))); + stream.Close(); + return; + } - T RawDeserialize(byte[] rawData, int position) - { - int rawsize = Marshal.SizeOf(typeof(T)); - if (rawsize > rawData.Length - position) - throw new ArgumentException("Not enough data to fill struct. Array length from position: " + (rawData.Length-position) + ", Struct length: " + rawsize); - IntPtr buffer = Marshal.AllocHGlobal(rawsize); - Marshal.Copy(rawData, position, buffer, rawsize); - T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T)); - Marshal.FreeHGlobal(buffer); - return retobj; - } + T RawDeserialize(byte[] rawData, int position) + { + int rawsize = Marshal.SizeOf(typeof(T)); + if (rawsize > rawData.Length - position) + throw new ArgumentException("Not enough data to fill struct. Array length from position: " + + (rawData.Length - position) + ", Struct length: " + rawsize); + IntPtr buffer = Marshal.AllocHGlobal(rawsize); + Marshal.Copy(rawData, position, buffer, rawsize); + T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T)); + Marshal.FreeHGlobal(buffer); + return retobj; + } - while (true) - { - byte[] b = new byte[expectedPlayerInputStateSize]; - var readBytes = stream.Read(b, 0, b.Length); - if (readBytes < expectedPlayerInputStateSize) - break; + while (true) + { + byte[] b = new byte[expectedPlayerInputStateSize]; + int readBytes = stream.Read(b, 0, b.Length); + if (readBytes < expectedPlayerInputStateSize) + break; - buffer.Add(RawDeserialize(b, 0)); - } + buffer.Add(RawDeserialize(b, 0)); + } - bufferEnumerable = buffer.GetEnumerator(); + bufferEnumerable = buffer.GetEnumerator(); - Console.Print("demo numstates: " + buffer.Count); + Console.Print("demo numstates: " + buffer.Count); - OnEndFrame(); // advances to first frame - } + OnEndFrame(); // advances to first frame + } - public override void OnEndFrame() - { - // TODO: check if the current state frame matches the current frame number before advancing + public override void OnEndFrame() + { + // TODO: check if the current state frame matches the current frame number before advancing - /*asdf++; - if (asdf < 8) - return;*/ + /*asdf++; + if (asdf < 8) + return;*/ - if (bufferEnumerable == null || !bufferEnumerable.MoveNext()) - { - if (buffer.Any()) - { - bufferEnumerable.Dispose(); - bufferEnumerable = null; - buffer.Clear(); - Console.Print("Demo ended"); - } - return; - } + if (bufferEnumerable == null || !bufferEnumerable.MoveNext()) + { + if (buffer.Any()) + { + bufferEnumerable.Dispose(); + bufferEnumerable = null; + buffer.Clear(); + Console.Print("Demo ended"); + } - //var actorState = currentState.actor; - currentState.input = bufferEnumerable.Current; - //frame++; - //currentState.actor = actorState; - } - } + return; + } + + //var actorState = currentState.actor; + currentState.input = bufferEnumerable.Current; + //frame++; + //currentState.actor = actorState; + } + } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerInputLocal.cs b/Source/Game/Player/PlayerInputLocal.cs index 0311fc4..aa685b6 100644 --- a/Source/Game/Player/PlayerInputLocal.cs +++ b/Source/Game/Player/PlayerInputLocal.cs @@ -1,117 +1,116 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; using System.Runtime.InteropServices; using FlaxEngine; using Console = Cabrito.Console; namespace Game { - public class PlayerInputLocal : PlayerInput - { - protected List buffer = new List(); - protected FileStream demoFileStream; + public class PlayerInputLocal : PlayerInput + { + protected List buffer = new List(); + protected FileStream demoFileStream; - public bool IsRecording { get { return demoFileStream != null; } } + public PlayerInputLocal() + { + } - public PlayerInputLocal() - { - } + public PlayerInputLocal(string demoPath) + { + demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write); + //stream.Position = 0; + //stream.SetLength(0); + demoFileStream.WriteByte(DemoVer); + demoFileStream.WriteByte((byte)Marshal.SizeOf(typeof(PlayerInputState))); + } - public PlayerInputLocal(string demoPath) - { - demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write); - //stream.Position = 0; - //stream.SetLength(0); - demoFileStream.WriteByte(DemoVer); - demoFileStream.WriteByte((byte)Marshal.SizeOf(typeof(PlayerInputState))); - } + public bool IsRecording => demoFileStream != null; - public override void OnUpdate() - { - // Collect all input here - // All axis values here should be accumulated - currentState.input.viewDeltaX += InputManager.GetAxisRaw("Mouse X"); - currentState.input.viewDeltaY += InputManager.GetAxisRaw("Mouse Y"); - currentState.input.viewDeltaX += InputManager.GetAxisRaw("LookRight") * Time.DeltaTime * 100; - currentState.input.viewDeltaY += -InputManager.GetAxisRaw("LookUp") * Time.DeltaTime * 100; + public override void OnUpdate() + { + // Collect all input here + // All axis values here should be accumulated + currentState.input.viewDeltaX += InputManager.GetAxisRaw("Mouse X"); + currentState.input.viewDeltaY += InputManager.GetAxisRaw("Mouse Y"); + currentState.input.viewDeltaX += InputManager.GetAxisRaw("LookRight") * Time.DeltaTime * 100; + currentState.input.viewDeltaY += -InputManager.GetAxisRaw("LookUp") * Time.DeltaTime * 100; - currentState.input.moveForward = InputManager.GetAxis("Vertical"); - currentState.input.moveRight = InputManager.GetAxis("Horizontal"); - currentState.input.attacking = InputManager.GetAction("Attack"); - currentState.input.jumping = InputManager.GetAction("Jump"); - } + currentState.input.moveForward = InputManager.GetAxis("Vertical"); + currentState.input.moveRight = InputManager.GetAxis("Horizontal"); + currentState.input.attacking = InputManager.GetAction("Attack"); + currentState.input.jumping = InputManager.GetAction("Jump"); + } - public override void OnFixedUpdate() - { - } + public override void OnFixedUpdate() + { + } - public override void OnEndFrame() - { - if (IsRecording) - { - currentState.input.verificationPosition = currentState.actor.position; - currentState.input.verificationVelocity = currentState.actor.velocity; - currentState.input.verificationViewAngles = currentState.actor.viewAngles; - currentState.input.verificationOrientation = currentState.actor.orientation; + public override void OnEndFrame() + { + if (IsRecording) + { + currentState.input.verificationPosition = currentState.actor.position; + currentState.input.verificationVelocity = currentState.actor.velocity; + currentState.input.verificationViewAngles = currentState.actor.viewAngles; + currentState.input.verificationOrientation = currentState.actor.orientation; - currentState.input.frame = frame; - buffer.Add(currentState.input); - } + currentState.input.frame = frame; + buffer.Add(currentState.input); + } - // Reset anything accumulatable here - currentState.input.viewDeltaX = 0; - currentState.input.viewDeltaY = 0; + // Reset anything accumulatable here + currentState.input.viewDeltaX = 0; + currentState.input.viewDeltaY = 0; - frame++; - } + frame++; + } - public override void RecordCurrentActorState(PlayerActorState actorState) - { - if (!IsRecording) - return; + public override void RecordCurrentActorState(PlayerActorState actorState) + { + if (!IsRecording) + return; - if (actorState.position.Length <= 0.01) - Console.Print("wrong recorded position?"); - currentState.actor = actorState; - } + if (actorState.position.Length <= 0.01) + Console.Print("wrong recorded position?"); + currentState.actor = actorState; + } - public void FlushDemo() - { - if (!IsRecording) - return; + public void FlushDemo() + { + if (!IsRecording) + return; - byte[] RawSerialize(object anything) - { - int rawSize = Marshal.SizeOf(anything); - IntPtr buffer = Marshal.AllocHGlobal(rawSize); - Marshal.StructureToPtr(anything, buffer, false); - byte[] rawDatas = new byte[rawSize]; - Marshal.Copy(buffer, rawDatas, 0, rawSize); - Marshal.FreeHGlobal(buffer); - return rawDatas; - } + byte[] RawSerialize(object anything) + { + int rawSize = Marshal.SizeOf(anything); + IntPtr buffer = Marshal.AllocHGlobal(rawSize); + Marshal.StructureToPtr(anything, buffer, false); + byte[] rawDatas = new byte[rawSize]; + Marshal.Copy(buffer, rawDatas, 0, rawSize); + Marshal.FreeHGlobal(buffer); + return rawDatas; + } - foreach (var state in buffer) - { - var bytes = RawSerialize(state); - demoFileStream.Write(bytes, 0, bytes.Length * sizeof(byte)); - } + foreach (PlayerInputState state in buffer) + { + byte[] bytes = RawSerialize(state); + demoFileStream.Write(bytes, 0, bytes.Length * sizeof(byte)); + } - buffer.Clear(); - } + buffer.Clear(); + } - public void StopRecording() - { - if (!IsRecording) - return; + public void StopRecording() + { + if (!IsRecording) + return; - FlushDemo(); - demoFileStream.Close(); - demoFileStream = null; + FlushDemo(); + demoFileStream.Close(); + demoFileStream = null; - Debug.Write(LogType.Info, "demo, wrote states: " + buffer.Count); - } - } + Debug.Write(LogType.Info, "demo, wrote states: " + buffer.Count); + } + } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerInputNetwork.cs b/Source/Game/Player/PlayerInputNetwork.cs index bbf413b..c3744a1 100644 --- a/Source/Game/Player/PlayerInputNetwork.cs +++ b/Source/Game/Player/PlayerInputNetwork.cs @@ -1,14 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Runtime.InteropServices; -using FlaxEngine; - -namespace Game +namespace Game { - public class PlayerInputNetwork : PlayerInput - { - - } + public class PlayerInputNetwork : PlayerInput + { + } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerMovement.cs b/Source/Game/Player/PlayerMovement.cs index c47b308..d01c7e3 100644 --- a/Source/Game/Player/PlayerMovement.cs +++ b/Source/Game/Player/PlayerMovement.cs @@ -1,1131 +1,1089 @@ using System; using System.Collections.Generic; using FlaxEngine; -using System.Diagnostics; -using System.Threading.Tasks; using Console = Cabrito.Console; -using Debug = FlaxEngine.Debug; -using Object = FlaxEngine.Object; namespace Game { - public struct TraceInfo - { - public RayCastHit[] hitInfos; - public bool startSolid; - - // closest hit - public float fraction; - public Vector3 endPosition; - public Vector3 hitNormal; - public Vector3 hitPosition; - - // furthest hit - //public float maxFraction; - //public Vector3 maxHitNormal; - //public Vector3 maxEndPosition; - } - - [Networked] - public class PlayerMovement : Script - { - [Limit(0, 9000), Tooltip("Base Movement speed")] - public float MoveSpeed { get; set; } = 320; - - private static Vector3 Gravity { get; set; } = new Vector3(0, -800.0f, 0f); - - private float viewPitch; - private float viewYaw; - private float viewRoll; - - private float viewPitchLastFrame; - private float viewYawLastFrame; - private float viewRollLastFrame; - - private bool predicting = false; - - private InputEvent onExit = new InputEvent("Exit"); - - // FIXME, should be much smaller but needed to avoid issues with box collider edges against brush edges diagonally - private const float collisionMargin = 0.031f * 1.666f * 1.85f; - private const float slopeNormal = 0.7f; - - private Actor rootActor; - private RigidBody rigidBody; - private PlayerInput input; - - public override void OnAwake() - { - base.OnAwake(); - - bool record = false; - //record = true; - - if (record) - { - input = new PlayerInputLocal(@"C:\dev\GoakeFlax\testdemo.gdem"); // record - } - else - { - input = new PlayerInputLocal(); - //input = new PlayerInputDemo(@"C:\dev\GoakeFlax\testdemo.gdem"); //playback - //input = new PlayerInputDemo(@"C:\dev\GoakeFlax\testdemo_desync.gdem"); //playback - } - - onExit.Triggered += () => - { - if (Console.IsSafeToQuit) - Engine.RequestExit(); - }; - - rootActor = Actor.GetChild(0); - - rigidBody = Actor.As(); - - //rigidBody.CollisionEnter += OnCollisionEnter; - //rigidBody.TriggerEnter += OnTriggerEnter; - //rigidBody.TriggerExit += OnTriggerExit; - - startupTime = Time.TimeSinceStartup; - } - - public override void OnDisable() - { - base.OnDisable(); - - if (input != null && input is PlayerInputLocal) // FIXME - (input as PlayerInputLocal).StopRecording(); - } - - private List touchingActors = new List(); - - private void OnTriggerEnter(PhysicsColliderActor colliderActor) - { - //if (colliderActor.AttachedRigidBody == null) - // return; - touchingActors.Add(colliderActor); - Console.Print("trogger: "); - } - - private void OnTriggerExit(PhysicsColliderActor colliderActor) - { - //if (colliderActor.AttachedRigidBody == null) - // return; - - touchingActors.Remove(colliderActor); - Console.Print("untrogger: "); - } - - private void OnCollisionEnter(Collision collision) - { - //Console.Print("collision: "); - } - - public override void OnDestroy() - { - base.OnDestroy(); - - onExit.Dispose(); - } - - public override void OnStart() - { - var initialEulerAngles = Actor.Orientation.EulerAngles; - viewPitch = initialEulerAngles.X; - viewYaw = initialEulerAngles.Y; - viewRoll = initialEulerAngles.Z; - viewPitchLastFrame = viewPitch; - viewYawLastFrame = viewYaw; - viewRollLastFrame = viewRoll; - } - - private int lastInputFrame = 0; - private int currentInputFrame = 0; - private int currentInputFrame2 = 0; - private float startupTime = 0f; - public override void OnUpdate() - { - //input.OnUpdate(); - - if (input is PlayerInputDemo /*&& currentInputFrame2 >= currentInputFrame*/) - return; - - input.OnUpdate(); - - /*if (input.frame > 0) - { - PlayerActorState actorState = input.GetCurrentActorState(); - Actor.Position = actorState.position; - currentVelocity = actorState.velocity; - viewYaw = actorState.viewYaw; - viewPitch = actorState.viewPitch; - viewRoll = actorState.viewRoll; - }*/ - - PlayerInputState inputState = input.GetCurrentInputState(); - - viewYaw = viewYawLastFrame; - viewPitch = viewPitchLastFrame; - viewRoll = viewRollLastFrame; - ApplyInputToCamera(inputState); - - /*input.RecordCurrentActorState(new PlayerActorState() - { - position = Actor.Position, - velocity = currentVelocity, - orientation = rootActor.Orientation, - viewYaw = viewYaw, - viewPitch = viewPitch, - viewRoll = viewRoll - });*/ - currentInputFrame2++; - } - - private bool demoDeltasVerify = true; - private bool demoDeltasCorrect = true; - - public override void OnFixedUpdate() - { - if (input is PlayerInputDemo) - input.OnUpdate(); - - float deltadif = Time.DeltaTime - (1.0f / Time.PhysicsFPS); - if (Math.Abs(deltadif) > 0.0001f) - Console.Print("drift: " + deltadif); - - input.OnFixedUpdate(); - PlayerInputState inputState = input.GetCurrentInputState(); - - if (input is PlayerInputDemo) - { - ApplyInputToCamera(inputState); - - // Verify view angles first - if (demoDeltasVerify) - { - Vector3 viewAngles = new Vector3(viewYaw, viewPitch, viewRoll); - float viewAnglesDelta = (viewAngles - inputState.verificationViewAngles).Length; - if (viewAnglesDelta > 0.00001) - { - Console.PrintError("Demo verification failed, view angles delta: " + viewAnglesDelta); - if (demoDeltasCorrect) - { - SetCameraEulerAngles(inputState.verificationViewAngles); - } - } - } - } - - SimulatePlayerMovement(inputState); - - if (input is PlayerInputDemo && demoDeltasVerify) - { - // verify - float positionDelta = (Actor.Position - inputState.verificationPosition).Length; - if (positionDelta > 0.00001) - Console.Print("Demo verification failed, position delta: " + positionDelta); - - float velocityDelta = (currentVelocity - inputState.verificationVelocity).Length; - if (velocityDelta > 0.00001) - Console.Print("Demo verification failed, velocity delta: " + velocityDelta); - - float orientationDelta = (rootActor.Orientation - inputState.verificationOrientation).Length; - if (orientationDelta > 0.00001) - { - Console.PrintError("Demo verification failed, orientation delta: " + orientationDelta); - if (demoDeltasCorrect) - { - - } - } - - - //if (currentInputFrame == 0) - /*{ - //Console.Print("repos: " + inputState.verificationPosition); - Actor.Position = inputState.verificationPosition; - currentVelocity = inputState.verificationVelocity; - rootActor.Orientation = inputState.verificationOrientation; - }*/ - } - - input.RecordCurrentActorState(new PlayerActorState() - { - position = Actor.Position, - velocity = currentVelocity, - orientation = rootActor.Orientation, - viewAngles = new Vector3(viewYaw, viewPitch, viewRoll), - }); - input.OnEndFrame(); - - - lastInputFrame = currentInputFrame; - currentInputFrame++; - - viewPitchLastFrame = viewPitch; - viewYawLastFrame = viewYaw; - viewRollLastFrame = viewRoll; - } - - private void ApplyInputToCamera(PlayerInputState inputState) - { - // Update camera viewf - float xAxis = inputState.viewDeltaX; - float yAxis = inputState.viewDeltaY; - if (xAxis == 0.0f && yAxis == 0.0f) - return; - - viewPitch = Mathf.Clamp(viewPitch + yAxis, -90.0f, 90.0f); - viewYaw += xAxis; - - SetCameraEulerAngles(new Vector3(viewYaw, viewPitch, viewRoll)); - } - - private void SetCameraEulerAngles(Vector3 viewAngles) - { - //Camera camera = rootActor.GetChild(); - Actor cameraHolder = rootActor.GetChild("CameraHolder"); - - // Root orientation must be set first - rootActor.Orientation = Quaternion.Euler(0, viewAngles.X, 0); - cameraHolder.Orientation = Quaternion.Euler(viewAngles.Y, viewAngles.X, viewAngles.Z); - } - - private static bool SweepPlayerCollider(Actor actor, Vector3 start, Vector3 end, out RayCastHit[] hits) - { - Vector3 delta = end - start; - float maxDistance = delta.Length; - Vector3 direction = delta.Normalized; - - bool collided = false; - var capsuleCollider = actor.GetChild(); - var boxCollider = actor.GetChild(); - var meshCollider = actor.GetChild(); - if (capsuleCollider && capsuleCollider.IsActive) - { - collided = Physics.CapsuleCastAll(start, - capsuleCollider.Radius, capsuleCollider.Height, - direction, out hits, capsuleCollider.Orientation, maxDistance, - uint.MaxValue, - false); - } - else if (meshCollider && meshCollider.IsActive) - { - collided = Physics.ConvexCastAll(start, - meshCollider.CollisionData, meshCollider.Scale, - direction, out hits, meshCollider.Orientation, maxDistance, - uint.MaxValue, - false); - } - else if (boxCollider && boxCollider.IsActive) - { - collided = Physics.BoxCastAll(start, - boxCollider.OrientedBox.Extents, - direction, out hits, boxCollider.Orientation, maxDistance, - uint.MaxValue, - false); - } - else - { - throw new Exception("Player does not have a collider"); - } - - return collided; - } - - private bool SweepPlayerCollider(Vector3 position, out PhysicsColliderActor[] colliders) - { - bool collided = false; - var capsuleCollider = Actor.GetChild(); - var boxCollider = Actor.GetChild(); - var meshCollider = Actor.GetChild(); - PhysicsColliderActor colliderActor = null; - if (capsuleCollider && capsuleCollider.IsActive) - { - colliderActor = capsuleCollider; - collided = Physics.OverlapCapsule(position, - capsuleCollider.Radius, capsuleCollider.Height, - out colliders, capsuleCollider.Orientation, - uint.MaxValue, - false); - } - else if (meshCollider && meshCollider.IsActive) - { - colliderActor = meshCollider; - collided = Physics.OverlapConvex(position, - meshCollider.CollisionData, meshCollider.Scale, - out colliders, meshCollider.Orientation, - uint.MaxValue, - false); - } - else if (boxCollider && boxCollider.IsActive) - { - colliderActor = boxCollider; - collided = Physics.OverlapBox(position, - boxCollider.OrientedBox.Extents, - out colliders, boxCollider.Orientation, - uint.MaxValue, - false); - } - else - { - throw new Exception("Player does not have a collider"); - } - - if (collided) - { - List collidersFiltered = new List(); - foreach (var collider in colliders) - { - if (collider.Parent == Actor) - continue; - - collidersFiltered.Add(collider); - } - - colliders = collidersFiltered.ToArray(); - if (colliders.Length == 0) - collided = false; - } - - return collided; - } - - /// - /// Sweeps the player rigidbody in world and returns geometry which was hit during the trace. - /// - /// Player actor - /// Start position - /// End position - /// - private static TraceInfo TracePlayer(Actor actor, Vector3 start, Vector3 end) - { - TraceInfo traceInfo = new TraceInfo(); - - Vector3 delta = end - start; - float maxDistance = delta.Length; - Vector3 direction = delta.Normalized; - - bool collided = SweepPlayerCollider(actor, start, end, out traceInfo.hitInfos); - if (collided) - { - List hitInfosFiltered = new List(); - RayCastHit closest = new RayCastHit(); - closest.Distance = float.MaxValue; - foreach (var hitInfo in traceInfo.hitInfos) - { - //if (hitInfo.Collider == colliderActor) - // continue; - if (hitInfo.Collider.Parent == actor) - continue; - - hitInfosFiltered.Add(hitInfo); - - if (hitInfo.Distance < closest.Distance && hitInfo.Distance != 0.0f) - closest = hitInfo; - } - - if (hitInfosFiltered.Count == 0 /*|| closest.Distance == float.MaxValue*/) - collided = false; // self-collision? - /*else if (closest.Distance == float.MaxValue) - { - bool startSolid = SweepPlayerCollider(start, out PhysicsColliderActor[] colliders); - if (startSolid) - { - traceInfo.hitInfos = hitInfosFiltered.ToArray(); - - traceInfo.fraction = 0f; - traceInfo.hitNormal = Vector3.Zero; - traceInfo.hitPosition = start; - traceInfo.endPosition = start; - traceInfo.startSolid = true; - - if (delta.Y >= 0f) - Console.Print("ovr: " + colliders[0].Parent.Name); - } - else - collided = false; - }*/ - else //if (closest.Distance > 0f) - { - if (closest.Distance == float.MaxValue) - { - foreach (var hitInfo in hitInfosFiltered) - { - if (hitInfo.Distance < closest.Distance) - closest = hitInfo; - } - } - - traceInfo.hitInfos = hitInfosFiltered.ToArray(); - - traceInfo.fraction = closest.Distance / maxDistance; - traceInfo.hitNormal = closest.Normal; - traceInfo.hitPosition = closest.Point; - traceInfo.endPosition = start + (delta * traceInfo.fraction); - - if (traceInfo.fraction == 0f && maxDistance > 0f) - traceInfo.startSolid = true; - - //if (delta.Y >= 0f) - // Console.Print("col: " + closest.Collider.Parent.Name + ", " + closest.Distance.ToString("G9")); - } - /*else - { - traceInfo.startSolid = true; - traceInfo.fraction = 0f; - }*/ - } - - if (!collided) - { - traceInfo.hitInfos = new RayCastHit[0]; - traceInfo.fraction = 1f; - traceInfo.endPosition = end; - } - - return traceInfo; - } + public struct TraceInfo + { + public RayCastHit[] hitInfos; + public bool startSolid; + + // closest hit + public float fraction; + public Vector3 endPosition; + public Vector3 hitNormal; + public Vector3 hitPosition; + + // furthest hit + //public float maxFraction; + //public Vector3 maxHitNormal; + //public Vector3 maxEndPosition; + } + + [Networked] + public class PlayerMovement : Script + { + // FIXME, should be much smaller but needed to avoid issues with box collider edges against brush edges diagonally + private const float collisionMargin = 0.031f * 1.666f * 1.85f; + private const float slopeNormal = 0.7f; + + /* + // QW + private const float friction = 4f; + private const float stopspeed = 100f; + private const float accelerationGround = 10f; + private const float jumpVelocity = 270f; + + private const float maxAirSpeed = 320f; + private const float maxAirStrafeSpeed = 30f; //Q2+ + private const float airAcceleration = 0f; //Q2+ + private const float airStopAcceleration = 0f; //Q2+ + private const float airStrafeAcceleration = 0f; //CPM? + private const float strafeAcceleration = 10f; //QW + private const float airControl = 0f; //CPM + private const float stepSize = 16f; + private const float autoJumpTime = 0.4f; + + private const int airStep = 0; + */ + + // GOA + private const float friction = 6f; + private const float stopspeed = 100f; + private const float accelerationGround = 12f; + private const float jumpVelocity = 270f; + private const float jumpBoostTime = 0.5f; + private const float jumpBoostVelocity = 100f; + private const int jumpBoostMaxJumps = 2; + private const bool jumpStairBehavior = true; + + private const float maxAirSpeed = 320f; + private const float maxAirStrafeSpeed = 30f; + private const float airAcceleration = 0.4f; + private const float airStopAcceleration = 2.5f; + private const float airStrafeAcceleration = 0f; + private const float strafeAcceleration = 10f; + private const float airControl = 0f; + private const float stepHeight = 16f; + private const float autoJumpTime = 0.4f; + + private const int airStep = 2; + private readonly bool demoDeltasCorrect = true; + + private readonly bool demoDeltasVerify = true; + + private readonly InputEvent onExit = new InputEvent("Exit"); + + private readonly bool predicting = false; + + private readonly List touchingActors = new List(); + private int currentInputFrame; + private int currentInputFrame2; + + private Vector3 currentVelocity; + private PlayerInput input; + + //private bool physicsInteractions = false; + + private bool jumped; + + private int lastInputFrame; + private float lastJumped = -1f; + private float lastLanded = -1f; + private int numJumps; + + [ReadOnly] public bool onGround; + private RigidBody rigidBody; + + private Actor rootActor; + private float startupTime; + + private float viewPitch; + + private float viewPitchLastFrame; + private float viewRoll; + private float viewRollLastFrame; + private float viewYaw; + private float viewYawLastFrame; + + [Limit(0, 9000)] + [Tooltip("Base Movement speed")] + public float MoveSpeed { get; set; } = 320; + + private static Vector3 Gravity { get; } = new Vector3(0, -800.0f, 0f); + + //private Vector3 safePosition; + + [ReadOnly] + public float CurrentVelocity + { + get => currentVelocity.Length; + set { } + } + + [ReadOnly] + public float UPS + { + get + { + Vector3 horizontalSpeed = currentVelocity; + horizontalSpeed.Y = 0f; + return horizontalSpeed.Length; + } + set { } + } + + public override void OnAwake() + { + base.OnAwake(); + + bool record = false; + //record = true; + + if (record) + input = new PlayerInputLocal(@"C:\dev\GoakeFlax\testdemo.gdem"); // record + else + input = new PlayerInputLocal(); + //input = new PlayerInputDemo(@"C:\dev\GoakeFlax\testdemo.gdem"); //playback + //input = new PlayerInputDemo(@"C:\dev\GoakeFlax\testdemo_desync.gdem"); //playback + + onExit.Triggered += () => + { + if (Console.IsSafeToQuit) + Engine.RequestExit(); + }; + + rootActor = Actor.GetChild(0); + + rigidBody = Actor.As(); + + //rigidBody.CollisionEnter += OnCollisionEnter; + //rigidBody.TriggerEnter += OnTriggerEnter; + //rigidBody.TriggerExit += OnTriggerExit; + + startupTime = Time.TimeSinceStartup; + } + + public override void OnDisable() + { + base.OnDisable(); + + if (input != null && input is PlayerInputLocal) // FIXME + (input as PlayerInputLocal).StopRecording(); + } + + private void OnTriggerEnter(PhysicsColliderActor colliderActor) + { + //if (colliderActor.AttachedRigidBody == null) + // return; + touchingActors.Add(colliderActor); + Console.Print("trogger: "); + } + + private void OnTriggerExit(PhysicsColliderActor colliderActor) + { + //if (colliderActor.AttachedRigidBody == null) + // return; + + touchingActors.Remove(colliderActor); + Console.Print("untrogger: "); + } + + private void OnCollisionEnter(Collision collision) + { + //Console.Print("collision: "); + } + + public override void OnDestroy() + { + base.OnDestroy(); + + onExit.Dispose(); + } + + public override void OnStart() + { + Vector3 initialEulerAngles = Actor.Orientation.EulerAngles; + viewPitch = initialEulerAngles.X; + viewYaw = initialEulerAngles.Y; + viewRoll = initialEulerAngles.Z; + viewPitchLastFrame = viewPitch; + viewYawLastFrame = viewYaw; + viewRollLastFrame = viewRoll; + } + + public override void OnUpdate() + { + //input.OnUpdate(); + + if (input is PlayerInputDemo /*&& currentInputFrame2 >= currentInputFrame*/) + return; + + input.OnUpdate(); + + /*if (input.frame > 0) + { + PlayerActorState actorState = input.GetCurrentActorState(); + Actor.Position = actorState.position; + currentVelocity = actorState.velocity; + viewYaw = actorState.viewYaw; + viewPitch = actorState.viewPitch; + viewRoll = actorState.viewRoll; + }*/ + + PlayerInputState inputState = input.GetCurrentInputState(); + + viewYaw = viewYawLastFrame; + viewPitch = viewPitchLastFrame; + viewRoll = viewRollLastFrame; + ApplyInputToCamera(inputState); + + /*input.RecordCurrentActorState(new PlayerActorState() + { + position = Actor.Position, + velocity = currentVelocity, + orientation = rootActor.Orientation, + viewYaw = viewYaw, + viewPitch = viewPitch, + viewRoll = viewRoll + });*/ + currentInputFrame2++; + } + + public override void OnFixedUpdate() + { + if (input is PlayerInputDemo) + input.OnUpdate(); + + float deltadif = Time.DeltaTime - 1.0f / Time.PhysicsFPS; + if (Math.Abs(deltadif) > 0.0001f) + Console.Print("drift: " + deltadif); + + input.OnFixedUpdate(); + PlayerInputState inputState = input.GetCurrentInputState(); + + if (input is PlayerInputDemo) + { + ApplyInputToCamera(inputState); + + // Verify view angles first + if (demoDeltasVerify) + { + Vector3 viewAngles = new Vector3(viewYaw, viewPitch, viewRoll); + float viewAnglesDelta = (viewAngles - inputState.verificationViewAngles).Length; + if (viewAnglesDelta > 0.00001) + { + Console.PrintError("Demo verification failed, view angles delta: " + viewAnglesDelta); + if (demoDeltasCorrect) + SetCameraEulerAngles(inputState.verificationViewAngles); + } + } + } + + SimulatePlayerMovement(inputState); + + if (input is PlayerInputDemo && demoDeltasVerify) + { + // verify + float positionDelta = (Actor.Position - inputState.verificationPosition).Length; + if (positionDelta > 0.00001) + Console.Print("Demo verification failed, position delta: " + positionDelta); + + float velocityDelta = (currentVelocity - inputState.verificationVelocity).Length; + if (velocityDelta > 0.00001) + Console.Print("Demo verification failed, velocity delta: " + velocityDelta); + + float orientationDelta = (rootActor.Orientation - inputState.verificationOrientation).Length; + if (orientationDelta > 0.00001) + { + Console.PrintError("Demo verification failed, orientation delta: " + orientationDelta); + if (demoDeltasCorrect) + { + } + } + + + //if (currentInputFrame == 0) + /*{ + //Console.Print("repos: " + inputState.verificationPosition); + Actor.Position = inputState.verificationPosition; + currentVelocity = inputState.verificationVelocity; + rootActor.Orientation = inputState.verificationOrientation; + }*/ + } + + input.RecordCurrentActorState(new PlayerActorState + { + position = Actor.Position, + velocity = currentVelocity, + orientation = rootActor.Orientation, + viewAngles = new Vector3(viewYaw, viewPitch, viewRoll) + }); + input.OnEndFrame(); + + + lastInputFrame = currentInputFrame; + currentInputFrame++; + + viewPitchLastFrame = viewPitch; + viewYawLastFrame = viewYaw; + viewRollLastFrame = viewRoll; + } + + private void ApplyInputToCamera(PlayerInputState inputState) + { + // Update camera viewf + float xAxis = inputState.viewDeltaX; + float yAxis = inputState.viewDeltaY; + if (xAxis == 0.0f && yAxis == 0.0f) + return; + + viewPitch = Mathf.Clamp(viewPitch + yAxis, -90.0f, 90.0f); + viewYaw += xAxis; + + SetCameraEulerAngles(new Vector3(viewYaw, viewPitch, viewRoll)); + } + + private void SetCameraEulerAngles(Vector3 viewAngles) + { + //Camera camera = rootActor.GetChild(); + Actor cameraHolder = rootActor.GetChild("CameraHolder"); + + // Root orientation must be set first + rootActor.Orientation = Quaternion.Euler(0, viewAngles.X, 0); + cameraHolder.Orientation = Quaternion.Euler(viewAngles.Y, viewAngles.X, viewAngles.Z); + } + + private static bool SweepPlayerCollider(Actor actor, Vector3 start, Vector3 end, out RayCastHit[] hits) + { + Vector3 delta = end - start; + float maxDistance = delta.Length; + Vector3 direction = delta.Normalized; + + bool collided = false; + CapsuleCollider capsuleCollider = actor.GetChild(); + BoxCollider boxCollider = actor.GetChild(); + MeshCollider meshCollider = actor.GetChild(); + if (capsuleCollider && capsuleCollider.IsActive) + collided = Physics.CapsuleCastAll(start, + capsuleCollider.Radius, capsuleCollider.Height, + direction, out hits, capsuleCollider.Orientation, maxDistance, + uint.MaxValue, + false); + else if (meshCollider && meshCollider.IsActive) + collided = Physics.ConvexCastAll(start, + meshCollider.CollisionData, meshCollider.Scale, + direction, out hits, meshCollider.Orientation, maxDistance, + uint.MaxValue, + false); + else if (boxCollider && boxCollider.IsActive) + collided = Physics.BoxCastAll(start, + boxCollider.OrientedBox.Extents, + direction, out hits, boxCollider.Orientation, maxDistance, + uint.MaxValue, + false); + else + throw new Exception("Player does not have a collider"); + + return collided; + } + + private bool SweepPlayerCollider(Vector3 position, out PhysicsColliderActor[] colliders) + { + bool collided = false; + CapsuleCollider capsuleCollider = Actor.GetChild(); + BoxCollider boxCollider = Actor.GetChild(); + MeshCollider meshCollider = Actor.GetChild(); + PhysicsColliderActor colliderActor = null; + if (capsuleCollider && capsuleCollider.IsActive) + { + colliderActor = capsuleCollider; + collided = Physics.OverlapCapsule(position, + capsuleCollider.Radius, capsuleCollider.Height, + out colliders, capsuleCollider.Orientation, + uint.MaxValue, + false); + } + else if (meshCollider && meshCollider.IsActive) + { + colliderActor = meshCollider; + collided = Physics.OverlapConvex(position, + meshCollider.CollisionData, meshCollider.Scale, + out colliders, meshCollider.Orientation, + uint.MaxValue, + false); + } + else if (boxCollider && boxCollider.IsActive) + { + colliderActor = boxCollider; + collided = Physics.OverlapBox(position, + boxCollider.OrientedBox.Extents, + out colliders, boxCollider.Orientation, + uint.MaxValue, + false); + } + else + { + throw new Exception("Player does not have a collider"); + } + + if (collided) + { + var collidersFiltered = new List(); + foreach (PhysicsColliderActor collider in colliders) + { + if (collider.Parent == Actor) + continue; + + collidersFiltered.Add(collider); + } + + colliders = collidersFiltered.ToArray(); + if (colliders.Length == 0) + collided = false; + } + + return collided; + } + + /// + /// Sweeps the player rigidbody in world and returns geometry which was hit during the trace. + /// + /// Player actor + /// Start position + /// End position + /// + private static TraceInfo TracePlayer(Actor actor, Vector3 start, Vector3 end) + { + TraceInfo traceInfo = new TraceInfo(); + + Vector3 delta = end - start; + float maxDistance = delta.Length; + Vector3 direction = delta.Normalized; + + bool collided = SweepPlayerCollider(actor, start, end, out traceInfo.hitInfos); + if (collided) + { + var hitInfosFiltered = new List(); + RayCastHit closest = new RayCastHit(); + closest.Distance = float.MaxValue; + foreach (RayCastHit hitInfo in traceInfo.hitInfos) + { + //if (hitInfo.Collider == colliderActor) + // continue; + if (hitInfo.Collider.Parent == actor) + continue; + + hitInfosFiltered.Add(hitInfo); + + if (hitInfo.Distance < closest.Distance && hitInfo.Distance != 0.0f) + closest = hitInfo; + } + + if (hitInfosFiltered.Count == 0 /*|| closest.Distance == float.MaxValue*/) + { + collided = false; // self-collision? + } + /*else if (closest.Distance == float.MaxValue) + { + bool startSolid = SweepPlayerCollider(start, out PhysicsColliderActor[] colliders); + if (startSolid) + { + traceInfo.hitInfos = hitInfosFiltered.ToArray(); + + traceInfo.fraction = 0f; + traceInfo.hitNormal = Vector3.Zero; + traceInfo.hitPosition = start; + traceInfo.endPosition = start; + traceInfo.startSolid = true; + + if (delta.Y >= 0f) + Console.Print("ovr: " + colliders[0].Parent.Name); + } + else + collided = false; + }*/ + else //if (closest.Distance > 0f) + { + if (closest.Distance == float.MaxValue) + foreach (RayCastHit hitInfo in hitInfosFiltered) + if (hitInfo.Distance < closest.Distance) + closest = hitInfo; + + traceInfo.hitInfos = hitInfosFiltered.ToArray(); + + traceInfo.fraction = closest.Distance / maxDistance; + traceInfo.hitNormal = closest.Normal; + traceInfo.hitPosition = closest.Point; + traceInfo.endPosition = start + delta * traceInfo.fraction; + + if (traceInfo.fraction == 0f && maxDistance > 0f) + traceInfo.startSolid = true; + + //if (delta.Y >= 0f) + // Console.Print("col: " + closest.Collider.Parent.Name + ", " + closest.Distance.ToString("G9")); + } + /*else + { + traceInfo.startSolid = true; + traceInfo.fraction = 0f; + }*/ + } + + if (!collided) + { + traceInfo.hitInfos = new RayCastHit[0]; + traceInfo.fraction = 1f; + traceInfo.endPosition = end; + } + + return traceInfo; + } #if FLAX_EDITOR - public override void OnDebugDraw() - { - base.OnDebugDraw(); + public override void OnDebugDraw() + { + base.OnDebugDraw(); - var capsuleCollider = Actor.GetChild(); - var boxCollider = Actor.GetChild(); - var meshCollider = Actor.GetChild(); - if (capsuleCollider && capsuleCollider.IsActive) - { - Quaternion rotation = capsuleCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); - DebugDraw.DrawWireTube(capsuleCollider.Position, rotation, capsuleCollider.Radius, - capsuleCollider.Height, Color.GreenYellow * 0.8f); - } - else if (meshCollider && meshCollider.IsActive) - { - //Quaternion rotation = meshCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); - DebugDraw.DrawWireCylinder(meshCollider.Position, meshCollider.Orientation, capsuleCollider.Radius, - capsuleCollider.Height + capsuleCollider.Radius * 2, Color.GreenYellow * 0.8f); - //DebugDraw.DrawWireTube(meshCollider.Position, rotation, meshCollider.Radius, meshCollider.Height, Color.GreenYellow * 0.8f); - } - else if (boxCollider && boxCollider.IsActive) - { - DebugDraw.DrawWireBox(boxCollider.OrientedBox.GetBoundingBox(), Color.GreenYellow * 0.8f); - } - } + CapsuleCollider capsuleCollider = Actor.GetChild(); + BoxCollider boxCollider = Actor.GetChild(); + MeshCollider meshCollider = Actor.GetChild(); + if (capsuleCollider && capsuleCollider.IsActive) + { + Quaternion rotation = capsuleCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); + DebugDraw.DrawWireTube(capsuleCollider.Position, rotation, capsuleCollider.Radius, + capsuleCollider.Height, Color.GreenYellow * 0.8f); + } + else if (meshCollider && meshCollider.IsActive) + { + //Quaternion rotation = meshCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); + DebugDraw.DrawWireCylinder(meshCollider.Position, meshCollider.Orientation, capsuleCollider.Radius, + capsuleCollider.Height + capsuleCollider.Radius * 2, Color.GreenYellow * 0.8f); + //DebugDraw.DrawWireTube(meshCollider.Position, rotation, meshCollider.Radius, meshCollider.Height, Color.GreenYellow * 0.8f); + } + else if (boxCollider && boxCollider.IsActive) + { + DebugDraw.DrawWireBox(boxCollider.OrientedBox.GetBoundingBox(), Color.GreenYellow * 0.8f); + } + } #endif - private static SlideMoveHit StepSlideMove(Actor actor, ref Vector3 position, ref Vector3 velocity, bool onGround) - { - if (velocity.IsZero) - return SlideMoveHit.Nothing; - - Vector3 gravityDirection = Gravity.Normalized; - - Vector3 originalPosition = position; - Vector3 originalVelocity = velocity; - - SlideMoveHit slideMoveHit = SlideMove(actor, ref position, ref velocity); - if (slideMoveHit == SlideMoveHit.Nothing) - { - // TODO: step down here - return slideMoveHit; - } - - // hit something, try to step up - float effectiveStepHeight = stepHeight; - if (!onGround) - { - // TODO: implement clipping here - /*if (pmove.jump_time > 0 && pmove.jump_time <= movevars.cliptime) - { - float zvel = pmove.velocity[2]; - VectorCopy(originalvel, pmove.velocity); - pmove.velocity[2] = min(pmove.velocity[2], zvel); // nullifies vertical clipping - }*/ - - if (!slideMoveHit.HasFlag(SlideMoveHit.Step)) - return slideMoveHit; - - if (airStep < 2) - { - //effectiveStepHeight = ? - } - } - - - Vector3 stepDelta = -gravityDirection * effectiveStepHeight; - - Vector3 slidePosition = position; - Vector3 slideVelocity = velocity; - position = originalPosition; - velocity = originalVelocity; - - // step up - Vector3 stepUp = position + stepDelta; - TraceInfo traceUp = TracePlayer(actor, position, stepUp); - if (traceUp.fraction > 0f) - position = traceUp.endPosition; - - // try moving from step up position - SlideMoveHit slideMoveStepHit = SlideMove(actor, ref position, ref velocity); - - // step down - Vector3 stepDown = position - stepDelta; - TraceInfo traceDown = TracePlayer(actor, position, stepDown); - if (traceDown.fraction < 1f && -Vector3.Dot(gravityDirection, traceDown.hitNormal) < slopeNormal) - { - // can't step down, slide move like normally - Console.Print("no stepping 1, frac: " + traceDown.fraction + ", dot: " + - (-Vector3.Dot(gravityDirection, traceDown.hitNormal)) + - ", norm: " + traceDown.hitNormal); - position = slidePosition; - velocity = slideVelocity; - return slideMoveHit; - } - else if (traceDown.fraction > 0f) - position = traceDown.endPosition; - - // add some margin from the ground in order to avoid getting stuck after stepping up - if (traceDown.fraction < 1f) - position.Y += collisionMargin; - - // ?? - var d1 = -Vector3.Dot(gravityDirection, position); - var d2 = -Vector3.Dot(gravityDirection, originalPosition); - if (d1 < d2) - { - //Console.Print("no stepping 2, " + d1 + " < " + d2); - position = slidePosition; - velocity = slideVelocity; - return slideMoveHit; - } - - Vector3 slidePosition2 = slidePosition; //down - Vector3 stepPosition2 = position; //up - - // FIXME, negate gravity - slidePosition2.Y = 0f; - stepPosition2.Y = 0f; - - // take the slide movement results if furthest away from original position - //if ((stepPosition2 - originalPosition).Length < (slidePosition2 - originalPosition).Length) - if ((slidePosition2 - originalPosition).Length >= (stepPosition2 - originalPosition).Length) - { - //Console.Print("no stepping 3"); - position = slidePosition; - velocity = slideVelocity; - return slideMoveHit; - } - - //return slideMoveStepHit; - return slideMoveHit; - } - - [Flags] - enum SlideMoveHit - { - Nothing = 0, - Step = 1, - Floor = 2, - Other = 4, - } - - private static SlideMoveHit SlideMove(Actor actor, ref Vector3 position, ref Vector3 velocity) - { - if (velocity.IsZero) - return SlideMoveHit.Nothing; - - Vector3 originalPosition = position; - Vector3 originalVelocity = velocity; - SlideMoveHit slideMoveHit = SlideMoveHit.Nothing; - - float timeleft = Time.DeltaTime; - - List hitNormals = new List(); - - for (int bump = 0; bump < 4; bump++) - { - Vector3 startPos = position; - Vector3 endPos = position + (velocity * timeleft); - - TraceInfo trace = TracePlayer(actor, startPos, endPos); - // TODO: handle portals here - - float fraction = trace.fraction; - Vector3 hitNormal = trace.hitNormal; - - if (trace.startSolid) - { - velocity = Vector3.Zero; - break; - } - - if (fraction > 0f) - { - position = trace.endPosition; - hitNormals.Clear(); // this is present in some forks, not in Q3 - } - - if (fraction >= 1f) - break; - - timeleft *= 1.0f - fraction; - - if (trace.hitNormal.Y > slopeNormal) - slideMoveHit |= SlideMoveHit.Floor; - else if (Math.Abs(trace.hitNormal.Y) < 0.0001f) - slideMoveHit |= SlideMoveHit.Step; - else - slideMoveHit |= SlideMoveHit.Other; - - // this doesn't seem to do anything, we never have any hitNormals stored here - bool hitPreviousNormal = false; - foreach (Vector3 normal in hitNormals) - { - if (Vector3.Dot(hitNormal, normal) > 0.99) - { - // nudge away from the same wall we hit earlier and try again - velocity += hitNormal; - hitPreviousNormal = true; - break; - } - } - - if (hitPreviousNormal) - continue; - - hitNormals.Add(hitNormal); - if (hitNormals.Count != 1) - Console.Print("hitNormals: " + hitNormals.Count); - - int plane; - Vector3 normalMargin = Vector3.Zero; - for (plane = 0; plane < hitNormals.Count; plane++) - { - Vector3 normal = hitNormals[plane]; - - // clip velocity - velocity -= normal * Vector3.Dot(velocity, normal); - //velocity = Vector3.ProjectOnPlane(velocity, normal); - - //traceOffset = normal * 1f; - normalMargin += normal; - //position += normal * 0.031f; - - int plane2; - for (plane2 = 0; plane2 < hitNormals.Count; plane2++) - { - if (plane == plane2) - continue; - - if (Vector3.Dot(velocity, hitNormals[plane2]) < 0f) - break; - } - - if (plane2 == hitNormals.Count) - break; - } - - // push off slightly away from the walls to not get stuck - position += normalMargin.Normalized * collisionMargin; - //Console.Print("pushin"); - - if (plane == hitNormals.Count) - { - if (hitNormals.Count == 2) - { - Vector3 dir = Vector3.Cross(hitNormals[0], hitNormals[1]); - //dir.Normalize(); - float dist = Vector3.Dot(dir, velocity); - velocity = dist * dir; - } - else - { - velocity = Vector3.Zero; - break; - } - } - else - { - // nudge very slightly away from the wall to avoid getting stuck - //position += trace.hitNormal * 0.01f; - //velocity += trace.hitNormal * 0.01f; - } - - // prevents bouncing against the wall - if ( /*velocity.Length > 0f && */Vector3.Dot(velocity, originalVelocity) <= 0f) - { - velocity = Vector3.Zero; - break; - } - } - - return slideMoveHit; - } - - [ReadOnly] public bool onGround = false; - - /* - // QW - private const float friction = 4f; - private const float stopspeed = 100f; - private const float accelerationGround = 10f; - private const float jumpVelocity = 270f; - - private const float maxAirSpeed = 320f; - private const float maxAirStrafeSpeed = 30f; //Q2+ - private const float airAcceleration = 0f; //Q2+ - private const float airStopAcceleration = 0f; //Q2+ - private const float airStrafeAcceleration = 0f; //CPM? - private const float strafeAcceleration = 10f; //QW - private const float airControl = 0f; //CPM - private const float stepSize = 16f; - private const float autoJumpTime = 0.4f; - - private const int airStep = 0; - */ - - // GOA - private const float friction = 6f; - private const float stopspeed = 100f; - private const float accelerationGround = 12f; - private const float jumpVelocity = 270f; - private const float jumpBoostTime = 0.5f; - private const float jumpBoostVelocity = 100f; - private const int jumpBoostMaxJumps = 2; - private const bool jumpStairBehavior = true; - - private const float maxAirSpeed = 320f; - private const float maxAirStrafeSpeed = 30f; - private const float airAcceleration = 0.4f; - private const float airStopAcceleration = 2.5f; - private const float airStrafeAcceleration = 0f; - private const float strafeAcceleration = 10f; - private const float airControl = 0f; - private const float stepHeight = 16f; - private const float autoJumpTime = 0.4f; - - private const int airStep = 2; - - //private bool physicsInteractions = false; - - private bool jumped = false; - private float lastJumped = -1f; - private int numJumps = 0; - private float lastLanded = -1f; - - //private Vector3 safePosition; - - [ReadOnly] - public float CurrentVelocity - { - get { return currentVelocity.Length; } - set { } - } - - [ReadOnly] - public float UPS - { - get - { - Vector3 horizontalSpeed = currentVelocity; - horizontalSpeed.Y = 0f; - return horizontalSpeed.Length; - } - set { } - } - - private Vector3 currentVelocity; - - public void SimulatePlayerMovement(PlayerInputState inputState) - { - Transform rootTrans = rootActor.Transform; - - Vector3 inputDirection = - new Vector3(inputState.moveRight, 0.0f, inputState.moveForward); - Vector3 moveDirection = rootTrans.TransformDirection(inputDirection); - - Vector3 position = rigidBody.Position; - Vector3 velocity = currentVelocity; //rigidBody.LinearVelocity; - - Vector3 wishVelocity = Vector3.Zero; - if (!inputDirection.IsZero) - wishVelocity = moveDirection.Normalized * MoveSpeed; - - // categorize position - bool lastGround = onGround; - Vector3 lastVelocity = velocity; - onGround = true; - - TraceInfo traceGround = CategorizePosition(position, ref velocity); - - bool jumpAction = inputState.jumping; - - if (jumped && !jumpAction) - jumped = false; // jump released - else if (jumped && Time.GameTime - lastJumped >= autoJumpTime) - jumped = false; // jump timeout - - // jump - if (onGround && jumpAction && !jumped) - { - if (OnJump(traceGround, ref position, ref velocity)) - { - jumped = true; - lastJumped = Time.GameTime; - numJumps++; - } - } - - if (Time.GameTime - lastJumped > jumpBoostTime) - numJumps = 0; - - //if (/*onGround && */lastGround != onGround) - if (onGround) - { - // ground friction - ApplyFriction(ref velocity); - - // ground acceleration - ApplyAcceleration(ref velocity, wishVelocity.Normalized, wishVelocity.Length, float.MaxValue, - accelerationGround); - } - else // air movement - { - ApplyAirAcceleration(ref velocity, wishVelocity); - } - - StepSlideMove(Actor, ref position, ref velocity, onGround); - - - TraceInfo traceGround2 = CategorizePosition(position, ref velocity); - - rigidBody.Position = position; - currentVelocity = velocity; - //rigidBody.LinearVelocity = velocity; - - const float landingVelocityThreshold = 120f; - const float landingHardVelocityThreshold = 500f; - if (currentVelocity.Y - lastVelocity.Y > landingVelocityThreshold) - { - if (Time.GameTime - lastJumped > 0.01) - { - bool hardLanding = currentVelocity.Y - lastVelocity.Y > landingHardVelocityThreshold; - OnLanded(currentVelocity - lastVelocity, hardLanding); - lastLanded = Time.GameTime; - } - } - } - - private TraceInfo CategorizePosition(Vector3 position, ref Vector3 velocity) - { - Vector3 groundDelta = Gravity.Normalized;//Gravity.Normalized * (collisionMargin * 2); - //if (velocity.Y < 0f) - // groundDelta = Gravity.Normalized * velocity.Y * Time.DeltaTime; - TraceInfo traceGround = TracePlayer(Actor, position, position + groundDelta); - //Console.PrintDebug(1, true, "startSolid: " + traceGround.startSolid); - - if (!traceGround.startSolid && traceGround.fraction < 1f && - -Vector3.Dot(Gravity.Normalized, traceGround.hitNormal) < slopeNormal) - { - // clip velocity - - Vector3 bounce = groundDelta; - //Vector3 velocityProjected = Vector3.ProjectOnPlane(velocity, normal); - float backoff = Vector3.Dot(bounce, traceGround.hitNormal) * 2f; - bounce -= traceGround.hitNormal * backoff; - //velocity = velocityProjected; - - Vector3 point = (position + groundDelta) + - (1f - traceGround.fraction) * bounce; - - Console.Print("backoff: " + backoff); - - // retrace - traceGround = TracePlayer(Actor, position, position + point); - } - - if (!traceGround.startSolid && (traceGround.fraction >= 1f || - -Vector3.Dot(Gravity.Normalized, traceGround.hitNormal) < slopeNormal)) - { - // falling or sliding down a slope - onGround = false; - //Console.PrintDebug(1, true, "fall or slide"); - } - else - { - //if (onGround != !traceGround.startSolid) - // Console.Print("slidefrac: " + traceGround.fraction); - onGround = !traceGround.startSolid; - //Console.PrintDebug(1, true, "issolid? :" + traceGround.startSolid); - } - - //if (onGround && !slope) - // velocity.Y = 0f; - - // TODO: snap to ground here? - return traceGround; - } - - private bool OnJump(TraceInfo traceGround, ref Vector3 position, ref Vector3 velocity) - { - float jumpVel = jumpVelocity; - if (Time.GameTime - lastJumped < jumpBoostTime) - jumpVel += jumpBoostVelocity; - - // Reset velocity from gravity - if (-Vector3.Dot(Gravity.Normalized, velocity) < 0 && - Vector3.Dot(velocity, traceGround.hitNormal) < -0.1) - { - velocity = Vector3.ProjectOnPlane(velocity, traceGround.hitNormal); - } - - velocity += Vector3.Up * jumpVel; - - bool ktjump = true; - if (ktjump) - { - if (velocity.Y < jumpVel) - velocity.Y = jumpVel; - } - - onGround = false; - - // Allow stairs to eat the first jump to allow easy stair jumps - if (jumpStairBehavior && jumpBoostMaxJumps >= 2 && numJumps == 0 && -Vector3.Dot(Gravity.Normalized, traceGround.hitNormal) > 0.85) - { - // Try stepping into stairs without vertical velocity - Vector3 stairCheckPosition = position; - Vector3 stairCheckVelocity = velocity.Normalized * (stepHeight / Time.DeltaTime); - stairCheckVelocity.Y = 0f; - - SlideMoveHit blocked = StepSlideMove(Actor, ref stairCheckPosition, ref stairCheckVelocity, true); - float movedUp = stairCheckPosition.Y - position.Y; - - if (movedUp > 0 && blocked.HasFlag(SlideMoveHit.Step)) - { - velocity.Y = 0f; - onGround = true; - } - } - - if (!predicting) - { - // Avoid overlapping with recent landing sound - if (Time.GameTime - lastLanded > 0.3) - { - PlayJumpLandSound(false, false); - } - } - - return true; - } - - private void OnLanded(Vector3 landingVelocity, bool hardLanding) - { - if (!predicting) - PlayJumpLandSound(true, hardLanding); - } - - private void PlayJumpLandSound(bool landing, bool hardLanding) - { - if (!landing) - lastLanded = -1; // Reset so double jumps have double sounds - - float volume1 = 0.8f; - float volume2 = volume1; - Vector2 pitchRange = new Vector2(0.9f, 1.05f); - Vector2 secondStepDelayRange = new Vector2(0.031f, 0.059f); - - if (landing) - volume2 *= 0.6f; - else - volume1 *= 0.6f; - - AudioManager.PlaySound("jumpland", Actor, 0, AudioFlags.None, rootActor.Position, volume1, pitchRange); - if (landing) - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, pitchRange); - } - - private static void ApplyFriction(ref Vector3 velocity) - { - float currentSpeed = velocity.Length; - - float control = currentSpeed < stopspeed ? stopspeed : currentSpeed; - var drop = control * friction * Time.DeltaTime; - - float newspeed = currentSpeed - drop; - if (newspeed < 0) - newspeed = 0; - - if (currentSpeed < 0.0001f) - velocity *= 0; - else - velocity *= newspeed / currentSpeed; - } - - private static void ApplyAirAcceleration(ref Vector3 velocity, Vector3 wishVelocity) - { - float wishspeed = wishVelocity.Length; - if (wishspeed > maxAirSpeed) - wishspeed = maxAirSpeed; - - Vector3 wishDir = wishVelocity.Normalized; - float wishspeedAirControl = wishspeed; - - if (airAcceleration != 0) - { - // Q2+ air acceleration - float accel = airAcceleration; - if (Vector3.Dot(velocity, wishDir) < 0) - accel = airStopAcceleration; - - if (airStrafeAcceleration != 0 && Mathf.Abs(wishVelocity.X) > 0 && wishVelocity.Y == 0) - { - // only strafe movement - if (wishspeed > maxAirStrafeSpeed) - wishspeed = maxAirStrafeSpeed; - - accel = airStrafeAcceleration; - } - - ApplyAcceleration(ref velocity, wishDir, wishspeed, float.MaxValue, accel); - } - - // QW air acceleration - if (strafeAcceleration != 0f) - { - ApplyAcceleration(ref velocity, wishDir, wishspeed, maxAirStrafeSpeed, - strafeAcceleration); - } - - // air control while holding forward/back buttons - //if (airControl != 0 && moveDirection.X == 0 && Mathf.Abs(moveDirection.Y) > 0) - // PM_Aircontrol(wishdir, wishspeedAirControl); - - // apply gravity - velocity += Gravity * Time.DeltaTime; - //Console.Print(Time.DeltaTime.ToString()); - } - - private static void ApplyAcceleration(ref Vector3 velocity, Vector3 wishDir, float wishspeed, float maxWishspeed, - float acceleration) - { - float wishspeedOrig = wishspeed; - if (wishspeed > maxWishspeed) - wishspeed = maxWishspeed; - - float currentSpeed = Vector3.Dot(velocity, wishDir); - float addSpeed = wishspeed - currentSpeed; - if (addSpeed <= 0f) - return; - - float accelSpeed = acceleration * wishspeedOrig * Time.DeltaTime; - if (accelSpeed > addSpeed) - accelSpeed = addSpeed; - - velocity += accelSpeed * wishDir; - } - } + private static SlideMoveHit StepSlideMove(Actor actor, ref Vector3 position, ref Vector3 velocity, + bool onGround) + { + if (velocity.IsZero) + return SlideMoveHit.Nothing; + + Vector3 gravityDirection = Gravity.Normalized; + + Vector3 originalPosition = position; + Vector3 originalVelocity = velocity; + + SlideMoveHit slideMoveHit = SlideMove(actor, ref position, ref velocity); + if (slideMoveHit == SlideMoveHit.Nothing) + // TODO: step down here + return slideMoveHit; + + // hit something, try to step up + float effectiveStepHeight = stepHeight; + if (!onGround) + { + // TODO: implement clipping here + /*if (pmove.jump_time > 0 && pmove.jump_time <= movevars.cliptime) + { + float zvel = pmove.velocity[2]; + VectorCopy(originalvel, pmove.velocity); + pmove.velocity[2] = min(pmove.velocity[2], zvel); // nullifies vertical clipping + }*/ + + if (!slideMoveHit.HasFlag(SlideMoveHit.Step)) + return slideMoveHit; + + if (airStep < 2) + { + //effectiveStepHeight = ? + } + } + + + Vector3 stepDelta = -gravityDirection * effectiveStepHeight; + + Vector3 slidePosition = position; + Vector3 slideVelocity = velocity; + position = originalPosition; + velocity = originalVelocity; + + // step up + Vector3 stepUp = position + stepDelta; + TraceInfo traceUp = TracePlayer(actor, position, stepUp); + if (traceUp.fraction > 0f) + position = traceUp.endPosition; + + // try moving from step up position + SlideMoveHit slideMoveStepHit = SlideMove(actor, ref position, ref velocity); + + // step down + Vector3 stepDown = position - stepDelta; + TraceInfo traceDown = TracePlayer(actor, position, stepDown); + if (traceDown.fraction < 1f && -Vector3.Dot(gravityDirection, traceDown.hitNormal) < slopeNormal) + { + // can't step down, slide move like normally + Console.Print("no stepping 1, frac: " + traceDown.fraction + ", dot: " + + -Vector3.Dot(gravityDirection, traceDown.hitNormal) + + ", norm: " + traceDown.hitNormal); + position = slidePosition; + velocity = slideVelocity; + return slideMoveHit; + } + + if (traceDown.fraction > 0f) + position = traceDown.endPosition; + + // add some margin from the ground in order to avoid getting stuck after stepping up + if (traceDown.fraction < 1f) + position.Y += collisionMargin; + + // ?? + float d1 = -Vector3.Dot(gravityDirection, position); + float d2 = -Vector3.Dot(gravityDirection, originalPosition); + if (d1 < d2) + { + //Console.Print("no stepping 2, " + d1 + " < " + d2); + position = slidePosition; + velocity = slideVelocity; + return slideMoveHit; + } + + Vector3 slidePosition2 = slidePosition; //down + Vector3 stepPosition2 = position; //up + + // FIXME, negate gravity + slidePosition2.Y = 0f; + stepPosition2.Y = 0f; + + // take the slide movement results if furthest away from original position + //if ((stepPosition2 - originalPosition).Length < (slidePosition2 - originalPosition).Length) + if ((slidePosition2 - originalPosition).Length >= (stepPosition2 - originalPosition).Length) + { + //Console.Print("no stepping 3"); + position = slidePosition; + velocity = slideVelocity; + return slideMoveHit; + } + + //return slideMoveStepHit; + return slideMoveHit; + } + + private static SlideMoveHit SlideMove(Actor actor, ref Vector3 position, ref Vector3 velocity) + { + if (velocity.IsZero) + return SlideMoveHit.Nothing; + + Vector3 originalPosition = position; + Vector3 originalVelocity = velocity; + SlideMoveHit slideMoveHit = SlideMoveHit.Nothing; + + float timeleft = Time.DeltaTime; + + var hitNormals = new List(); + + for (int bump = 0; bump < 4; bump++) + { + Vector3 startPos = position; + Vector3 endPos = position + velocity * timeleft; + + TraceInfo trace = TracePlayer(actor, startPos, endPos); + // TODO: handle portals here + + float fraction = trace.fraction; + Vector3 hitNormal = trace.hitNormal; + + if (trace.startSolid) + { + velocity = Vector3.Zero; + break; + } + + if (fraction > 0f) + { + position = trace.endPosition; + hitNormals.Clear(); // this is present in some forks, not in Q3 + } + + if (fraction >= 1f) + break; + + timeleft *= 1.0f - fraction; + + if (trace.hitNormal.Y > slopeNormal) + slideMoveHit |= SlideMoveHit.Floor; + else if (Math.Abs(trace.hitNormal.Y) < 0.0001f) + slideMoveHit |= SlideMoveHit.Step; + else + slideMoveHit |= SlideMoveHit.Other; + + // this doesn't seem to do anything, we never have any hitNormals stored here + bool hitPreviousNormal = false; + foreach (Vector3 normal in hitNormals) + if (Vector3.Dot(hitNormal, normal) > 0.99) + { + // nudge away from the same wall we hit earlier and try again + velocity += hitNormal; + hitPreviousNormal = true; + break; + } + + if (hitPreviousNormal) + continue; + + hitNormals.Add(hitNormal); + if (hitNormals.Count != 1) + Console.Print("hitNormals: " + hitNormals.Count); + + int plane; + Vector3 normalMargin = Vector3.Zero; + for (plane = 0; plane < hitNormals.Count; plane++) + { + Vector3 normal = hitNormals[plane]; + + // clip velocity + velocity -= normal * Vector3.Dot(velocity, normal); + //velocity = Vector3.ProjectOnPlane(velocity, normal); + + //traceOffset = normal * 1f; + normalMargin += normal; + //position += normal * 0.031f; + + int plane2; + for (plane2 = 0; plane2 < hitNormals.Count; plane2++) + { + if (plane == plane2) + continue; + + if (Vector3.Dot(velocity, hitNormals[plane2]) < 0f) + break; + } + + if (plane2 == hitNormals.Count) + break; + } + + // push off slightly away from the walls to not get stuck + position += normalMargin.Normalized * collisionMargin; + //Console.Print("pushin"); + + if (plane == hitNormals.Count) + { + if (hitNormals.Count == 2) + { + Vector3 dir = Vector3.Cross(hitNormals[0], hitNormals[1]); + //dir.Normalize(); + float dist = Vector3.Dot(dir, velocity); + velocity = dist * dir; + } + else + { + velocity = Vector3.Zero; + break; + } + } + + // prevents bouncing against the wall + if ( /*velocity.Length > 0f && */Vector3.Dot(velocity, originalVelocity) <= 0f) + { + velocity = Vector3.Zero; + break; + } + } + + return slideMoveHit; + } + + public void SimulatePlayerMovement(PlayerInputState inputState) + { + Transform rootTrans = rootActor.Transform; + + Vector3 inputDirection = + new Vector3(inputState.moveRight, 0.0f, inputState.moveForward); + Vector3 moveDirection = rootTrans.TransformDirection(inputDirection); + + Vector3 position = rigidBody.Position; + Vector3 velocity = currentVelocity; //rigidBody.LinearVelocity; + + Vector3 wishVelocity = Vector3.Zero; + if (!inputDirection.IsZero) + wishVelocity = moveDirection.Normalized * MoveSpeed; + + // categorize position + bool lastGround = onGround; + Vector3 lastVelocity = velocity; + onGround = true; + + TraceInfo traceGround = CategorizePosition(position, ref velocity); + + bool jumpAction = inputState.jumping; + + if (jumped && !jumpAction) + jumped = false; // jump released + else if (jumped && Time.GameTime - lastJumped >= autoJumpTime) + jumped = false; // jump timeout + + // jump + if (onGround && jumpAction && !jumped) + if (OnJump(traceGround, ref position, ref velocity)) + { + jumped = true; + lastJumped = Time.GameTime; + numJumps++; + } + + if (Time.GameTime - lastJumped > jumpBoostTime) + numJumps = 0; + + //if (/*onGround && */lastGround != onGround) + if (onGround) + { + // ground friction + ApplyFriction(ref velocity); + + // ground acceleration + ApplyAcceleration(ref velocity, wishVelocity.Normalized, wishVelocity.Length, float.MaxValue, + accelerationGround); + } + else // air movement + { + ApplyAirAcceleration(ref velocity, wishVelocity); + } + + StepSlideMove(Actor, ref position, ref velocity, onGround); + + + TraceInfo traceGround2 = CategorizePosition(position, ref velocity); + + rigidBody.Position = position; + currentVelocity = velocity; + //rigidBody.LinearVelocity = velocity; + + const float landingVelocityThreshold = 120f; + const float landingHardVelocityThreshold = 500f; + if (currentVelocity.Y - lastVelocity.Y > landingVelocityThreshold) + if (Time.GameTime - lastJumped > 0.01) + { + bool hardLanding = currentVelocity.Y - lastVelocity.Y > landingHardVelocityThreshold; + OnLanded(currentVelocity - lastVelocity, hardLanding); + lastLanded = Time.GameTime; + } + } + + private TraceInfo CategorizePosition(Vector3 position, ref Vector3 velocity) + { + Vector3 groundDelta = Gravity.Normalized; //Gravity.Normalized * (collisionMargin * 2); + //if (velocity.Y < 0f) + // groundDelta = Gravity.Normalized * velocity.Y * Time.DeltaTime; + TraceInfo traceGround = TracePlayer(Actor, position, position + groundDelta); + //Console.PrintDebug(1, true, "startSolid: " + traceGround.startSolid); + + if (!traceGround.startSolid && traceGround.fraction < 1f && + -Vector3.Dot(Gravity.Normalized, traceGround.hitNormal) < slopeNormal) + { + // clip velocity + + Vector3 bounce = groundDelta; + //Vector3 velocityProjected = Vector3.ProjectOnPlane(velocity, normal); + float backoff = Vector3.Dot(bounce, traceGround.hitNormal) * 2f; + bounce -= traceGround.hitNormal * backoff; + //velocity = velocityProjected; + + Vector3 point = position + groundDelta + + (1f - traceGround.fraction) * bounce; + + Console.Print("backoff: " + backoff); + + // retrace + traceGround = TracePlayer(Actor, position, position + point); + } + + if (!traceGround.startSolid && (traceGround.fraction >= 1f || + -Vector3.Dot(Gravity.Normalized, traceGround.hitNormal) < slopeNormal)) + // falling or sliding down a slope + onGround = false; + //Console.PrintDebug(1, true, "fall or slide"); + else + //if (onGround != !traceGround.startSolid) + // Console.Print("slidefrac: " + traceGround.fraction); + onGround = !traceGround.startSolid; + //Console.PrintDebug(1, true, "issolid? :" + traceGround.startSolid); + + //if (onGround && !slope) + // velocity.Y = 0f; + + // TODO: snap to ground here? + return traceGround; + } + + private bool OnJump(TraceInfo traceGround, ref Vector3 position, ref Vector3 velocity) + { + float jumpVel = jumpVelocity; + if (Time.GameTime - lastJumped < jumpBoostTime) + jumpVel += jumpBoostVelocity; + + // Reset velocity from gravity + if (-Vector3.Dot(Gravity.Normalized, velocity) < 0 && + Vector3.Dot(velocity, traceGround.hitNormal) < -0.1) + velocity = Vector3.ProjectOnPlane(velocity, traceGround.hitNormal); + + velocity += Vector3.Up * jumpVel; + + bool ktjump = true; + if (ktjump) + if (velocity.Y < jumpVel) + velocity.Y = jumpVel; + + onGround = false; + + // Allow stairs to eat the first jump to allow easy stair jumps + if (jumpStairBehavior && jumpBoostMaxJumps >= 2 && numJumps == 0 && + -Vector3.Dot(Gravity.Normalized, traceGround.hitNormal) > 0.85) + { + // Try stepping into stairs without vertical velocity + Vector3 stairCheckPosition = position; + Vector3 stairCheckVelocity = velocity.Normalized * (stepHeight / Time.DeltaTime); + stairCheckVelocity.Y = 0f; + + SlideMoveHit blocked = StepSlideMove(Actor, ref stairCheckPosition, ref stairCheckVelocity, true); + float movedUp = stairCheckPosition.Y - position.Y; + + if (movedUp > 0 && blocked.HasFlag(SlideMoveHit.Step)) + { + velocity.Y = 0f; + onGround = true; + } + } + + if (!predicting) + // Avoid overlapping with recent landing sound + if (Time.GameTime - lastLanded > 0.3) + PlayJumpLandSound(false, false); + + return true; + } + + private void OnLanded(Vector3 landingVelocity, bool hardLanding) + { + if (!predicting) + PlayJumpLandSound(true, hardLanding); + } + + private void PlayJumpLandSound(bool landing, bool hardLanding) + { + if (!landing) + lastLanded = -1; // Reset so double jumps have double sounds + + float volume1 = 0.8f; + float volume2 = volume1; + Vector2 pitchRange = new Vector2(0.9f, 1.05f); + Vector2 secondStepDelayRange = new Vector2(0.031f, 0.059f); + + if (landing) + volume2 *= 0.6f; + else + volume1 *= 0.6f; + + AudioManager.PlaySound("jumpland", Actor, 0, AudioFlags.None, rootActor.Position, volume1, pitchRange); + if (landing) + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + } + + private static void ApplyFriction(ref Vector3 velocity) + { + float currentSpeed = velocity.Length; + + float control = currentSpeed < stopspeed ? stopspeed : currentSpeed; + float drop = control * friction * Time.DeltaTime; + + float newspeed = currentSpeed - drop; + if (newspeed < 0) + newspeed = 0; + + if (currentSpeed < 0.0001f) + velocity *= 0; + else + velocity *= newspeed / currentSpeed; + } + + private static void ApplyAirAcceleration(ref Vector3 velocity, Vector3 wishVelocity) + { + float wishspeed = wishVelocity.Length; + if (wishspeed > maxAirSpeed) + wishspeed = maxAirSpeed; + + Vector3 wishDir = wishVelocity.Normalized; + float wishspeedAirControl = wishspeed; + + if (airAcceleration != 0) + { + // Q2+ air acceleration + float accel = airAcceleration; + if (Vector3.Dot(velocity, wishDir) < 0) + accel = airStopAcceleration; + + if (airStrafeAcceleration != 0 && Mathf.Abs(wishVelocity.X) > 0 && wishVelocity.Y == 0) + { + // only strafe movement + if (wishspeed > maxAirStrafeSpeed) + wishspeed = maxAirStrafeSpeed; + + accel = airStrafeAcceleration; + } + + ApplyAcceleration(ref velocity, wishDir, wishspeed, float.MaxValue, accel); + } + + // QW air acceleration + if (strafeAcceleration != 0f) + ApplyAcceleration(ref velocity, wishDir, wishspeed, maxAirStrafeSpeed, + strafeAcceleration); + + // air control while holding forward/back buttons + //if (airControl != 0 && moveDirection.X == 0 && Mathf.Abs(moveDirection.Y) > 0) + // PM_Aircontrol(wishdir, wishspeedAirControl); + + // apply gravity + velocity += Gravity * Time.DeltaTime; + //Console.Print(Time.DeltaTime.ToString()); + } + + private static void ApplyAcceleration(ref Vector3 velocity, Vector3 wishDir, float wishspeed, + float maxWishspeed, + float acceleration) + { + float wishspeedOrig = wishspeed; + if (wishspeed > maxWishspeed) + wishspeed = maxWishspeed; + + float currentSpeed = Vector3.Dot(velocity, wishDir); + float addSpeed = wishspeed - currentSpeed; + if (addSpeed <= 0f) + return; + + float accelSpeed = acceleration * wishspeedOrig * Time.DeltaTime; + if (accelSpeed > addSpeed) + accelSpeed = addSpeed; + + velocity += accelSpeed * wishDir; + } + + [Flags] + private enum SlideMoveHit + { + Nothing = 0, + Step = 1, + Floor = 2, + Other = 4 + } + } } \ No newline at end of file