namespacify everything

This commit is contained in:
2024-04-06 14:42:10 +03:00
parent 68b735b59c
commit 6430cc9b4d
42 changed files with 10066 additions and 10107 deletions

View File

@@ -6,251 +6,250 @@ using FlaxEngine;
using Console = Game.Console; using Console = Game.Console;
using Object = FlaxEngine.Object; using Object = FlaxEngine.Object;
namespace Game namespace Game;
{
[Flags]
public enum AudioFlags
{
None = 0,
/// Avoid replacing the existing playing audio source in this channel. [Flags]
ContinuePlayingExistingSource = 1 public enum AudioFlags
{
None = 0,
/// Avoid replacing the existing playing audio source in this channel.
ContinuePlayingExistingSource = 1
}
public static class AudioManager
{
private static readonly Random random = new Random();
private static readonly Dictionary<string, AudioInfo> cachedAudioInfos = new Dictionary<string, AudioInfo>();
private static readonly Dictionary<Actor, ActorAudioChannels> actorAudioChannels =
new Dictionary<Actor, ActorAudioChannels>();
public static void PlaySound(string soundName, Actor actor, Float3 position, float volume = 1f)
{
PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, Float2.One, Float2.Zero);
} }
public static class AudioManager public static void PlaySound(string soundName, Actor actor, Float3 position, float volume, Float2 pitchRange)
{ {
private static readonly Random random = new Random(); PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, pitchRange, Float2.Zero);
}
private static readonly Dictionary<string, AudioInfo> cachedAudioInfos = new Dictionary<string, AudioInfo>(); public static void PlaySound(string soundName, Actor actor, int channel, Float3 position, float volume = 1f)
{
PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, Float2.Zero);
}
private static readonly Dictionary<Actor, ActorAudioChannels> actorAudioChannels = public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position,
new Dictionary<Actor, ActorAudioChannels>(); float volume = 1f)
{
PlaySoundInternal(soundName, actor, channel, flags, position, volume, Float2.One, Float2.Zero);
}
public static void PlaySound(string soundName, Actor actor, Float3 position, float volume = 1f) public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position,
float volume, Float2 pitchRange)
{
PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, Float2.Zero);
}
public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel,
Float3 position, float volume = 1f)
{
PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, delayRange);
}
public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel,
Float3 position, float volume, Float2 pitchRange)
{
PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, pitchRange, delayRange);
}
public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel,
AudioFlags flags, Float3 position, float volume, Float2 pitchRange)
{
PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, delayRange);
}
private static void PlaySoundInternal(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position,
float volume, Float2 pitchRange, Float2 delayRange)
{
AudioInfo audio = GetSound(soundName);
if (audio.AudioClips.Length == 0)
return;
channel = Math.Max(channel, 0);
ActorAudioChannels actorChannels = null;
if (channel > 0)
{ {
PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, Float2.One, Float2.Zero); if (!actorAudioChannels.TryGetValue(actor, out actorChannels))
}
public static void PlaySound(string soundName, Actor actor, Float3 position, float volume, Float2 pitchRange)
{
PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, pitchRange, Float2.Zero);
}
public static void PlaySound(string soundName, Actor actor, int channel, Float3 position, float volume = 1f)
{
PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, Float2.Zero);
}
public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position,
float volume = 1f)
{
PlaySoundInternal(soundName, actor, channel, flags, position, volume, Float2.One, Float2.Zero);
}
public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position,
float volume, Float2 pitchRange)
{
PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, Float2.Zero);
}
public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel,
Float3 position, float volume = 1f)
{
PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, delayRange);
}
public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel,
Float3 position, float volume, Float2 pitchRange)
{
PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, pitchRange, delayRange);
}
public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel,
AudioFlags flags, Float3 position, float volume, Float2 pitchRange)
{
PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, delayRange);
}
private static void PlaySoundInternal(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position,
float volume, Float2 pitchRange, Float2 delayRange)
{
AudioInfo audio = GetSound(soundName);
if (audio.AudioClips.Length == 0)
return;
channel = Math.Max(channel, 0);
ActorAudioChannels actorChannels = null;
if (channel > 0)
{ {
if (!actorAudioChannels.TryGetValue(actor, out actorChannels)) actorChannels = new ActorAudioChannels();
{ actorAudioChannels.Add(actor, actorChannels);
actorChannels = new ActorAudioChannels();
actorAudioChannels.Add(actor, actorChannels);
}
if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource))
actorChannels.channelSources.Add(channel, null);
if (existingAudioSource && existingAudioSource != null &&
existingAudioSource.State == AudioSource.States.Playing)
{
if (flags.HasFlag(AudioFlags.ContinuePlayingExistingSource))
return;
existingAudioSource.Stop();
Object.Destroy(existingAudioSource);
actorChannels.channelSources[channel] = null;
}
} }
AudioClip audioClip;
if (audio.AudioClips.Length > 1)
{
// Randomize selected clip while avoiding the last used clip
int randomIndex = 0;
for (int i = 0; i < 10; i++)
{
randomIndex = random.Next(audio.AudioClips.Length);
if (randomIndex != audio.lastAudioPlayed)
break;
}
audioClip = audio.AudioClips[randomIndex];
audio.lastAudioPlayed = randomIndex;
}
else
{
audioClip = audio.AudioClips[0];
}
// Randomized pitch
float pitch;
if (pitchRange[0] < pitchRange[1])
pitch = (float)(pitchRange[0] + random.NextDouble() * (pitchRange[1] - pitchRange[0]));
else
pitch = pitchRange[0];
// Randomized start delay
float randomDelay;
if (delayRange[0] < delayRange[1])
randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0]));
else
randomDelay = delayRange[0];
AudioSourceDelayed audioSource = new AudioSourceDelayed();
audioSource.Delay = randomDelay;
audioSource.Clip = audioClip;
audioSource.Position = position;
audioSource.Parent = actor.Parent;
audioSource.Pitch = pitch;
audioSource.Name = Path.GetFileNameWithoutExtension(audioClip.Path);
audioSource.Volume = volume;
audioSource.PlayOnStart = randomDelay == 0;
if (volume != 1f)
audioSource.Name += ", vol: " + volume;
if (pitch != 1f)
audioSource.Name += ", pitch: " + pitch;
if (pitch != 0f)
audioSource.Name += ", delay: " + randomDelay;
Console.Print("playing sound " + audioSource.Name);
if (channel > 0)
actorChannels.channelSources[channel] = audioSource;
}
public static void StopSound(Actor actor, int channel)
{
if (channel <= 0)
return;
if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels))
return;
if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource))
return; actorChannels.channelSources.Add(channel, null);
if (existingAudioSource && existingAudioSource != null && if (existingAudioSource && existingAudioSource != null &&
existingAudioSource.State == AudioSource.States.Playing) existingAudioSource.State == AudioSource.States.Playing)
{ {
if (flags.HasFlag(AudioFlags.ContinuePlayingExistingSource))
return;
existingAudioSource.Stop(); existingAudioSource.Stop();
Object.Destroy(existingAudioSource); Object.Destroy(existingAudioSource);
actorChannels.channelSources[channel] = null; actorChannels.channelSources[channel] = null;
} }
} }
public static bool IsSoundPlaying(Actor actor, int channel) AudioClip audioClip;
if (audio.AudioClips.Length > 1)
{ {
if (channel <= 0) // Randomize selected clip while avoiding the last used clip
return false; int randomIndex = 0;
for (int i = 0; i < 10; i++)
{
randomIndex = random.Next(audio.AudioClips.Length);
if (randomIndex != audio.lastAudioPlayed)
break;
}
if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels)) audioClip = audio.AudioClips[randomIndex];
return false; audio.lastAudioPlayed = randomIndex;
}
else
{
audioClip = audio.AudioClips[0];
}
if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) // Randomized pitch
return false; float pitch;
if (pitchRange[0] < pitchRange[1])
pitch = (float)(pitchRange[0] + random.NextDouble() * (pitchRange[1] - pitchRange[0]));
else
pitch = pitchRange[0];
if (existingAudioSource && existingAudioSource != null && // Randomized start delay
existingAudioSource.State == AudioSource.States.Playing) float randomDelay;
return true; if (delayRange[0] < delayRange[1])
randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0]));
else
randomDelay = delayRange[0];
AudioSourceDelayed audioSource = new AudioSourceDelayed();
audioSource.Delay = randomDelay;
audioSource.Clip = audioClip;
audioSource.Position = position;
audioSource.Parent = actor.Parent;
audioSource.Pitch = pitch;
audioSource.Name = Path.GetFileNameWithoutExtension(audioClip.Path);
audioSource.Volume = volume;
audioSource.PlayOnStart = randomDelay == 0;
if (volume != 1f)
audioSource.Name += ", vol: " + volume;
if (pitch != 1f)
audioSource.Name += ", pitch: " + pitch;
if (pitch != 0f)
audioSource.Name += ", delay: " + randomDelay;
Console.Print("playing sound " + audioSource.Name);
if (channel > 0)
actorChannels.channelSources[channel] = audioSource;
}
public static void StopSound(Actor actor, int channel)
{
if (channel <= 0)
return;
if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels))
return;
if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource))
return;
if (existingAudioSource && existingAudioSource != null &&
existingAudioSource.State == AudioSource.States.Playing)
{
existingAudioSource.Stop();
Object.Destroy(existingAudioSource);
actorChannels.channelSources[channel] = null;
}
}
public static bool IsSoundPlaying(Actor actor, int channel)
{
if (channel <= 0)
return false; return false;
}
private static AudioInfo GetSound(string soundName) if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels))
return false;
if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource))
return false;
if (existingAudioSource && existingAudioSource != null &&
existingAudioSource.State == AudioSource.States.Playing)
return true;
return false;
}
private static AudioInfo GetSound(string soundName)
{
if (cachedAudioInfos.TryGetValue(soundName, out AudioInfo cachedAudio))
return cachedAudio;
AudioInfo audio = new AudioInfo();
string audioBasePath = Path.Combine(AssetManager.ContentPath, "Audio");
AudioClip audioClip = Content.Load<AudioClip>(Path.Combine(audioBasePath, soundName + ".flax"));
if (audioClip != null)
{ {
if (cachedAudioInfos.TryGetValue(soundName, out AudioInfo cachedAudio)) audio.AudioClips = new[] { audioClip };
return cachedAudio; }
else
AudioInfo audio = new AudioInfo(); {
// Check if this audio has multiple variations
string audioBasePath = Path.Combine(AssetManager.ContentPath, "Audio"); var audioClips = new List<AudioClip>();
AudioClip audioClip = Content.Load<AudioClip>(Path.Combine(audioBasePath, soundName + ".flax")); for (int i = 1; i < 50; i++)
if (audioClip != null)
{ {
audio.AudioClips = new[] { audioClip }; // TODO: make this more efficient, maybe get a list of assets and filter by name?
AudioClip audioClipVariation =
Content.Load<AudioClip>(Path.Combine(audioBasePath, soundName + "_var" + i + ".flax"));
if (audioClipVariation == null)
break;
audioClips.Add(audioClipVariation);
} }
if (audioClips.Count > 0)
audio.AudioClips = audioClips.ToArray();
else else
{ Console.PrintError("AudioClip '" + soundName + "' not found");
// Check if this audio has multiple variations
var audioClips = new List<AudioClip>();
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<AudioClip>(Path.Combine(audioBasePath, soundName + "_var" + i + ".flax"));
if (audioClipVariation == null)
break;
audioClips.Add(audioClipVariation);
}
if (audioClips.Count > 0)
audio.AudioClips = audioClips.ToArray();
else
Console.PrintError("AudioClip '" + soundName + "' not found");
}
audio.lastAudioPlayed = audio.AudioClips.Length + 1;
cachedAudioInfos.Add(soundName, audio);
return audio;
} }
private class AudioInfo audio.lastAudioPlayed = audio.AudioClips.Length + 1;
{
public AudioClip[] AudioClips;
public int lastAudioPlayed;
}
private class ActorAudioChannels cachedAudioInfos.Add(soundName, audio);
{ return audio;
public readonly Dictionary<int, AudioSource> channelSources; }
public ActorAudioChannels() private class AudioInfo
{ {
channelSources = new Dictionary<int, AudioSource>(); public AudioClip[] AudioClips;
} public int lastAudioPlayed;
}
private class ActorAudioChannels
{
public readonly Dictionary<int, AudioSource> channelSources;
public ActorAudioChannels()
{
channelSources = new Dictionary<int, AudioSource>();
} }
} }
} }

View File

@@ -5,43 +5,42 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Game namespace Game;
{
public class AudioSourceDelayed : AudioSource
{
public float Delay = 0f;
public override void OnBeginPlay()
{
if (Delay >= 0f)
{
PlayOnStart = false;
var script = AddScript<AudioSourceDelayedScript>();
script.PlayStartTime = Delay + FlaxEngine.Time.GameTime;
}
base.OnBeginPlay(); public class AudioSourceDelayed : AudioSource
{
public float Delay = 0f;
public override void OnBeginPlay()
{
if (Delay >= 0f)
{
PlayOnStart = false;
var script = AddScript<AudioSourceDelayedScript>();
script.PlayStartTime = Delay + FlaxEngine.Time.GameTime;
} }
base.OnBeginPlay();
} }
public class AudioSourceDelayedScript : Script }
public class AudioSourceDelayedScript : Script
{
[NoSerialize]
[HideInEditor]
public float PlayStartTime = 0f;
public override void OnStart()
{ {
[NoSerialize] FlaxEngine.Object.Destroy(Actor, Actor.As<AudioSourceDelayed>().Clip.Length + Actor.As<AudioSourceDelayed>().Delay);
[HideInEditor] }
public float PlayStartTime = 0f;
public override void OnStart() public override void OnFixedUpdate()
{
if (FlaxEngine.Time.GameTime >= PlayStartTime)
{ {
FlaxEngine.Object.Destroy(Actor, Actor.As<AudioSourceDelayed>().Clip.Length + Actor.As<AudioSourceDelayed>().Delay); PlayStartTime = float.MaxValue;
} Actor.As<AudioSourceDelayed>().Play();
public override void OnFixedUpdate()
{
if (FlaxEngine.Time.GameTime >= PlayStartTime)
{
PlayStartTime = float.MaxValue;
Actor.As<AudioSourceDelayed>().Play();
}
} }
} }
} }

View File

@@ -1,99 +1,98 @@
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
public class CameraMovement : Script
{ {
public class CameraMovement : Script private float viewPitch;
private float viewRoll;
private float viewYaw;
[Limit(0, 9000)]
[Tooltip("Camera speed")]
public float MoveSpeed { get; set; } = 400;
public override void OnStart()
{ {
private float viewPitch; Float3 initialEulerAngles = Actor.Orientation.EulerAngles;
private float viewRoll; viewPitch = initialEulerAngles.X;
private float viewYaw; viewYaw = initialEulerAngles.Y;
viewRoll = initialEulerAngles.Z;
}
[Limit(0, 9000)] public override void OnUpdate()
[Tooltip("Camera speed")] {
public float MoveSpeed { get; set; } = 400; Transform camTrans = Actor.Transform;
Actor rootActor = Actor.GetChild(0);
Camera camera = rootActor.GetChild<Camera>();
public override void OnStart() float xAxis = InputManager.GetAxisRaw("Mouse X");
float yAxis = InputManager.GetAxisRaw("Mouse Y");
if (xAxis != 0.0f || yAxis != 0.0f)
{ {
Float3 initialEulerAngles = Actor.Orientation.EulerAngles; viewPitch += yAxis;
viewPitch = initialEulerAngles.X; viewYaw += xAxis;
viewYaw = initialEulerAngles.Y;
viewRoll = initialEulerAngles.Z; 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);
} }
public override void OnUpdate() float inputH = InputManager.GetAxis("Horizontal");
float inputV = InputManager.GetAxis("Vertical");
Float3 move = new Float3(inputH, 0.0f, inputV);
if (!move.IsZero)
{ {
Transform camTrans = Actor.Transform; move.Normalize();
Actor rootActor = Actor.GetChild(0); move = camera.Transform.TransformDirection(move) * MoveSpeed;
Camera camera = rootActor.GetChild<Camera>();
float xAxis = InputManager.GetAxisRaw("Mouse X");
float yAxis = InputManager.GetAxisRaw("Mouse Y");
if (xAxis != 0.0f || yAxis != 0.0f)
{ {
viewPitch += yAxis; Float3 delta = move * Time.UnscaledDeltaTime;
viewYaw += xAxis; float movementLeft = delta.Length;
viewPitch = Mathf.Clamp(viewPitch, -90.0f, 90.0f); // TODO: check multiple times in case we get stuck in walls
// root orientation must be set first float sphereRadius = 10.0f; // TODO: use collider radius
rootActor.Orientation = Quaternion.Euler(0, viewYaw, 0); RayCastHit[] hitInfos;
camera.Orientation = Quaternion.Euler(viewPitch, viewYaw, viewRoll); float moveDist = delta.Length;
} Physics.SphereCastAll(Actor.Transform.Translation, sphereRadius, move.Normalized, out hitInfos,
moveDist);
float inputH = InputManager.GetAxis("Horizontal");
float inputV = InputManager.GetAxis("Vertical");
Float3 move = new Float3(inputH, 0.0f, inputV);
if (!move.IsZero)
{
move.Normalize();
move = camera.Transform.TransformDirection(move) * MoveSpeed;
//bool nohit = true;
float hitDistance = moveDist;
Float3 hitNormal = move.Normalized;
foreach (RayCastHit hitInfo in hitInfos)
{ {
Float3 delta = move * Time.UnscaledDeltaTime; if (hitInfo.Collider.Parent == Parent)
float movementLeft = delta.Length; continue;
// TODO: check multiple times in case we get stuck in walls if (hitInfo.Distance < hitDistance)
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;
Float3 hitNormal = move.Normalized;
foreach (RayCastHit hitInfo in hitInfos)
{ {
if (hitInfo.Collider.Parent == Parent) hitDistance = hitInfo.Distance;
continue; hitNormal = hitInfo.Normal;
if (hitInfo.Distance < hitDistance)
{
hitDistance = hitInfo.Distance;
hitNormal = hitInfo.Normal;
}
//nohit = false;
//break;
} }
//nohit = false;
if (hitDistance != moveDist) //break;
//camTrans.Translation = Float3.Lerp(Actor.Transform.Translation, camTrans.Translation, hitDistance);
//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 += -Float3.Dot(delta, hitNormal) * hitNormal; // correct?
camTrans.Translation += delta;
} }
}
Actor.Transform = camTrans; if (hitDistance != moveDist)
//camTrans.Translation = Float3.Lerp(Actor.Transform.Translation, camTrans.Translation, hitDistance);
//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 += -Float3.Dot(delta, hitNormal) * hitNormal; // correct?
camTrans.Translation += delta;
}
} }
Actor.Transform = camTrans;
} }
} }

View File

@@ -1,182 +1,181 @@
using FlaxEditor.Content.Settings; using FlaxEditor.Content.Settings;
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
/// <summary>
/// CameraRender Script.
/// </summary>
[ExecuteInEditMode]
public class CameraRender : PostProcessEffect
{ {
/// <summary> public Camera camera;
/// CameraRender Script.
/// </summary> private bool lastEnabled;
[ExecuteInEditMode] public Material material;
public class CameraRender : PostProcessEffect
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 bool useMainCamera = true;
public bool rescaleModel = true;
private Actor viewModelHolder;
private void CreateTextures(int width, int height)
{ {
public Camera camera; GPUTextureDescription textureDesc = GPUTextureDescription.New2D(width, height, PixelFormat.R8G8B8A8_UNorm);
private bool lastEnabled; // Prepare texture and SceneRenderTask for viewmodel camera
public Material material; if (texture == null)
texture = new GPUTexture();
if (texture.Init(ref textureDesc))
Console.Print("Failed to create camera texture");
private MaterialInstance materialInstance; // Prepare depth texture and SceneRenderTask for viewmodel camera
private SceneRenderTask sceneTask; textureDesc.Format = PixelFormat.R8_UNorm;
private SceneRenderTask sceneTask2; if (texture2 == null)
texture2 = new GPUTexture();
if (texture2.Init(ref textureDesc))
Console.Print("Failed to create camera depth texture");
}
private GPUTexture texture; public override void OnAwake()
private GPUTexture texture2; {
viewModelHolder = Parent.Parent.Parent.Parent.GetChild("ViewModelHolder");
//public override PostProcessEffectLocation Location => PostProcessEffectLocation.Default; if (useMainCamera)
//public override int Order => 110;
//public override bool CanRender => camera.IsActive;
private bool useMainCamera = true;
public bool rescaleModel = true;
private Actor viewModelHolder;
private void CreateTextures(int width, int height)
{ {
GPUTextureDescription textureDesc = GPUTextureDescription.New2D(width, height, PixelFormat.R8G8B8A8_UNorm); camera.IsActive = false;
void foo(Actor actor)
// Prepare texture and SceneRenderTask for viewmodel camera
if (texture == null)
texture = new GPUTexture();
if (texture.Init(ref textureDesc))
Console.Print("Failed to create camera texture");
// Prepare depth texture and SceneRenderTask for viewmodel camera
textureDesc.Format = PixelFormat.R8_UNorm;
if (texture2 == null)
texture2 = new GPUTexture();
if (texture2.Init(ref textureDesc))
Console.Print("Failed to create camera depth texture");
}
public override void OnAwake()
{
viewModelHolder = Parent.Parent.Parent.Parent.GetChild("ViewModelHolder");
if (useMainCamera)
{ {
camera.IsActive = false; actor.Layer = 0;
void foo(Actor actor) foreach (Actor actChild in actor.GetChildren<Actor>())
{ foo(actChild);
actor.Layer = 0;
foreach (Actor actChild in actor.GetChildren<Actor>())
foo(actChild);
}
foo(viewModelHolder);
} }
if (!camera.IsActive) foo(viewModelHolder);
return;
CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height);
// Color pass
sceneTask = new SceneRenderTask();
sceneTask.Order = -1;
sceneTask.Camera = camera;
sceneTask.ViewMode = ViewMode.Default;
sceneTask.Output = texture;
sceneTask.ViewFlags = ViewFlags.DefaultGame;
sceneTask.Enabled = true;
sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage;
// Depth pass
sceneTask2 = new SceneRenderTask();
sceneTask2.Order = -2;
sceneTask2.Camera = camera;
sceneTask2.ViewMode = ViewMode.Depth;
sceneTask2.Output = texture2;
sceneTask2.ViewFlags = ViewFlags.None;
sceneTask2.Enabled = true;
sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage;
// Setup material instance and parameters
if (materialInstance == null)
materialInstance = material.CreateVirtualInstance();
materialInstance.SetParameterValue("Input", texture);
materialInstance.SetParameterValue("Depth", texture2);
materialInstance.SetParameterValue("New parameter", true);
materialInstance.SetParameterValue("New parameter 0", ChannelMask.Blue);
materialInstance.SetParameterValue("New parameter 1", new Color(0.67f));
materialInstance.SetParameterValue("New parameter 3", 123f);
materialInstance.SetParameterValue("New parameter 4", new Float2(1,2));
materialInstance.SetParameterValue("New parameter 5", new Float3(1,2,3));
materialInstance.SetParameterValue("New parameter 6", new Float4(1,2,3,4));
materialInstance.SetParameterValue("New parameter 8", 123);
materialInstance.SetParameterValue("New parameter 9", new Matrix(0.666f));
materialInstance.SetParameterValue("New parameter 11", new Quaternion(0.5f, 0.5f, 0.5f, 0.5f));
materialInstance.SetParameterValue("New parameter 13", new Vector2(1,2));
materialInstance.SetParameterValue("New parameter 14", new Vector3(1,2,3));
materialInstance.SetParameterValue("New parameter 15", new Vector4(1,2,3,4));
//materialInstance.SetParameterValue("New parameter 16", new Transform(new Vector3(1,2,3), new Quaternion(0.5f, 0.5f, 0.5f, 0.5f)));
lastEnabled = true;
} }
public override void OnDestroy() if (!camera.IsActive)
{ return;
Destroy(ref sceneTask);
Destroy(ref sceneTask2);
Destroy(ref texture);
Destroy(ref texture2);
}
public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height);
GPUTexture output)
{
if (texture == null || texture2 == null)
return;
Renderer.DrawPostFxMaterial(context, ref renderContext, materialInstance, output, input.View()); // Color pass
} sceneTask = new SceneRenderTask();
sceneTask.Order = -1;
sceneTask.Camera = camera;
sceneTask.ViewMode = ViewMode.Default;
sceneTask.Output = texture;
sceneTask.ViewFlags = ViewFlags.DefaultGame;
sceneTask.Enabled = true;
sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage;
private bool lastRescale = false; // Depth pass
public override void OnUpdate() sceneTask2 = new SceneRenderTask();
{ sceneTask2.Order = -2;
sceneTask2.Camera = camera;
sceneTask2.ViewMode = ViewMode.Depth;
sceneTask2.Output = texture2;
sceneTask2.ViewFlags = ViewFlags.None;
sceneTask2.Enabled = true;
sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage;
// Setup material instance and parameters
if (materialInstance == null)
materialInstance = material.CreateVirtualInstance();
materialInstance.SetParameterValue("Input", texture);
materialInstance.SetParameterValue("Depth", texture2);
materialInstance.SetParameterValue("New parameter", true);
materialInstance.SetParameterValue("New parameter 0", ChannelMask.Blue);
materialInstance.SetParameterValue("New parameter 1", new Color(0.67f));
materialInstance.SetParameterValue("New parameter 3", 123f);
materialInstance.SetParameterValue("New parameter 4", new Float2(1,2));
materialInstance.SetParameterValue("New parameter 5", new Float3(1,2,3));
materialInstance.SetParameterValue("New parameter 6", new Float4(1,2,3,4));
materialInstance.SetParameterValue("New parameter 8", 123);
materialInstance.SetParameterValue("New parameter 9", new Matrix(0.666f));
materialInstance.SetParameterValue("New parameter 11", new Quaternion(0.5f, 0.5f, 0.5f, 0.5f));
materialInstance.SetParameterValue("New parameter 13", new Vector2(1,2));
materialInstance.SetParameterValue("New parameter 14", new Vector3(1,2,3));
materialInstance.SetParameterValue("New parameter 15", new Vector4(1,2,3,4));
//materialInstance.SetParameterValue("New parameter 16", new Transform(new Vector3(1,2,3), new Quaternion(0.5f, 0.5f, 0.5f, 0.5f)));
lastEnabled = true;
}
public override void OnDestroy()
{
Destroy(ref sceneTask);
Destroy(ref sceneTask2);
Destroy(ref texture);
Destroy(ref texture2);
}
public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input,
GPUTexture output)
{
if (texture == null || texture2 == null)
return;
Renderer.DrawPostFxMaterial(context, ref renderContext, materialInstance, output, input.View());
}
private bool lastRescale = false;
public override void OnUpdate()
{
#if FLAX_EDITOR #if FLAX_EDITOR
if (Input.GetKeyDown(KeyboardKeys.F7)) if (Input.GetKeyDown(KeyboardKeys.F7))
{ {
PhysicsSettings physicsSettings = GameSettings.Load<PhysicsSettings>(); PhysicsSettings physicsSettings = GameSettings.Load<PhysicsSettings>();
physicsSettings.EnableSubstepping = !physicsSettings.EnableSubstepping; physicsSettings.EnableSubstepping = !physicsSettings.EnableSubstepping;
GameSettings.Save(physicsSettings); GameSettings.Save(physicsSettings);
//GameSettings.Apply(); //GameSettings.Apply();
} }
#endif #endif
if (lastEnabled != camera.IsActive) if (lastEnabled != camera.IsActive)
{
lastEnabled = camera.IsActive;
sceneTask.Enabled = lastEnabled;
sceneTask2.Enabled = lastEnabled;
sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f;
sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f;
}
if (useMainCamera && rescaleModel != lastRescale)
{
lastRescale = rescaleModel;
if (rescaleModel)
viewModelHolder.Scale = new Float3(0.75f);
else
viewModelHolder.Scale = new Float3(1.0f);
}
if (!camera.IsActive)
return;
if (texture == null)
OnAwake();
if ((int)camera.Viewport.Width != texture.Width || (int)camera.Viewport.Height != texture.Height)
CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height);
}
public override void OnEnable()
{ {
//OnAwake(); lastEnabled = camera.IsActive;
sceneTask.Enabled = lastEnabled;
sceneTask2.Enabled = lastEnabled;
sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f;
sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f;
} }
if (useMainCamera && rescaleModel != lastRescale)
{
lastRescale = rescaleModel;
if (rescaleModel)
viewModelHolder.Scale = new Float3(0.75f);
else
viewModelHolder.Scale = new Float3(1.0f);
}
if (!camera.IsActive)
return;
if (texture == null)
OnAwake();
if ((int)camera.Viewport.Width != texture.Width || (int)camera.Viewport.Height != texture.Height)
CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height);
}
public override void OnEnable()
{
//OnAwake();
} }
} }

View File

@@ -1,75 +1,74 @@
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
public class CameraSpring : Script
{ {
public class CameraSpring : Script private bool lastGround;
private Float3 lastPosition;
public float percY;
private Actor playerActor;
private PlayerMovement playerMovement;
public float speed = 240f;
private Float3 targetOffset;
private Actor viewModelHolder;
public override void OnStart()
{ {
private bool lastGround; playerActor = Actor.Parent.Parent;
private Float3 lastPosition; playerMovement = playerActor.GetScript<PlayerMovement>();
public float percY; viewModelHolder = playerActor.GetChild("ViewModelHolder");
private Actor playerActor; lastGround = playerMovement.OnGround;
private PlayerMovement playerMovement; targetOffset = Actor.LocalPosition;
}
public float speed = 240f; private void UpdatePosition(Float3 position)
private Float3 targetOffset; {
private Actor viewModelHolder; Actor.Position = position;
viewModelHolder.Position = position;
}
public override void OnStart() public override void OnUpdate()
{
Float3 position = Actor.Parent.Position + targetOffset;
Float3 targetPosition = position;
if (playerMovement.OnGround)
{ {
playerActor = Actor.Parent.Parent; float deltaY = position.Y - lastPosition.Y;
playerMovement = playerActor.GetScript<PlayerMovement>(); //if (Mathf.Abs(deltaY) < 10f)
viewModelHolder = playerActor.GetChild("ViewModelHolder"); if (deltaY > 0)
lastGround = playerMovement.OnGround;
targetOffset = Actor.LocalPosition;
}
private void UpdatePosition(Float3 position)
{
Actor.Position = position;
viewModelHolder.Position = position;
}
public override void OnUpdate()
{
Float3 position = Actor.Parent.Position + targetOffset;
Float3 targetPosition = position;
if (playerMovement.OnGround)
{ {
float deltaY = position.Y - lastPosition.Y; if (deltaY > 100f)
//if (Mathf.Abs(deltaY) < 10f)
if (deltaY > 0)
{ {
if (deltaY > 100f) // Teleported, snap instantly
{ UpdatePosition(position);
// Teleported, snap instantly }
UpdatePosition(position); else
} {
else const float catchUpDistance = 10f;
{ const float catchUpMinMultip = 0.25f;
const float catchUpDistance = 10f; percY = Mathf.Abs(deltaY) / catchUpDistance;
const float catchUpMinMultip = 0.25f; percY = Mathf.Min(1.0f, percY + catchUpMinMultip);
percY = Mathf.Abs(deltaY) / catchUpDistance; percY *= percY;
percY = Mathf.Min(1.0f, percY + catchUpMinMultip);
percY *= percY;
float adjustSpeed = speed * Time.DeltaTime * percY; float adjustSpeed = speed * Time.DeltaTime * percY;
position.Y = lastPosition.Y; //-= deltaY; position.Y = lastPosition.Y; //-= deltaY;
position.Y = Mathf.MoveTowards(position.Y, targetPosition.Y, adjustSpeed); position.Y = Mathf.MoveTowards(position.Y, targetPosition.Y, adjustSpeed);
UpdatePosition(position); UpdatePosition(position);
}
} }
} }
else
{
UpdatePosition(position);
}
lastPosition = position;
lastGround = playerMovement.OnGround;
} }
else
{
UpdatePosition(position);
}
lastPosition = position;
lastGround = playerMovement.OnGround;
} }
} }

View File

@@ -1,83 +1,82 @@
using System; using System;
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
public class WeaponSway : Script
{ {
public class WeaponSway : Script private Actor cameraHolder;
private Actor rootActor;
public float swaySpeed = 3000f;
private float timeRemainder;
public override void OnStart()
{ {
private Actor cameraHolder; rootActor = Actor.Parent.GetChild("RootActor");
cameraHolder = rootActor.GetChild("CameraHolder");
Actor.LocalOrientation = GetRotation();
}
private Actor rootActor; private Quaternion GetRotation()
public float swaySpeed = 3000f; {
Quaternion pitch = cameraHolder.LocalOrientation;
Quaternion yawRoll = rootActor.LocalOrientation;
return yawRoll * pitch;
}
private float timeRemainder; public override void OnLateUpdate()
{
Quaternion rotation = GetRotation();
public override void OnStart() Float3 targetAngles = rotation.EulerAngles;
Float3 angles = Actor.LocalOrientation.EulerAngles;
// Ensure the swaying is smooth when framerate fluctuates slightly
float remaining = Time.DeltaTime + timeRemainder;
const float minTime = 1f / 120f;
do
{ {
rootActor = Actor.Parent.GetChild("RootActor"); float stepTime = Mathf.Min(Time.DeltaTime, minTime);
cameraHolder = rootActor.GetChild("CameraHolder"); remaining -= stepTime;
Actor.LocalOrientation = GetRotation(); float swaySpeedScaled = swaySpeed * stepTime;
}
private Quaternion GetRotation() float deltaX = Mathf.DeltaAngle(angles.X, targetAngles.X);
{ float deltaY = Mathf.DeltaAngle(angles.Y, targetAngles.Y);
Quaternion pitch = cameraHolder.LocalOrientation; float deltaZ = Mathf.DeltaAngle(angles.Z, targetAngles.Z);
Quaternion yawRoll = rootActor.LocalOrientation;
return yawRoll * pitch;
}
public override void OnLateUpdate() const float maxAngle = 30f;
{ if (deltaX > maxAngle)
Quaternion rotation = GetRotation(); angles.X -= maxAngle - deltaX;
else if (deltaX < -maxAngle)
angles.X += maxAngle + deltaX;
if (deltaY > maxAngle)
angles.Y -= maxAngle - deltaY;
else if (deltaY < -maxAngle)
angles.Y += maxAngle + deltaY;
if (deltaZ > maxAngle)
angles.Z -= maxAngle - deltaZ;
else if (deltaZ < -maxAngle)
angles.Z += maxAngle + deltaZ;
Float3 targetAngles = rotation.EulerAngles; float percX = Mathf.Abs(deltaX) / maxAngle;
Float3 angles = Actor.LocalOrientation.EulerAngles; float percY = Mathf.Abs(deltaY) / maxAngle;
float percZ = Mathf.Abs(deltaZ) / maxAngle;
float minSpeed = swaySpeedScaled * 0.00001f * 0f;
// Ensure the swaying is smooth when framerate fluctuates slightly Func<float, float> fun = f => Mathf.Pow(f, 1.3f);
float remaining = Time.DeltaTime + timeRemainder;
const float minTime = 1f / 120f;
do
{
float stepTime = Mathf.Min(Time.DeltaTime, minTime);
remaining -= stepTime;
float swaySpeedScaled = swaySpeed * stepTime;
float deltaX = Mathf.DeltaAngle(angles.X, targetAngles.X); angles.X = Mathf.MoveTowardsAngle(angles.X, targetAngles.X,
float deltaY = Mathf.DeltaAngle(angles.Y, targetAngles.Y); Math.Max(swaySpeedScaled * fun(percX), minSpeed));
float deltaZ = Mathf.DeltaAngle(angles.Z, targetAngles.Z); angles.Y = Mathf.MoveTowardsAngle(angles.Y, targetAngles.Y,
Math.Max(swaySpeedScaled * fun(percY), minSpeed));
angles.Z = Mathf.MoveTowardsAngle(angles.Z, targetAngles.Z,
Math.Max(swaySpeedScaled * fun(percZ), minSpeed));
} while (remaining > minTime);
const float maxAngle = 30f; timeRemainder -= remaining;
if (deltaX > maxAngle)
angles.X -= maxAngle - deltaX;
else if (deltaX < -maxAngle)
angles.X += maxAngle + deltaX;
if (deltaY > maxAngle)
angles.Y -= maxAngle - deltaY;
else if (deltaY < -maxAngle)
angles.Y += maxAngle + deltaY;
if (deltaZ > maxAngle)
angles.Z -= maxAngle - deltaZ;
else if (deltaZ < -maxAngle)
angles.Z += maxAngle + deltaZ;
float percX = Mathf.Abs(deltaX) / maxAngle; Actor.LocalOrientation = Quaternion.Euler(angles);
float percY = Mathf.Abs(deltaY) / maxAngle;
float percZ = Mathf.Abs(deltaZ) / maxAngle;
float minSpeed = swaySpeedScaled * 0.00001f * 0f;
Func<float, float> fun = f => Mathf.Pow(f, 1.3f);
angles.X = Mathf.MoveTowardsAngle(angles.X, targetAngles.X,
Math.Max(swaySpeedScaled * fun(percX), minSpeed));
angles.Y = Mathf.MoveTowardsAngle(angles.Y, targetAngles.Y,
Math.Max(swaySpeedScaled * fun(percY), minSpeed));
angles.Z = Mathf.MoveTowardsAngle(angles.Z, targetAngles.Z,
Math.Max(swaySpeedScaled * fun(percZ), minSpeed));
} while (remaining > minTime);
timeRemainder -= remaining;
Actor.LocalOrientation = Quaternion.Euler(angles);
}
} }
} }

View File

@@ -1,42 +1,41 @@
using System; using System;
namespace Game namespace Game;
// Holds common miscellaneous Console variables and commands
public static class CommonCommands
{ {
// Holds common miscellaneous Console variables and commands [ConsoleVariable("developer")]
public static class CommonCommands public static string Developer
{ {
[ConsoleVariable("developer")] get => Console.DebugVerbosity.ToString();
public static string Developer set
{ {
get => Console.DebugVerbosity.ToString(); if (int.TryParse(value, out int intValue) && intValue >= 0)
set Console.DebugVerbosity = intValue;
{
if (int.TryParse(value, out int intValue) && intValue >= 0)
Console.DebugVerbosity = intValue;
}
}
[ConsoleCommand("")]
public static void NullCommand()
{
}
[ConsoleCommand("echo")]
public static void EchoCommand()
{
Console.Print("nothing");
}
[ConsoleCommand("echo")]
public static void EchoCommand(string[] text)
{
Console.Print(string.Join(" ", text));
}
[ConsoleCommand("debugthrow")]
public static void DebugThrowCommand(string[] text)
{
throw new Exception(string.Join(" ", text));
} }
} }
[ConsoleCommand("")]
public static void NullCommand()
{
}
[ConsoleCommand("echo")]
public static void EchoCommand()
{
Console.Print("nothing");
}
[ConsoleCommand("echo")]
public static void EchoCommand(string[] text)
{
Console.Print(string.Join(" ", text));
}
[ConsoleCommand("debugthrow")]
public static void DebugThrowCommand(string[] text)
{
throw new Exception(string.Join(" ", text));
}
} }

View File

@@ -1,34 +1,33 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Game namespace Game;
public class Config
{ {
public class Config private Dictionary<string, string> dictionary = new Dictionary<string, string>();
public Config()
{ {
private Dictionary<string, string> dictionary = new Dictionary<string, string>(); }
public Config() public string this[string key]
{ {
} get => dictionary[key];
set => dictionary[key] = value;
}
public string this[string key] // This is for debugging only, remove this later
{ public string[] Commands;
get => dictionary[key];
set => dictionary[key] = value;
}
// This is for debugging only, remove this later public string[] GetLines()
public string[] Commands; {
string[] lines = new string[dictionary.Count + Commands.Length];
int lineIndex = 0;
foreach (var kvp in dictionary)
lines[lineIndex++] = $"{kvp.Key} {kvp.Value}";
foreach (var cmd in Commands)
lines[lineIndex++] = cmd;
public string[] GetLines() return lines;
{
string[] lines = new string[dictionary.Count + Commands.Length];
int lineIndex = 0;
foreach (var kvp in dictionary)
lines[lineIndex++] = $"{kvp.Key} {kvp.Value}";
foreach (var cmd in Commands)
lines[lineIndex++] = cmd;
return lines;
}
} }
} }

View File

@@ -1,51 +1,50 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
namespace Game namespace Game;
public class ConfigParser
{ {
public class ConfigParser private ConfigParser()
{ {
private ConfigParser()
}
public static Config ParseFile(string path)
{
Config config = new Config();
if (!File.Exists(path))
{ {
Console.Print($"Config file not found in path: {path}");
}
public static Config ParseFile(string path)
{
Config config = new Config();
if (!File.Exists(path))
{
Console.Print($"Config file not found in path: {path}");
return config;
}
/*using*/ FileStream file = File.OpenRead(path);
/*using*/ StreamReader sr = new StreamReader(file);
List<string> commands = new List<string>();
string line;
while ((line = sr.ReadLine()?.Trim()) != null)
{
if (line.StartsWith(@"//"))
continue;
int spacePos = line.IndexOf(' ');
if (spacePos == -1)
{
commands.Add(line);
continue;
}
string key = line.Substring(0, spacePos);
string value = line.Substring(spacePos+1);
value = value.Trim('"');
config[key] = value;
}
config.Commands = commands.ToArray();
return config; return config;
} }
/*using*/ FileStream file = File.OpenRead(path);
/*using*/ StreamReader sr = new StreamReader(file);
List<string> commands = new List<string>();
string line;
while ((line = sr.ReadLine()?.Trim()) != null)
{
if (line.StartsWith(@"//"))
continue;
int spacePos = line.IndexOf(' ');
if (spacePos == -1)
{
commands.Add(line);
continue;
}
string key = line.Substring(0, spacePos);
string value = line.Substring(spacePos+1);
value = value.Trim('"');
config[key] = value;
}
config.Commands = commands.ToArray();
return config;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,48 +2,48 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Game namespace Game;
[AttributeUsage(AttributeTargets.All)]
public abstract class ConsoleBaseAttribute : Attribute
{ {
[AttributeUsage(AttributeTargets.All)] // Additional aliases for this command, these should only be used with user interaction.
public abstract class ConsoleBaseAttribute : Attribute // Commands such as 'cvarlist' should not list these in order to avoid clutter.
internal string[] aliases = new string[0];
internal string name;
public ConsoleBaseAttribute(string name)
{ {
// Additional aliases for this command, these should only be used with user interaction. this.name = name.ToLowerInvariant();
// Commands such as 'cvarlist' should not list these in order to avoid clutter.
internal string[] aliases = new string[0];
internal string name;
public ConsoleBaseAttribute(string name)
{
this.name = name.ToLowerInvariant();
}
public ConsoleBaseAttribute(params string[] names)
{
name = names[0].ToLowerInvariant();
aliases = new List<string>(names).Skip(1).Select(x => x.ToLowerInvariant()).ToArray();
}
public ConsoleFlags flags { get; private set; }
} }
[AttributeUsage(AttributeTargets.All)] public ConsoleBaseAttribute(params string[] names)
public class ConsoleVariableAttribute : ConsoleBaseAttribute
{ {
public ConsoleVariableAttribute(string name) : base(name) name = names[0].ToLowerInvariant();
{ aliases = new List<string>(names).Skip(1).Select(x => x.ToLowerInvariant()).ToArray();
}
} }
[AttributeUsage(AttributeTargets.All)] public ConsoleFlags flags { get; private set; }
public class ConsoleCommandAttribute : ConsoleBaseAttribute }
[AttributeUsage(AttributeTargets.All)]
public class ConsoleVariableAttribute : ConsoleBaseAttribute
{
public ConsoleVariableAttribute(string name) : base(name)
{ {
}
}
[AttributeUsage(AttributeTargets.All)]
public class ConsoleCommandAttribute : ConsoleBaseAttribute
{
/// <summary> /// <summary>
/// Registers a command to Console system. /// Registers a command to Console system.
/// </summary> /// </summary>
/// <param name="name">Name used for calling this command.</param> /// <param name="name">Name used for calling this command.</param>
public ConsoleCommandAttribute(string name) : base(name) public ConsoleCommandAttribute(string name) : base(name)
{ {
} }
/// <summary> /// <summary>
/// Registers a command to Console system. /// Registers a command to Console system.
@@ -53,15 +53,14 @@ namespace Game
/// names are aliases. /// names are aliases.
/// </param> /// </param>
public ConsoleCommandAttribute(params string[] names) : base(names) public ConsoleCommandAttribute(params string[] names) : base(names)
{
}
}
/// <summary>
/// Constructor for the subsystem, must be called first before registering console commands.
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class ConsoleSubsystemInitializer : Attribute
{ {
} }
}
/// <summary>
/// Constructor for the subsystem, must be called first before registering console commands.
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class ConsoleSubsystemInitializer : Attribute
{
} }

View File

@@ -1,78 +1,77 @@
using System; using System;
using System.Reflection; using System.Reflection;
namespace Game namespace Game;
internal struct ConsoleCommand
{ {
internal struct ConsoleCommand public string name { get; }
private readonly MethodInfo[] methods;
public ConsoleCommand(string name, MethodInfo[] method)
{ {
public string name { get; } this.name = name;
methods = method;
}
private readonly MethodInfo[] methods; public void Invoke()
{
public ConsoleCommand(string name, MethodInfo[] method) foreach (MethodInfo method in methods)
{ {
this.name = name; var methodParameters = method.GetParameters();
methods = method; if (methodParameters.Length == 0)
method.Invoke(null, null);
else if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[]))
method.Invoke(null, new object[] { Array.Empty<string>() });
else
continue;
return;
} }
public void Invoke() throw new Exception("Unexpected number of parameters.");
}
public void Invoke(string[] parameters)
{
MethodInfo match = null;
foreach (MethodInfo method in methods)
{ {
foreach (MethodInfo method in methods) var methodParameters = method.GetParameters();
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[]))
{ {
var methodParameters = method.GetParameters(); match = method;
if (methodParameters.Length == 0) continue;
method.Invoke(null, null);
else if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[]))
method.Invoke(null, new object[] { Array.Empty<string>() });
else
continue;
return;
} }
throw new Exception("Unexpected number of parameters."); if (methodParameters.Length != parameters.Length)
} continue;
public void Invoke(string[] parameters) // TODO: try to parse string parameters to needed types first,
{ // may require finding the exact match first instead of first matching one.
MethodInfo match = null; for (int i = 0; i < methodParameters.Length; i++)
foreach (MethodInfo method in methods) //if (methodParameters[i].ParameterType != parameters[i].GetType())
{ if (methodParameters[i].ParameterType != typeof(string))
var methodParameters = method.GetParameters();
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[]))
{
match = method;
continue; continue;
}
if (match != null)
// Prefer exact number of parameters over string[] match
if (methodParameters.Length != parameters.Length) if (methodParameters.Length != parameters.Length)
continue; continue;
// TODO: try to parse string parameters to needed types first, match = method;
// 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 (match != null)
// Prefer exact number of parameters over string[] match
if (methodParameters.Length != parameters.Length)
continue;
match = method;
}
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);
return;
}
throw new Exception("Unexpected number of parameters.");
} }
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);
return;
}
throw new Exception("Unexpected number of parameters.");
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,171 +2,170 @@
using System.Linq; using System.Linq;
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
public class ConsoleInputTextBox : ConsoleTextBoxBase
{ {
public class ConsoleInputTextBox : ConsoleTextBoxBase private readonly ConsoleContentTextBox contentBox;
private int inputHistoryIndex = -1;
public ConsoleInputTextBox()
{ {
private readonly ConsoleContentTextBox contentBox; }
private int inputHistoryIndex = -1; 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() public override string TextPrefix => Console.LinePrefix;
protected override Rectangle TextRectangle => new Rectangle(0, 0, Width, Height);
protected override Rectangle TextClipRectangle => new Rectangle(0, 0, Width, Height);
private bool IsConsoleKeyPressed(KeyboardKeys key = KeyboardKeys.None)
{
// Ignore any characters generated by the key which opens the console
string inputTextLower = Input.InputText.ToLowerInvariant();
IEnumerable<ActionConfig> 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)
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;
protected override Rectangle TextRectangle => new Rectangle(0, 0, Width, Height);
protected override Rectangle TextClipRectangle => new Rectangle(0, 0, Width, Height);
private bool IsConsoleKeyPressed(KeyboardKeys key = KeyboardKeys.None)
{
// Ignore any characters generated by the key which opens the console
string inputTextLower = Input.InputText.ToLowerInvariant();
IEnumerable<ActionConfig> 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('ö') ||
if ((mapping.Key == KeyboardKeys.Backslash || mapping.Key == KeyboardKeys.BackQuote) && inputTextLower.Contains('æ') ||
(inputTextLower.Contains('ö') || inputTextLower.Contains('ø')))
inputTextLower.Contains('æ') || continue; // Scandinavian keyboard layouts
inputTextLower.Contains('ø'))) if (mapping.Key == KeyboardKeys.BackQuote && inputTextLower.Contains('\''))
continue; // Scandinavian keyboard layouts continue;
if (mapping.Key == KeyboardKeys.BackQuote && inputTextLower.Contains('\'')) if (mapping.Key == KeyboardKeys.Backslash &&
continue; (inputTextLower.Contains('\\') || inputTextLower.Contains('|')))
if (mapping.Key == KeyboardKeys.Backslash && continue;
(inputTextLower.Contains('\\') || inputTextLower.Contains('|')))
continue;
}
if (Input.GetKey(mapping.Key))
return true;
} }
return false; if (Input.GetKey(mapping.Key))
return true;
} }
public override bool OnCharInput(char c) return false;
}
public override bool OnCharInput(char c)
{
if (IsConsoleKeyPressed())
return true;
return base.OnCharInput(c);
}
public override bool OnKeyDown(KeyboardKeys key)
{
bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
bool ctrlDown = Root.GetKey(KeyboardKeys.Control);
if (IsConsoleKeyPressed(key))
{ {
if (IsConsoleKeyPressed()) Clear();
return true;
return base.OnCharInput(c);
}
public override bool OnKeyDown(KeyboardKeys key)
{
bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
bool ctrlDown = Root.GetKey(KeyboardKeys.Control);
if (IsConsoleKeyPressed(key))
{
Clear();
return true;
}
if (key == KeyboardKeys.Escape)
{
Console.Close();
Clear();
return true;
}
if (key == KeyboardKeys.Return)
{
try
{
Console.Execute(Text, true);
inputHistoryIndex = -1;
}
finally
{
Clear();
}
contentBox.ScrollOffset = 0;
return true;
}
if (key == KeyboardKeys.ArrowUp)
{
inputHistoryIndex++;
string line = Console.GetBufferHistory(inputHistoryIndex);
if (line == null)
{
inputHistoryIndex--;
return true;
}
SetText(line);
SetSelection(TextLength);
return true;
}
if (key == KeyboardKeys.ArrowDown)
{
if (inputHistoryIndex > 0)
inputHistoryIndex--;
string line = Console.GetBufferHistory(inputHistoryIndex);
if (line == null)
return true;
SetText(line);
SetSelection(TextLength);
return true;
}
if (key == KeyboardKeys.PageUp || key == KeyboardKeys.PageDown)
return contentBox.OnKeyDown(key);
#if FLAX_EDITOR
if (key == KeyboardKeys.F5)
return false;
#endif
return base.OnKeyDown(key);
}
public override void OnLostFocus()
{
// Prevent caret location getting reset back to beginning
bool oldEditing = _isEditing;
_isEditing = false;
base.OnLostFocus();
_isEditing = oldEditing;
}
public override bool OnMouseDown(Float2 location, MouseButton button)
{
base.OnMouseDown(location, button);
return true; return true;
} }
public override bool OnMouseWheel(Float2 location, float delta) if (key == KeyboardKeys.Escape)
{ {
return contentBox.OnMouseWheel(location, delta); Console.Close();
Clear();
return true;
} }
public override void Draw() if (key == KeyboardKeys.Return)
{ {
Profiler.BeginEvent("ConsoleInputTextBoxDraw"); try
base.Draw(); {
Profiler.EndEvent(); Console.Execute(Text, true);
inputHistoryIndex = -1;
}
finally
{
Clear();
}
contentBox.ScrollOffset = 0;
return true;
} }
if (key == KeyboardKeys.ArrowUp)
{
inputHistoryIndex++;
string line = Console.GetBufferHistory(inputHistoryIndex);
if (line == null)
{
inputHistoryIndex--;
return true;
}
SetText(line);
SetSelection(TextLength);
return true;
}
if (key == KeyboardKeys.ArrowDown)
{
if (inputHistoryIndex > 0)
inputHistoryIndex--;
string line = Console.GetBufferHistory(inputHistoryIndex);
if (line == null)
return true;
SetText(line);
SetSelection(TextLength);
return true;
}
if (key == KeyboardKeys.PageUp || key == KeyboardKeys.PageDown)
return contentBox.OnKeyDown(key);
#if FLAX_EDITOR
if (key == KeyboardKeys.F5)
return false;
#endif
return base.OnKeyDown(key);
}
public override void OnLostFocus()
{
// Prevent caret location getting reset back to beginning
bool oldEditing = _isEditing;
_isEditing = false;
base.OnLostFocus();
_isEditing = oldEditing;
}
public override bool OnMouseDown(Float2 location, MouseButton button)
{
base.OnMouseDown(location, button);
return true;
}
public override bool OnMouseWheel(Float2 location, float delta)
{
return contentBox.OnMouseWheel(location, delta);
}
public override void Draw()
{
Profiler.BeginEvent("ConsoleInputTextBoxDraw");
base.Draw();
Profiler.EndEvent();
} }
} }

View File

@@ -6,178 +6,177 @@ using System.Linq;
using FlaxEditor; using FlaxEditor;
#endif #endif
namespace Game namespace Game;
public class ConsolePlugin : GamePlugin
{ {
public class ConsolePlugin : GamePlugin public ConsolePlugin()
{ {
public ConsolePlugin()
{
#if FLAX_EDITOR #if FLAX_EDITOR
_description = ConsoleEditorPlugin.DescriptionInternal; _description = ConsoleEditorPlugin.DescriptionInternal;
#endif #endif
}
private InputEvent onExit;
public override void Initialize()
{
//FlaxEngine.Debug.Log("ConsolePlugin initialized");
Console.Init();
onExit = new InputEvent("Exit");
onExit.Pressed += () =>
{
if (Console.IsSafeToQuit)
Engine.RequestExit();
};
//AssetManager.Init(); // TODO: move these elsewhere
#if !FLAX_EDITOR
Level.SceneLoading += OnSceneLoading;
Level.SceneLoaded += OnSceneLoaded;
#endif
}
public override void Deinitialize()
{
onExit.Dispose();
#if !FLAX_EDITOR
Level.SceneLoading -= OnSceneLoading;
Level.SceneLoaded -= OnSceneLoaded;
#endif
}
private void OnSceneLoading(Scene scene, Guid guid)
{
Level.SceneLoading -= OnSceneLoading;
LoadConfig();
}
private void OnSceneLoaded(Scene scene, Guid guid)
{
Level.SceneLoaded -= OnSceneLoaded;
#if !FLAX_EDITOR
//GameMode.Connect();
//GameMode.StartServer(true);
//NetworkManager.StartServer();
//GameModeManager.Init();
NetworkManager.ConnectServer();
#endif
}
private void LoadConfig()
{
Console.Print("Loading config file (GamePlugin)");
AssetManager.Globals.ResetValues();
foreach (var line in AssetManager.Config.GetLines())
Console.Execute(line, false, true);
}
} }
#if FLAX_EDITOR private InputEvent onExit;
public class ConsoleEditorPlugin : EditorPlugin
public override void Initialize()
{ {
public static PluginDescription DescriptionInternal = new PluginDescription //FlaxEngine.Debug.Log("ConsolePlugin initialized");
Console.Init();
onExit = new InputEvent("Exit");
onExit.Pressed += () =>
{ {
Author = "Ari Vuollet", if (Console.IsSafeToQuit)
Name = "Console", Engine.RequestExit();
Description = "Quake-like console",
Version = Version.Parse("0.1.0"),
IsAlpha = true,
Category = "Game"
}; };
public override Type GamePluginType => typeof(ConsolePlugin); //AssetManager.Init(); // TODO: move these elsewhere
#if !FLAX_EDITOR
Level.SceneLoading += OnSceneLoading;
Level.SceneLoaded += OnSceneLoaded;
#endif
}
public ConsoleEditorPlugin() public override void Deinitialize()
{
onExit.Dispose();
#if !FLAX_EDITOR
Level.SceneLoading -= OnSceneLoading;
Level.SceneLoaded -= OnSceneLoaded;
#endif
}
private void OnSceneLoading(Scene scene, Guid guid)
{
Level.SceneLoading -= OnSceneLoading;
LoadConfig();
}
private void OnSceneLoaded(Scene scene, Guid guid)
{
Level.SceneLoaded -= OnSceneLoaded;
#if !FLAX_EDITOR
//GameMode.Connect();
//GameMode.StartServer(true);
//NetworkManager.StartServer();
//GameModeManager.Init();
NetworkManager.ConnectServer();
#endif
}
private void LoadConfig()
{
Console.Print("Loading config file (GamePlugin)");
AssetManager.Globals.ResetValues();
foreach (var line in AssetManager.Config.GetLines())
Console.Execute(line, false, true);
}
}
#if FLAX_EDITOR
public class ConsoleEditorPlugin : EditorPlugin
{
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 Type GamePluginType => typeof(ConsolePlugin);
public ConsoleEditorPlugin()
{
_description = DescriptionInternal;
}
//private readonly InputEvent onExit = new InputEvent("Exit");
public override void Initialize()
{
//FlaxEngine.Debug.Log("ConsolePlugin initialized");
Console.Init();
/*onExit.Triggered += () =>
{ {
_description = DescriptionInternal; if (Console.IsSafeToQuit)
} Engine.RequestExit();
};*/
//private readonly InputEvent onExit = new InputEvent("Exit"); //AssetManager.Init();
public override void Initialize() Level.SceneLoading += OnSceneLoading;
{
//FlaxEngine.Debug.Log("ConsolePlugin initialized");
Console.Init();
/*onExit.Triggered += () => FlaxEditor.Editor.Instance.PlayModeBegin += OnPlayModeBegin;
{ FlaxEditor.Editor.Instance.PlayModeEnd += OnPlayModeEnd;
if (Console.IsSafeToQuit) //Scripting.Exit += OnScriptingExit;
Engine.RequestExit();
};*/
//AssetManager.Init(); /*AssetManager.Init(); // TODO: move these elsewhere
AssetManager.Globals.ResetValues();
Level.SceneLoading += OnSceneLoading; foreach (var line in AssetManager.Config.GetLines())
Console.Execute(line, false, true);*/
}
FlaxEditor.Editor.Instance.PlayModeBegin += OnPlayModeBegin; /*private void OnScriptingExit()
FlaxEditor.Editor.Instance.PlayModeEnd += OnPlayModeEnd; {
//Scripting.Exit += OnScriptingExit; FlaxEditor.Editor.Instance.PlayModeBegin -= OnPlayModeBegin;
FlaxEditor.Editor.Instance.PlayModeEnd -= OnPlayModeEnd;
}*/
/*AssetManager.Init(); // TODO: move these elsewhere
AssetManager.Globals.ResetValues();
foreach (var line in AssetManager.Config.GetLines()) private void OnPlayModeBegin()
Console.Execute(line, false, true);*/ {
} //FlaxEditor.Editor.Instance.PlayModeBegin -= Instance_PlayModeBegin;
LoadConfig();
//GameMode.Connect();
//WorldStateManager.Init();
NetworkManager.StartServer();
/*private void OnScriptingExit() //GameMode.StartServer(true);
}
private void OnPlayModeEnd()
{
//onExit.Dispose();
//GameMode.StopServer();
NetworkManager.Cleanup();
}
public override void Deinitialize()
{
//onExit.Dispose();
//Level.SceneLoaded -= OnSceneLoaded;
Level.SceneLoading -= OnSceneLoading;
if (FlaxEditor.Editor.Instance != null)
{ {
FlaxEditor.Editor.Instance.PlayModeBegin -= OnPlayModeBegin; FlaxEditor.Editor.Instance.PlayModeBegin -= OnPlayModeBegin;
FlaxEditor.Editor.Instance.PlayModeEnd -= OnPlayModeEnd; FlaxEditor.Editor.Instance.PlayModeEnd -= OnPlayModeEnd;
}*/
private void OnPlayModeBegin()
{
//FlaxEditor.Editor.Instance.PlayModeBegin -= Instance_PlayModeBegin;
LoadConfig();
//GameMode.Connect();
//WorldStateManager.Init();
NetworkManager.StartServer();
//GameMode.StartServer(true);
} }
private void OnPlayModeEnd()
{
//onExit.Dispose();
//GameMode.StopServer();
NetworkManager.Cleanup();
}
public override void Deinitialize()
{
//onExit.Dispose();
//Level.SceneLoaded -= OnSceneLoaded;
Level.SceneLoading -= OnSceneLoading;
if (FlaxEditor.Editor.Instance != null)
{
FlaxEditor.Editor.Instance.PlayModeBegin -= OnPlayModeBegin;
FlaxEditor.Editor.Instance.PlayModeEnd -= OnPlayModeEnd;
}
}
private void OnSceneLoading(Scene scene, Guid guid)
{
//Level.SceneLoaded -= OnSceneLoaded;
Level.SceneLoading -= OnSceneLoading;
LoadConfig();
}
private void LoadConfig()
{
Console.Print("Loading config file (EditorPlugin)");
AssetManager.Globals.ResetValues();
foreach (var line in AssetManager.Config.GetLines())
Console.Execute(line, false, true);
}
} }
private void OnSceneLoading(Scene scene, Guid guid)
{
//Level.SceneLoaded -= OnSceneLoaded;
Level.SceneLoading -= OnSceneLoading;
LoadConfig();
}
private void LoadConfig()
{
Console.Print("Loading config file (EditorPlugin)");
AssetManager.Globals.ResetValues();
foreach (var line in AssetManager.Config.GetLines())
Console.Execute(line, false, true);
}
}
#endif #endif
}

View File

@@ -6,393 +6,392 @@ using FlaxEngine.Assertions;
using FlaxEngine.GUI; using FlaxEngine.GUI;
using Object = FlaxEngine.Object; using Object = FlaxEngine.Object;
namespace Game namespace Game;
public class ConsoleScript : Script
{ {
public class ConsoleScript : Script public Color BackgroundColor;
public Texture BackgroundTexture;
private ConsoleContentTextBox consoleBox;
public FontAsset ConsoleFont;
[Limit(5, 720)] public int ConsoleFontSize = 16;
[Limit(0.05f, 1.0f)] public float ConsoleHeight = 0.65f;
private ConsoleInputTextBox consoleInputBox;
internal InputEvent consoleInputEvent;
private ConsoleContentTextBox consoleNotifyBox;
[Limit(0)] public int ConsoleNotifyLines = 15;
[Limit(0f)] public float ConsoleSpeed = 3500f;
private UIControl rootControl;
private int fontHeight;
public override void OnStart()
{ {
public Color BackgroundColor; consoleInputEvent = new InputEvent("Console");
consoleInputEvent.Pressed += OnConsoleInputEvent;
public Texture BackgroundTexture; FontReference fontReference = new FontReference(ConsoleFont, ConsoleFontSize);
private ConsoleContentTextBox consoleBox; Font fontRaw = fontReference.GetFont();
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);
public FontAsset ConsoleFont; rootControl = Actor.AddChild<UIControl>();
[Limit(5, 720)] public int ConsoleFontSize = 16; rootControl.Name = "ConsoleRoot";
rootControl.Control = rootContainerControl;
[Limit(0.05f, 1.0f)] public float ConsoleHeight = 0.65f; VerticalPanel contentContainer = new VerticalPanel
private ConsoleInputTextBox consoleInputBox;
internal InputEvent consoleInputEvent;
private ConsoleContentTextBox consoleNotifyBox;
[Limit(0)] public int ConsoleNotifyLines = 15;
[Limit(0f)] public float ConsoleSpeed = 3500f;
private UIControl rootControl;
private int fontHeight;
public override void OnStart()
{ {
consoleInputEvent = new InputEvent("Console"); AutoSize = true,
consoleInputEvent.Pressed += OnConsoleInputEvent; Margin = Margin.Zero,
Spacing = 0,
Bounds = new Rectangle(),
BackgroundColor = BackgroundColor
};
contentContainer.SetAnchorPreset(AnchorPresets.StretchAll, true);
FontReference fontReference = new FontReference(ConsoleFont, ConsoleFontSize); UIControl contentContainerControl = rootControl.AddChild<UIControl>();
Font fontRaw = fontReference.GetFont(); contentContainerControl.Name = "ContentContainer";
fontHeight = (int)(fontRaw.Height / Platform.DpiScale); contentContainerControl.Control = contentContainer;
// 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);
rootControl = Actor.AddChild<UIControl>(); {
rootControl.Name = "ConsoleRoot"; if (consoleBox == null)
rootControl.Control = rootContainerControl;
VerticalPanel contentContainer = new VerticalPanel
{ {
AutoSize = true, //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight);
Margin = Margin.Zero, consoleBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0);
Spacing = 0,
Bounds = new Rectangle(),
BackgroundColor = BackgroundColor
};
contentContainer.SetAnchorPreset(AnchorPresets.StretchAll, true);
UIControl contentContainerControl = rootControl.AddChild<UIControl>();
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);
consoleBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); consoleBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true);
//consoleBox.AnchorMax = new Float2(1.0f, ConsoleHeight); //consoleBox.AnchorMax = new Float2(1.0f, ConsoleHeight);
//consoleBox.Height = consoleSize.Y - fontHeight; //consoleBox.Height = consoleSize.Y - fontHeight;
//consoleBox.HorizontalAlignment = TextAlignment.Near; //consoleBox.HorizontalAlignment = TextAlignment.Near;
//consoleBox.VerticalAlignment = TextAlignment.Near; //consoleBox.VerticalAlignment = TextAlignment.Near;
consoleBox.HeightMultiplier = ConsoleHeight; consoleBox.HeightMultiplier = ConsoleHeight;
consoleBox.Wrapping = TextWrapping.WrapWords; consoleBox.Wrapping = TextWrapping.WrapWords;
consoleBox.BackgroundColor = Color.Transparent; consoleBox.BackgroundColor = Color.Transparent;
consoleBox.BackgroundSelectedColor = Color.Transparent; consoleBox.BackgroundSelectedColor = Color.Transparent;
consoleBox.BackgroundSelectedFlashSpeed = 0; consoleBox.BackgroundSelectedFlashSpeed = 0;
consoleBox.BorderSelectedColor = Color.Transparent; consoleBox.BorderSelectedColor = Color.Transparent;
consoleBox.CaretFlashSpeed = 0; consoleBox.CaretFlashSpeed = 0;
}
Float2 locationFix = consoleBox.Location;
UIControl parentControl = contentContainerControl.AddChild<UIControl>();
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 Float2(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;
consoleNotifyBox.SelectionAllowed = false;
}
Float2 locationFix2 = consoleNotifyBox.Location;
UIControl parentControl2 = Actor.AddChild<UIControl>();
parentControl2.Name = "ConsoleNotifyContent";
parentControl2.Control = consoleNotifyBox;
consoleNotifyBox.Location = locationFix2; // workaround to UIControl.Control overriding the old position
} }
Float2 locationFix = consoleBox.Location;
UIControl parentControl = contentContainerControl.AddChild<UIControl>();
parentControl.Name = "ConsoleContent";
parentControl.Control = consoleBox;
consoleBox.Location = locationFix; // workaround to UIControl.Control overriding the old position
if (consoleNotifyBox == null)
{ {
if (consoleInputBox == null) //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight);
{ consoleNotifyBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0);
//consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, consoleSize.Y - fontHeight, consoleSize.X, fontHeight); consoleNotifyBox.HeightMultiplier = 0;
consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, 0, 0, 0); consoleNotifyBox.Height = ConsoleNotifyLines * fontHeight;
consoleInputBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); consoleNotifyBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true);
//consoleInputBox.Location = new Float2(0, consoleSize.Y - fontHeight); //consoleBox.AnchorMax = new Float2(1.0f, ConsoleHeight);
consoleInputBox.Height = fontHeight;
consoleInputBox.Font = fontReference; //consoleBox.HorizontalAlignment = TextAlignment.Near;
consoleBox.inputBox = consoleInputBox; //consoleBox.VerticalAlignment = TextAlignment.Near;
//consoleNotifyBox.HeightMultiplier = ConsoleHeight;
consoleInputBox.Wrapping = TextWrapping.WrapWords; consoleNotifyBox.Wrapping = TextWrapping.WrapWords;
consoleInputBox.BackgroundColor = Color.Transparent; consoleNotifyBox.BackgroundColor = Color.Transparent;
consoleInputBox.BackgroundSelectedColor = Color.Transparent; consoleNotifyBox.BackgroundSelectedColor = Color.Transparent;
consoleInputBox.BackgroundSelectedFlashSpeed = 0; consoleNotifyBox.BackgroundSelectedFlashSpeed = 0;
consoleInputBox.BorderSelectedColor = Color.Transparent; consoleNotifyBox.BorderSelectedColor = Color.Transparent;
consoleInputBox.CaretFlashSpeed = 0; consoleNotifyBox.CaretFlashSpeed = 0;
} consoleNotifyBox.SelectionAllowed = false;
Float2 locationFix = consoleInputBox.Location;
UIControl parentControl = contentContainerControl.AddChild<UIControl>();
parentControl.Name = "ConsoleInput";
parentControl.Control = consoleInputBox;
consoleInputBox.Location = locationFix; // workaround to UIControl.Control overriding the old position
} }
Float2 locationFix2 = consoleNotifyBox.Location;
UIControl parentControl2 = Actor.AddChild<UIControl>();
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 Float2(0, consoleSize.Y - fontHeight);
consoleInputBox.Height = fontHeight;
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;
}
Float2 locationFix = consoleInputBox.Location;
UIControl parentControl = contentContainerControl.AddChild<UIControl>();
parentControl.Name = "ConsoleInput";
parentControl.Control = consoleInputBox;
consoleInputBox.Location = locationFix; // workaround to UIControl.Control overriding the old position
}
#if false #if false
//for (int i = 0; i < 10; i++) //for (int i = 0; i < 10; i++)
{ {
string[] teststr = { string[] teststr = {
/* /*
"...loading 'scripts/devilpunch.shader'", "...loading 'scripts/devilpunch.shader'",
"...loading 'scripts/mkoxide.shader'", "...loading 'scripts/mkoxide.shader'",
"...loading 'scripts/cpm22.shader'", "...loading 'scripts/cpm22.shader'",
"...loading 'scripts/cpm27.shader'", "...loading 'scripts/cpm27.shader'",
"...loading 'scripts/island.shader'", "...loading 'scripts/island.shader'",
"...loading 'scripts/noodtex3.shader'", "...loading 'scripts/noodtex3.shader'",
"...loading 'scripts/nood_cosdglass.shader'", "...loading 'scripts/nood_cosdglass.shader'",
"...loading 'scripts/nood_fog_1000.shader'", "...loading 'scripts/nood_fog_1000.shader'",
"...loading 'scripts/nood_lightbeams.shader'", "...loading 'scripts/nood_lightbeams.shader'",
"...loading 'scripts/nood_nightsky_nolight.shader'", "...loading 'scripts/nood_nightsky_nolight.shader'",
"Rescanning shaders", "Rescanning shaders",
@"Raw input type 0: [0] \\?\HID#VID_046D&PID_C231#2&229a2ea&0&0000#", @"Raw input type 0: [0] \\?\HID#VID_046D&PID_C231#2&229a2ea&0&0000#",
@"Raw input type 0: [18] \\?\HID#VID_046D&PID_C539&MI_01&Col01#8&24523410&0&0000#", @"Raw input type 0: [18] \\?\HID#VID_046D&PID_C539&MI_01&Col01#8&24523410&0&0000#",
@"Raw input type 0: [19] \\?\HID#VID_28DE&PID_1102&MI_01#8&2fb9bb60&0&0000#", @"Raw input type 0: [19] \\?\HID#VID_28DE&PID_1102&MI_01#8&2fb9bb60&0&0000#",
@"Raw input type 0: [20] \\?\HID#VID_04D9&PID_A131&MI_02&Col01#8&197f95af&0&0000#", @"Raw input type 0: [20] \\?\HID#VID_04D9&PID_A131&MI_02&Col01#8&197f95af&0&0000#",
"Raw input: initialized with 5 mice and 0 keyboards", "Raw input: initialized with 5 mice and 0 keyboards",
"WASAPI: overriding channels", "WASAPI: overriding channels",
"WASAPI: 2 channel 32bit 48000khz non-exclusive", "WASAPI: 2 channel 32bit 48000khz non-exclusive",
"WASAPI: requested periodicity: 128, default: 480, min: 128, max: 480, step: 16", "WASAPI: requested periodicity: 128, default: 480, min: 128, max: 480, step: 16",
"WASAPI: Low latency mode enabled", "WASAPI: Low latency mode enabled",
"WASAPI: buffer size: 280", "WASAPI: buffer size: 280",
"OpenGL renderer initialized", "OpenGL renderer initialized",
"video restart took 0.291480 seconds", "video restart took 0.291480 seconds",
"main thread video restart took 0.291798 secs", "main thread video restart took 0.291798 secs",
"[------ Goake Initialized ------]", "[------ Goake Initialized ------]",
"Initializing menu.dat", "Initializing menu.dat",
"Couldn't load sound/ambience/water1.wav", "Couldn't load sound/ambience/water1.wav",
"Couldn't load sound/ambience/wind2.wav", "Couldn't load sound/ambience/wind2.wav",
"Couldn't load sound/misc/menu2.wav", "Couldn't load sound/misc/menu2.wav",
"Couldn't load sound/misc/menu3.wav", "Couldn't load sound/misc/menu3.wav",
"]cl_maxfps 120", "]cl_maxfps 120",
"a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat" "a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat"
*/ */
"Warning: Unsupported entity field 'light'", "Warning: Unsupported entity field 'light'",
"Warning: Unsupported entity field '_keeplights'", "Warning: Unsupported entity field '_keeplights'",
"Warning: Unsupported entity field 'light'", "Warning: Unsupported entity field 'light'",
"Warning: Unsupported entity field '_keeplights'", "Warning: Unsupported entity field '_keeplights'",
"Warning: Unsupported entity field 'light'", "Warning: Unsupported entity field 'light'",
"Warning: Unsupported entity field '_keeplights'", "Warning: Unsupported entity field '_keeplights'",
"Warning: Unsupported entity field 'light'", "Warning: Unsupported entity field 'light'",
"Warning: Unsupported entity field '_keeplights'", "Warning: Unsupported entity field '_keeplights'",
"maps/aerowalk.bsp: Using lightmap format E5BGR9_UF", "maps/aerowalk.bsp: Using lightmap format E5BGR9_UF",
"maps/devbox.bsp: Using lightmap format E5BGR9_UF", "maps/devbox.bsp: Using lightmap format E5BGR9_UF",
"what", "what",
"Game mode changed to: Free For All", "Game mode changed to: Free For All",
"what", "what",
"641 additional FS searches", "641 additional FS searches",
"fixangle frame: 1427", "fixangle frame: 1427",
"Couldn't load sound/ambience/wind2.wav", "Couldn't load sound/ambience/wind2.wav",
"Couldn't load sound/ambience/water1.wav" "Couldn't load sound/ambience/water1.wav"
}; };
foreach (var l in teststr) foreach (var l in teststr)
Console.Print(l); Console.Print(l);
} }
#endif #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");*/
Debug.Logger.LogHandler.SendLog += OnSendLog;
Debug.Logger.LogHandler.SendExceptionLog += OnSendExceptionLog;
Console.OnOpen += OnConsoleOpen;
Console.OnClose += OnConsoleClose;
Console.OnPrint += OnPrint;
// hide console by default, and close it instantly
Console.Close();
OnConsoleClose();
Float2 rootlocation = rootControl.Control.Location;
rootlocation.Y = -rootControl.Control.Height;
rootControl.Control.Location = rootlocation;
Console.Print("Renderer: " + GPUDevice.Instance.RendererType);
}
private void OnSendLog(LogType level, string msg, Object obj, string stackTrace)
{ {
Console.Print("[DEBUG] " + msg);
}
private void OnSendExceptionLog(Exception exception, Object obj) };*/
/*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;
Console.OnOpen += OnConsoleOpen;
Console.OnClose += OnConsoleClose;
Console.OnPrint += OnPrint;
// hide console by default, and close it instantly
Console.Close();
OnConsoleClose();
Float2 rootlocation = rootControl.Control.Location;
rootlocation.Y = -rootControl.Control.Height;
rootControl.Control.Location = rootlocation;
Console.Print("Renderer: " + GPUDevice.Instance.RendererType);
}
private void OnSendLog(LogType level, string msg, Object obj, string stackTrace)
{
Console.Print("[DEBUG] " + msg);
}
private void OnSendExceptionLog(Exception exception, Object obj)
{
AssertionException assert = exception as AssertionException;
if (assert != null)
{ {
AssertionException assert = exception as AssertionException; string[] assertLines = assert.Message.Split('\n');
if (assert != null) if (assertLines.Length > 2)
{ Console.Print("Assert Failure: " + assertLines[2]);
string[] assertLines = assert.Message.Split('\n');
if (assertLines.Length > 2)
Console.Print("Assert Failure: " + assertLines[2]);
else
Console.Print("Assert Failure: " + assert.Message);
}
else else
{ Console.Print("Assert Failure: " + assert.Message);
Console.Print("[EXCEP] " + exception.ToString());
}
} }
else
public override void OnDestroy()
{ {
base.OnDestroy(); Console.Print("[EXCEP] " + exception.ToString());
//consoleInputEvent.Triggered -= OnConsoleInputEvent;
consoleInputEvent?.Dispose();
consoleBox?.Dispose();
consoleNotifyBox?.Dispose();
Console.OnOpen -= OnConsoleOpen;
Console.OnClose -= OnConsoleClose;
Console.OnPrint -= OnPrint;
Debug.Logger.LogHandler.SendLog -= OnSendLog;
Debug.Logger.LogHandler.SendExceptionLog -= OnSendExceptionLog;
}
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);
// 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();
}
public void OnConsoleOpen()
{
Screen.CursorVisible = true;
Screen.CursorLock = CursorLockMode.None;
consoleInputBox.Focus();
Parent.As<UICanvas>().ReceivesEvents = true;
}
public void OnConsoleClose()
{
Screen.CursorVisible = false;
Screen.CursorLock = CursorLockMode.Locked;
consoleInputBox.Defocus();
#if FLAX_EDITOR
Editor.Instance.Windows.GameWin.Focus();
#endif
Parent.As<UICanvas>().ReceivesEvents = false;
}
public override void OnUpdate()
{
base.OnUpdate();
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;
Float2 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;
location.Y += Time.UnscaledDeltaTime * ConsoleSpeed;
if (location.Y > targetY)
location.Y = targetY;
}
rootControl.Control.Location = location;
if (Console.IsOpen)
{
consoleNotifyBox.Visible = false;
consoleInputBox.Visible = true;
}
else if (!Console.IsOpen)
{
if (location.Y < -conHeight * ConsoleHeight + fontHeight)
{
consoleNotifyBox.Visible = true;
consoleInputBox.Visible = false;
}
}
}
}
public void OnPrint(string text)
{
consoleNotifyBox.Height = Math.Min(ConsoleNotifyLines, Console.Lines.Length) * fontHeight;
}
public void SetInput(string text)
{
consoleInputBox.Text = text;
} }
} }
public override void OnDestroy()
{
base.OnDestroy();
//consoleInputEvent.Triggered -= OnConsoleInputEvent;
consoleInputEvent?.Dispose();
consoleBox?.Dispose();
consoleNotifyBox?.Dispose();
Console.OnOpen -= OnConsoleOpen;
Console.OnClose -= OnConsoleClose;
Console.OnPrint -= OnPrint;
Debug.Logger.LogHandler.SendLog -= OnSendLog;
Debug.Logger.LogHandler.SendExceptionLog -= OnSendExceptionLog;
}
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);
// 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();
}
public void OnConsoleOpen()
{
Screen.CursorVisible = true;
Screen.CursorLock = CursorLockMode.None;
consoleInputBox.Focus();
Parent.As<UICanvas>().ReceivesEvents = true;
}
public void OnConsoleClose()
{
Screen.CursorVisible = false;
Screen.CursorLock = CursorLockMode.Locked;
consoleInputBox.Defocus();
#if FLAX_EDITOR
Editor.Instance.Windows.GameWin.Focus();
#endif
Parent.As<UICanvas>().ReceivesEvents = false;
}
public override void OnUpdate()
{
base.OnUpdate();
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;
Float2 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;
location.Y += Time.UnscaledDeltaTime * ConsoleSpeed;
if (location.Y > targetY)
location.Y = targetY;
}
rootControl.Control.Location = location;
if (Console.IsOpen)
{
consoleNotifyBox.Visible = false;
consoleInputBox.Visible = true;
}
else if (!Console.IsOpen)
{
if (location.Y < -conHeight * ConsoleHeight + fontHeight)
{
consoleNotifyBox.Visible = true;
consoleInputBox.Visible = false;
}
}
}
}
public void OnPrint(string text)
{
consoleNotifyBox.Height = Math.Min(ConsoleNotifyLines, Console.Lines.Length) * fontHeight;
}
public void SetInput(string text)
{
consoleInputBox.Text = text;
}
} }

View File

@@ -2,389 +2,388 @@
using FlaxEngine; using FlaxEngine;
using FlaxEngine.GUI; using FlaxEngine.GUI;
namespace Game namespace Game;
// Mostly based on TextBox
public class ConsoleTextBoxBase : TextBoxBase
{ {
// Mostly based on TextBox private readonly Stopwatch lastDoubleClick = new Stopwatch();
public class ConsoleTextBoxBase : TextBoxBase protected TextLayoutOptions _layout;
private bool doubleClicked;
private Float2 lastDoubleClickLocation = new Float2(0, 0);
/// <summary>
/// Gets or sets the color of the selection (Transparent if not used).
/// </summary>
[EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the selection (Transparent if not used).")]
public Color SelectionColor = new Color(0x00, 0x7A, 0xCC);
/// <summary>
/// Gets or sets the color of the text.
/// </summary>
[EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the text.")]
public Color TextColor = Color.White;
//[HideInEditor]
//public override string Text => _text;
public ConsoleTextBoxBase()
{ {
private readonly Stopwatch lastDoubleClick = new Stopwatch(); }
protected TextLayoutOptions _layout;
private bool doubleClicked; public ConsoleTextBoxBase(float x, float y, float width, float height) : base(false, x, y, width)
private Float2 lastDoubleClickLocation = new Float2(0, 0); {
Height = height;
/// <summary> IsReadOnly = false;
/// Gets or sets the color of the selection (Transparent if not used). CaretColor = new Color(1f, 1f, 1f, 1f);
/// </summary> AutoFocus = true;
[EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the selection (Transparent if not used).")] EndEditOnClick = false;
public Color SelectionColor = new Color(0x00, 0x7A, 0xCC);
/// <summary> _layout = TextLayoutOptions.Default;
/// Gets or sets the color of the text. _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center;
/// </summary> _layout.TextWrapping = TextWrapping.NoWrap;
[EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the text.")] _layout.Bounds =
public Color TextColor = Color.White; new Rectangle(0, 0, Width,
Height); //new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2);
}
//[HideInEditor] /// <summary>
//public override string Text => _text; /// Gets or sets the text wrapping within the control bounds.
/// </summary>
[EditorDisplay("Style")]
[EditorOrder(2000)]
[Tooltip("The text wrapping within the control bounds.")]
public TextWrapping Wrapping
{
get => _layout.TextWrapping;
set => _layout.TextWrapping = value;
}
public ConsoleTextBoxBase() /// <summary>
/// Gets or sets the font.
/// </summary>
[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];
} }
public ConsoleTextBoxBase(float x, float y, float width, float height) : base(false, x, y, width) if (Text != value)
{ {
Height = height; Deselect();
ResetViewOffset();
IsReadOnly = false; Text = value;
CaretColor = new Color(1f, 1f, 1f, 1f);
AutoFocus = true;
EndEditOnClick = false;
_layout = TextLayoutOptions.Default; OnTextChanged();
_layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; }
_layout.TextWrapping = TextWrapping.NoWrap; }*/
_layout.Bounds =
new Rectangle(0, 0, Width, public int GetFontHeight()
Height); //new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); {
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 Float2(0, 0);
}
/*public void ScrollToEnd()
{
float maxY = TextSize.Y - Height;
float spacing = GetRealLineSpacing();
maxY += spacing;
TargetViewOffset = new Float2(0, Math.Max(0, maxY));
}*/
public override void ScrollToCaret()
{
if (Text.Length == 0)
return;
Rectangle caretBounds = CaretBounds;
float maxY = TextSize.Y - Height;
Float2 newLocation = CaretBounds.Location;
TargetViewOffset = Float2.Clamp(newLocation, new Float2(0, 0), new Float2(_targetViewOffset.X, maxY));
}
/*const bool smoothScrolling = false;
public override bool OnMouseWheel(Float2 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 = Float2.Clamp(_targetViewOffset - new Float2(0, delta), new Float2(0, offset), new Float2(_targetViewOffset.X, maxY));
return true;
}*/
public override Float2 GetTextSize()
{
Font font = Font.GetFont();
if (font == null)
return Float2.Zero;
return font.MeasureText(Text, ref _layout);
}
public override Float2 GetCharPosition(int index, out float height)
{
Font font = Font.GetFont();
if (font == null)
{
height = Height;
return Float2.Zero;
} }
/// <summary> height = GetFontHeight();
/// Gets or sets the text wrapping within the control bounds. return font.GetCharPosition(Text, index, ref _layout);
/// </summary> }
[EditorDisplay("Style")]
[EditorOrder(2000)] public override int HitTestText(Float2 location)
[Tooltip("The text wrapping within the control bounds.")] {
public TextWrapping Wrapping Font font = Font.GetFont();
if (font == null)
return 0;
if (TextPrefix != "")
{ {
get => _layout.TextWrapping; Float2 prefixSize = font.MeasureText(TextPrefix);
set => _layout.TextWrapping = value; location.X -= prefixSize.X;
} }
/// <summary> return font.HitTestText(Text, location, ref _layout);
/// Gets or sets the font. }
/// </summary>
[EditorDisplay("Style")]
[EditorOrder(2000)]
public FontReference Font { get; set; }
[HideInEditor] public virtual string TextPrefix { get; set; } = ""; protected override void OnIsMultilineChanged()
{
base.OnIsMultilineChanged();
/*protected override void SetText(string value) _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)
{ {
// Prevent from null problems if (!IsReadOnly)
if (value == null) SetSelection(_selectionStart, 0);
value = string.Empty; return true;
}
// Filter text if (shiftDown && key == KeyboardKeys.End)
if (value.IndexOf('\r') != -1) {
value = value.Replace("\r", ""); if (!IsReadOnly)
SetSelection(_selectionStart, TextLength);
return true;
}
// Clamp length return base.OnKeyDown(key);
if (value.Length > MaxLength) }
value = value.Substring(0, MaxLength);
// Ensure to use only single line public override bool OnMouseDown(Float2 location, MouseButton button)
if (_isMultiline == false && value.Length > 0) {
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(Float2 location, MouseButton button)
{
doubleClicked = true;
lastDoubleClick.Restart();
lastDoubleClickLocation = location;
return base.OnMouseDoubleClick(location, button);
}
public bool OnMouseTripleClick(Float2 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(Float2.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)
{
Float2 leftEdge = font.GetCharPosition(text, SelectionLeft + TextPrefix.Length, ref _layout);
Float2 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
{ {
// Extract only the first line Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight);
value = value.GetLines()[0]; Render2D.FillRectangle(r1, selectionColor);
} }
else // Selected is more than one line
if (Text != value)
{ {
Deselect(); float leftMargin = _layout.Bounds.Location.X;
ResetViewOffset(); Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight);
Render2D.FillRectangle(r1, selectionColor);
Text = value; for (int i = 3; i <= selectedLinesCount; i++)
{
leftEdge.Y += fontHeight;
Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight);
Render2D.FillRectangle(r, selectionColor);
}
OnTextChanged(); Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight);
Render2D.FillRectangle(r2, selectionColor);
} }
}*/
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() Render2D.DrawText(font, text, TextColor, ref _layout);
if (CaretPosition > -1)
{ {
// Can't clear the text while user is editing it... Float2 prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Float2();
bool oldEditing = _isEditing;
_isEditing = false;
base.Clear();
_isEditing = oldEditing;
}
public override void ResetViewOffset()
{
TargetViewOffset = new Float2(0, 0);
}
/*public void ScrollToEnd()
{
float maxY = TextSize.Y - Height;
float spacing = GetRealLineSpacing();
maxY += spacing;
TargetViewOffset = new Float2(0, Math.Max(0, maxY));
}*/
public override void ScrollToCaret()
{
if (Text.Length == 0)
return;
Rectangle caretBounds = CaretBounds; Rectangle caretBounds = CaretBounds;
caretBounds.X += prefixSize.X;
float maxY = TextSize.Y - Height; float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f);
alpha = alpha * alpha * alpha * alpha * alpha * alpha;
Float2 newLocation = CaretBounds.Location; Render2D.FillRectangle(caretBounds, CaretColor * alpha);
TargetViewOffset = Float2.Clamp(newLocation, new Float2(0, 0), new Float2(_targetViewOffset.X, maxY));
} }
/*const bool smoothScrolling = false; // Restore rendering state
if (useViewOffset)
Render2D.PopTransform();
Render2D.PopClip();
}
public override bool OnMouseWheel(Float2 location, float delta) public override void Paste()
{
if (IsReadOnly)
return;
string clipboardText = Clipboard.Text;
// Handle newlines in clipboard text
if (!string.IsNullOrEmpty(clipboardText))
{ {
if (!IsMultiline || Text.Length == 0) // TODO: probably better to just split these lines and parse each line separately
return false; clipboardText = clipboardText.Replace("\r\n", "");
clipboardText = clipboardText.Replace("\n", "");
if (!smoothScrolling)
delta = GetFontHeight() * Math.Sign(delta) * 3;
else
delta *= 30;
float maxY = TextSize.Y - Height;
float offset = GetRealLineSpacing();
maxY += offset;
TargetViewOffset = Float2.Clamp(_targetViewOffset - new Float2(0, delta), new Float2(0, offset), new Float2(_targetViewOffset.X, maxY));
return true;
}*/
public override Float2 GetTextSize()
{
Font font = Font.GetFont();
if (font == null)
return Float2.Zero;
return font.MeasureText(Text, ref _layout);
} }
public override Float2 GetCharPosition(int index, out float height) if (!string.IsNullOrEmpty(clipboardText))
{ {
Font font = Font.GetFont(); int right = SelectionRight;
if (font == null) Insert(clipboardText);
{ SetSelection(Mathf.Max(right, 0) + clipboardText.Length);
height = Height;
return Float2.Zero;
}
height = GetFontHeight();
return font.GetCharPosition(Text, index, ref _layout);
}
public override int HitTestText(Float2 location)
{
Font font = Font.GetFont();
if (font == null)
return 0;
if (TextPrefix != "")
{
Float2 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(Float2 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(Float2 location, MouseButton button)
{
doubleClicked = true;
lastDoubleClick.Restart();
lastDoubleClickLocation = location;
return base.OnMouseDoubleClick(location, button);
}
public bool OnMouseTripleClick(Float2 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(Float2.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)
{
Float2 leftEdge = font.GetCharPosition(text, SelectionLeft + TextPrefix.Length, ref _layout);
Float2 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)
{
Float2 prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Float2();
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);
}
} }
} }
} }

View File

@@ -1,75 +1,74 @@
using System; using System;
using System.Reflection; using System.Reflection;
namespace Game namespace Game;
{
[Flags]
public enum ConsoleFlags
{
NoSerialize = 1, // Value does not persist
Alias = NoSerialize [Flags]
public enum ConsoleFlags
{
NoSerialize = 1, // Value does not persist
Alias = NoSerialize
}
internal struct ConsoleVariable
{
public string name { get; }
public ConsoleFlags flags { get; }
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;
getter = null;
setter = null;
} }
internal struct ConsoleVariable public ConsoleVariable(string name, ConsoleFlags flags, MethodInfo getter, MethodInfo setter)
{ {
public string name { get; } this.name = name;
public ConsoleFlags flags { get; } this.flags = flags;
field = null;
this.getter = getter;
this.setter = setter;
}
private readonly FieldInfo field; public string GetValueString()
private readonly MethodInfo getter; {
private readonly MethodInfo setter; Type type = field != null ? field.FieldType : getter.ReturnType;
if (type == typeof(string))
public ConsoleVariable(string name, ConsoleFlags flags, FieldInfo field)
{ {
this.name = name; if (field != null)
this.flags = flags; return (string)field.GetValue(null);
this.field = field; if (setter != null)
getter = null; return (string)getter.Invoke(null, null);
setter = null; }
else
{
throw new Exception("cvar is not type of string");
} }
public ConsoleVariable(string name, ConsoleFlags flags, MethodInfo getter, MethodInfo setter) throw new Exception("GetValueString no field or getter specified");
}
public void SetValue(string value)
{
Type type = field != null ? field.FieldType : getter.ReturnType;
if (type == typeof(string))
{ {
this.name = name; if (field != null)
this.flags = flags; field.SetValue(null, value);
field = null; else if (setter != null)
this.getter = getter; setter.Invoke(null, new object[] { value });
this.setter = setter;
} }
else
public string GetValueString()
{ {
Type type = field != null ? field.FieldType : getter.ReturnType; throw new Exception("Unsupported type for SetValue: " + type.Name);
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");
}
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);
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,25 @@
using System; using System;
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
public static class Utilities
{ {
public static class Utilities public static ScopeProfiler ProfileScope(string eventName)
{ {
public static ScopeProfiler ProfileScope(string eventName) return new ScopeProfiler(eventName);
}
public class ScopeProfiler : IDisposable
{
public ScopeProfiler(string eventName)
{ {
return new ScopeProfiler(eventName); Profiler.BeginEvent(eventName);
} }
public class ScopeProfiler : IDisposable public void Dispose()
{ {
public ScopeProfiler(string eventName) Profiler.EndEvent();
{
Profiler.BeginEvent(eventName);
}
public void Dispose()
{
Profiler.EndEvent();
}
} }
} }
} }

View File

@@ -7,199 +7,198 @@ using FlaxEngine.Assertions;
using FlaxEngine.Networking; using FlaxEngine.Networking;
using Console = Game.Console; using Console = Game.Console;
namespace Game namespace Game;
[AttributeUsage(AttributeTargets.Class)]
public class NetworkPredictedAttribute : Attribute
{ {
[AttributeUsage(AttributeTargets.Class)] }
public class NetworkPredictedAttribute : Attribute
// TODO: insert code to update variables with this attribute?
// rename to NetworkReplicatedAttribute?
[AttributeUsage(AttributeTargets.Class)]
public class NetworkedAttribute : Attribute
{
}
// NetworkMulticastAttribute: calls methods marked with this in all clients
public enum NetworkMessageType : byte
{
Handshake = 1,
Message
}
public static partial class NetworkManager
{
public delegate bool OnMessageDecl(ref NetworkEvent networkEvent);
private static bool initialized;
public static NetworkPeer server;
public static NetworkPeer client;
private static readonly ushort ServerPort = 59183;
private static string ServerAddress;
private static readonly ushort MTU = 1500;
private static readonly ushort MaximumClients = 32;
private static List<OnMessageDecl> OnClientMessageDelegates = new(3);
private static List<OnMessageDecl> OnServerMessageDelegates = new(3);
public static void RegisterClientMessageCallback(OnMessageDecl deleg)
{ {
Assert.IsTrue(!OnClientMessageDelegates.Contains(deleg));
OnClientMessageDelegates.Add(deleg);
}
public static void UnregisterClientMessageCallback(OnMessageDecl deleg)
{
Assert.IsTrue(OnClientMessageDelegates.Contains(deleg));
OnClientMessageDelegates.Remove(deleg);
} }
// TODO: insert code to update variables with this attribute? public static void RegisterServerMessageCallback(OnMessageDecl deleg)
// rename to NetworkReplicatedAttribute?
[AttributeUsage(AttributeTargets.Class)]
public class NetworkedAttribute : Attribute
{ {
Assert.IsTrue(!OnServerMessageDelegates.Contains(deleg));
OnServerMessageDelegates.Add(deleg);
}
public static void UnregisterServerMessageCallback(OnMessageDecl deleg)
{
Assert.IsTrue(OnServerMessageDelegates.Contains(deleg));
OnServerMessageDelegates.Remove(deleg);
} }
// NetworkMulticastAttribute: calls methods marked with this in all clients public static string DebugLastHandledMessage = "";
public enum NetworkMessageType : byte //public static bool IsServer = false;
//public static bool IsClient = false;
// static bool IsLocalClient = false; // Context dependant, true when message is handled by local client
public static void Init()
{ {
Handshake = 1, if (initialized)
Message return;
}
/*if (Engine.CommandLine.Contains("-server"))
public static partial class NetworkManager
{
public delegate bool OnMessageDecl(ref NetworkEvent networkEvent);
private static bool initialized;
public static NetworkPeer server;
public static NetworkPeer client;
private static readonly ushort ServerPort = 59183;
private static string ServerAddress;
private static readonly ushort MTU = 1500;
private static readonly ushort MaximumClients = 32;
private static List<OnMessageDecl> OnClientMessageDelegates = new(3);
private static List<OnMessageDecl> OnServerMessageDelegates = new(3);
public static void RegisterClientMessageCallback(OnMessageDecl deleg)
{ {
Assert.IsTrue(!OnClientMessageDelegates.Contains(deleg)); StartServer();
OnClientMessageDelegates.Add(deleg); ServerAddress = "localhost";
ConnectServer();
} }
public static void UnregisterClientMessageCallback(OnMessageDecl deleg) else if (Engine.CommandLine.Contains("-client"))
{ {
Assert.IsTrue(OnClientMessageDelegates.Contains(deleg)); ServerAddress = "localhost";
OnClientMessageDelegates.Remove(deleg); ConnectServer();
} }
public static void RegisterServerMessageCallback(OnMessageDecl deleg)
{
Assert.IsTrue(!OnServerMessageDelegates.Contains(deleg));
OnServerMessageDelegates.Add(deleg);
}
public static void UnregisterServerMessageCallback(OnMessageDecl deleg)
{
Assert.IsTrue(OnServerMessageDelegates.Contains(deleg));
OnServerMessageDelegates.Remove(deleg);
}
public static string DebugLastHandledMessage = "";
//public static bool IsServer = false;
//public static bool IsClient = false;
// static bool IsLocalClient = false; // Context dependant, true when message is handled by local client
public static void Init()
{
if (initialized)
return;
/*if (Engine.CommandLine.Contains("-server"))
{
StartServer();
ServerAddress = "localhost";
ConnectServer();
}
else if (Engine.CommandLine.Contains("-client"))
{
ServerAddress = "localhost";
ConnectServer();
}
//#if FLAX_EDITOR //#if FLAX_EDITOR
else else
{ {
StartServer(); StartServer();
ServerAddress = "localhost"; ServerAddress = "localhost";
ConnectServer(); ConnectServer();
}*/ }*/
//#endif //#endif
initialized = true; initialized = true;
#if FLAX_EDITOR #if FLAX_EDITOR
Editor.Instance.PlayModeEnd += Cleanup; Editor.Instance.PlayModeEnd += Cleanup;
#endif #endif
//WorldStateManager.Init(); // FIXME //WorldStateManager.Init(); // FIXME
}
public static void Cleanup()
{
Scripting.Exit -= Cleanup;
Scripting.FixedUpdate -= OnDemoUpdate;
Scripting.FixedUpdate -= OnServerNetworkUpdate;
Level.ActorSpawned -= OnServerActorSpawned;
Scripting.FixedUpdate -= OnClientNetworkUpdate;
Level.ActorSpawned -= OnClientActorSpawned;
if (server != null)
{
NetworkPeer.ShutdownPeer(server);
server = null;
} }
public static void Cleanup() if (client != null)
{ {
Scripting.Exit -= Cleanup; NetworkPeer.ShutdownPeer(client);
Scripting.FixedUpdate -= OnDemoUpdate; client = null;
Scripting.FixedUpdate -= OnServerNetworkUpdate; }
Level.ActorSpawned -= OnServerActorSpawned;
Scripting.FixedUpdate -= OnClientNetworkUpdate;
Level.ActorSpawned -= OnClientActorSpawned;
if (server != null) LocalPlayerClientId = 0;
{
NetworkPeer.ShutdownPeer(server);
server = null;
}
if (client != null)
{
NetworkPeer.ShutdownPeer(client);
client = null;
}
LocalPlayerClientId = 0;
#if FLAX_EDITOR #if FLAX_EDITOR
Editor.Instance.PlayModeEnd -= Cleanup; Editor.Instance.PlayModeEnd -= Cleanup;
//GameModeManager.Cleanup(); // FIXME //GameModeManager.Cleanup(); // FIXME
#endif #endif
if (clientWorldStateManager != null) if (clientWorldStateManager != null)
{ {
clientWorldStateManager.Cleanup(); clientWorldStateManager.Cleanup();
clientWorldStateManager = null; clientWorldStateManager = null;
} }
if (serverWorldStateManager != null) if (serverWorldStateManager != null)
{ {
serverWorldStateManager.Cleanup(); serverWorldStateManager.Cleanup();
serverWorldStateManager = null; serverWorldStateManager = null;
}
StopRecording();
initialized = false;
} }
private static void OnNetworkMessage(ref NetworkEvent networkEvent, Span<OnMessageDecl> funcs) StopRecording();
initialized = false;
}
private static void OnNetworkMessage(ref NetworkEvent networkEvent, Span<OnMessageDecl> funcs)
{
byte messageTypeByte = networkEvent.Message.ReadByte();
if (!Enum.IsDefined(typeof(NetworkMessageType), messageTypeByte))
{ {
byte messageTypeByte = networkEvent.Message.ReadByte(); Console.PrintError($"Unsupported message type received from client: {messageTypeByte}");
if (!Enum.IsDefined(typeof(NetworkMessageType), messageTypeByte)) return;
}
NetworkMessageType messageType = (NetworkMessageType)messageTypeByte;
switch (messageType)
{
case NetworkMessageType.Handshake:
{ {
Console.PrintError($"Unsupported message type received from client: {messageTypeByte}"); string message = networkEvent.Message.ReadString();
return; Console.Print($"Received handshake from {networkEvent.Sender.ConnectionId}, msg: " + message);
break;
} }
case NetworkMessageType.Message:
NetworkMessageType messageType = (NetworkMessageType)messageTypeByte;
switch (messageType)
{ {
case NetworkMessageType.Handshake: var messageStartPosition = networkEvent.Message.Position;
foreach (var func in funcs)
{ {
string message = networkEvent.Message.ReadString(); networkEvent.Message.Position = messageStartPosition;
Console.Print($"Received handshake from {networkEvent.Sender.ConnectionId}, msg: " + message); bool handled = func(ref networkEvent);
break; if (handled)
break;
} }
case NetworkMessageType.Message:
{ //if (OnMessage != null)
var messageStartPosition = networkEvent.Message.Position; /*{
foreach (var func in funcs) // GetInvocationList() allocates, use allocation-free but unsafe way to iterate
//
//foreach (OnMessageDecl func in OnMessage.GetInvocationList()
// .Cast<OnMessageDecl>().ToArray())
foreach (OnMessageDecl func in OnMessageDelegates)
{ {
networkEvent.Message.Position = messageStartPosition; bool ret = func.Invoke(ref networkEvent);
bool handled = func(ref networkEvent); if (ret)
if (handled)
break; break;
} }
}*/
//if (OnMessage != null) break;
/*{
// GetInvocationList() allocates, use allocation-free but unsafe way to iterate
//
//foreach (OnMessageDecl func in OnMessage.GetInvocationList()
// .Cast<OnMessageDecl>().ToArray())
foreach (OnMessageDecl func in OnMessageDelegates)
{
bool ret = func.Invoke(ref networkEvent);
if (ret)
break;
}
}*/
break;
}
default:
Console.PrintError($"Unsupported message type received from client: {messageTypeByte}");
break;
} }
default:
Console.PrintError($"Unsupported message type received from client: {messageTypeByte}");
break;
} }
} }
} }

View File

@@ -6,153 +6,152 @@ using FlaxEngine.Networking;
using Console = Game.Console; using Console = Game.Console;
using Object = FlaxEngine.Object; using Object = FlaxEngine.Object;
namespace Game namespace Game;
public static partial class NetworkManager
{ {
public static partial class NetworkManager public static uint LocalPlayerClientId { get; /*private*/ set; }
public static INetworkDriver ClientNetworkDriver { get; set; }
public static WorldStateManager clientWorldStateManager = null;
public static bool ConnectServer(string serverAddress = "localhost", bool listenServer = false)
{ {
public static uint LocalPlayerClientId { get; /*private*/ set; } if (!listenServer)
public static INetworkDriver ClientNetworkDriver { get; set; }
public static WorldStateManager clientWorldStateManager = null;
public static bool ConnectServer(string serverAddress = "localhost", bool listenServer = false)
{ {
if (!listenServer) Cleanup();
{ //WorldStateManager.Init();
Cleanup(); clientWorldStateManager = new WorldStateManager(isClient: true);
//WorldStateManager.Init(); }
clientWorldStateManager = new WorldStateManager(isClient: true); else
} {
else clientWorldStateManager = new WorldStateManager(isLocalClient: true);
{ }
clientWorldStateManager = new WorldStateManager(isLocalClient: true); ServerAddress = serverAddress;
}
ServerAddress = serverAddress;
//var driver = Object.New(typeof(ENetDriver)); //var driver = Object.New(typeof(ENetDriver));
//ClientNetworkDriver = null; //ClientNetworkDriver = null;
NetworkLagDriver driver = Object.New<NetworkLagDriver>(); NetworkLagDriver driver = Object.New<NetworkLagDriver>();
driver.Lag = 0f; driver.Lag = 0f;
ClientNetworkDriver = driver; ClientNetworkDriver = driver;
client = NetworkPeer.CreatePeer(new NetworkConfig
{
NetworkDriver = driver,
ConnectionsLimit = MaximumClients,
MessagePoolSize = 2048,
MessageSize = MTU,
Address = ServerAddress == "localhost" ? "127.0.0.1" : ServerAddress,
Port = ServerPort
});
if (client == null)
{
Console.Print("Failed to create NetworkPeer.");
return false;
}
if (!client.Connect())
{
Console.Print("Failed to connect to the server.");
return false;
}
Scripting.FixedUpdate += OnClientNetworkUpdate;
if (!listenServer)
{
Scripting.Exit += Cleanup;
//Level.ActorSpawned += OnClientActorSpawned;
}
string demoPath = System.IO.Path.Combine(AssetManager.DemoPath, $"{DateTimeOffset.Now.UtcTicks}_v2.gdem");
RecordDemo(demoPath);
return true;
}
private static void OnClientNetworkUpdate()
{
using var _ = Utilities.ProfileScope("NetworkManager_OnClientNetworkUpdate");
while (client.PopEvent(out NetworkEvent networkEvent))
{
RecordMessage(ref networkEvent);
try
{
//IsLocalClient = server != null;
//IsClient = true;
OnClientReadMessage(ref networkEvent);
}
finally
{
//IsLocalClient = false;
//IsClient = false;
if (networkEvent.EventType == NetworkEventType.Message)
client.RecycleMessage(networkEvent.Message);
}
client = NetworkPeer.CreatePeer(new NetworkConfig
{
NetworkDriver = driver,
ConnectionsLimit = MaximumClients,
MessagePoolSize = 2048,
MessageSize = MTU,
Address = ServerAddress == "localhost" ? "127.0.0.1" : ServerAddress,
Port = ServerPort
});
if (client == null)
{
Console.Print("Failed to create NetworkPeer.");
return false;
}
if (!client.Connect())
{
Console.Print("Failed to connect to the server.");
return false;
}
Scripting.FixedUpdate += OnClientNetworkUpdate;
if (!listenServer)
{
Scripting.Exit += Cleanup;
//Level.ActorSpawned += OnClientActorSpawned;
}
string demoPath = System.IO.Path.Combine(AssetManager.DemoPath, $"{DateTimeOffset.Now.UtcTicks}_v2.gdem");
RecordDemo(demoPath);
return true;
}
private static void OnClientNetworkUpdate()
{
using var _ = Utilities.ProfileScope("NetworkManager_OnClientNetworkUpdate");
while (client.PopEvent(out NetworkEvent networkEvent))
{
RecordMessage(ref networkEvent);
try
{
//IsLocalClient = server != null;
//IsClient = true;
OnClientReadMessage(ref networkEvent);
}
finally
{
//IsLocalClient = false;
//IsClient = false;
if (networkEvent.EventType == NetworkEventType.Message)
client.RecycleMessage(networkEvent.Message);
}
}
}
private static void OnClientReadMessage(ref NetworkEvent networkEvent)
{
using var _ = Utilities.ProfileScope("NetworkManager_OnClientReadMessage");
switch (networkEvent.EventType)
{
case NetworkEventType.Connected:
{
LocalPlayerClientId = networkEvent.Sender.ConnectionId;
Console.Print("Connected to server, ConnectionId: " + networkEvent.Sender.ConnectionId);
break;
}
case NetworkEventType.Disconnected:
{
Console.Print("Disconnected from server, timeout.");
LocalPlayerClientId = 0;
break;
}
case NetworkEventType.Timeout:
{
Console.Print("Disconnected from server, connection closed.");
LocalPlayerClientId = 0;
break;
}
case NetworkEventType.Message:
{
OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnClientMessageDelegates));
if (networkEvent.Message.Position > 0 &&
networkEvent.Message.Position < networkEvent.Message.Length)
{
string err =
$"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}.\n";
networkEvent.Message.Position = 0;
byte[] messageBytes = new byte[networkEvent.Message.Length];
unsafe
{
fixed (byte* messageBytePtr = &messageBytes[0])
networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length);
}
//string messageBytesStr = string.Join(", ", messageBytes.Select(x => "0x" + ((int)x).ToString("X2")));
string messageBytesStr = string.Join("", messageBytes.Select(x => ((int)x).ToString("X2")));
Console.PrintError(err + $"Message dump: {messageBytesStr}");
}
break;
}
default:
throw new ArgumentOutOfRangeException();
}
DebugLastHandledMessage = "";
}
private static void OnClientActorSpawned(Actor actor)
{
} }
} }
private static void OnClientReadMessage(ref NetworkEvent networkEvent)
{
using var _ = Utilities.ProfileScope("NetworkManager_OnClientReadMessage");
switch (networkEvent.EventType)
{
case NetworkEventType.Connected:
{
LocalPlayerClientId = networkEvent.Sender.ConnectionId;
Console.Print("Connected to server, ConnectionId: " + networkEvent.Sender.ConnectionId);
break;
}
case NetworkEventType.Disconnected:
{
Console.Print("Disconnected from server, timeout.");
LocalPlayerClientId = 0;
break;
}
case NetworkEventType.Timeout:
{
Console.Print("Disconnected from server, connection closed.");
LocalPlayerClientId = 0;
break;
}
case NetworkEventType.Message:
{
OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnClientMessageDelegates));
if (networkEvent.Message.Position > 0 &&
networkEvent.Message.Position < networkEvent.Message.Length)
{
string err =
$"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}.\n";
networkEvent.Message.Position = 0;
byte[] messageBytes = new byte[networkEvent.Message.Length];
unsafe
{
fixed (byte* messageBytePtr = &messageBytes[0])
networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length);
}
//string messageBytesStr = string.Join(", ", messageBytes.Select(x => "0x" + ((int)x).ToString("X2")));
string messageBytesStr = string.Join("", messageBytes.Select(x => ((int)x).ToString("X2")));
Console.PrintError(err + $"Message dump: {messageBytesStr}");
}
break;
}
default:
throw new ArgumentOutOfRangeException();
}
DebugLastHandledMessage = "";
}
private static void OnClientActorSpawned(Actor actor)
{
}
} }

View File

@@ -11,165 +11,195 @@ using FlaxEngine.Networking;
using Console = Game.Console; using Console = Game.Console;
using Object = FlaxEngine.Object; using Object = FlaxEngine.Object;
namespace Game namespace Game;
public static unsafe partial class NetworkManager
{ {
public static unsafe partial class NetworkManager public const byte DemoVer = 2;
private struct DemoNetworkEventHeader
{
public NetworkEventType EventType;
public uint SenderId;
public uint MessageId;
public uint Length;
}
private static List<NetworkEvent> buffer;
private static IEnumerator<NetworkEvent> bufferEnumerable;
private static GZipStream demoStream; // record
private static FileStream demoFileStream;
public static bool IsRecording => demoStream != null;
private static long flushedFrames = 0;
public static bool IsDemoPlaying => bufferEnumerable != null;
public static bool PlayDemo(string demoName)
{ {
public const byte DemoVer = 2;
private struct DemoNetworkEventHeader
{ {
public NetworkEventType EventType; Cleanup();
public uint SenderId; clientWorldStateManager = new WorldStateManager(isServer: true);
public uint MessageId;
public uint Length;
} }
private static List<NetworkEvent> buffer; if (!ReadDemo(demoName))
private static IEnumerator<NetworkEvent> bufferEnumerable;
private static GZipStream demoStream; // record
private static FileStream demoFileStream;
public static bool IsRecording => demoStream != null;
private static long flushedFrames = 0;
public static bool IsDemoPlaying => bufferEnumerable != null;
public static bool PlayDemo(string demoName)
{ {
{ Console.Print("Failed to read demo.");
Cleanup(); return false;
clientWorldStateManager = new WorldStateManager(isServer: true); }
}
/*if (client == null)
{
Console.Print("Failed to create NetworkPeer.");
return false;
}
if (!client.Connect())
{
Console.Print("Failed to connect to the server.");
return false;
}*/
if (!ReadDemo(demoName)) Scripting.FixedUpdate += OnDemoUpdate;
//if (!listenServer)
{
Scripting.Exit += Cleanup;
//Level.ActorSpawned += OnClientActorSpawned;
}
return true;
}
public static void RecordDemo(string demoPath)
{
if (IsRecording)
StopRecording();
buffer = new List<NetworkEvent>();
var demoFolder = Directory.GetParent(demoPath);
if (!demoFolder.Exists)
Directory.CreateDirectory(demoFolder.FullName);
demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write);
demoStream = new GZipStream(demoFileStream, CompressionMode.Compress);
demoStream.WriteByte(DemoVer);
}
private static void RecordMessage(ref NetworkEvent networkEvent)
{
if (!IsRecording)
return;
NetworkEvent recordedEvent = networkEvent;
recordedEvent.Message.Buffer = (byte*)NativeMemory.Alloc(recordedEvent.Message.Length);
recordedEvent.Sender.ConnectionId = networkEvent.Sender.ConnectionId;
NativeMemory.Copy(networkEvent.Message.Buffer, recordedEvent.Message.Buffer, recordedEvent.Message.Length);
buffer.Add(recordedEvent);
}
public static void FlushDemo()
{
if (!IsRecording)
return;
Stopwatch sw = Stopwatch.StartNew();
Span<byte> bytes = stackalloc byte[Unsafe.SizeOf<DemoNetworkEventHeader>()];
foreach (ref NetworkEvent networkEvent in CollectionsMarshal.AsSpan(buffer))
{
DemoNetworkEventHeader header = new DemoNetworkEventHeader()
{ {
Console.Print("Failed to read demo."); EventType = networkEvent.EventType,
return false; SenderId = networkEvent.Sender.ConnectionId,
} Length = networkEvent.Message.Length,
MessageId = networkEvent.Message.MessageId,
};
MemoryMarshal.Write(bytes, header);
demoStream.Write(bytes);
Span<byte> messageBytes = new Span<byte>(networkEvent.Message.Buffer, (int)networkEvent.Message.Length);
demoStream.Write(messageBytes);
/*if (client == null) // TODO: free networkEvent buffer
{
Console.Print("Failed to create NetworkPeer.");
return false;
}
if (!client.Connect())
{
Console.Print("Failed to connect to the server.");
return false;
}*/
Scripting.FixedUpdate += OnDemoUpdate;
//if (!listenServer)
{
Scripting.Exit += Cleanup;
//Level.ActorSpawned += OnClientActorSpawned;
}
return true;
} }
public static void RecordDemo(string demoPath) sw.Stop();
flushedFrames += buffer.Count;
buffer.Clear();
FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}");
}
public static void StopRecording()
{
if (!IsRecording)
return;
FlushDemo();
demoStream.Close();
demoStream = null;
demoFileStream.Close();
demoFileStream = null;
}
private static unsafe bool ReadDemo(string demoPath)
{
if (!File.Exists(demoPath))
return false;
buffer = new List<NetworkEvent>();
Stopwatch sw = Stopwatch.StartNew();
using FileStream fileStream = File.OpenRead(demoPath);
using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress);
int ver = stream.ReadByte();
if (ver != DemoVer)
{ {
if (IsRecording) Console.Print($"Demo version mismatch, expected {DemoVer}, got {ver}");
StopRecording(); stream.Close();
return false;
buffer = new List<NetworkEvent>();
var demoFolder = Directory.GetParent(demoPath);
if (!demoFolder.Exists)
Directory.CreateDirectory(demoFolder.FullName);
demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write);
demoStream = new GZipStream(demoFileStream, CompressionMode.Compress);
demoStream.WriteByte(DemoVer);
} }
private static void RecordMessage(ref NetworkEvent networkEvent) int headerSize = Unsafe.SizeOf<DemoNetworkEventHeader>();
Span<byte> headerBuffer = stackalloc byte[headerSize];
while (true)
{ {
if (!IsRecording)
return;
NetworkEvent recordedEvent = networkEvent;
recordedEvent.Message.Buffer = (byte*)NativeMemory.Alloc(recordedEvent.Message.Length);
recordedEvent.Sender.ConnectionId = networkEvent.Sender.ConnectionId;
NativeMemory.Copy(networkEvent.Message.Buffer, recordedEvent.Message.Buffer, recordedEvent.Message.Length);
buffer.Add(recordedEvent);
}
public static void FlushDemo()
{
if (!IsRecording)
return;
Stopwatch sw = Stopwatch.StartNew();
Span<byte> bytes = stackalloc byte[Unsafe.SizeOf<DemoNetworkEventHeader>()];
foreach (ref NetworkEvent networkEvent in CollectionsMarshal.AsSpan(buffer))
{ {
DemoNetworkEventHeader header = new DemoNetworkEventHeader() int bytesLeftInBuffer = headerSize;
do
{ {
EventType = networkEvent.EventType, int readBytes = stream.Read(headerBuffer.Slice(headerSize - bytesLeftInBuffer, bytesLeftInBuffer));
SenderId = networkEvent.Sender.ConnectionId, if (readBytes == 0)
Length = networkEvent.Message.Length, break;
MessageId = networkEvent.Message.MessageId, bytesLeftInBuffer -= readBytes;
}; } while (bytesLeftInBuffer > 0);
MemoryMarshal.Write(bytes, header);
demoStream.Write(bytes);
Span<byte> messageBytes = new Span<byte>(networkEvent.Message.Buffer, (int)networkEvent.Message.Length); if (bytesLeftInBuffer > 0)
demoStream.Write(messageBytes); break; // EOF;
// TODO: free networkEvent buffer
} }
sw.Stop(); DemoNetworkEventHeader header = MemoryMarshal.Read<DemoNetworkEventHeader>(headerBuffer);
flushedFrames += buffer.Count;
buffer.Clear();
FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}"); //buffer.Add(new NetworkEvent());
} //ref NetworkEvent networkEvent = ref buffer[buffer.Length]; // collectionmarshal
NetworkEvent networkEvent = new NetworkEvent();
networkEvent.EventType = header.EventType;
networkEvent.Message.Position = 0;
networkEvent.Message.MessageId = header.MessageId;
networkEvent.Message.Length = networkEvent.Message.BufferSize = header.Length;
networkEvent.Sender.ConnectionId = header.SenderId;
public static void StopRecording() if (header.Length > 0)
{
if (!IsRecording)
return;
FlushDemo();
demoStream.Close();
demoStream = null;
demoFileStream.Close();
demoFileStream = null;
}
private static unsafe bool ReadDemo(string demoPath)
{
if (!File.Exists(demoPath))
return false;
buffer = new List<NetworkEvent>();
Stopwatch sw = Stopwatch.StartNew();
using FileStream fileStream = File.OpenRead(demoPath);
using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress);
int ver = stream.ReadByte();
if (ver != DemoVer)
{ {
Console.Print($"Demo version mismatch, expected {DemoVer}, got {ver}"); networkEvent.Message.Buffer = (byte*)NativeMemory.Alloc(header.Length);
stream.Close();
return false;
}
int headerSize = Unsafe.SizeOf<DemoNetworkEventHeader>(); Span<byte> messageBufferSpan = new Span<byte>(networkEvent.Message.Buffer, (int)networkEvent.Message.BufferSize);
Span<byte> headerBuffer = stackalloc byte[headerSize];
while (true)
{
{ {
int bytesLeftInBuffer = headerSize; int bytesLeftInBuffer = (int)header.Length;
do do
{ {
int readBytes = stream.Read(headerBuffer.Slice(headerSize - bytesLeftInBuffer, bytesLeftInBuffer)); int readBytes = stream.Read(messageBufferSpan.Slice((int)header.Length - bytesLeftInBuffer, bytesLeftInBuffer));
if (readBytes == 0) if (readBytes == 0)
break; break;
bytesLeftInBuffer -= readBytes; bytesLeftInBuffer -= readBytes;
@@ -178,101 +208,70 @@ namespace Game
if (bytesLeftInBuffer > 0) if (bytesLeftInBuffer > 0)
break; // EOF; break; // EOF;
} }
DemoNetworkEventHeader header = MemoryMarshal.Read<DemoNetworkEventHeader>(headerBuffer);
//buffer.Add(new NetworkEvent());
//ref NetworkEvent networkEvent = ref buffer[buffer.Length]; // collectionmarshal
NetworkEvent networkEvent = new NetworkEvent();
networkEvent.EventType = header.EventType;
networkEvent.Message.Position = 0;
networkEvent.Message.MessageId = header.MessageId;
networkEvent.Message.Length = networkEvent.Message.BufferSize = header.Length;
networkEvent.Sender.ConnectionId = header.SenderId;
if (header.Length > 0)
{
networkEvent.Message.Buffer = (byte*)NativeMemory.Alloc(header.Length);
Span<byte> messageBufferSpan = new Span<byte>(networkEvent.Message.Buffer, (int)networkEvent.Message.BufferSize);
{
int bytesLeftInBuffer = (int)header.Length;
do
{
int readBytes = stream.Read(messageBufferSpan.Slice((int)header.Length - bytesLeftInBuffer, bytesLeftInBuffer));
if (readBytes == 0)
break;
bytesLeftInBuffer -= readBytes;
} while (bytesLeftInBuffer > 0);
if (bytesLeftInBuffer > 0)
break; // EOF;
}
}
buffer.Add(networkEvent);
} }
buffer.Add(networkEvent);
sw.Stop();
bufferEnumerable = buffer.GetEnumerator();
Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} ");
DemoEndFrame(); // advances to first frame
return true;
} }
private static void DemoEndFrame() sw.Stop();
bufferEnumerable = buffer.GetEnumerator();
Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} ");
DemoEndFrame(); // advances to first frame
return true;
}
private static void DemoEndFrame()
{
// TODO: check if the current state frame matches the current frame number before advancing
/*asdf++;
if (asdf < 8)
return;*/
if (bufferEnumerable == null || !bufferEnumerable.MoveNext())
{ {
// TODO: check if the current state frame matches the current frame number before advancing if (buffer.Any())
/*asdf++;
if (asdf < 8)
return;*/
if (bufferEnumerable == null || !bufferEnumerable.MoveNext())
{ {
if (buffer.Any()) bufferEnumerable.Dispose();
{ bufferEnumerable = null;
bufferEnumerable.Dispose(); buffer.Clear();
bufferEnumerable = null; Console.Print("Demo ended");
buffer.Clear();
Console.Print("Demo ended");
}
return;
} }
//var actorState = currentState.actor; return;
//currentState.input = bufferEnumerable.Current;
//frame++;
//currentState.actor = actorState;
} }
private static void OnDemoUpdate() //var actorState = currentState.actor;
//currentState.input = bufferEnumerable.Current;
//frame++;
//currentState.actor = actorState;
}
private static void OnDemoUpdate()
{
if (!IsDemoPlaying)
return;
using Utilities.ScopeProfiler _ = Utilities.ProfileScope("NetworkManager_OnDemoUpdate");
NetworkEvent demoEvent = bufferEnumerable.Current; // ref?
// TODO: change/randomize Sender.ConnectionId?
try
{ {
if (!IsDemoPlaying) //IsLocalClient = server != null;
return; //IsClient = true;
OnClientReadMessage(ref demoEvent);
using Utilities.ScopeProfiler _ = Utilities.ProfileScope("NetworkManager_OnDemoUpdate");
NetworkEvent demoEvent = bufferEnumerable.Current; // ref?
// TODO: change/randomize Sender.ConnectionId?
try
{
//IsLocalClient = server != null;
//IsClient = true;
OnClientReadMessage(ref demoEvent);
}
finally
{
//IsLocalClient = false;
//IsClient = false;
//TODO: recycle event?
}
DemoEndFrame();
} }
finally
{
//IsLocalClient = false;
//IsClient = false;
//TODO: recycle event?
}
DemoEndFrame();
} }
} }

View File

@@ -8,206 +8,205 @@ using FlaxEngine.Networking;
using Console = Game.Console; using Console = Game.Console;
using Object = FlaxEngine.Object; using Object = FlaxEngine.Object;
namespace Game namespace Game;
public static partial class NetworkManager
{ {
public static partial class NetworkManager private static List<NetworkConnection> ConnectedClients;
private static List<Type> NetworkedTypes;
public static INetworkDriver ServerNetworkDriver { get; set; }
public static WorldStateManager serverWorldStateManager = null;
public static bool StartServer(bool listenServer = true)
{ {
private static List<NetworkConnection> ConnectedClients; Cleanup();
ConnectedClients = new List<NetworkConnection>(MaximumClients);
private static List<Type> NetworkedTypes;
public static INetworkDriver ServerNetworkDriver { get; set; } //var driver = Object.New(typeof(ENetDriver));
//ServerNetworkDriver = null;
NetworkLagDriver driver = Object.New<NetworkLagDriver>();
driver.Lag = 200f;
ServerNetworkDriver = driver;
public static WorldStateManager serverWorldStateManager = null; server = NetworkPeer.CreatePeer(new NetworkConfig
public static bool StartServer(bool listenServer = true)
{ {
Cleanup(); NetworkDriver = driver,
ConnectedClients = new List<NetworkConnection>(MaximumClients); ConnectionsLimit = MaximumClients,
MessagePoolSize = 2048,
MessageSize = MTU,
Address = "any",
Port = ServerPort
});
if (!server.Listen())
{
Console.PrintError("Failed to start the server.");
return false;
}
Scripting.FixedUpdate += OnServerNetworkUpdate;
Scripting.Exit += Cleanup;
Level.ActorSpawned += OnServerActorSpawned;
//var driver = Object.New(typeof(ENetDriver)); NetworkedTypes = new List<Type>();
//ServerNetworkDriver = null;
NetworkLagDriver driver = Object.New<NetworkLagDriver>();
driver.Lag = 200f;
ServerNetworkDriver = driver;
server = NetworkPeer.CreatePeer(new NetworkConfig
{
NetworkDriver = driver,
ConnectionsLimit = MaximumClients,
MessagePoolSize = 2048,
MessageSize = MTU,
Address = "any",
Port = ServerPort
});
if (!server.Listen())
{
Console.PrintError("Failed to start the server.");
return false;
}
Scripting.FixedUpdate += OnServerNetworkUpdate;
Scripting.Exit += Cleanup;
Level.ActorSpawned += OnServerActorSpawned;
NetworkedTypes = new List<Type>();
#if false #if false
var assemblies = Utils.GetAssemblies(); var assemblies = Utils.GetAssemblies();
foreach (Assembly assembly in assemblies) foreach (Assembly assembly in assemblies)
{ {
// Skip common assemblies // Skip common assemblies
string assemblyName = assembly.GetName().Name; string assemblyName = assembly.GetName().Name;
if (assemblyName == "System" || if (assemblyName == "System" ||
assemblyName.StartsWith("System.") || assemblyName.StartsWith("System.") ||
assemblyName.StartsWith("Mono.") || assemblyName.StartsWith("Mono.") ||
assemblyName == "mscorlib" || assemblyName == "mscorlib" ||
assemblyName == "Newtonsoft.Json" || assemblyName == "Newtonsoft.Json" ||
assemblyName == "Snippets" || assemblyName == "Snippets" ||
assemblyName == "netstandard" || assemblyName == "netstandard" ||
assemblyName == "Anonymously Hosted DynamicMethods Assembly" || assemblyName == "Anonymously Hosted DynamicMethods Assembly" ||
assemblyName.StartsWith("FlaxEngine.") || assemblyName.StartsWith("FlaxEngine.") ||
assemblyName.StartsWith("JetBrains.") || assemblyName.StartsWith("JetBrains.") ||
assemblyName.StartsWith("Microsoft.") || assemblyName.StartsWith("Microsoft.") ||
assemblyName.StartsWith("nunit.")) assemblyName.StartsWith("nunit."))
continue; continue;
foreach (Type type in assembly.GetTypes()) foreach (Type type in assembly.GetTypes())
if (type.GetCustomAttributes().Any(x => x is NetworkedAttribute)) if (type.GetCustomAttributes().Any(x => x is NetworkedAttribute))
NetworkedTypes.Add(type); NetworkedTypes.Add(type);
} }
foreach (Type type in NetworkedTypes) foreach (Type type in NetworkedTypes)
Console.Print("tracking networked type: " + type.Name); Console.Print("tracking networked type: " + type.Name);
#endif #endif
serverWorldStateManager = new WorldStateManager(isServer: true); serverWorldStateManager = new WorldStateManager(isServer: true);
//WorldStateManager.Init(); //WorldStateManager.Init();
if (listenServer) if (listenServer)
return ConnectServer(listenServer: true); return ConnectServer(listenServer: true);
return true; return true;
} }
public static NetworkMessage ServerBeginSendMessage() public static NetworkMessage ServerBeginSendMessage()
{
NetworkMessage message = server.BeginSendMessage();
message.WriteByte((byte)NetworkMessageType.Message);
return message;
}
public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection connection, NetworkChannelType channelType = NetworkChannelType.Reliable)
{
server.EndSendMessage(channelType, message, connection);
}
public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection[] connections, NetworkChannelType channelType = NetworkChannelType.Reliable)
{
server.EndSendMessage(channelType, message, connections);
}
public static NetworkMessage ClientBeginSendMessage()
{
NetworkMessage message = client.BeginSendMessage();
message.WriteByte((byte)NetworkMessageType.Message);
return message;
}
public static void ClientEndSendMessage(ref NetworkMessage message, NetworkChannelType channelType = NetworkChannelType.Reliable)
{
client.EndSendMessage(channelType, message);
}
private static void OnServerNetworkUpdate()
{
using var _ = Utilities.ProfileScope("NetworkManager_OnServerNetworkUpdate");
while (server.PopEvent(out NetworkEvent networkEvent))
OnServerReadMessage(ref networkEvent);
}
private static void OnServerReadMessage(ref NetworkEvent networkEvent)
{
using var _ = Utilities.ProfileScope("NetworkManager_OnServerReadMessage");
switch (networkEvent.EventType)
{ {
NetworkMessage message = server.BeginSendMessage(); case NetworkEventType.Connected:
message.WriteByte((byte)NetworkMessageType.Message);
return message;
}
public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection connection, NetworkChannelType channelType = NetworkChannelType.Reliable)
{
server.EndSendMessage(channelType, message, connection);
}
public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection[] connections, NetworkChannelType channelType = NetworkChannelType.Reliable)
{
server.EndSendMessage(channelType, message, connections);
}
public static NetworkMessage ClientBeginSendMessage()
{
NetworkMessage message = client.BeginSendMessage();
message.WriteByte((byte)NetworkMessageType.Message);
return message;
}
public static void ClientEndSendMessage(ref NetworkMessage message, NetworkChannelType channelType = NetworkChannelType.Reliable)
{
client.EndSendMessage(channelType, message);
}
private static void OnServerNetworkUpdate()
{
using var _ = Utilities.ProfileScope("NetworkManager_OnServerNetworkUpdate");
while (server.PopEvent(out NetworkEvent networkEvent))
OnServerReadMessage(ref networkEvent);
}
private static void OnServerReadMessage(ref NetworkEvent networkEvent)
{
using var _ = Utilities.ProfileScope("NetworkManager_OnServerReadMessage");
switch (networkEvent.EventType)
{ {
case NetworkEventType.Connected: Console.Print($"Client({networkEvent.Sender.ConnectionId}) is trying to connect");
try
{ {
Console.Print($"Client({networkEvent.Sender.ConnectionId}) is trying to connect"); //IsServer = true;
try if (serverWorldStateManager.OnClientConnecting(networkEvent.Sender))
{ {
//IsServer = true; ConnectedClients.Add(networkEvent.Sender);
if (serverWorldStateManager.OnClientConnecting(networkEvent.Sender)) Console.Print(
{ $"Client({networkEvent.Sender.ConnectionId}) connected. Total clients: {ConnectedClients.Count}");
ConnectedClients.Add(networkEvent.Sender);
Console.Print(
$"Client({networkEvent.Sender.ConnectionId}) connected. Total clients: {ConnectedClients.Count}");
serverWorldStateManager.OnClientConnected(networkEvent.Sender); serverWorldStateManager.OnClientConnected(networkEvent.Sender);
}
else
Console.Print($"Client({networkEvent.Sender.ConnectionId}) connection refused");
} }
finally else
{ Console.Print($"Client({networkEvent.Sender.ConnectionId}) connection refused");
//IsServer = false;
}
break;
} }
case NetworkEventType.Disconnected: finally
case NetworkEventType.Timeout:
{ {
Console.Print($"Client({networkEvent.Sender.ConnectionId}) disconnected!"); //IsServer = false;
ConnectedClients.Remove(networkEvent.Sender);
Console.Print("Connected clients: " + ConnectedClients.Count);
break;
} }
case NetworkEventType.Message:
{
try
{
//IsServer = true;
OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnServerMessageDelegates));
if (networkEvent.Message.Position > 0 && break;
networkEvent.Message.Position < networkEvent.Message.Length)
{
string err =
$"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}.";
networkEvent.Message.Position = 0;
byte[] messageBytes = new byte[networkEvent.Message.Length];
unsafe
{
fixed (byte* messageBytePtr = &messageBytes[0])
networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length);
}
string messageBytesStr = string.Join(", ",
messageBytes.Select(x => "0x" + ((int)x).ToString("X2")));
Console.PrintError(err + $"Message dump: {messageBytesStr}");
}
}
finally
{
//IsServer = false;
server.RecycleMessage(networkEvent.Message);
}
break;
}
default:
throw new ArgumentOutOfRangeException();
} }
} case NetworkEventType.Disconnected:
case NetworkEventType.Timeout:
{
Console.Print($"Client({networkEvent.Sender.ConnectionId}) disconnected!");
private static void OnServerActorSpawned(Actor actor) ConnectedClients.Remove(networkEvent.Sender);
{ Console.Print("Connected clients: " + ConnectedClients.Count);
//Console.Print($"actor spawned: {actor.Name} ({actor.TypeName})"); break;
}
case NetworkEventType.Message:
{
try
{
//IsServer = true;
OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnServerMessageDelegates));
if (networkEvent.Message.Position > 0 &&
networkEvent.Message.Position < networkEvent.Message.Length)
{
string err =
$"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}.";
networkEvent.Message.Position = 0;
byte[] messageBytes = new byte[networkEvent.Message.Length];
unsafe
{
fixed (byte* messageBytePtr = &messageBytes[0])
networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length);
}
string messageBytesStr = string.Join(", ",
messageBytes.Select(x => "0x" + ((int)x).ToString("X2")));
Console.PrintError(err + $"Message dump: {messageBytesStr}");
}
}
finally
{
//IsServer = false;
server.RecycleMessage(networkEvent.Message);
}
break;
}
default:
throw new ArgumentOutOfRangeException();
} }
} }
private static void OnServerActorSpawned(Actor actor)
{
//Console.Print($"actor spawned: {actor.Name} ({actor.TypeName})");
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,23 +3,22 @@ using System.Diagnostics;
using FlaxEngine; using FlaxEngine;
using FlaxEngine.GUI; using FlaxEngine.GUI;
namespace Game namespace Game;
{
public class CrosshairWidget : Script public class CrosshairWidget : Script
{
private Image control;
public override void OnAwake()
{ {
private Image control; var uiControl = Actor.As<UIControl>();
//control = new Image();
//uiControl.Control = control;
public override void OnAwake() control = (Image)uiControl.Control;
{ }
var uiControl = Actor.As<UIControl>();
//control = new Image();
//uiControl.Control = control;
control = (Image)uiControl.Control; public override void OnUpdate()
} {
public override void OnUpdate()
{
}
} }
} }

View File

@@ -3,71 +3,71 @@ using System.Diagnostics;
using FlaxEngine; using FlaxEngine;
using FlaxEngine.GUI; using FlaxEngine.GUI;
namespace Game namespace Game;
[ExecuteInEditMode]
public class PerformanceWidget : Script
{ {
[ExecuteInEditMode] private const double updateInterval = 0.2;
public class PerformanceWidget : Script
public UIControl control;
private Label label;
private Stopwatch stopwatch;
private Stopwatch stopwatch2;
private double updateTimeAvg;
private double updateTimeAvg2;
private ulong updateTimeCount;
private ulong updateTimeCount2;
public override void OnAwake()
{ {
private const double updateInterval = 0.2; label = (Label)control.Control;
label.Text = $"0fps";
control.HideFlags = HideFlags.None;
public UIControl control; stopwatch = Stopwatch.StartNew();
private Label label; stopwatch2 = Stopwatch.StartNew();
}
private Stopwatch stopwatch; public override void OnUpdate()
private Stopwatch stopwatch2; {
updateTimeCount2++;
private double updateTimeAvg; double elapsed2 = stopwatch2.Elapsed.TotalSeconds;
private double updateTimeAvg2; if (elapsed2 >= updateInterval * 10)
private ulong updateTimeCount;
private ulong updateTimeCount2;
public override void OnAwake()
{ {
label = (Label)control.Control; stopwatch2.Restart();
label.Text = $"0fps"; updateTimeAvg2 = elapsed2 / updateTimeCount2;
control.HideFlags = HideFlags.None; updateTimeCount2 = 0;
stopwatch = Stopwatch.StartNew();
stopwatch2 = Stopwatch.StartNew();
} }
public override void OnUpdate() updateTimeCount++;
double elapsed = stopwatch.Elapsed.TotalSeconds;
if (elapsed >= updateInterval)
{ {
updateTimeCount2++; stopwatch.Restart();
double elapsed2 = stopwatch2.Elapsed.TotalSeconds; updateTimeAvg = elapsed / updateTimeCount;
if (elapsed2 >= updateInterval * 10) updateTimeCount = 0;
{
stopwatch2.Restart();
updateTimeAvg2 = elapsed2 / updateTimeCount2;
updateTimeCount2 = 0;
}
updateTimeCount++; label.Text = "";
double elapsed = stopwatch.Elapsed.TotalSeconds;
if (elapsed >= updateInterval)
{
stopwatch.Restart();
updateTimeAvg = elapsed / updateTimeCount;
updateTimeCount = 0;
label.Text = ""; long triangles = 0;
long drawCalls = 0;
long triangles = 0;
long drawCalls = 0;
#if BUILD_DEBUG || BUILD_DEVELOPMENT #if BUILD_DEBUG || BUILD_DEVELOPMENT
var gpuEvents = ProfilingTools.EventsGPU; var gpuEvents = ProfilingTools.EventsGPU;
if (gpuEvents.Length > 0) if (gpuEvents.Length > 0)
{ {
triangles = gpuEvents[0].Stats.Triangles; triangles = gpuEvents[0].Stats.Triangles;
drawCalls = gpuEvents[0].Stats.DrawCalls; drawCalls = gpuEvents[0].Stats.DrawCalls;
} }
#endif #endif
label.Text += $"{triangles} tris\n {drawCalls} drawcalls\n"; label.Text += $"{triangles} tris\n {drawCalls} drawcalls\n";
label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg2)}fps2\n"; label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg2)}fps2\n";
label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg)}fps"; label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg)}fps";
} }
#if false #if false
#if BUILD_DEVELOPMENT #if BUILD_DEVELOPMENT
@@ -98,6 +98,5 @@ namespace Game
} }
#endif #endif
#endif #endif
}
} }
} }

View File

@@ -3,18 +3,17 @@ using System.Diagnostics;
using FlaxEngine; using FlaxEngine;
using FlaxEngine.GUI; using FlaxEngine.GUI;
namespace Game namespace Game;
public class SpeedWidget : Script
{ {
public class SpeedWidget : Script public override void OnAwake()
{ {
public override void OnAwake()
{
} }
public override void OnUpdate() public override void OnUpdate()
{ {
}
} }
} }

View File

@@ -1,23 +1,22 @@
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
/// <summary>
/// List of supported materials for loaded levels.
/// Maps the given texture/shader name to Flax Material/MaterialInstance.
/// </summary>
public class BrushMaterialList
{ {
/// <summary> [EditorDisplay(name: "Material Assets")]
/// List of supported materials for loaded levels. public BrushMaterialListEntry[] materialAssets;
/// Maps the given texture/shader name to Flax Material/MaterialInstance. }
/// </summary>
public class BrushMaterialList
{
[EditorDisplay(name: "Material Assets")]
public BrushMaterialListEntry[] materialAssets;
}
public struct BrushMaterialListEntry public struct BrushMaterialListEntry
{ {
[EditorOrder(1)] [EditorDisplay(name: "Name")] [EditorOrder(1)] [EditorDisplay(name: "Name")]
public string name; public string name;
[EditorOrder(2)] [EditorDisplay(name: "Material")] [EditorOrder(2)] [EditorDisplay(name: "Material")]
public MaterialBase asset; public MaterialBase asset;
}
} }

View File

@@ -1,10 +1,9 @@
using System; using System;
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
[ExecuteInEditMode]
public class BrushScript : Script
{ {
[ExecuteInEditMode]
public class BrushScript : Script
{
}
} }

View File

@@ -2,18 +2,17 @@
using FlaxEngine; using FlaxEngine;
using FlaxEditor; using FlaxEditor;
namespace Game namespace Game;
{
[ExecuteInEditMode]
public class LevelScript : Script
{
public string MapName;
public DateTime MapTimestamp;
}
public class LevelScript2 : Script [ExecuteInEditMode]
{ public class LevelScript : Script
public string MapName; {
public DateTime MapTimestamp; public string MapName;
} public DateTime MapTimestamp;
}
public class LevelScript2 : Script
{
public string MapName;
public DateTime MapTimestamp;
} }

View File

@@ -7,523 +7,522 @@ using FlaxEngine;
// https://web.archive.org/web/20160316213335/http://forums.ubergames.net/topic/2658-understanding-the-quake-3-map-format/ // https://web.archive.org/web/20160316213335/http://forums.ubergames.net/topic/2658-understanding-the-quake-3-map-format/
// https://web.archive.org/web/20210228125854/https://forums.thedarkmod.com/index.php?/topic/15668-plugin-request-save-map-in-quake-3-format/ // https://web.archive.org/web/20210228125854/https://forums.thedarkmod.com/index.php?/topic/15668-plugin-request-save-map-in-quake-3-format/
namespace Game namespace Game;
public struct MapFacePlane
{ {
public struct MapFacePlane public Float3 v1, v2, v3;
{ public Plane plane;
public Float3 v1, v2, v3; public string texture;
public Plane plane; public Float2 offset;
public string texture; public float rotation;
public Float2 offset; public Float2 scale;
public float rotation; public int contentFlags, surfaceFlags, surfaceValue;
public Float2 scale; }
public int contentFlags, surfaceFlags, surfaceValue;
}
public class MapBrush public class MapBrush
{ {
public MapFacePlane[] planes; public MapFacePlane[] planes;
} }
public class MapPatch public class MapPatch
{ {
public string name; public string name;
} }
public struct PatchVertex public struct PatchVertex
{ {
public Float3 v; public Float3 v;
public Float2 uv; public Float2 uv;
} }
public class MapEntity public class MapEntity
{ {
public List<MapBrush> brushes = new List<MapBrush>(); public List<MapBrush> brushes = new List<MapBrush>();
public List<MapEntity> entities = new List<MapEntity>(); public List<MapEntity> entities = new List<MapEntity>();
public List<MapPatch> patches = new List<MapPatch>(); public List<MapPatch> patches = new List<MapPatch>();
public Dictionary<string, string> properties = new Dictionary<string, string>(); public Dictionary<string, string> properties = new Dictionary<string, string>();
} }
public static class MapParser public static class MapParser
{
public static MapEntity Parse(byte[] data)
{ {
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<data.Length; i++)
do
{ {
if (data.Length == 0) char c = (char)data[index];
return null; char c1 = index + 1 < data.Length ? (char)data[index + 1] : (char)0;
MapEntity rootEntity = new MapEntity(); switch (c)
MapEntity currentEntity = rootEntity;
int level = 0;
int index = 0;
//for (int i=0; i<data.Length; i++)
do
{ {
char c = (char)data[index]; case '\n':
char c1 = index + 1 < data.Length ? (char)data[index + 1] : (char)0; case '\r':
switch (c)
{
case '\n':
case '\r':
break;
case '/':
if (c1 == '/')
ParseComment(data, ref index);
else
throw new Exception("unexpected character: '" + c + "'");
break;
case '{':
{
currentEntity = new MapEntity();
rootEntity.entities.Add(currentEntity);
level++;
for (; index < data.Length; index++)
if (data[index] == '\n')
break;
index++;
ParseEntity(currentEntity, data, ref index);
break;
}
case '}':
{
//currentEntity = rootEntity;
break;
}
default:
throw new Exception("unsupported character: '" + c + "'");
}
//if (level < 0 || level > 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; break;
} case '/':
if (c1 == '/')
private static void ParseEntity(MapEntity currentEntity, byte[] data, ref int index) ParseComment(data, ref index);
{ else
bool entityParsed = false; throw new Exception("unexpected character: '" + c + "'");
do break;
{ case '{':
char c = (char)data[index];
char c1 = index + 1 < data.Length ? (char)data[index + 1] : (char)0;
switch (c)
{ {
case '\n': currentEntity = new MapEntity();
case '\r': rootEntity.entities.Add(currentEntity);
break;
case '/': level++;
if (c1 == '/')
ParseComment(data, ref index);
else
throw new Exception("unexpected character: '" + c + "'");
break;
// "name" "value" for (; index < data.Length; index++)
case '"': if (data[index] == '\n')
{
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; break;
index++;
if (data[index] == '(') ParseEntity(currentEntity, data, ref index);
currentEntity.brushes.Add(ParseBrush(data, ref index)); break;
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); case '}':
}
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]);
}
//if (data[index] == '\n')
// 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 Float3 ParseFloat3(byte[] data, ref int index)
{
return new Float3(
ParseFloat(data, ref index),
ParseFloat(data, ref index),
ParseFloat(data, ref index)
);
}
private static Float2 ParseFloat2(byte[] data, ref int index)
{
return new Float2(
ParseFloat(data, ref index),
ParseFloat(data, ref index)
);
}
private static Float3 ParsePlaneFloat3(byte[] data, ref int index)
{
index++;
while (index < data.Length)
{
if (data[index] != ' ')
break;
index++;
}
Float3 vector = ParseFloat3(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<MapFacePlane>(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': //currentEntity = rootEntity;
case '\n': break;
}
default:
throw new Exception("unsupported character: '" + c + "'");
}
//if (level < 0 || level > 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; break;
// brush face (quake format): if (data[index] == '(')
// ( <v1> ) ( <v2> ) ( <v3> ) <shader> <x_shift> <y_shift> <rotation> <x_scale> <y_scale> 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;
}
// brush face (quake3 format): default:
// ( <v1> ) ( <v2> ) ( <v3> ) <shader> <x_shift> <y_shift> <rotation> <x_scale> <y_scale> <content_flags> <surface_flags> <value> throw new Exception("unsupported character: '" + c + "'");
}
} while (index++ < data.Length && !entityParsed);
}
// brush face (valve format): private static string ParseQuotedString(byte[] data, ref int index)
// ( <v1> ) ( <v2> ) ( <v3> ) <shader> [ <tx1> <ty1> <tz1> <toffs1> ] [ <tx2> <ty2> <tz2> <toffs2> ] <rotation> <x_scale> <y_scale> {
case '(': 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]);
}
//if (data[index] == '\n')
// 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 Float3 ParseFloat3(byte[] data, ref int index)
{
return new Float3(
ParseFloat(data, ref index),
ParseFloat(data, ref index),
ParseFloat(data, ref index)
);
}
private static Float2 ParseFloat2(byte[] data, ref int index)
{
return new Float2(
ParseFloat(data, ref index),
ParseFloat(data, ref index)
);
}
private static Float3 ParsePlaneFloat3(byte[] data, ref int index)
{
index++;
while (index < data.Length)
{
if (data[index] != ' ')
break;
index++;
}
Float3 vector = ParseFloat3(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<MapFacePlane>(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):
// ( <v1> ) ( <v2> ) ( <v3> ) <shader> <x_shift> <y_shift> <rotation> <x_scale> <y_scale>
// brush face (quake3 format):
// ( <v1> ) ( <v2> ) ( <v3> ) <shader> <x_shift> <y_shift> <rotation> <x_scale> <y_scale> <content_flags> <surface_flags> <value>
// brush face (valve format):
// ( <v1> ) ( <v2> ) ( <v3> ) <shader> [ <tx1> <ty1> <tz1> <toffs1> ] [ <tx2> <ty2> <tz2> <toffs2> ] <rotation> <x_scale> <y_scale>
case '(':
{
MapFacePlane plane = new MapFacePlane();
plane.v1 = ParsePlaneFloat3(data, ref index);
plane.v2 = ParsePlaneFloat3(data, ref index);
plane.v3 = ParsePlaneFloat3(data, ref index);
plane.texture = ParseString(data, ref index);
if (true) // quake or quake3 format
{ {
MapFacePlane plane = new MapFacePlane(); plane.offset = ParseFloat2(data, ref index);
plane.v1 = ParsePlaneFloat3(data, ref index); plane.rotation = ParseFloat(data, ref index);
plane.v2 = ParsePlaneFloat3(data, ref index); plane.scale = ParseFloat2(data, ref index);
plane.v3 = ParsePlaneFloat3(data, ref index);
plane.texture = ParseString(data, ref index);
if (true) // quake or quake3 format char peekChar = (char)data[index];
if (peekChar != '\n') // quake3 format
{ {
plane.offset = ParseFloat2(data, ref index); plane.contentFlags = ParseInt(data, ref index);
plane.rotation = ParseFloat(data, ref index); plane.surfaceFlags = ParseInt(data, ref index);
plane.scale = ParseFloat2(data, ref index); plane.surfaceValue = ParseInt(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 Float3(plane.v1.X, plane.v1.Z, plane.v1.Y);
plane.v2 = new Float3(plane.v2.X, plane.v2.Z, plane.v2.Y);
plane.v3 = new Float3(plane.v3.X, plane.v3.Z, plane.v3.Y);
plane.plane = new Plane(plane.v1, plane.v2, plane.v3);
planes.Add(plane);
break;
} }
case '}': // Flip Y and Z
{ plane.v1 = new Float3(plane.v1.X, plane.v1.Z, plane.v1.Y);
brushParsed = true; plane.v2 = new Float3(plane.v2.X, plane.v2.Z, plane.v2.Y);
break; plane.v3 = new Float3(plane.v3.X, plane.v3.Z, plane.v3.Y);
}
default: plane.plane = new Plane(plane.v1, plane.v2, plane.v3);
if (char.IsLetter(c) || char.IsNumber(c))
{
// patch name
}
throw new Exception("unsupported character: '" + c + "'"); planes.Add(plane);
break;
} }
} while (index++ < data.Length && !brushParsed);
brush.planes = planes.ToArray(); case '}':
{
brushParsed = true;
break;
}
return brush; 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++;
} }
private static MapPatch ParsePatch(byte[] data, ref int 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)
{ {
MapPatch patch = new MapPatch(); if (data[index] == ')')
break;
patch.name = ParseString(data, ref index); 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 index++;
private static void ParsePatchInner(MapPatch patch, byte[] data, ref int index)
while (index < data.Length)
{ {
string shaderName = ParseString(data, ref index); if (data[index] == '(')
break;
while (index < data.Length)
{
if (data[index] == '(')
break;
index++;
}
index++; index++;
}
int width = ParseInt(data, ref index); 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) 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)
{ {
if (data[index] == ')') case '\r':
case '\n':
break; break;
index++;
}
index++; case '(':
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': index++;
case '\n':
break;
case '(': for (int iw = 0; iw < width; iw++)
{ {
index++; while (index < data.Length)
for (int iw = 0; iw < width; iw++)
{ {
while (index < data.Length) if (data[index] == '(')
{ break;
if (data[index] == '(')
break;
index++;
}
index++;
while (index < data.Length)
{
if (data[index] != ' ')
break;
index++;
}
vertices[vertexIndex].v = ParseFloat3(data, ref index);
vertices[vertexIndex].uv =
new Float2(ParseFloat(data, ref index), ParseFloat(data, ref index));
vertexIndex++;
while (index < data.Length)
{
if (data[index] == ')')
break;
index++;
}
index++; index++;
} }
index++;
break; while (index < data.Length)
{
if (data[index] != ' ')
break;
index++;
}
vertices[vertexIndex].v = ParseFloat3(data, ref index);
vertices[vertexIndex].uv =
new Float2(ParseFloat(data, ref index), ParseFloat(data, ref index));
vertexIndex++;
while (index < data.Length)
{
if (data[index] == ')')
break;
index++;
}
index++;
} }
case '}':
{
verticesParsed = true;
break;
}
default: break;
throw new Exception("unsupported character: '" + c + "'");
} }
} while (index++ < data.Length && !verticesParsed);
} case '}':
{
verticesParsed = true;
break;
}
default:
throw new Exception("unsupported character: '" + c + "'");
}
} while (index++ < data.Length && !verticesParsed);
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -51,114 +51,114 @@ namespace Game;
/// </summary> /// </summary>
public class ConvexHullCalculator public class ConvexHullCalculator
{ {
/// <summary> /// <summary>
/// Constant representing a point that has yet to be assigned to a /// Constant representing a point that has yet to be assigned to a
/// face. It's only used immediately after constructing the seed hull. /// face. It's only used immediately after constructing the seed hull.
/// </summary> /// </summary>
private const int UNASSIGNED = -2; private const int UNASSIGNED = -2;
/// <summary> /// <summary>
/// Constant representing a point that is inside the convex hull, and /// Constant representing a point that is inside the convex hull, and
/// thus is behind all faces. In the openSet array, all points with /// thus is behind all faces. In the openSet array, all points with
/// INSIDE are at the end of the array, with indexes larger /// INSIDE are at the end of the array, with indexes larger
/// openSetTail. /// openSetTail.
/// </summary> /// </summary>
private const int INSIDE = -1; private const int INSIDE = -1;
/// <summary> /// <summary>
/// Epsilon value. If the coordinates of the point space are /// Epsilon value. If the coordinates of the point space are
/// exceptionally close to each other, this value might need to be /// exceptionally close to each other, this value might need to be
/// adjusted. /// adjusted.
/// </summary> /// </summary>
private const float EPSILON = 0.001f; private const float EPSILON = 0.001f;
/// <summary> /// <summary>
/// When adding a new face to the faces Dictionary, use this for the /// When adding a new face to the faces Dictionary, use this for the
/// key and then increment it. /// key and then increment it.
/// </summary> /// </summary>
private int faceCount; private int faceCount;
/// <summary> /// <summary>
/// A dictionary storing the faces of the currently generated convex /// A dictionary storing the faces of the currently generated convex
/// hull. The key is the id of the face, used in the Face, PointFace /// hull. The key is the id of the face, used in the Face, PointFace
/// and HorizonEdge struct. /// and HorizonEdge struct.
/// This is a Dictionary, because we need both random access to it, /// This is a Dictionary, because we need both random access to it,
/// the ability to loop through it, and ability to quickly delete /// the ability to loop through it, and ability to quickly delete
/// faces (in the ConstructCone method), and Dictionary is the obvious /// faces (in the ConstructCone method), and Dictionary is the obvious
/// candidate that can do all of those things. /// candidate that can do all of those things.
/// I'm wondering if using a Dictionary is best idea, though. It might /// I'm wondering if using a Dictionary is best idea, though. It might
/// be better to just have them in a <![CDATA[List<Face>]]> and mark a face as /// be better to just have them in a <![CDATA[List<Face>]]> and mark a face as
/// deleted by adding a field to the Face struct. The downside is that /// 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 /// 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 /// looping through the points in openSet, we would have to loop
/// through all the Faces EVER created in the algorithm, and skip the /// through all the Faces EVER created in the algorithm, and skip the
/// ones that have been marked as deleted. However, looping through a /// ones that have been marked as deleted. However, looping through a
/// list is fairly fast, and it might be worth it to avoid Dictionary /// list is fairly fast, and it might be worth it to avoid Dictionary
/// overhead. /// overhead.
/// TODO test converting to a <![CDATA[List<Face>]]> instead. /// TODO test converting to a <![CDATA[List<Face>]]> instead.
/// </summary> /// </summary>
private Dictionary<int, Face> faces; private Dictionary<int, Face> faces;
/// <summary> /// <summary>
/// The current horizon. Generated by the FindHorizon() DFS search, /// The current horizon. Generated by the FindHorizon() DFS search,
/// and used in ConstructCone to construct new faces. The list of /// and used in ConstructCone to construct new faces. The list of
/// edges are in CCW order. /// edges are in CCW order.
/// </summary> /// </summary>
private List<HorizonEdge> horizon; private List<HorizonEdge> horizon;
/// <summary> /// <summary>
/// If SplitVerts is false, this Dictionary is used to keep track of /// If SplitVerts is false, this Dictionary is used to keep track of
/// which points we've added to the final mesh. /// which points we've added to the final mesh.
/// </summary> /// </summary>
private Dictionary<int, int> hullVerts; private Dictionary<int, int> hullVerts;
/// <summary> /// <summary>
/// Set of faces which are "lit" by the current point in the set. This /// 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 /// is used in the FindHorizon() DFS search to keep track of which
/// faces we've already visited, and in the ReassignPoints() method to /// faces we've already visited, and in the ReassignPoints() method to
/// know which points need to be reassigned. /// know which points need to be reassigned.
/// </summary> /// </summary>
private HashSet<int> litFaces; private HashSet<int> litFaces;
/// <summary> /// <summary>
/// The set of points to be processed. "openSet" is a misleading name, /// 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 /// 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 /// convex hull) and the closed set (points that are inside the convex
/// hull). The first part of the array (with <![CDATA[indexes <= openSetTail]]>) /// hull). The first part of the array (with <![CDATA[indexes <= openSetTail]]>)
/// is the openSet, the last part of the array (with <![CDATA[indexes > /// is the openSet, the last part of the array (with <![CDATA[indexes >
/// openSetTail]]>) are the closed set, with /// openSetTail]]>) are the closed set, with
/// Face set to INSIDE. The /// Face set to INSIDE. The
/// closed set is largely irrelevant to the algorithm, the open set is /// closed set is largely irrelevant to the algorithm, the open set is
/// what matters. /// what matters.
/// Storing the entire open set in one big list has a downside: when /// Storing the entire open set in one big list has a downside: when
/// we're reassigning points after ConstructCone, we only need to /// we're reassigning points after ConstructCone, we only need to
/// reassign points that belong to the faces that have been removed, /// reassign points that belong to the faces that have been removed,
/// but storing it in one array, we have to loop through the entire /// but storing it in one array, we have to loop through the entire
/// list, and checking litFaces to determine which we can skip and /// list, and checking litFaces to determine which we can skip and
/// which need to be reassigned. /// which need to be reassigned.
/// The alternative here is to give each face in Face array it's own /// 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 /// openSet. I don't like that solution, because then you have to
/// juggle so many more heap-allocated <![CDATA[List<T>'s]]>, we'd have to use /// juggle so many more heap-allocated <![CDATA[List<T>'s]]>, we'd have to use
/// object pools and such. It would do a lot more allocation, and it /// object pools and such. It would do a lot more allocation, and it
/// would have worse locality. I should maybe test that solution, but /// would have worse locality. I should maybe test that solution, but
/// it probably wont be faster enough (if at all) to justify the extra /// it probably wont be faster enough (if at all) to justify the extra
/// allocations. /// allocations.
/// </summary> /// </summary>
private List<PointFace> openSet; private List<PointFace> openSet;
/// <summary> /// <summary>
/// The "tail" of the openSet, the last index of a vertex that has /// The "tail" of the openSet, the last index of a vertex that has
/// been assigned to a face. /// been assigned to a face.
/// </summary> /// </summary>
private int openSetTail = -1; private int openSetTail = -1;
/// <summary> /// <summary>
/// Generate a convex hull from points in points array, and store the /// Generate a convex hull from points in points array, and store the
/// mesh in Unity-friendly format in verts and tris. If splitVerts is /// 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 /// true, the the verts will be split, if false, the same vert will be
/// used for more than one triangle. /// used for more than one triangle.
/// </summary> /// </summary>
public void GenerateHull( public void GenerateHull(
List<Float3> points, List<Float3> points,
bool splitVerts, bool splitVerts,
ref List<Float3> verts, ref List<Float3> verts,
@@ -179,11 +179,11 @@ public class ConvexHullCalculator
//VerifyMesh(points, ref verts, ref tris); //VerifyMesh(points, ref verts, ref tris);
} }
/// <summary> /// <summary>
/// Make sure all the buffers and variables needed for the algorithm /// Make sure all the buffers and variables needed for the algorithm
/// are initialized. /// are initialized.
/// </summary> /// </summary>
private void Initialize(List<Float3> points, bool splitVerts) private void Initialize(List<Float3> points, bool splitVerts)
{ {
faceCount = 0; faceCount = 0;
openSetTail = -1; openSetTail = -1;
@@ -225,10 +225,10 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Create initial seed hull. /// Create initial seed hull.
/// </summary> /// </summary>
private void GenerateInitialHull(List<Float3> points) private void GenerateInitialHull(List<Float3> points)
{ {
// Find points suitable for use as the seed hull. Some varieties of // Find points suitable for use as the seed hull. Some varieties of
// this algorithm pick extreme points here, but I'm not convinced // this algorithm pick extreme points here, but I'm not convinced
@@ -356,11 +356,11 @@ public class ConvexHullCalculator
VerifyOpenSet(points); VerifyOpenSet(points);
} }
/// <summary> /// <summary>
/// Find four points in the point cloud that are not coplanar for the /// Find four points in the point cloud that are not coplanar for the
/// seed hull /// seed hull
/// </summary> /// </summary>
private void FindInitialHullIndices(List<Float3> points, out int b0, out int b1, out int b2, out int b3) private void FindInitialHullIndices(List<Float3> points, out int b0, out int b1, out int b2, out int b3)
{ {
int count = points.Count; int count = points.Count;
@@ -399,12 +399,12 @@ public class ConvexHullCalculator
throw new ArgumentException("Can't generate hull, points are coplanar"); throw new ArgumentException("Can't generate hull, points are coplanar");
} }
/// <summary> /// <summary>
/// Grow the hull. This method takes the current hull, and expands it /// Grow the hull. This method takes the current hull, and expands it
/// to encompass the point in openSet with the point furthest away /// to encompass the point in openSet with the point furthest away
/// from its face. /// from its face.
/// </summary> /// </summary>
private void GrowHull(List<Float3> points) private void GrowHull(List<Float3> points)
{ {
Assert.IsTrue(openSetTail >= 0); Assert.IsTrue(openSetTail >= 0);
Assert.IsTrue(openSet[0].Face != INSIDE); Assert.IsTrue(openSet[0].Face != INSIDE);
@@ -439,21 +439,21 @@ public class ConvexHullCalculator
ReassignPoints(points); ReassignPoints(points);
} }
/// <summary> /// <summary>
/// Start the search for the horizon. /// Start the search for the horizon.
/// The search is a DFS search that searches neighboring triangles in /// The search is a DFS search that searches neighboring triangles in
/// a counter-clockwise fashion. When it find a neighbor which is not /// 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 /// 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 /// proceeds counter-clockwise, the edges of the horizon will be found
/// in counter-clockwise order. /// in counter-clockwise order.
/// The heart of the search can be found in the recursive /// The heart of the search can be found in the recursive
/// SearchHorizon() method, but the the first iteration of the search /// SearchHorizon() method, but the the first iteration of the search
/// is special, because it has to visit three neighbors (all the /// is special, because it has to visit three neighbors (all the
/// neighbors of the initial triangle), while the rest of the search /// neighbors of the initial triangle), while the rest of the search
/// only has to visit two (because one of them has already been /// only has to visit two (because one of them has already been
/// visited, the one you came from). /// visited, the one you came from).
/// </summary> /// </summary>
private void FindHorizon(List<Float3> points, Float3 point, int fi, Face face) private void FindHorizon(List<Float3> points, Float3 point, int fi, Face face)
{ {
// TODO should I use epsilon in the PointFaceDistance comparisons? // TODO should I use epsilon in the PointFaceDistance comparisons?
@@ -529,10 +529,10 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Recursively search to find the horizon or lit set. /// Recursively search to find the horizon or lit set.
/// </summary> /// </summary>
private void SearchHorizon(List<Float3> points, Float3 point, int prevFaceIndex, int faceCount, Face face) private void SearchHorizon(List<Float3> points, Float3 point, int prevFaceIndex, int faceCount, Face face)
{ {
Assert.IsTrue(prevFaceIndex >= 0); Assert.IsTrue(prevFaceIndex >= 0);
Assert.IsTrue(litFaces.Contains(prevFaceIndex)); Assert.IsTrue(litFaces.Contains(prevFaceIndex));
@@ -622,18 +622,18 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Remove all lit faces and construct new faces from the horizon in a /// Remove all lit faces and construct new faces from the horizon in a
/// "cone-like" fashion. /// "cone-like" fashion.
/// This is a relatively straight-forward procedure, given that the /// This is a relatively straight-forward procedure, given that the
/// horizon is handed to it in already sorted counter-clockwise. 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 /// 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 /// 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. We also have to update the face
/// on the other side of the horizon to reflect it's new neighbor from /// on the other side of the horizon to reflect it's new neighbor from
/// the cone. /// the cone.
/// </summary> /// </summary>
private void ConstructCone(List<Float3> points, int farthestPoint) private void ConstructCone(List<Float3> points, int farthestPoint)
{ {
foreach (int fi in litFaces) foreach (int fi in litFaces)
{ {
@@ -713,21 +713,21 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Reassign points based on the new faces added by ConstructCone(). /// Reassign points based on the new faces added by ConstructCone().
/// Only points that were previous assigned to a removed face need to /// Only points that were previous assigned to a removed face need to
/// be updated, so check litFaces while looping through the open set. /// be updated, so check litFaces while looping through the open set.
/// There is a potential optimization here: there's no reason to loop /// There is a potential optimization here: there's no reason to loop
/// through the entire openSet here. If each face had it's own /// through the entire openSet here. If each face had it's own
/// openSet, we could just loop through the openSets in the removed /// openSet, we could just loop through the openSets in the removed
/// faces. That would make the loop here shorter. /// faces. That would make the loop here shorter.
/// However, to do that, we would have to juggle A LOT more <![CDATA[List<T>'s]]>, /// However, to do that, we would have to juggle A LOT more <![CDATA[List<T>'s]]>,
/// and we would need an object pool to manage them all without /// 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 /// 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 /// doing that to make this loop shorter, a straight for-loop through
/// a list is pretty darn fast. Still, it might be worth trying /// a list is pretty darn fast. Still, it might be worth trying
/// </summary> /// </summary>
private void ReassignPoints(List<Float3> points) private void ReassignPoints(List<Float3> points)
{ {
for (int i = 0; i <= openSetTail; i++) for (int i = 0; i <= openSetTail; i++)
{ {
@@ -780,13 +780,13 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Final step in algorithm, export the faces of the convex hull in a /// Final step in algorithm, export the faces of the convex hull in a
/// mesh-friendly format. /// mesh-friendly format.
/// TODO normals calculation for non-split vertices. Right now it just /// TODO normals calculation for non-split vertices. Right now it just
/// leaves the normal array empty. /// leaves the normal array empty.
/// </summary> /// </summary>
private void ExportMesh( private void ExportMesh(
List<Float3> points, List<Float3> points,
bool splitVerts, bool splitVerts,
ref List<Float3> verts, ref List<Float3> verts,
@@ -855,40 +855,40 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Signed distance from face to point (a positive number means that /// Signed distance from face to point (a positive number means that
/// the point is above the face) /// the point is above the face)
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private float PointFaceDistance(Float3 point, Float3 pointOnFace, Face face) private float PointFaceDistance(Float3 point, Float3 pointOnFace, Face face)
{ {
return Dot(face.Normal, point - pointOnFace); return Dot(face.Normal, point - pointOnFace);
} }
/// <summary> /// <summary>
/// Calculate normal for triangle /// Calculate normal for triangle
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private Float3 Normal(Float3 v0, Float3 v1, Float3 v2) private Float3 Normal(Float3 v0, Float3 v1, Float3 v2)
{ {
return Cross(v1 - v0, v2 - v0).Normalized; return Cross(v1 - v0, v2 - v0).Normalized;
} }
/// <summary> /// <summary>
/// Dot product, for convenience. /// Dot product, for convenience.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float Dot(Float3 a, Float3 b) private static float Dot(Float3 a, Float3 b)
{ {
return a.X * b.X + a.Y * b.Y + a.Z * b.Z; return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
} }
/// <summary> /// <summary>
/// Float3.Cross i left-handed, the algorithm is right-handed. Also, /// Float3.Cross i left-handed, the algorithm is right-handed. Also,
/// i wanna test to see if using aggressive inlining makes any /// i wanna test to see if using aggressive inlining makes any
/// difference here. /// difference here.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Float3 Cross(Float3 a, Float3 b) private static Float3 Cross(Float3 a, Float3 b)
{ {
return new Float3( return new Float3(
@@ -897,28 +897,28 @@ public class ConvexHullCalculator
a.X * b.Y - a.Y * b.X); a.X * b.Y - a.Y * b.X);
} }
/// <summary> /// <summary>
/// Check if two points are coincident /// Check if two points are coincident
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AreCoincident(Float3 a, Float3 b) private bool AreCoincident(Float3 a, Float3 b)
{ {
return (a - b).Length <= EPSILON; return (a - b).Length <= EPSILON;
} }
/// <summary> /// <summary>
/// Check if three points are collinear /// Check if three points are collinear
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AreCollinear(Float3 a, Float3 b, Float3 c) private bool AreCollinear(Float3 a, Float3 b, Float3 c)
{ {
return Cross(c - a, c - b).Length <= EPSILON; return Cross(c - a, c - b).Length <= EPSILON;
} }
/// <summary> /// <summary>
/// Check if four points are coplanar /// Check if four points are coplanar
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AreCoplanar(Float3 a, Float3 b, Float3 c, Float3 d) private bool AreCoplanar(Float3 a, Float3 b, Float3 c, Float3 d)
{ {
Float3 n1 = Cross(c - a, c - b); Float3 n1 = Cross(c - a, c - b);
@@ -934,12 +934,12 @@ public class ConvexHullCalculator
1.0f / m2 * n2); 1.0f / m2 * n2);
} }
/// <summary> /// <summary>
/// Method used for debugging, verifies that the openSet is in a /// Method used for debugging, verifies that the openSet is in a
/// sensible state. Conditionally compiled if DEBUG_QUICKHULL if /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if
/// defined. /// defined.
/// </summary> /// </summary>
private void VerifyOpenSet(List<Float3> points) private void VerifyOpenSet(List<Float3> points)
{ {
for (int i = 0; i < openSet.Count; i++) for (int i = 0; i < openSet.Count; i++)
if (i > openSetTail) if (i > openSetTail)
@@ -958,12 +958,12 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Method used for debugging, verifies that the horizon is in a /// Method used for debugging, verifies that the horizon is in a
/// sensible state. Conditionally compiled if DEBUG_QUICKHULL if /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if
/// defined. /// defined.
/// </summary> /// </summary>
private void VerifyHorizon() private void VerifyHorizon()
{ {
for (int i = 0; i < horizon.Count; i++) for (int i = 0; i < horizon.Count; i++)
{ {
@@ -974,12 +974,12 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Method used for debugging, verifies that the faces array is in a /// Method used for debugging, verifies that the faces array is in a
/// sensible state. Conditionally compiled if DEBUG_QUICKHULL if /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if
/// defined. /// defined.
/// </summary> /// </summary>
private void VerifyFaces(List<Float3> points) private void VerifyFaces(List<Float3> points)
{ {
foreach (var kvp in faces) foreach (var kvp in faces)
{ {
@@ -1009,12 +1009,12 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Method used for debugging, verifies that the final mesh is /// Method used for debugging, verifies that the final mesh is
/// actually a convex hull of all the points. Conditionally compiled /// actually a convex hull of all the points. Conditionally compiled
/// if DEBUG_QUICKHULL if defined. /// if DEBUG_QUICKHULL if defined.
/// </summary> /// </summary>
private void VerifyMesh(List<Float3> points, ref List<Float3> verts, ref List<int> tris) private void VerifyMesh(List<Float3> points, ref List<Float3> verts, ref List<int> tris)
{ {
Assert.IsTrue(tris.Count % 3 == 0); Assert.IsTrue(tris.Count % 3 == 0);
@@ -1032,28 +1032,28 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Does face f have a face with vertexes e0 and e1? Used only for /// Does face f have a face with vertexes e0 and e1? Used only for
/// debugging. /// debugging.
/// </summary> /// </summary>
private bool HasEdge(Face f, int e0, int e1) private bool HasEdge(Face f, int e0, int e1)
{ {
return (f.Vertex0 == e0 && f.Vertex1 == e1) return (f.Vertex0 == e0 && f.Vertex1 == e1)
|| (f.Vertex1 == e0 && f.Vertex2 == e1) || (f.Vertex1 == e0 && f.Vertex2 == e1)
|| (f.Vertex2 == e0 && f.Vertex0 == e1); || (f.Vertex2 == e0 && f.Vertex0 == e1);
} }
/// <summary> /// <summary>
/// Struct representing a single face. /// Struct representing a single face.
/// Vertex0, Vertex1 and Vertex2 are the vertices in CCW order. They /// Vertex0, Vertex1 and Vertex2 are the vertices in CCW order. They
/// acutal points are stored in the points array, these are just /// acutal points are stored in the points array, these are just
/// indexes into that array. /// indexes into that array.
/// Opposite0, Opposite1 and Opposite2 are the keys to the faces which /// Opposite0, Opposite1 and Opposite2 are the keys to the faces which
/// share an edge with this face. Opposite0 is the face opposite /// share an edge with this face. Opposite0 is the face opposite
/// Vertex0 (so it has an edge with Vertex2 and Vertex1), etc. /// Vertex0 (so it has an edge with Vertex2 and Vertex1), etc.
/// Normal is (unsurprisingly) the normal of the triangle. /// Normal is (unsurprisingly) the normal of the triangle.
/// </summary> /// </summary>
private struct Face private struct Face
{ {
public readonly int Vertex0; public readonly int Vertex0;
public readonly int Vertex1; public readonly int Vertex1;
@@ -1088,14 +1088,14 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Struct representing a mapping between a point and a face. These /// Struct representing a mapping between a point and a face. These
/// are used in the openSet array. /// are used in the openSet array.
/// Point is the index of the point in the points array, Face is the /// 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 /// key of the face in the Key dictionary, Distance is the distance
/// from the face to the point. /// from the face to the point.
/// </summary> /// </summary>
private struct PointFace private struct PointFace
{ {
public readonly int Point; public readonly int Point;
public int Face; public int Face;
@@ -1109,14 +1109,14 @@ public class ConvexHullCalculator
} }
} }
/// <summary> /// <summary>
/// Struct representing a single edge in the horizon. /// Struct representing a single edge in the horizon.
/// Edge0 and Edge1 are the vertexes of edge in CCW order, Face is the /// Edge0 and Edge1 are the vertexes of edge in CCW order, Face is the
/// face on the other side of the horizon. /// face on the other side of the horizon.
/// TODO Edge1 isn't actually needed, you can just index the next item /// TODO Edge1 isn't actually needed, you can just index the next item
/// in the horizon array. /// in the horizon array.
/// </summary> /// </summary>
private struct HorizonEdge private struct HorizonEdge
{ {
public int Face; public int Face;
public int Edge0; public int Edge0;

View File

@@ -1,28 +1,27 @@
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
public static class InputManager
{ {
public static class InputManager public static bool GetAction(string name)
{ {
public static bool GetAction(string name) if (Console.IsOpen)
{ return false;
if (Console.IsOpen) return Input.GetAction(name);
return false; }
return Input.GetAction(name);
}
public static float GetAxis(string name) public static float GetAxis(string name)
{ {
if (Console.IsOpen) if (Console.IsOpen)
return 0.0f; return 0.0f;
return Input.GetAxis(name); return Input.GetAxis(name);
} }
public static float GetAxisRaw(string name) public static float GetAxisRaw(string name)
{ {
if (Console.IsOpen) if (Console.IsOpen)
return 0.0f; return 0.0f;
return Input.GetAxisRaw(name); return Input.GetAxisRaw(name);
}
} }
} }

View File

@@ -2,173 +2,172 @@
using FlaxEngine; using FlaxEngine;
using FlaxEngine.Networking; using FlaxEngine.Networking;
namespace Game namespace Game;
[StructLayout(LayoutKind.Sequential)]
public struct PlayerInputState
{ {
[StructLayout(LayoutKind.Sequential)] /*static PlayerInputState()
public struct PlayerInputState
{ {
/*static PlayerInputState() NetworkReplicator.AddSerializer(typeof(PlayerInputState),
{ (ptr, streamPtr) =>
NetworkReplicator.AddSerializer(typeof(PlayerInputState),
(ptr, streamPtr) =>
{
},
(ptr, streamPtr) =>
{
});
}*/
#if false
public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight,
bool attacking, bool jumping)
{
this.frame = frame;
this.viewDeltaX = viewDeltaX;
this.viewDeltaY = viewDeltaY;
this.moveForward = moveForward;
this.moveRight = moveRight;
this.attacking = attacking;
this.jumping = jumping;
/*this.verificationPosition = verificationPosition;
this.verificationVelocity = verificationVelocity;
this.verificationViewAngles = verificationViewAngles;
this.verificationOrientation = verificationOrientation;*/
}
public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight,
bool attacking, bool jumping, Float3 verificationPosition, Float3 verificationVelocity,
Float3 verificationViewAngles, Quaternion verificationOrientation)
{
this.frame = frame;
this.viewDeltaX = viewDeltaX;
this.viewDeltaY = viewDeltaY;
this.moveForward = moveForward;
this.moveRight = moveRight;
this.attacking = attacking;
this.jumping = jumping;
this.verificationPosition = verificationPosition;
this.verificationVelocity = verificationVelocity;
this.verificationViewAngles = verificationViewAngles;
this.verificationOrientation = verificationOrientation;
}
#endif
public ulong frame;
public float viewDeltaX, viewDeltaY;
public float moveForward;
public float moveRight;
public bool attacking;
public bool jumping;
public Float3 verificationPosition;
public Float3 verificationVelocity;
public Float3 verificationViewAngles;
public Quaternion verificationOrientation;
}
[StructLayout(LayoutKind.Sequential)]
public struct PlayerActorState
{
public Float3 position;
public Float3 velocity;
public Quaternion orientation;
public Float3 viewAngles; // yaw, pitch, roll
public float lastJumpTime;
public int numJumps;
public bool jumped;
}
[StructLayout(LayoutKind.Sequential)]
public struct PlayerState
{
public PlayerInputState input;
public PlayerActorState actor;
}
public class PlayerInput
{
public const byte DemoVer = 1;
public const int MaxPlayerStates = 120;
public PlayerState currentState;
public ulong frame;
//public ulong oldestFrame;
private PlayerState[] states = new PlayerState[MaxPlayerStates];
public virtual bool Predict => false;
public virtual void OnUpdate()
{
}
public virtual void OnFixedUpdate()
{
}
public virtual void OnEndFrame()
{
//Console.Print("recorded frame " + frame);
states[frame % MaxPlayerStates] = currentState;
/*ulong oldest = ulong.MaxValue;
for (int i = 0; i < 120; i++)
oldest = states[i].input.frame < oldest ? states[i].input.frame : oldest;
oldestFrame = oldest;*/
frame++;
currentState.input.frame = frame;
}
public virtual void RecordCurrentActorState(PlayerActorState actorState)
{
if (actorState.position.Length <= 0.01)
Console.Print("wrong recorded position?");
currentState.actor = actorState;
}
public bool GetState(ulong frame, out PlayerInputState inputState, out PlayerActorState actorState)
{
int frameIndex = (int)frame % MaxPlayerStates;
if (states[frameIndex].input.frame != frame)
{ {
inputState = default;
actorState = default;
return false;
}
inputState = states[frameIndex].input; },
actorState = states[frameIndex].actor; (ptr, streamPtr) =>
return true; {
}
public void SetState(ulong frame, PlayerInputState inputState) });
}*/
#if false
public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight,
bool attacking, bool jumping)
{
this.frame = frame;
this.viewDeltaX = viewDeltaX;
this.viewDeltaY = viewDeltaY;
this.moveForward = moveForward;
this.moveRight = moveRight;
this.attacking = attacking;
this.jumping = jumping;
/*this.verificationPosition = verificationPosition;
this.verificationVelocity = verificationVelocity;
this.verificationViewAngles = verificationViewAngles;
this.verificationOrientation = verificationOrientation;*/
}
public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight,
bool attacking, bool jumping, Float3 verificationPosition, Float3 verificationVelocity,
Float3 verificationViewAngles, Quaternion verificationOrientation)
{
this.frame = frame;
this.viewDeltaX = viewDeltaX;
this.viewDeltaY = viewDeltaY;
this.moveForward = moveForward;
this.moveRight = moveRight;
this.attacking = attacking;
this.jumping = jumping;
this.verificationPosition = verificationPosition;
this.verificationVelocity = verificationVelocity;
this.verificationViewAngles = verificationViewAngles;
this.verificationOrientation = verificationOrientation;
}
#endif
public ulong frame;
public float viewDeltaX, viewDeltaY;
public float moveForward;
public float moveRight;
public bool attacking;
public bool jumping;
public Float3 verificationPosition;
public Float3 verificationVelocity;
public Float3 verificationViewAngles;
public Quaternion verificationOrientation;
}
[StructLayout(LayoutKind.Sequential)]
public struct PlayerActorState
{
public Float3 position;
public Float3 velocity;
public Quaternion orientation;
public Float3 viewAngles; // yaw, pitch, roll
public float lastJumpTime;
public int numJumps;
public bool jumped;
}
[StructLayout(LayoutKind.Sequential)]
public struct PlayerState
{
public PlayerInputState input;
public PlayerActorState actor;
}
public class PlayerInput
{
public const byte DemoVer = 1;
public const int MaxPlayerStates = 120;
public PlayerState currentState;
public ulong frame;
//public ulong oldestFrame;
private PlayerState[] states = new PlayerState[MaxPlayerStates];
public virtual bool Predict => false;
public virtual void OnUpdate()
{
}
public virtual void OnFixedUpdate()
{
}
public virtual void OnEndFrame()
{
//Console.Print("recorded frame " + frame);
states[frame % MaxPlayerStates] = currentState;
/*ulong oldest = ulong.MaxValue;
for (int i = 0; i < 120; i++)
oldest = states[i].input.frame < oldest ? states[i].input.frame : oldest;
oldestFrame = oldest;*/
frame++;
currentState.input.frame = frame;
}
public virtual void RecordCurrentActorState(PlayerActorState actorState)
{
if (actorState.position.Length <= 0.01)
Console.Print("wrong recorded position?");
currentState.actor = actorState;
}
public bool GetState(ulong frame, out PlayerInputState inputState, out PlayerActorState actorState)
{
int frameIndex = (int)frame % MaxPlayerStates;
if (states[frameIndex].input.frame != frame)
{ {
int frameIndex = (int)frame % MaxPlayerStates; inputState = default;
states[frameIndex].input = inputState; actorState = default;
states[frameIndex].input.frame = frame; return false;
states[frameIndex].actor = new PlayerActorState();
} }
public void SetState(ulong frame, PlayerInputState inputState, PlayerActorState actorState) inputState = states[frameIndex].input;
{ actorState = states[frameIndex].actor;
int frameIndex = (int)frame % MaxPlayerStates; return true;
states[frameIndex].input = inputState; }
states[frameIndex].input.frame = frame;
states[frameIndex].actor = actorState;
}
public PlayerInputState GetCurrentInputState() public void SetState(ulong frame, PlayerInputState inputState)
{ {
return currentState.input; int frameIndex = (int)frame % MaxPlayerStates;
} states[frameIndex].input = inputState;
states[frameIndex].input.frame = frame;
states[frameIndex].actor = new PlayerActorState();
}
public PlayerActorState GetCurrentActorState() public void SetState(ulong frame, PlayerInputState inputState, PlayerActorState actorState)
{ {
return currentState.actor; int frameIndex = (int)frame % MaxPlayerStates;
} states[frameIndex].input = inputState;
states[frameIndex].input.frame = frame;
states[frameIndex].actor = actorState;
}
public PlayerInputState GetCurrentInputState()
{
return currentState.input;
}
public PlayerActorState GetCurrentActorState()
{
return currentState.actor;
} }
} }

View File

@@ -8,95 +8,94 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Console = Game.Console; using Console = Game.Console;
namespace Game namespace Game;
public class PlayerInputDemo : PlayerInput
{ {
public class PlayerInputDemo : PlayerInput protected List<PlayerInputState> buffer = new List<PlayerInputState>();
protected IEnumerator<PlayerInputState> bufferEnumerable;
public bool IsPlaying
{ {
protected List<PlayerInputState> buffer = new List<PlayerInputState>(); get
protected IEnumerator<PlayerInputState> bufferEnumerable;
public bool IsPlaying
{ {
get return bufferEnumerable != null;
{
return bufferEnumerable != null;
}
}
public PlayerInputDemo(string demoPath)
{
if (!File.Exists(demoPath))
return;
int expectedPlayerInputStateSize = Unsafe.SizeOf<PlayerInputState>();
Stopwatch sw = Stopwatch.StartNew();
using FileStream fileStream = File.OpenRead(demoPath);
using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress);
int ver = stream.ReadByte();
int inputStateSize = stream.ReadByte();
if (ver != DemoVer || inputStateSize != expectedPlayerInputStateSize)
{
Console.Print("demover mismatch: version " + ver + " != " + DemoVer + ", inputStateSize " +
inputStateSize + " != " + Unsafe.SizeOf<PlayerInputState>());
stream.Close();
return;
}
Span<byte> b = stackalloc byte[expectedPlayerInputStateSize];
while (true)
{
int bytesLeftInBuffer = expectedPlayerInputStateSize;
do
{
int readBytes = stream.Read(b.Slice(expectedPlayerInputStateSize - bytesLeftInBuffer, bytesLeftInBuffer));
if (readBytes == 0)
break;
bytesLeftInBuffer -= readBytes;
} while (bytesLeftInBuffer > 0);
if (bytesLeftInBuffer > 0)
break; // EOF;
buffer.Add(MemoryMarshal.Read<PlayerInputState>(b));
}
sw.Stop();
bufferEnumerable = buffer.GetEnumerator();
Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} ");
OnEndFrame(); // advances to first frame
}
public override void OnEndFrame()
{
// TODO: check if the current state frame matches the current frame number before advancing
/*asdf++;
if (asdf < 8)
return;*/
if (bufferEnumerable == null || !bufferEnumerable.MoveNext())
{
if (buffer.Any())
{
bufferEnumerable.Dispose();
bufferEnumerable = null;
buffer.Clear();
Console.Print("Demo ended");
}
return;
}
//var actorState = currentState.actor;
currentState.input = bufferEnumerable.Current;
//frame++;
//currentState.actor = actorState;
} }
} }
public PlayerInputDemo(string demoPath)
{
if (!File.Exists(demoPath))
return;
int expectedPlayerInputStateSize = Unsafe.SizeOf<PlayerInputState>();
Stopwatch sw = Stopwatch.StartNew();
using FileStream fileStream = File.OpenRead(demoPath);
using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress);
int ver = stream.ReadByte();
int inputStateSize = stream.ReadByte();
if (ver != DemoVer || inputStateSize != expectedPlayerInputStateSize)
{
Console.Print("demover mismatch: version " + ver + " != " + DemoVer + ", inputStateSize " +
inputStateSize + " != " + Unsafe.SizeOf<PlayerInputState>());
stream.Close();
return;
}
Span<byte> b = stackalloc byte[expectedPlayerInputStateSize];
while (true)
{
int bytesLeftInBuffer = expectedPlayerInputStateSize;
do
{
int readBytes = stream.Read(b.Slice(expectedPlayerInputStateSize - bytesLeftInBuffer, bytesLeftInBuffer));
if (readBytes == 0)
break;
bytesLeftInBuffer -= readBytes;
} while (bytesLeftInBuffer > 0);
if (bytesLeftInBuffer > 0)
break; // EOF;
buffer.Add(MemoryMarshal.Read<PlayerInputState>(b));
}
sw.Stop();
bufferEnumerable = buffer.GetEnumerator();
Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} ");
OnEndFrame(); // advances to first frame
}
public override void OnEndFrame()
{
// TODO: check if the current state frame matches the current frame number before advancing
/*asdf++;
if (asdf < 8)
return;*/
if (bufferEnumerable == null || !bufferEnumerable.MoveNext())
{
if (buffer.Any())
{
bufferEnumerable.Dispose();
bufferEnumerable = null;
buffer.Clear();
Console.Print("Demo ended");
}
return;
}
//var actorState = currentState.actor;
currentState.input = bufferEnumerable.Current;
//frame++;
//currentState.actor = actorState;
}
} }

View File

@@ -9,142 +9,141 @@ using FlaxEngine;
using FlaxEngine.Networking; using FlaxEngine.Networking;
using Console = Game.Console; using Console = Game.Console;
namespace Game namespace Game;
public class PlayerInputLocal : PlayerInput
{ {
public class PlayerInputLocal : PlayerInput protected List<PlayerInputState> buffer = new List<PlayerInputState>();
protected GZipStream demoStream;
protected FileStream demoFileStream;
private PlayerActor playerActor;
public bool IsNetworked => NetworkManager.client != null;
private long flushedFrames = 0;
/*public PlayerInputLocal()
{ {
protected List<PlayerInputState> buffer = new List<PlayerInputState>(); }*/
protected GZipStream demoStream;
protected FileStream demoFileStream;
private PlayerActor playerActor; public override bool Predict => true;
public bool IsNetworked => NetworkManager.client != null;
private long flushedFrames = 0;
/*public PlayerInputLocal() public PlayerInputLocal(PlayerActor playerActor, string demoPath = null)
{
this.playerActor = playerActor;
if (demoPath == null)
return;
var demoFolder = Directory.GetParent(demoPath);
if (!demoFolder.Exists)
Directory.CreateDirectory(demoFolder.FullName);
demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write);
demoStream = new GZipStream(demoFileStream, CompressionMode.Compress);
demoStream.WriteByte(DemoVer);
demoStream.WriteByte((byte)Unsafe.SizeOf<PlayerInputState>());
}
public bool IsRecording => demoStream != null;
public override void OnUpdate()
{
// Collect all input here
// All axis values here should be accumulated
float sensitivity = 1.0f / 8.0f;
sensitivity = 1.0f;
//var asf = InputManager.GetAxisRaw("Mouse X");
//if (asf != 0.0f)
// Console.Print(InputManager.GetAxisRaw("Mouse X").ToString("G9", System.Globalization.CultureInfo.InvariantCulture));
currentState.input.viewDeltaX += InputManager.GetAxisRaw("Mouse X") * sensitivity;
currentState.input.viewDeltaY += InputManager.GetAxisRaw("Mouse Y") * sensitivity;
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");
}
public override void OnFixedUpdate()
{
}
public override void OnEndFrame()
{
currentState.input.frame = frame;
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 bool Predict => true; buffer.Add(currentState.input);
public PlayerInputLocal(PlayerActor playerActor, string demoPath = null)
{
this.playerActor = playerActor;
if (demoPath == null)
return;
var demoFolder = Directory.GetParent(demoPath);
if (!demoFolder.Exists)
Directory.CreateDirectory(demoFolder.FullName);
demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write);
demoStream = new GZipStream(demoFileStream, CompressionMode.Compress);
demoStream.WriteByte(DemoVer);
demoStream.WriteByte((byte)Unsafe.SizeOf<PlayerInputState>());
} }
public bool IsRecording => demoStream != null; if (playerActor != null)
public override void OnUpdate()
{ {
// Collect all input here //playerActor.UpdateNetworkInput(currentState.input.frame, new Float4(currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight), currentState.input.attacking, currentState.input.jumping);
// All axis values here should be accumulated
float sensitivity = 1.0f / 8.0f;
sensitivity = 1.0f;
//var asf = InputManager.GetAxisRaw("Mouse X");
//if (asf != 0.0f)
// Console.Print(InputManager.GetAxisRaw("Mouse X").ToString("G9", System.Globalization.CultureInfo.InvariantCulture));
currentState.input.viewDeltaX += InputManager.GetAxisRaw("Mouse X") * sensitivity;
currentState.input.viewDeltaY += InputManager.GetAxisRaw("Mouse Y") * sensitivity;
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");
}
public override void OnFixedUpdate()
{
}
public override void OnEndFrame()
{
currentState.input.frame = frame;
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;
buffer.Add(currentState.input);
}
if (playerActor != null)
{
//playerActor.UpdateNetworkInput(currentState.input.frame, new Float4(currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight), currentState.input.attacking, currentState.input.jumping);
}
//playerActor.UpdateNetworkInput(currentState.input.frame, currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight, currentState.input.attacking, currentState.input.jumping, currentState.input.verificationPosition, currentState.input.verificationVelocity, currentState.input.verificationViewAngles, currentState.input.verificationOrientation);
if (IsNetworked)
{
var message = NetworkManager.ClientBeginSendMessage();
message.WriteByte((byte)GameModeMessageType.PlayerInput);
message.WriteUInt64(currentState.input.frame);
message.WriteSingle(currentState.input.viewDeltaX);
message.WriteSingle(currentState.input.viewDeltaY);
message.WriteSingle(currentState.input.moveForward);
message.WriteSingle(currentState.input.moveRight);
message.WriteBoolean(currentState.input.attacking);
message.WriteBoolean(currentState.input.jumping);
NetworkManager.ClientEndSendMessage(ref message);
}
base.OnEndFrame();
// Reset anything accumulatable here
currentState.input.viewDeltaX = 0;
currentState.input.viewDeltaY = 0;
}
public void FlushDemo()
{
if (!IsRecording)
return;
Stopwatch sw = Stopwatch.StartNew();
Span<byte> bytes = stackalloc byte[Unsafe.SizeOf<PlayerInputState>()];
foreach (ref PlayerInputState state in CollectionsMarshal.AsSpan(buffer))
{
MemoryMarshal.Write(bytes, state);
demoStream.Write(bytes);
}
sw.Stop();
flushedFrames += buffer.Count;
buffer.Clear();
FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}");
} }
//playerActor.UpdateNetworkInput(currentState.input.frame, currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight, currentState.input.attacking, currentState.input.jumping, currentState.input.verificationPosition, currentState.input.verificationVelocity, currentState.input.verificationViewAngles, currentState.input.verificationOrientation);
public void StopRecording() if (IsNetworked)
{ {
if (!IsRecording) var message = NetworkManager.ClientBeginSendMessage();
return; message.WriteByte((byte)GameModeMessageType.PlayerInput);
message.WriteUInt64(currentState.input.frame);
FlushDemo(); message.WriteSingle(currentState.input.viewDeltaX);
demoStream.Close(); message.WriteSingle(currentState.input.viewDeltaY);
demoStream = null; message.WriteSingle(currentState.input.moveForward);
demoFileStream.Close(); message.WriteSingle(currentState.input.moveRight);
demoFileStream = null; message.WriteBoolean(currentState.input.attacking);
message.WriteBoolean(currentState.input.jumping);
NetworkManager.ClientEndSendMessage(ref message);
} }
base.OnEndFrame();
// Reset anything accumulatable here
currentState.input.viewDeltaX = 0;
currentState.input.viewDeltaY = 0;
}
public void FlushDemo()
{
if (!IsRecording)
return;
Stopwatch sw = Stopwatch.StartNew();
Span<byte> bytes = stackalloc byte[Unsafe.SizeOf<PlayerInputState>()];
foreach (ref PlayerInputState state in CollectionsMarshal.AsSpan(buffer))
{
MemoryMarshal.Write(bytes, state);
demoStream.Write(bytes);
}
sw.Stop();
flushedFrames += buffer.Count;
buffer.Clear();
FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}");
}
public void StopRecording()
{
if (!IsRecording)
return;
FlushDemo();
demoStream.Close();
demoStream = null;
demoFileStream.Close();
demoFileStream = null;
} }
} }

View File

@@ -1,13 +1,12 @@
namespace Game namespace Game;
{
public class PlayerInputNetwork : PlayerInput
{
public override bool Predict => true;
public override void OnEndFrame() public class PlayerInputNetwork : PlayerInput
{ {
currentState.input.frame = frame; public override bool Predict => true;
base.OnEndFrame();
} public override void OnEndFrame()
{
currentState.input.frame = frame;
base.OnEndFrame();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,26 @@
using System.IO; using System.IO;
using FlaxEngine; using FlaxEngine;
namespace Game namespace Game;
public static class AssetManager
{ {
public static class AssetManager public static string ContentPath { get; private set; } =
Path.Combine(Directory.GetCurrentDirectory(), "Content");
public static string DemoPath { get; private set; } =
Path.Combine(Directory.GetCurrentDirectory(), "Demos");
public static string CachePath { get; private set; } =
Path.Combine(Directory.GetCurrentDirectory(), "Cache");
public static GameplayGlobals Globals { get; private set; }
public static Config Config { get; private set; }
//public static void Init()
static AssetManager()
{ {
public static string ContentPath { get; private set; } = Globals = Content.Load<GameplayGlobals>(Path.Combine(ContentPath, "Settings", "GameSettings", "GameplayGlobals.flax"));
Path.Combine(Directory.GetCurrentDirectory(), "Content"); Config = ConfigParser.ParseFile(Path.Combine(ContentPath, "config.cfg"));
public static string DemoPath { get; private set; } =
Path.Combine(Directory.GetCurrentDirectory(), "Demos");
public static string CachePath { get; private set; } =
Path.Combine(Directory.GetCurrentDirectory(), "Cache");
public static GameplayGlobals Globals { get; private set; }
public static Config Config { get; private set; }
//public static void Init()
static AssetManager()
{
Globals = Content.Load<GameplayGlobals>(Path.Combine(ContentPath, "Settings", "GameSettings", "GameplayGlobals.flax"));
Config = ConfigParser.ParseFile(Path.Combine(ContentPath, "config.cfg"));
}
} }
} }