Merge branch 'master' into local

This commit is contained in:
Nejcraft
2021-08-23 20:40:37 +02:00
committed by GitHub
79 changed files with 21230 additions and 640 deletions

View File

@@ -4,7 +4,7 @@ https://github.com/flaxengine/FlaxEngine/issues?q=is%3Aissue
**Issue description:**
<!-- What happened, and what was expected. -->
<!-- Log file, can be found in the project directory's `Logs` folder (optional) -->
**Steps to reproduce:**
<!-- Enter minimal reproduction steps if available. -->

View File

@@ -32,14 +32,14 @@ namespace FlaxEditor.Content.GUI
/// </summary>
public enum SortType
{
/// <summary>
/// <summary>
/// The classic alphabetic sort method (A-Z).
/// </summary>
AlphabeticOrder,
/// <summary>
/// <summary>
/// The reverse alphabetic sort method (Z-A).
/// </summary>
/// </summary>
AlphabeticReverse
}
@@ -272,18 +272,14 @@ namespace FlaxEditor.Content.GUI
if (sortType == SortType.AlphabeticReverse)
{
if (control.CompareTo(control1) > 0)
{
return -1;
}
return -1;
if (control.CompareTo(control1) == 0)
{
return 0;
}
return 0;
return 1;
}
return control.CompareTo(control1);
}));
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();

View File

@@ -625,10 +625,11 @@ namespace FlaxEditor.CustomEditors.Dedicated
group.Panel.Open(false);
// Customize
var typeAttributes = scriptType.GetAttributes(true);
var typeAttributes = scriptType.GetAttributes(false);
group.Panel.TooltipText = scriptType.TypeName;
var tooltip = (TooltipAttribute)typeAttributes.FirstOrDefault(x => x is TooltipAttribute);
if (tooltip != null)
group.Panel.TooltipText = tooltip.Text;
group.Panel.TooltipText += '\n' + tooltip.Text;
if (script.HasPrefabLink)
group.Panel.HeaderTextColor = FlaxEngine.GUI.Style.Current.ProgressNormal;

View File

@@ -83,7 +83,18 @@ namespace FlaxEditor.GUI
{
_editor._mainPanel.ViewOffset += delta;
_movingViewLastPos = location;
Cursor = CursorType.SizeAll;
switch (_editor.EnablePanning)
{
case UseMode.Vertical:
Cursor = CursorType.SizeNS;
break;
case UseMode.Horizontal:
Cursor = CursorType.SizeWE;
break;
case UseMode.On:
Cursor = CursorType.SizeAll;
break;
}
}
return;

View File

@@ -46,10 +46,16 @@ namespace FlaxEditor.GUI.Timeline.GUI
var areaLeft = -X;
var areaRight = Parent.Width + mediaBackground.ControlsBounds.BottomRight.X;
var height = Height;
var leftSideMin = PointFromParent(Vector2.Zero);
var leftSideMax = BottomLeft;
var rightSideMin = UpperRight;
var rightSideMax = PointFromParent(Parent.BottomRight) + mediaBackground.ControlsBounds.BottomRight;
// Calculate the timeline range in the view to optimize background drawing
Render2D.PeekClip(out var globalClipping);
Render2D.PeekTransform(out var globalTransform);
var globalRect = new Rectangle(globalTransform.M31 + areaLeft, globalTransform.M32, areaRight * globalTransform.M11, height * globalTransform.M22);
var globalMask = Rectangle.Shared(globalClipping, globalRect);
var globalTransformInv = Matrix3x3.Invert(globalTransform);
var localRect = Rectangle.FromPoints(Matrix3x3.Transform2D(globalMask.UpperLeft, globalTransformInv), Matrix3x3.Transform2D(globalMask.BottomRight, globalTransformInv));
var localRectMin = localRect.UpperLeft;
var localRectMax = localRect.BottomRight;
// Draw lines between tracks
Render2D.DrawLine(new Vector2(areaLeft, 0.5f), new Vector2(areaRight, 0.5f), linesColor);
@@ -77,8 +83,8 @@ namespace FlaxEditor.GUI.Timeline.GUI
var minDistanceBetweenTicks = 50.0f;
var maxDistanceBetweenTicks = 100.0f;
var zoom = Timeline.UnitsPerSecond * _timeline.Zoom;
var left = Vector2.Min(leftSideMin, rightSideMax).X;
var right = Vector2.Max(leftSideMin, rightSideMax).X;
var left = Vector2.Min(localRectMin, localRectMax).X;
var right = Vector2.Max(localRectMin, localRectMax).X;
var leftFrame = Mathf.Floor((left - Timeline.StartOffset) / zoom) * _timeline.FramesPerSecond;
var rightFrame = Mathf.Ceil((right - Timeline.StartOffset) / zoom) * _timeline.FramesPerSecond;
var min = leftFrame;
@@ -146,9 +152,15 @@ namespace FlaxEditor.GUI.Timeline.GUI
}
// Darken area outside the duration
var outsideDurationAreaColor = new Color(0, 0, 0, 100);
Render2D.FillRectangle(new Rectangle(leftSideMin, leftSideMax.X - leftSideMin.X, height), outsideDurationAreaColor);
Render2D.FillRectangle(new Rectangle(rightSideMin, rightSideMax.X - rightSideMin.X, height), outsideDurationAreaColor);
{
var outsideDurationAreaColor = new Color(0, 0, 0, 100);
var leftSideMin = PointFromParent(Vector2.Zero);
var leftSideMax = BottomLeft;
var rightSideMin = UpperRight;
var rightSideMax = PointFromParent(Parent.BottomRight) + mediaBackground.ControlsBounds.BottomRight;
Render2D.FillRectangle(new Rectangle(leftSideMin, leftSideMax.X - leftSideMin.X, height), outsideDurationAreaColor);
Render2D.FillRectangle(new Rectangle(rightSideMin, rightSideMax.X - rightSideMin.X, height), outsideDurationAreaColor);
}
// Draw time axis header
var timeAxisHeaderOffset = -_timeline.MediaBackground.ViewOffset.Y;
@@ -211,11 +223,30 @@ namespace FlaxEditor.GUI.Timeline.GUI
// Zoom in/out
if (IsMouseOver && Root.GetKey(KeyboardKeys.Control))
{
// TODO: preserve the view center point for easier zooming
var locationTimeOld = _timeline.MediaBackground.PointFromParent(_timeline, _timeline.Size * 0.5f).X;
var frame = (locationTimeOld - Timeline.StartOffset * 2.0f) / _timeline.Zoom / Timeline.UnitsPerSecond * _timeline.FramesPerSecond;
_timeline.Zoom += delta * 0.1f;
var locationTimeNew = frame / _timeline.FramesPerSecond * Timeline.UnitsPerSecond * _timeline.Zoom + Timeline.StartOffset * 2.0f;
var locationTimeDelta = locationTimeNew - locationTimeOld;
var scroll = _timeline.MediaBackground.HScrollBar;
if (scroll.Visible && scroll.Enabled)
scroll.TargetValue += locationTimeDelta;
return true;
}
// Scroll view horizontally
if (IsMouseOver && Root.GetKey(KeyboardKeys.Shift))
{
var scroll = _timeline.MediaBackground.HScrollBar;
if (scroll.Visible && scroll.Enabled)
{
scroll.TargetValue -= delta * Timeline.UnitsPerSecond / _timeline.Zoom;
return true;
}
}
return false;
}

View File

@@ -18,6 +18,9 @@ namespace FlaxEditor.GUI.Timeline.GUI
public BackgroundArea(Timeline timeline)
: base(ScrollBars.Both)
{
ScrollBarsSize = 18.0f;
VScrollBar.ThumbThickness = 10.0f;
HScrollBar.ThumbThickness = 10.0f;
_timeline = timeline;
}

View File

@@ -17,7 +17,90 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// <seealso cref="MemberTrack" />
public abstract class CurvePropertyTrackBase : MemberTrack
{
private sealed class Splitter : Control
{
private bool _clicked;
internal CurvePropertyTrackBase _track;
public override void Draw()
{
var style = Style.Current;
if (IsMouseOver || _clicked)
Render2D.FillRectangle(new Rectangle(Vector2.Zero, Size), _clicked ? style.BackgroundSelected : style.BackgroundHighlighted);
}
public override void OnEndMouseCapture()
{
base.OnEndMouseCapture();
_clicked = false;
}
public override void Defocus()
{
base.Defocus();
_clicked = false;
}
public override void OnMouseEnter(Vector2 location)
{
base.OnMouseEnter(location);
Cursor = CursorType.SizeNS;
}
public override void OnMouseLeave()
{
Cursor = CursorType.Default;
base.OnMouseLeave();
}
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_clicked = true;
Focus();
StartMouseCapture();
return true;
}
return base.OnMouseDown(location, button);
}
public override void OnMouseMove(Vector2 location)
{
base.OnMouseMove(location);
if (_clicked)
{
var height = Mathf.Clamp(PointToParent(location).Y, 40.0f, 1000.0f);
if (!Mathf.NearEqual(height, _track._expandedHeight))
{
_track.Height = _track._expandedHeight = height;
_track.Timeline.ArrangeTracks();
}
}
}
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left && _clicked)
{
_clicked = false;
EndMouseCapture();
return true;
}
return base.OnMouseUp(location, button);
}
}
private byte[] _curveEditingStartData;
private float _expandedHeight = 120.0f;
private Splitter _splitter;
/// <summary>
/// The curve editor.
@@ -25,7 +108,6 @@ namespace FlaxEditor.GUI.Timeline.Tracks
public CurveEditorBase Curve;
private const float CollapsedHeight = 20.0f;
private const float ExpandedHeight = 120.0f;
/// <inheritdoc />
public CurvePropertyTrackBase(ref TrackCreateOptions options)
@@ -155,6 +237,17 @@ namespace FlaxEditor.GUI.Timeline.Tracks
Curve.EnablePanning = expanded ? CurveEditorBase.UseMode.Vertical : CurveEditorBase.UseMode.Off;
Curve.ScrollBars = expanded ? ScrollBars.Vertical : ScrollBars.None;
Curve.UpdateKeyframes();
if (expanded)
{
if(_splitter == null)
_splitter = new Splitter
{
_track = this,
Parent = Curve,
};
var splitterHeight = 4.0f;
_splitter.Bounds = new Rectangle(0, Curve.Height - splitterHeight, Curve.Width, splitterHeight);
}
}
private void OnKeyframesEdited()
@@ -217,10 +310,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{
if (Curve == null)
return;
Curve.Edited -= OnKeyframesEdited;
Curve.Dispose();
Curve = null;
_splitter = null;
}
/// <inheritdoc />
@@ -257,7 +350,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// <inheritdoc />
protected override void OnExpandedChanged()
{
Height = IsExpanded ? ExpandedHeight : CollapsedHeight;
Height = IsExpanded ? _expandedHeight : CollapsedHeight;
UpdateCurve();
if (IsExpanded)
Curve.ShowWholeCurve();

View File

@@ -153,17 +153,16 @@ namespace FlaxEditor.Modules
};
}
}
Profiler.BeginEvent("ContentFinding.Search");
string type = ".*";
string name = charsToFind.Trim();
if (charsToFind.Contains(':'))
{
var args = charsToFind.Split(':');
type = ".*" + args[1].Trim() + ".*";
name = ".*" + args[0].Trim() + ".*";
}
if (name.Equals(string.Empty))
name = ".*";
@@ -173,17 +172,28 @@ namespace FlaxEditor.Modules
foreach (var project in Editor.Instance.ContentDatabase.Projects)
{
Profiler.BeginEvent(project.Project.Name);
ProcessItems(nameRegex, typeRegex, project.Folder.Children, matches);
Profiler.EndEvent();
}
//ProcessSceneNodes(nameRegex, typeRegex, Editor.Instance.Scene.Root, matches);
ProcessActors(nameRegex, typeRegex, Editor.Instance.Scene.Root, matches);
_quickActions.ForEach(action =>
{
if (nameRegex.Match(action.Name).Success && typeRegex.Match("Quick Action").Success)
matches.Add(new SearchResult { Name = action.Name, Type = "Quick Action", Item = action });
});
Profiler.BeginEvent("Actors");
ProcessActors(nameRegex, typeRegex, Editor.Instance.Scene.Root, matches);
Profiler.EndEvent();
}
{
Profiler.BeginEvent("QuickActions");
_quickActions.ForEach(action =>
{
if (nameRegex.Match(action.Name).Success && typeRegex.Match("Quick Action").Success)
matches.Add(new SearchResult { Name = action.Name, Type = "Quick Action", Item = action });
});
Profiler.EndEvent();
}
Profiler.EndEvent();
return matches;
}

View File

@@ -239,7 +239,6 @@ bool ScriptsBuilder::RunBuildTool(const StringView& args)
cmdLine.Append(buildToolPath);
cmdLine.Append(TEXT("\" "));
cmdLine.Append(args.Get(), args.Length());
cmdLine.Append(TEXT('\0'));
// TODO: Set env var for the mono MONO_GC_PARAMS=nursery-size64m to boost build performance -> profile it
// Call build tool

View File

@@ -107,14 +107,18 @@ namespace FlaxEditor.Surface.ContextMenu
private void BuildList(List<SearchResult> items)
{
_resultPanel.DisposeChildren();
LockChildrenRecursive();
var dpiScale = DpiScale;
var window = RootWindow.Window;
if (items.Count == 0)
{
Height = _searchBox.Height + 1;
_resultPanel.ScrollBars = ScrollBars.None;
RootWindow.Window.ClientSize = new Vector2(RootWindow.Window.ClientSize.X, Height * dpiScale);
window.ClientSize = new Vector2(window.ClientSize.X, Height * dpiScale);
UnlockChildrenRecursive();
PerformLayout();
return;
}
@@ -148,8 +152,9 @@ namespace FlaxEditor.Surface.ContextMenu
MatchedItems.Add(searchItem);
}
RootWindow.Window.ClientSize = new Vector2(RootWindow.Window.ClientSize.X, Height * dpiScale);
window.ClientSize = new Vector2(window.ClientSize.X, Height * dpiScale);
UnlockChildrenRecursive();
PerformLayout();
}

View File

@@ -221,16 +221,20 @@ namespace FlaxEditor
var snapshotInstances = (object[])snapshotInstance;
if (snapshotInstances == null || snapshotInstances.Length != SnapshotInstances.Length)
throw new ArgumentException("Invalid multi undo action objects.");
var actions = new List<UndoActionObject>();
List<UndoActionObject> actions = null;
for (int i = 0; i < snapshotInstances.Length; i++)
{
var diff = Snapshot[i].Compare(snapshotInstances[i]);
if (diff.Count == 0)
continue;
if (actions == null)
actions = new List<UndoActionObject>();
actions.Add(new UndoActionObject(diff, ActionString, SnapshotInstances[i]));
}
if (actions.Count == 0)
if (actions == null)
return null;
if (actions.Count == 1)
return actions[0];
return new MultiUndoAction(actions);
}
}

View File

@@ -816,6 +816,6 @@ bool EditorUtilities::ReplaceInFile(const StringView& file, const StringView& fi
String text;
if (File::ReadAllText(file, text))
return true;
text.Replace(findWhat.GetText(), replaceWith.GetText());
text.Replace(findWhat.Get(), findWhat.Length(), replaceWith.Get(), replaceWith.Length());
return File::WriteAllText(file, text, Encoding::ANSI);
}

View File

@@ -145,18 +145,31 @@ namespace FlaxEditor.Viewport.Previews
const uint maxSamplesPerIndex = 64;
uint samplesPerIndexDiff = Math.Max(1, samplesPerIndex / Math.Min(samplesPerIndex, maxSamplesPerIndex));
// Calculate the clip range in the view to optimize drawing (eg. if only part fo the clip is visible)
Render2D.PeekClip(out var globalClipping);
Render2D.PeekTransform(out var globalTransform);
var globalRect = new Rectangle(globalTransform.M31, globalTransform.M32, width * globalTransform.M11, height * globalTransform.M22);
var globalMask = Rectangle.Shared(globalClipping, globalRect);
var globalTransformInv = Matrix3x3.Invert(globalTransform);
var localRect = Rectangle.FromPoints(Matrix3x3.Transform2D(globalMask.UpperLeft, globalTransformInv), Matrix3x3.Transform2D(globalMask.BottomRight, globalTransformInv));
var localRectMin = localRect.UpperLeft;
var localRectMax = localRect.BottomRight;
// Render each clip separately
for (uint clipIndex = 0; clipIndex < Mathf.CeilToInt(clipsInView); clipIndex++)
{
var clipX = clipWidth * clipIndex;
var clipRight = Mathf.Min(width, clipX + clipWidth);
var clipStart = clipWidth * clipIndex;
var clipEnd = clipStart + clipWidth;
var xStart = Mathf.Max(clipStart, localRectMin.X);
var xEnd = Mathf.Min(Mathf.Min(width, clipEnd), localRectMax.X);
var samplesOffset = (uint)((xStart - clipStart) * samplesPerIndex);
// Render every audio channel separately
for (uint channelIndex = 0; channelIndex < info.NumChannels; channelIndex++)
{
uint currentSample = channelIndex;
uint currentSample = channelIndex + samplesOffset;
float yCenter = Y + ((2 * channelIndex) + 1) * height / (2.0f * info.NumChannels);
for (float pixelX = clipX; pixelX < clipRight; pixelX++)
for (float pixelX = xStart; pixelX < xEnd; pixelX++)
{
float samplesSum = 0;
int samplesInPixel = 0;

View File

@@ -538,9 +538,9 @@ namespace FlaxEditor.Windows.Assets
for (int i = 0; i < meshData.IndexBuffer.Length; i += 3)
{
// Cache triangle indices
int i0 = meshData.IndexBuffer[i + 0];
int i1 = meshData.IndexBuffer[i + 1];
int i2 = meshData.IndexBuffer[i + 2];
uint i0 = meshData.IndexBuffer[i + 0];
uint i1 = meshData.IndexBuffer[i + 1];
uint i2 = meshData.IndexBuffer[i + 2];
// Cache triangle uvs positions and transform positions to output target
Vector2 uv0 = meshData.VertexBuffer[i0].TexCoord * uvScale;
@@ -562,9 +562,9 @@ namespace FlaxEditor.Windows.Assets
for (int i = 0; i < meshData.IndexBuffer.Length; i += 3)
{
// Cache triangle indices
int i0 = meshData.IndexBuffer[i + 0];
int i1 = meshData.IndexBuffer[i + 1];
int i2 = meshData.IndexBuffer[i + 2];
uint i0 = meshData.IndexBuffer[i + 0];
uint i1 = meshData.IndexBuffer[i + 1];
uint i2 = meshData.IndexBuffer[i + 2];
// Cache triangle uvs positions and transform positions to output target
Vector2 uv0 = meshData.VertexBuffer[i0].LightmapUVs * uvScale;

View File

@@ -645,9 +645,9 @@ namespace FlaxEditor.Windows.Assets
for (int i = 0; i < meshData.IndexBuffer.Length; i += 3)
{
// Cache triangle indices
int i0 = meshData.IndexBuffer[i + 0];
int i1 = meshData.IndexBuffer[i + 1];
int i2 = meshData.IndexBuffer[i + 2];
uint i0 = meshData.IndexBuffer[i + 0];
uint i1 = meshData.IndexBuffer[i + 1];
uint i2 = meshData.IndexBuffer[i + 2];
// Cache triangle uvs positions and transform positions to output target
Vector2 uv0 = meshData.VertexBuffer[i0].TexCoord * uvScale;
@@ -820,7 +820,7 @@ namespace FlaxEditor.Windows.Assets
private struct MeshData
{
public int[] IndexBuffer;
public uint[] IndexBuffer;
public SkinnedMesh.Vertex[] VertexBuffer;
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Xml;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEditor.Options;
@@ -484,5 +485,31 @@ namespace FlaxEditor.Windows
return result;
}
/// <inheritdoc />
public override bool UseLayoutData => true;
/// <inheritdoc />
public override void OnLayoutSerialize(XmlWriter writer)
{
writer.WriteAttributeString("ShowGUI", ShowGUI.ToString());
writer.WriteAttributeString("ShowDebugDraw", ShowDebugDraw.ToString());
}
/// <inheritdoc />
public override void OnLayoutDeserialize(XmlElement node)
{
if (bool.TryParse(node.GetAttribute("ShowGUI"), out bool value1))
ShowGUI = value1;
if (bool.TryParse(node.GetAttribute("ShowDebugDraw"), out value1))
ShowDebugDraw = value1;
}
/// <inheritdoc />
public override void OnLayoutDeserialize()
{
ShowGUI = true;
ShowDebugDraw = false;
}
}
}

View File

@@ -192,6 +192,7 @@ namespace FlaxEditor.Windows
_contextMenu.AddButton("Clear log", Clear);
_contextMenu.AddButton("Copy selection", _output.Copy);
_contextMenu.AddButton("Select All", _output.SelectAll);
_contextMenu.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(Path.Combine(Globals.ProjectFolder, "Logs")));
_contextMenu.AddButton("Scroll to bottom", () => { _vScroll.TargetValue = _vScroll.Maximum; }).Icon = Editor.Icons.ArrowDown12;
// Setup editor options

View File

@@ -131,7 +131,8 @@ const Char* SplashScreenQuotes[] =
TEXT("ZOINKS"),
TEXT("Scooby dooby doo"),
TEXT("You shall not load!"),
TEXT("The roof, the roof, the roof is on fire!")
TEXT("The roof, the roof, the roof is on fire!"),
TEXT("I've seen better documentation ...\nFrom ransomware gangs !")
};
SplashScreen::~SplashScreen()

View File

@@ -384,12 +384,12 @@ Asset* Content::LoadAsyncInternal(const StringView& internalPath, MClass* type)
CHECK_RETURN(type, nullptr);
const auto scriptingType = Scripting::FindScriptingType(type->GetFullName());
if (scriptingType)
return LoadAsyncInternal(internalPath.GetText(), scriptingType);
return LoadAsyncInternal(internalPath, scriptingType);
LOG(Error, "Failed to find asset type '{0}'.", String(type->GetFullName()));
return nullptr;
}
Asset* Content::LoadAsyncInternal(const Char* internalPath, const ScriptingTypeHandle& type)
Asset* Content::LoadAsyncInternal(const StringView& internalPath, const ScriptingTypeHandle& type)
{
#if USE_EDITOR
const String path = Globals::EngineContentFolder / internalPath + ASSET_FILES_EXTENSION_WITH_DOT;
@@ -411,6 +411,11 @@ Asset* Content::LoadAsyncInternal(const Char* internalPath, const ScriptingTypeH
return asset;
}
Asset* Content::LoadAsyncInternal(const Char* internalPath, const ScriptingTypeHandle& type)
{
return LoadAsyncInternal(StringView(internalPath), type);
}
FLAXENGINE_API Asset* LoadAsset(const Guid& id, const ScriptingTypeHandle& type)
{
return Content::LoadAsync(id, type);

View File

@@ -187,6 +187,14 @@ public:
/// <returns>The loaded asset or null if failed.</returns>
API_FUNCTION(Attributes="HideInEditor") static Asset* LoadAsyncInternal(const StringView& internalPath, MClass* type);
/// <summary>
/// Loads internal engine asset and holds it until it won't be referenced by any object. Returns null if asset is missing. Actual asset data loading is performed on a other thread in async.
/// </summary>
/// <param name="internalPath">The path of the asset relative to the engine internal content (excluding the extension).</param>
/// <param name="type">The asset type. If loaded object has different type (excluding types derived from the given) the loading fails.</param>
/// <returns>The loaded asset or null if failed.</returns>
static Asset* LoadAsyncInternal(const StringView& internalPath, const ScriptingTypeHandle& type);
/// <summary>
/// Loads internal engine asset and holds it until it won't be referenced by any object. Returns null if asset is missing. Actual asset data loading is performed on a other thread in async.
/// </summary>

View File

@@ -141,11 +141,6 @@ namespace Math
return exp2f(value);
}
static FORCE_INLINE double Abs(const double value)
{
return fabs(value);
}
static FORCE_INLINE float Abs(const float value)
{
return fabsf(value);
@@ -161,11 +156,6 @@ namespace Math
return value < 0 ? -value : value;
}
static FORCE_INLINE double Mod(const double a, const double b)
{
return fmod(a, b);
}
static FORCE_INLINE float Mod(const float a, const float b)
{
return fmodf(a, b);
@@ -435,14 +425,6 @@ namespace Math
return amount <= 0 ? 0 : amount >= 1 ? 1 : amount * amount * amount * (amount * (amount * 6 - 15) + 10);
}
// Determines whether the specified value is close to zero (0.0)
// @param a The floating value
// @returns True if the specified value is close to zero (0.0). otherwise false
inline bool IsZero(double a)
{
return Abs(a) < 1e-7;
}
// Determines whether the specified value is close to zero (0.0f)
// @param a The floating value
// @returns True if the specified value is close to zero (0.0f). otherwise false

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,362 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include <math.h>
#include "Engine/Core/Types/BaseTypes.h"
namespace Math
{
/// <summary>
/// Computes the sine and cosine of a scalar double.
/// </summary>
/// <param name="angle">The input angle (in radians).</param>
/// <param name="sine">The output sine value.</param>
/// <param name="cosine">The output cosine value.</param>
FLAXENGINE_API void SinCos(double angle, double& sine, double& cosine);
static FORCE_INLINE double Trunc(double value)
{
return trunc(value);
}
static FORCE_INLINE double Round(double value)
{
return round(value);
}
static FORCE_INLINE double Floor(double value)
{
return floor(value);
}
static FORCE_INLINE double Ceil(double value)
{
return ceil(value);
}
static FORCE_INLINE double Sin(double value)
{
return sin(value);
}
static FORCE_INLINE double Asin(double value)
{
return asin(value < -1. ? -1. : value < 1. ? value : 1.);
}
static FORCE_INLINE double Sinh(double value)
{
return sinh(value);
}
static FORCE_INLINE double Cos(double value)
{
return cos(value);
}
static FORCE_INLINE double Acos(double value)
{
return acos(value < -1. ? -1. : value < 1. ? value : 1.);
}
static FORCE_INLINE double Tan(double value)
{
return tan(value);
}
static FORCE_INLINE double Atan(double value)
{
return atan(value);
}
static FORCE_INLINE double Atan2(double y, double x)
{
return atan2(y, x);
}
static FORCE_INLINE double InvSqrt(double value)
{
return 1.0f / sqrt(value);
}
static FORCE_INLINE double Log(const double value)
{
return log(value);
}
static FORCE_INLINE double Log2(const double value)
{
return log2(value);
}
static FORCE_INLINE double Log10(const double value)
{
return log10(value);
}
static FORCE_INLINE double Pow(const double base, const double exponent)
{
return pow(base, exponent);
}
static FORCE_INLINE double Sqrt(const double value)
{
return sqrt(value);
}
static FORCE_INLINE double Exp(const double value)
{
return exp(value);
}
static FORCE_INLINE double Exp2(const double value)
{
return exp2(value);
}
static FORCE_INLINE double Abs(const double value)
{
return fabs(value);
}
static FORCE_INLINE double Mod(const double a, const double b)
{
return fmod(a, b);
}
static FORCE_INLINE double ModF(double a, double* b)
{
return modf(a, b);
}
/// <summary>
/// Returns signed fractional part of a double.
/// </summary>
/// <param name="value">Double point value to convert.</param>
/// <returns>A double between [0 ; 1) for nonnegative input. A double between [-1; 0) for negative input.</returns>
static FORCE_INLINE double Fractional(double value)
{
return value - Trunc(value);
}
static int64 TruncToInt(double value)
{
return (int64)value;
}
static int64 FloorToInt(double value)
{
return TruncToInt(floor(value));
}
static FORCE_INLINE int64 RoundToInt(double value)
{
return FloorToInt(value + 0.5);
}
static FORCE_INLINE int64 CeilToInt(double value)
{
return TruncToInt(ceil(value));
}
// Performs smooth (cubic Hermite) interpolation between 0 and 1
// @param amount Value between 0 and 1 indicating interpolation amount
static double SmoothStep(double amount)
{
return amount <= 0. ? 0. : amount >= 1. ? 1. : amount * amount * (3. - 2. * amount);
}
// Performs a smooth(er) interpolation between 0 and 1 with 1st and 2nd order derivatives of zero at endpoints
// @param amount Value between 0 and 1 indicating interpolation amount
static double SmootherStep(double amount)
{
return amount <= 0. ? 0. : amount >= 1. ? 1. : amount * amount * amount * (amount * (amount * 6. - 15.) + 10.);
}
// Determines whether the specified value is close to zero (0.0)
// @param a The floating value
// @returns True if the specified value is close to zero (0.0). otherwise false
inline bool IsZero(double a)
{
return Abs(a) < ZeroTolerance;
}
// Determines whether the specified value is close to one (1.0f)
// @param a The floating value
// @returns True if the specified value is close to one (1.0f). otherwise false
inline bool IsOne(double a)
{
return IsZero(a - 1.);
}
// Returns a value indicating the sign of a number
// @returns A number that indicates the sign of value
inline double Sign(double v)
{
return v > 0. ? 1. : v < 0. ? -1. : 0.;
}
/// <summary>
/// Compares the sign of two double values.
/// </summary>
/// <param name="a">The first value.</param>
/// <param name="b">The second value.</param>
/// <returns>True if given values have the same sign (both positive or negative); otherwise false.</returns>
inline bool SameSign(const double a, const double b)
{
return a * b >= 0.;
}
/// <summary>
/// Compares the sign of two double values.
/// </summary>
/// <param name="a">The first value.</param>
/// <param name="b">The second value.</param>
/// <returns>True if given values don't have the same sign (first is positive and second is negative or vice versa); otherwise false.</returns>
inline bool NotSameSign(const double a, const double b)
{
return a * b < 0.;
}
/// <summary>
/// Checks if a and b are not even almost equal, taking into account the magnitude of double numbers
/// </summary>
/// <param name="a">The left value to compare</param>
/// <param name="b">The right value to compare</param>
/// <returns>False if a almost equal to b, otherwise true</returns>
static bool NotNearEqual(double a, double b)
{
// Check if the numbers are really close - needed when comparing numbers near zero
if (IsZero(a - b))
return false;
// Original from Bruce Dawson: http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
const int64 aInt = *(int64*)&a;
const int64 bInt = *(int64*)&b;
// Different signs means they do not match
if (aInt < 0 != bInt < 0)
return true;
// Find the difference in ULPs
const int64 ulp = Math::Abs(aInt - bInt);
// Choose of maxUlp = 4
// according to http://code.google.com/p/googletest/source/browse/trunk/include/gtest/internal/gtest-internal.h
const int maxUlp = 4;
return ulp > maxUlp;
}
/// <summary>
/// Checks if a and b are almost equals within the given epsilon value.
/// </summary>
/// <param name="a">The left value to compare.</param>
/// <param name="b">The right value to compare.</param>
/// <param name="eps">The comparision epsilon value. Should be 1e-4 or less.</param>
/// <returns>True if a almost equal to b, otherwise false</returns>
static bool NearEqual(double a, double b, double eps)
{
return Abs(a - b) < eps;
}
/// <summary>
/// Remaps the specified value from the specified range to another.
/// </summary>
/// <param name="value">The value to remap.</param>
/// <param name="fromMin">The source range minimum.</param>
/// <param name="fromMax">The source range maximum.</param>
/// <param name="toMin">The destination range minimum.</param>
/// <param name="toMax">The destination range maximum.</param>
/// <returns>The remapped value.</returns>
static double Remap(double value, double fromMin, double fromMax, double toMin, double toMax)
{
return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin;
}
static double ClampAxis(double angle)
{
angle = Mod(angle, 360.);
if (angle < 0.)
angle += 360.;
return angle;
}
static double NormalizeAxis(double angle)
{
angle = ClampAxis(angle);
if (angle > 180.)
angle -= 360.;
return angle;
}
// Find the smallest angle between two headings (in radians).
static double FindDeltaAngle(double a1, double a2)
{
double delta = a2 - a1;
if (delta > PI)
delta = delta - TWO_PI;
else if (delta < -PI)
delta = delta + TWO_PI;
return delta;
}
// Given a heading which may be outside the +/- PI range, 'unwind' it back into that range
static double UnwindRadians(double a)
{
while (a > PI)
a -= TWO_PI;
while (a < -PI)
a += TWO_PI;
return a;
}
// Utility to ensure angle is between +/- 180 degrees by unwinding
static double UnwindDegrees(double a)
{
while (a > 180.)
a -= 360.;
while (a < -180.)
a += 360.;
return a;
}
/// <summary>
/// Returns value based on comparand. The main purpose of this function is to avoid branching based on floating point comparison which can be avoided via compiler intrinsics.
/// </summary>
/// <remarks>
/// Please note that this doesn't define what happens in the case of NaNs as there might be platform specific differences.
/// </remarks>
/// <param name="comparand">Comparand the results are based on.</param>
/// <param name="valueGEZero">The result value if comparand >= 0.</param>
/// <param name="valueLTZero">The result value if comparand < 0.</param>
/// <returns>the valueGEZero if comparand >= 0, valueLTZero otherwise</returns>
static double DoubleSelect(double comparand, double valueGEZero, double valueLTZero)
{
return comparand >= 0. ? valueGEZero : valueLTZero;
}
/// <summary>
/// Returns a smooth Hermite interpolation between 0 and 1 for the value X (where X ranges between A and B). Clamped to 0 for X <= A and 1 for X >= B.
/// </summary>
/// <param name="a">The minimum value of x.</param>
/// <param name="b">The maximum value of x.</param>
/// <param name="x">The x.</param>
/// <returns>The smoothed value between 0 and 1.</returns>
static double SmoothStep(double a, double b, double x)
{
if (x < a)
return 0.;
if (x >= b)
return 1.;
const double fraction = (x - a) / (b - a);
return fraction * fraction * (3. - 2. * fraction);
}
//TODO: When double vectors are implemented
// Rotates position about the given axis by the given angle, in radians, and returns the offset to position
//Vector3 FLAXENGINE_API RotateAboutAxis(const Vector3& normalizedRotationAxis, float angle, const Vector3& positionOnAxis, const Vector3& position);
//Vector3 FLAXENGINE_API ExtractLargestComponent(const Vector3& v);
}

View File

@@ -6,7 +6,7 @@ using System.ComponentModel;
namespace FlaxEngine
{
/// <summary>
/// A collection of common math functions.
/// A collection of common math functions on single floating-points.
/// </summary>
[HideInEditor]
public static class Mathf

View File

@@ -226,7 +226,9 @@ bool String::IsANSI() const
bool String::StartsWith(const StringView& prefix, StringSearchCase searchCase) const
{
if (prefix.IsEmpty() || prefix.Length() > Length())
if (prefix.IsEmpty())
return true;
if (prefix.Length() > Length())
return false;
if (searchCase == StringSearchCase::IgnoreCase)
return !StringUtils::CompareIgnoreCase(this->GetText(), *prefix, prefix.Length());
@@ -235,7 +237,9 @@ bool String::StartsWith(const StringView& prefix, StringSearchCase searchCase) c
bool String::EndsWith(const StringView& suffix, StringSearchCase searchCase) const
{
if (suffix.IsEmpty() || suffix.Length() > Length())
if (suffix.IsEmpty())
return true;
if (suffix.Length() > Length())
return false;
if (searchCase == StringSearchCase::IgnoreCase)
return !StringUtils::CompareIgnoreCase(&(*this)[Length() - suffix.Length()], *suffix);

View File

@@ -65,10 +65,11 @@ public:
/// <summary>
/// Lexicographically tests how this string compares to the other given string.
/// In case sensitive mode 'A' is less than 'a'.
/// </summary>
/// <param name="str">The another string test against.</param>
/// <param name="searchCase">The case sensitivity mode.</param>
/// <returns>0 if equal, -1 if less than, 1 if greater than.</returns>
/// <returns>0 if equal, negative number if less than, positive number if greater than.</returns>
int32 Compare(const StringBase& str, StringSearchCase searchCase = StringSearchCase::CaseSensitive) const
{
if (searchCase == StringSearchCase::CaseSensitive)
@@ -352,7 +353,7 @@ public:
bool StartsWith(T c, StringSearchCase searchCase = StringSearchCase::CaseSensitive) const
{
const int32 length = Length();
if (searchCase == StringSearchCase::IgnoreCase)
if (searchCase == StringSearchCase::CaseSensitive)
return length > 0 && _data[0] == c;
return length > 0 && StringUtils::ToLower(_data[0]) == StringUtils::ToLower(c);
}
@@ -360,14 +361,16 @@ public:
bool EndsWith(T c, StringSearchCase searchCase = StringSearchCase::CaseSensitive) const
{
const int32 length = Length();
if (searchCase == StringSearchCase::IgnoreCase)
if (searchCase == StringSearchCase::CaseSensitive)
return length > 0 && _data[length - 1] == c;
return length > 0 && StringUtils::ToLower(_data[length - 1]) == StringUtils::ToLower(c);
}
bool StartsWith(const StringBase& prefix, StringSearchCase searchCase = StringSearchCase::CaseSensitive) const
{
if (prefix.IsEmpty() || Length() < prefix.Length())
if (prefix.IsEmpty())
return true;
if (Length() < prefix.Length())
return false;
if (searchCase == StringSearchCase::IgnoreCase)
return StringUtils::CompareIgnoreCase(this->GetText(), *prefix, prefix.Length()) == 0;
@@ -376,7 +379,9 @@ public:
bool EndsWith(const StringBase& suffix, StringSearchCase searchCase = StringSearchCase::CaseSensitive) const
{
if (suffix.IsEmpty() || Length() < suffix.Length())
if (suffix.IsEmpty())
return true;
if (Length() < suffix.Length())
return false;
if (searchCase == StringSearchCase::IgnoreCase)
return StringUtils::CompareIgnoreCase(&(*this)[Length() - suffix.Length()], *suffix) == 0;
@@ -413,67 +418,94 @@ public:
return replacedChars;
}
/// <summary>
/// Replaces all occurences of searchText within current string with replacementText.
/// </summary>
/// <param name="searchText">String to search for. If empty or null no replacements are done.</param>
/// <param name="replacementText">String to replace with. Null is treated as empty string.</param>
/// <returns>Number of replacements made. (In case-sensitive mode if search text and replacement text are equal no replacements are done, and zero is returned.)</returns>
int32 Replace(const T* searchText, const T* replacementText, StringSearchCase searchCase = StringSearchCase::CaseSensitive)
{
int32 replacedCount = 0;
if (HasChars() && searchText && *searchText && replacementText && (searchCase == StringSearchCase::IgnoreCase || StringUtils::Compare(searchText, replacementText) != 0))
const int32 searchTextLength = StringUtils::Length(searchText);
const int32 replacementTextLength = StringUtils::Length(replacementText);
return Replace(searchText, searchTextLength, replacementText, replacementTextLength, searchCase);
}
/// <summary>
/// Replaces all occurences of searchText within current string with replacementText.
/// </summary>
/// <param name="searchText">String to search for.</param>
/// <param name="searchTextLength">Length of searchText. Must be greater than zero.</param>
/// <param name="replacementText">String to replace with. Null is treated as empty string.</param>
/// <param name="replacementTextLength">Length of replacementText.</param>
/// <returns>Number of replacements made (in other words number of occurences of searchText).</returns>
int32 Replace(const T* searchText, int32 searchTextLength, const T* replacementText, int32 replacementTextLength, StringSearchCase searchCase = StringSearchCase::CaseSensitive)
{
if (!HasChars())
return 0;
if (searchTextLength == 0)
{
const int32 searchTextLength = StringUtils::Length(searchText);
const int32 replacementTextLength = StringUtils::Length(replacementText);
if (searchTextLength == replacementTextLength)
ASSERT(false); // Empty search text never makes sense, and is always sign of a bug in calling code.
return 0;
}
int32 replacedCount = 0;
if (searchTextLength == replacementTextLength)
{
T* pos = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(_data, searchText) : StringUtils::Find(_data, searchText));
while (pos != nullptr)
{
T* pos = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(_data, searchText) : StringUtils::Find(_data, searchText));
while (pos != nullptr)
{
replacedCount++;
replacedCount++;
for (int32 i = 0; i < replacementTextLength; i++)
pos[i] = replacementText[i];
for (int32 i = 0; i < replacementTextLength; i++)
pos[i] = replacementText[i];
if (pos + searchTextLength - **this < Length())
pos = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(pos + searchTextLength, searchText) : StringUtils::Find(pos + searchTextLength, searchText));
else
break;
}
if (pos + searchTextLength - **this < Length())
pos = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(pos + searchTextLength, searchText) : StringUtils::Find(pos + searchTextLength, searchText));
else
break;
}
else if (Contains(searchText, searchCase))
}
else if (Contains(searchText, searchCase))
{
T* readPosition = _data;
T* searchPosition = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(readPosition, searchText) : StringUtils::Find(readPosition, searchText));
while (searchPosition != nullptr)
{
T* readPosition = _data;
T* searchPosition = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(readPosition, searchText) : StringUtils::Find(readPosition, searchText));
while (searchPosition != nullptr)
{
replacedCount++;
readPosition = searchPosition + searchTextLength;
searchPosition = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(readPosition, searchText) : StringUtils::Find(readPosition, searchText));
}
const auto oldLength = _length;
const auto oldData = _data;
_length += replacedCount * (replacementTextLength - searchTextLength);
_data = (T*)Platform::Allocate((_length + 1) * sizeof(T), 16);
T* writePosition = _data;
readPosition = oldData;
replacedCount++;
readPosition = searchPosition + searchTextLength;
searchPosition = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(readPosition, searchText) : StringUtils::Find(readPosition, searchText));
while (searchPosition != nullptr)
{
const int32 writeOffset = (int32)(searchPosition - readPosition);
Platform::MemoryCopy(writePosition, readPosition, writeOffset * sizeof(T));
writePosition += writeOffset;
Platform::MemoryCopy(writePosition, replacementText, replacementTextLength * sizeof(T));
writePosition += replacementTextLength;
readPosition = searchPosition + searchTextLength;
searchPosition = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(readPosition, searchText) : StringUtils::Find(readPosition, searchText));
}
const int32 writeOffset = (int32)(oldData - readPosition) + oldLength;
Platform::MemoryCopy(writePosition, readPosition, writeOffset * sizeof(T));
_data[_length] = 0;
Platform::Free(oldData);
}
const auto oldLength = _length;
const auto oldData = _data;
_length += replacedCount * (replacementTextLength - searchTextLength);
_data = (T*)Platform::Allocate((_length + 1) * sizeof(T), 16);
T* writePosition = _data;
readPosition = oldData;
searchPosition = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(readPosition, searchText) : StringUtils::Find(readPosition, searchText));
while (searchPosition != nullptr)
{
const int32 writeOffset = (int32)(searchPosition - readPosition);
Platform::MemoryCopy(writePosition, readPosition, writeOffset * sizeof(T));
writePosition += writeOffset;
if (replacementTextLength > 0)
Platform::MemoryCopy(writePosition, replacementText, replacementTextLength * sizeof(T));
writePosition += replacementTextLength;
readPosition = searchPosition + searchTextLength;
searchPosition = (T*)(searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(readPosition, searchText) : StringUtils::Find(readPosition, searchText));
}
const int32 writeOffset = (int32)(oldData - readPosition) + oldLength;
Platform::MemoryCopy(writePosition, readPosition, writeOffset * sizeof(T));
_data[_length] = 0;
Platform::Free(oldData);
}
return replacedCount;

View File

@@ -18,12 +18,12 @@ StringView::StringView(const String& str)
bool StringView::operator==(const String& other) const
{
return StringUtils::Compare(this->GetText(), *other) == 0;
return this->Compare(StringView(other)) == 0;
}
bool StringView::operator!=(const String& other) const
{
return StringUtils::Compare(this->GetText(), *other) != 0;
return this->Compare(StringView(other)) != 0;
}
StringView StringView::Left(int32 count) const
@@ -62,12 +62,12 @@ StringAnsi StringView::ToStringAnsi() const
bool operator==(const String& a, const StringView& b)
{
return a.Length() == b.Length() && StringUtils::Compare(a.GetText(), b.GetText(), b.Length()) == 0;
return a.Length() == b.Length() && StringUtils::Compare(a.GetText(), b.GetNonTerminatedText(), b.Length()) == 0;
}
bool operator!=(const String& a, const StringView& b)
{
return a.Length() != b.Length() || StringUtils::Compare(a.GetText(), b.GetText(), b.Length()) != 0;
return a.Length() != b.Length() || StringUtils::Compare(a.GetText(), b.GetNonTerminatedText(), b.Length()) != 0;
}
StringAnsiView StringAnsiView::Empty;
@@ -79,12 +79,12 @@ StringAnsiView::StringAnsiView(const StringAnsi& str)
bool StringAnsiView::operator==(const StringAnsi& other) const
{
return StringUtils::Compare(this->GetText(), *other) == 0;
return this->Compare(StringAnsiView(other)) == 0;
}
bool StringAnsiView::operator!=(const StringAnsi& other) const
{
return StringUtils::Compare(this->GetText(), *other) != 0;
return this->Compare(StringAnsiView(other)) != 0;
}
StringAnsi StringAnsiView::Substring(int32 startIndex) const
@@ -111,10 +111,10 @@ StringAnsi StringAnsiView::ToStringAnsi() const
bool operator==(const StringAnsi& a, const StringAnsiView& b)
{
return a.Length() == b.Length() && StringUtils::Compare(a.GetText(), b.GetText(), b.Length()) == 0;
return a.Length() == b.Length() && StringUtils::Compare(a.GetText(), b.GetNonTerminatedText(), b.Length()) == 0;
}
bool operator!=(const StringAnsi& a, const StringAnsiView& b)
{
return a.Length() != b.Length() || StringUtils::Compare(a.GetText(), b.GetText(), b.Length()) != 0;
return a.Length() != b.Length() || StringUtils::Compare(a.GetText(), b.GetNonTerminatedText(), b.Length()) != 0;
}

View File

@@ -54,18 +54,23 @@ public:
/// <summary>
/// Lexicographically tests how this string compares to the other given string.
/// In case sensitive mode 'A' is less than 'a'.
/// </summary>
/// <param name="str">The another string test against.</param>
/// <param name="searchCase">The case sensitivity mode.</param>
/// <returns>0 if equal, -1 if less than, 1 if greater than.</returns>
/// <returns>0 if equal, negative number if less than, positive number if greater than.</returns>
int32 Compare(const StringViewBase& str, StringSearchCase searchCase = StringSearchCase::CaseSensitive) const
{
const int32 lengthDiff = Length() - str.Length();
if (lengthDiff != 0)
return lengthDiff;
if (searchCase == StringSearchCase::CaseSensitive)
return StringUtils::Compare(this->GetText(), str.GetText(), Length());
return StringUtils::CompareIgnoreCase(this->GetText(), str.GetText(), Length());
const bool thisIsShorter = Length() < str.Length();
const int32 minLength = thisIsShorter ? Length() : str.Length();
const int32 prefixCompare = (searchCase == StringSearchCase::CaseSensitive)
? StringUtils::Compare(this->GetNonTerminatedText(), str.GetNonTerminatedText(), minLength)
: StringUtils::CompareIgnoreCase(this->GetNonTerminatedText(), str.GetNonTerminatedText(), minLength);
if (prefixCompare != 0)
return prefixCompare;
if (Length() == str.Length())
return 0;
return thisIsShorter ? -1 : 1;
}
public:
@@ -95,7 +100,7 @@ public:
}
/// <summary>
/// Gets the pointer to the string.
/// Gets the pointer to the string. Pointer can be null, and won't be null-terminated.
/// </summary>
FORCE_INLINE constexpr const T* operator*() const
{
@@ -103,7 +108,7 @@ public:
}
/// <summary>
/// Gets the pointer to the string.
/// Gets the pointer to the string. Pointer can be null, and won't be null-terminated.
/// </summary>
FORCE_INLINE constexpr const T* Get() const
{
@@ -111,9 +116,9 @@ public:
}
/// <summary>
/// Gets the pointer to the string or to the static empty text if string is null. Returned pointer is always valid (read-only).
/// Gets the pointer to the string or to the static empty text if string is null. Returned pointer is always non-null, but is not null-terminated.
/// </summary>
FORCE_INLINE const T* GetText() const
FORCE_INLINE const T* GetNonTerminatedText() const
{
return _data ? _data : (const T*)TEXT("");
}
@@ -177,15 +182,17 @@ public:
{
if (prefix.IsEmpty() || Length() < prefix.Length())
return false;
// We know that this StringView is not empty, and therefore Get() below is valid.
if (searchCase == StringSearchCase::IgnoreCase)
return StringUtils::CompareIgnoreCase(this->GetText(), *prefix, prefix.Length()) == 0;
return StringUtils::Compare(this->GetText(), *prefix, prefix.Length()) == 0;
return StringUtils::CompareIgnoreCase(this->Get(), *prefix, prefix.Length()) == 0;
return StringUtils::Compare(this->Get(), *prefix, prefix.Length()) == 0;
}
bool EndsWith(const StringViewBase& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const
{
if (suffix.IsEmpty() || Length() < suffix.Length())
return false;
// We know that this StringView is not empty, and therefore accessing data below is valid.
if (searchCase == StringSearchCase::IgnoreCase)
return StringUtils::CompareIgnoreCase(&(*this)[Length() - suffix.Length()], *suffix) == 0;
return StringUtils::Compare(&(*this)[Length() - suffix.Length()], *suffix) == 0;
@@ -232,7 +239,7 @@ public:
/// <summary>
/// Initializes a new instance of the <see cref="StringView"/> class.
/// </summary>
/// <param name="str">The characters sequence.</param>
/// <param name="str">The characters sequence. If null, constructed StringView will be empty.</param>
StringView(const Char* str)
{
_data = str;
@@ -242,7 +249,7 @@ public:
/// <summary>
/// Initializes a new instance of the <see cref="StringView"/> class.
/// </summary>
/// <param name="str">The characters sequence.</param>
/// <param name="str">The characters sequence. Can be null if length is zero.</param>
/// <param name="length">The characters sequence length (excluding null-terminator character).</param>
constexpr StringView(const Char* str, int32 length)
: StringViewBase<Char>(str, length)
@@ -270,7 +277,7 @@ public:
/// <returns>True if this string is lexicographically equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator==(const StringView& other) const
{
return StringUtils::Compare(this->GetText(), other.GetText()) == 0;
return this->Compare(other) == 0;
}
/// <summary>
@@ -280,7 +287,7 @@ public:
/// <returns>True if this string is lexicographically is not equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator!=(const StringView& other) const
{
return StringUtils::Compare(this->GetText(), other.GetText()) != 0;
return this->Compare(other) != 0;
}
/// <summary>
@@ -290,7 +297,7 @@ public:
/// <returns>True if this string is lexicographically equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator==(const Char* other) const
{
return StringUtils::Compare(this->GetText(), other ? other : TEXT("")) == 0;
return this->Compare(StringView(other)) == 0;
}
/// <summary>
@@ -300,7 +307,7 @@ public:
/// <returns>True if this string is lexicographically is not equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator!=(const Char* other) const
{
return StringUtils::Compare(this->GetText(), other ? other : TEXT("")) != 0;
return this->Compare(StringView(other)) != 0;
}
/// <summary>
@@ -459,7 +466,7 @@ public:
/// <returns>True if this string is lexicographically equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator==(const StringAnsiView& other) const
{
return StringUtils::Compare(this->GetText(), other.GetText()) == 0;
return this->Compare(other) == 0;
}
/// <summary>
@@ -469,7 +476,7 @@ public:
/// <returns>True if this string is lexicographically is not equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator!=(const StringAnsiView& other) const
{
return StringUtils::Compare(this->GetText(), other.GetText()) != 0;
return this->Compare(other) != 0;
}
/// <summary>
@@ -479,7 +486,7 @@ public:
/// <returns>True if this string is lexicographically equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator==(const char* other) const
{
return StringUtils::Compare(this->GetText(), other ? other : "") == 0;
return this->Compare(StringAnsiView(other)) == 0;
}
/// <summary>
@@ -489,7 +496,7 @@ public:
/// <returns>True if this string is lexicographically is not equivalent to the other, otherwise false.</returns>
FORCE_INLINE bool operator!=(const char* other) const
{
return StringUtils::Compare(this->GetText(), other ? other : "") != 0;
return this->Compare(StringAnsiView(other)) != 0;
}
/// <summary>

View File

@@ -6,6 +6,7 @@
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Content/Asset.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Mathd.h"
#include "Engine/Core/Math/BoundingBox.h"
#include "Engine/Core/Math/BoundingSphere.h"
#include "Engine/Core/Math/Vector2.h"

View File

@@ -270,8 +270,8 @@ public:
explicit operator float() const;
explicit operator double() const;
explicit operator void*() const;
explicit operator StringView() const;
explicit operator StringAnsiView() const;
explicit operator StringView() const; // Returned StringView, if not empty, is guaranteed to point to a null terminated buffer.
explicit operator StringAnsiView() const; // Returned StringView, if not empty, is guaranteed to point to a null terminated buffer.
explicit operator ScriptingObject*() const;
explicit operator struct _MonoObject*() const;
explicit operator Asset*() const;

View File

@@ -144,7 +144,7 @@ namespace FlaxEngine
if (colors != null && colors.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(colors));
if (Internal_UpdateMeshInt(
if (Internal_UpdateMeshUInt(
__unmanagedPtr,
vertices.Length,
triangles.Length / 3,
@@ -190,7 +190,100 @@ namespace FlaxEngine
if (colors != null && colors.Count != vertices.Count)
throw new ArgumentOutOfRangeException(nameof(colors));
if (Internal_UpdateMeshInt(
if (Internal_UpdateMeshUInt(
__unmanagedPtr,
vertices.Count,
triangles.Count / 3,
Utils.ExtractArrayFromList(vertices),
Utils.ExtractArrayFromList(triangles),
Utils.ExtractArrayFromList(normals),
Utils.ExtractArrayFromList(tangents),
Utils.ExtractArrayFromList(uv),
Utils.ExtractArrayFromList(colors)
))
throw new FlaxException("Failed to update mesh data.");
}
/// <summary>
/// Updates the model mesh vertex and index buffer data.
/// Can be used only for virtual assets (see <see cref="Asset.IsVirtual"/> and <see cref="Content.CreateVirtualAsset{T}"/>).
/// Mesh data will be cached and uploaded to the GPU with a delay.
/// </summary>
/// <param name="vertices">The mesh vertices positions. Cannot be null.</param>
/// <param name="triangles">The mesh index buffer (clockwise triangles). Uses 32-bit stride buffer. Cannot be null.</param>
/// <param name="normals">The normal vectors (per vertex).</param>
/// <param name="tangents">The normal vectors (per vertex). Use null to compute them from normal vectors.</param>
/// <param name="uv">The texture coordinates (per vertex).</param>
/// <param name="colors">The vertex colors (per vertex).</param>
public void UpdateMesh(Vector3[] vertices, uint[] triangles, Vector3[] normals = null, Vector3[] tangents = null, Vector2[] uv = null, Color32[] colors = null)
{
// Validate state and input
if (!ParentModel.IsVirtual)
throw new InvalidOperationException("Only virtual models can be updated at runtime.");
if (vertices == null)
throw new ArgumentNullException(nameof(vertices));
if (triangles == null)
throw new ArgumentNullException(nameof(triangles));
if (triangles.Length == 0 || triangles.Length % 3 != 0)
throw new ArgumentOutOfRangeException(nameof(triangles));
if (normals != null && normals.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(normals));
if (tangents != null && tangents.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(tangents));
if (tangents != null && normals == null)
throw new ArgumentException("If you specify tangents then you need to also provide normals for the mesh.");
if (uv != null && uv.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(uv));
if (colors != null && colors.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(colors));
if (Internal_UpdateMeshUInt(
__unmanagedPtr,
vertices.Length,
triangles.Length / 3,
vertices, triangles,
normals,
tangents,
uv,
colors
))
throw new FlaxException("Failed to update mesh data.");
}
/// <summary>
/// Updates the model mesh vertex and index buffer data.
/// Can be used only for virtual assets (see <see cref="Asset.IsVirtual"/> and <see cref="Content.CreateVirtualAsset{T}"/>).
/// Mesh data will be cached and uploaded to the GPU with a delay.
/// </summary>
/// <param name="vertices">The mesh vertices positions. Cannot be null.</param>
/// <param name="triangles">The mesh index buffer (clockwise triangles). Uses 32-bit stride buffer. Cannot be null.</param>
/// <param name="normals">The normal vectors (per vertex).</param>
/// <param name="tangents">The normal vectors (per vertex). Use null to compute them from normal vectors.</param>
/// <param name="uv">The texture coordinates (per vertex).</param>
/// <param name="colors">The vertex colors (per vertex).</param>
public void UpdateMesh(List<Vector3> vertices, List<uint> triangles, List<Vector3> normals = null, List<Vector3> tangents = null, List<Vector2> uv = null, List<Color32> colors = null)
{
// Validate state and input
if (!ParentModel.IsVirtual)
throw new InvalidOperationException("Only virtual models can be updated at runtime.");
if (vertices == null)
throw new ArgumentNullException(nameof(vertices));
if (triangles == null)
throw new ArgumentNullException(nameof(triangles));
if (triangles.Count == 0 || triangles.Count % 3 != 0)
throw new ArgumentOutOfRangeException(nameof(triangles));
if (normals != null && normals.Count != vertices.Count)
throw new ArgumentOutOfRangeException(nameof(normals));
if (tangents != null && tangents.Count != vertices.Count)
throw new ArgumentOutOfRangeException(nameof(tangents));
if (tangents != null && normals == null)
throw new ArgumentException("If you specify tangents then you need to also provide normals for the mesh.");
if (uv != null && uv.Count != vertices.Count)
throw new ArgumentOutOfRangeException(nameof(uv));
if (colors != null && colors.Count != vertices.Count)
throw new ArgumentOutOfRangeException(nameof(colors));
if (Internal_UpdateMeshUInt(
__unmanagedPtr,
vertices.Count,
triangles.Count / 3,
@@ -314,7 +407,7 @@ namespace FlaxEngine
if (triangles.Length == 0 || triangles.Length % 3 != 0)
throw new ArgumentOutOfRangeException(nameof(triangles));
if (Internal_UpdateTrianglesInt(
if (Internal_UpdateTrianglesUInt(
__unmanagedPtr,
triangles.Length / 3,
triangles
@@ -338,7 +431,7 @@ namespace FlaxEngine
if (triangles.Count == 0 || triangles.Count % 3 != 0)
throw new ArgumentOutOfRangeException(nameof(triangles));
if (Internal_UpdateTrianglesInt(
if (Internal_UpdateTrianglesUInt(
__unmanagedPtr,
triangles.Count / 3,
Utils.ExtractArrayFromList(triangles)
@@ -499,10 +592,10 @@ namespace FlaxEngine
/// <remarks>If mesh index buffer format (see <see cref="IndexBufferFormat"/>) is <see cref="PixelFormat.R16_UInt"/> then it's faster to call .</remarks>
/// <param name="forceGpu">If set to <c>true</c> the data will be downloaded from the GPU, otherwise it can be loaded from the drive (source asset file) or from memory (if cached). Downloading mesh from GPU requires this call to be made from the other thread than main thread. Virtual assets are always downloaded from GPU memory due to lack of dedicated storage container for the asset data.</param>
/// <returns>The gathered data.</returns>
public int[] DownloadIndexBuffer(bool forceGpu = false)
public uint[] DownloadIndexBuffer(bool forceGpu = false)
{
var triangles = TriangleCount;
var result = new int[triangles * 3];
var result = new uint[triangles * 3];
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.IB32))
throw new FlaxException("Failed to download mesh data.");
return result;

View File

@@ -608,7 +608,7 @@ ScriptingObject* Mesh::GetParentModel()
return _model;
}
bool Mesh::UpdateMeshInt(int32 vertexCount, int32 triangleCount, MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj, MonoArray* colorsObj)
bool Mesh::UpdateMeshUInt(int32 vertexCount, int32 triangleCount, MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj, MonoArray* colorsObj)
{
return ::UpdateMesh<uint32>(this, (uint32)vertexCount, (uint32)triangleCount, verticesObj, trianglesObj, normalsObj, tangentsObj, uvObj, colorsObj);
}
@@ -618,7 +618,7 @@ bool Mesh::UpdateMeshUShort(int32 vertexCount, int32 triangleCount, MonoArray* v
return ::UpdateMesh<uint16>(this, (uint32)vertexCount, (uint32)triangleCount, verticesObj, trianglesObj, normalsObj, tangentsObj, uvObj, colorsObj);
}
bool Mesh::UpdateTrianglesInt(int32 triangleCount, MonoArray* trianglesObj)
bool Mesh::UpdateTrianglesUInt(int32 triangleCount, MonoArray* trianglesObj)
{
return ::UpdateTriangles<uint32>(this, triangleCount, trianglesObj);
}

View File

@@ -404,9 +404,9 @@ private:
// Internal bindings
API_FUNCTION(NoProxy) ScriptingObject* GetParentModel();
API_FUNCTION(NoProxy) bool UpdateMeshInt(int32 vertexCount, int32 triangleCount, MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj, MonoArray* colorsObj);
API_FUNCTION(NoProxy) bool UpdateMeshUInt(int32 vertexCount, int32 triangleCount, MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj, MonoArray* colorsObj);
API_FUNCTION(NoProxy) bool UpdateMeshUShort(int32 vertexCount, int32 triangleCount, MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj, MonoArray* colorsObj);
API_FUNCTION(NoProxy) bool UpdateTrianglesInt(int32 triangleCount, MonoArray* trianglesObj);
API_FUNCTION(NoProxy) bool UpdateTrianglesUInt(int32 triangleCount, MonoArray* trianglesObj);
API_FUNCTION(NoProxy) bool UpdateTrianglesUShort(int32 triangleCount, MonoArray* trianglesObj);
API_FUNCTION(NoProxy) bool DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI);
};

View File

@@ -418,9 +418,9 @@ bool UpdateMesh(SkinnedMesh* mesh, MonoArray* verticesObj, MonoArray* trianglesO
return mesh->UpdateMesh(vertexCount, triangleCount, vb.Get(), ib);
}
bool SkinnedMesh::UpdateMeshInt(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj)
bool SkinnedMesh::UpdateMeshUInt(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj)
{
return ::UpdateMesh<int32>(this, verticesObj, trianglesObj, blendIndicesObj, blendWeightsObj, normalsObj, tangentsObj, uvObj);
return ::UpdateMesh<uint32>(this, verticesObj, trianglesObj, blendIndicesObj, blendWeightsObj, normalsObj, tangentsObj, uvObj);
}
bool SkinnedMesh::UpdateMeshUShort(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj)

View File

@@ -117,6 +117,19 @@ public:
return UpdateMesh(vertexCount, triangleCount, vb, ib, false);
}
/// <summary>
/// Updates the model mesh (used by the virtual models created with Init rather than Load).
/// </summary>
/// <param name="vertexCount">The amount of vertices in the vertex buffer.</param>
/// <param name="triangleCount">The amount of triangles in the index buffer.</param>
/// <param name="vb">The vertex buffer data.</param>
/// <param name="ib">The index buffer in clockwise order.</param>
/// <returns>True if failed, otherwise false.</returns>
FORCE_INLINE bool UpdateMesh(uint32 vertexCount, uint32 triangleCount, VB0SkinnedElementType* vb, uint32* ib)
{
return UpdateMesh(vertexCount, triangleCount, vb, ib, false);
}
/// <summary>
/// Updates the model mesh (used by the virtual models created with Init rather than Load).
/// </summary>
@@ -245,7 +258,7 @@ private:
// Internal bindings
API_FUNCTION(NoProxy) ScriptingObject* GetParentModel();
API_FUNCTION(NoProxy) bool UpdateMeshInt(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj);
API_FUNCTION(NoProxy) bool UpdateMeshUInt(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj);
API_FUNCTION(NoProxy) bool UpdateMeshUShort(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj);
API_FUNCTION(NoProxy) bool DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI);
};

View File

@@ -130,7 +130,43 @@ namespace FlaxEngine
if (uv != null && uv.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(uv));
if (Internal_UpdateMeshInt(__unmanagedPtr, vertices, triangles, blendIndices, blendWeights, normals, tangents, uv))
if (Internal_UpdateMeshUInt(__unmanagedPtr, vertices, triangles, blendIndices, blendWeights, normals, tangents, uv))
throw new FlaxException("Failed to update mesh data.");
}
/// <summary>
/// Updates the skinned model mesh vertex and index buffer data.
/// Can be used only for virtual assets (see <see cref="Asset.IsVirtual"/> and <see cref="Content.CreateVirtualAsset{T}"/>).
/// Mesh data will be cached and uploaded to the GPU with a delay.
/// </summary>
/// <param name="vertices">The mesh vertices positions. Cannot be null.</param>
/// <param name="triangles">The mesh index buffer (clockwise triangles). Uses 32-bit stride buffer. Cannot be null.</param>
/// <param name="blendIndices">The skinned mesh blend indices buffer. Contains indices of the skeleton bones (up to 4 bones per vertex) to use for vertex position blending. Cannot be null.</param>
/// <param name="blendWeights">The skinned mesh blend weights buffer (normalized). Contains weights per blend bone (up to 4 bones per vertex) of the skeleton bones to mix for vertex position blending. Cannot be null.</param>
/// <param name="normals">The normal vectors (per vertex).</param>
/// <param name="tangents">The normal vectors (per vertex). Use null to compute them from normal vectors.</param>
/// <param name="uv">The texture coordinates (per vertex).</param>
public void UpdateMesh(Vector3[] vertices, uint[] triangles, Int4[] blendIndices, Vector4[] blendWeights, Vector3[] normals = null, Vector3[] tangents = null, Vector2[] uv = null)
{
// Validate state and input
if (!ParentSkinnedModel.IsVirtual)
throw new InvalidOperationException("Only virtual skinned models can be updated at runtime.");
if (vertices == null)
throw new ArgumentNullException(nameof(vertices));
if (triangles == null)
throw new ArgumentNullException(nameof(triangles));
if (triangles.Length == 0 || triangles.Length % 3 != 0)
throw new ArgumentOutOfRangeException(nameof(triangles));
if (normals != null && normals.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(normals));
if (tangents != null && tangents.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(tangents));
if (tangents != null && normals == null)
throw new ArgumentException("If you specify tangents then you need to also provide normals for the mesh.");
if (uv != null && uv.Length != vertices.Length)
throw new ArgumentOutOfRangeException(nameof(uv));
if (Internal_UpdateMeshUInt(__unmanagedPtr, vertices, triangles, blendIndices, blendWeights, normals, tangents, uv))
throw new FlaxException("Failed to update mesh data.");
}
@@ -227,10 +263,10 @@ namespace FlaxEngine
/// <remarks>If mesh index buffer format (see <see cref="IndexBufferFormat"/>) is <see cref="PixelFormat.R16_UInt"/> then it's faster to call .</remarks>
/// <param name="forceGpu">If set to <c>true</c> the data will be downloaded from the GPU, otherwise it can be loaded from the drive (source asset file) or from memory (if cached). Downloading mesh from GPU requires this call to be made from the other thread than main thread. Virtual assets are always downloaded from GPU memory due to lack of dedicated storage container for the asset data.</param>
/// <returns>The gathered data.</returns>
public int[] DownloadIndexBuffer(bool forceGpu = false)
public uint[] DownloadIndexBuffer(bool forceGpu = false)
{
var triangles = TriangleCount;
var result = new int[triangles * 3];
var result = new uint[triangles * 3];
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.IB32))
throw new FlaxException("Failed to download mesh data.");
return result;

View File

@@ -1424,10 +1424,24 @@ Actor* Actor::Intersects(const Ray& ray, float& distance, Vector3& normal)
}
void Actor::LookAt(const Vector3& worldPos)
{
const Quaternion orientation = LookingAt(worldPos);
SetOrientation(orientation);
}
void Actor::LookAt(const Vector3& worldPos, const Vector3& worldUp)
{
const Quaternion orientation = LookingAt(worldPos, worldUp);
SetOrientation(orientation);
}
Quaternion Actor::LookingAt(const Vector3& worldPos)
{
const Vector3 direction = worldPos - _transform.Translation;
if (direction.LengthSquared() < ZeroTolerance)
return;
return _parent->GetOrientation();
const Vector3 newForward = Vector3::Normalize(direction);
const Vector3 oldForward = _transform.Orientation * Vector3::Forward;
@@ -1447,26 +1461,25 @@ void Actor::LookAt(const Vector3& worldPos)
orientation = rotQuat * _transform.Orientation;
}
SetOrientation(orientation);
return orientation;
}
void Actor::LookAt(const Vector3& worldPos, const Vector3& worldUp)
Quaternion Actor::LookingAt(const Vector3& worldPos, const Vector3& worldUp)
{
const Vector3 direction = worldPos - _transform.Translation;
if (direction.LengthSquared() < ZeroTolerance)
return;
return _parent->GetOrientation();
const Vector3 forward = Vector3::Normalize(direction);
const Vector3 up = Vector3::Normalize(worldUp);
if (Math::IsOne(Vector3::Dot(forward, up)))
{
LookAt(worldPos);
return;
return LookingAt(worldPos);
}
Quaternion orientation;
Quaternion::LookRotation(direction, up, orientation);
SetOrientation(orientation);
return orientation;
}
void WriteObjectToBytes(SceneObject* obj, rapidjson_flax::StringBuffer& buffer, MemoryWriteStream& output)
@@ -1580,6 +1593,7 @@ bool Actor::FromBytes(const Span<byte>& data, Array<Actor*>& output, ISerializeM
modifier->EngineBuild = engineBuild;
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
sceneObjects->Resize(objectsCount);
SceneObjectsFactory::Context context(modifier);
// Deserialize objects
Scripting::ObjectsLookupIdMapping.Set(&modifier->IdsMapping);
@@ -1610,7 +1624,7 @@ bool Actor::FromBytes(const Span<byte>& data, Array<Actor*>& output, ISerializeM
}
// Create object
auto obj = SceneObjectsFactory::Spawn(document, modifier);
auto obj = SceneObjectsFactory::Spawn(context, document);
sceneObjects->At(i) = obj;
if (obj == nullptr)
{
@@ -1655,7 +1669,7 @@ bool Actor::FromBytes(const Span<byte>& data, Array<Actor*>& output, ISerializeM
// Deserialize object
auto obj = sceneObjects->At(i);
if (obj)
SceneObjectsFactory::Deserialize(obj, document, modifier);
SceneObjectsFactory::Deserialize(context, obj, document);
else
SceneObjectsFactory::HandleObjectDeserializationError(document);
}

View File

@@ -792,6 +792,19 @@ public:
/// <param name="worldUp">The up direction that Constrains y axis orientation to a plane this vector lies on. This rule might be broken if forward and up direction are nearly parallel.</param>
API_FUNCTION() void LookAt(const Vector3& worldPos, const Vector3& worldUp);
/// <summary>
/// Gets rotation of the actor oriented towards the specified world position.
/// </summary>
/// <param name="worldPos">The world position to orient towards.</param>
API_FUNCTION() Quaternion LookingAt(const Vector3& worldPos);
/// <summary>
/// Gets rotation of the actor oriented towards the specified world position with upwards direction.
/// </summary>
/// <param name="worldPos">The world position to orient towards.</param>
/// <param name="worldUp">The up direction that Constrains y axis orientation to a plane this vector lies on. This rule might be broken if forward and up direction are nearly parallel.</param>
API_FUNCTION() Quaternion LookingAt(const Vector3& worldPos, const Vector3& worldUp);
public:
/// <summary>

View File

@@ -382,13 +382,11 @@ public:
Guid SceneId;
AssetReference<JsonAsset> SceneAsset;
bool AutoInitialize;
LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset, bool autoInitialize)
LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset)
{
SceneId = sceneId;
SceneAsset = sceneAsset;
AutoInitialize = autoInitialize;
}
bool CanDo() const override
@@ -410,7 +408,7 @@ public:
}
// Load scene
if (Level::loadScene(SceneAsset.Get(), AutoInitialize))
if (Level::loadScene(SceneAsset.Get()))
{
LOG(Error, "Failed to deserialize scene {0}", SceneId);
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId);
@@ -580,7 +578,7 @@ public:
}
// Load scene
if (Level::loadScene(document, false))
if (Level::loadScene(document))
{
LOG(Error, "Failed to deserialize scene {0}", scenes[i].Name);
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, scenes[i].ID);
@@ -589,18 +587,6 @@ public:
}
scenes.Resize(0);
// Initialize scenes (will link references and create managed objects using new assembly)
if (Level::Scenes.HasItems())
{
LOG(Info, "Prepare scene objects");
SceneBeginData beginData;
for (auto scene : Level::Scenes)
{
scene->BeginPlay(&beginData);
}
beginData.OnDone();
}
// Fire event
LOG(Info, "Scripts reloading end. Total time: {0}ms", static_cast<int32>((DateTime::NowUTC() - startTime).GetTotalMilliseconds()));
Level::ScriptsReloadEnd();
@@ -805,13 +791,13 @@ bool LevelImpl::unloadScenes()
return false;
}
bool Level::loadScene(const Guid& sceneId, bool autoInitialize)
bool Level::loadScene(const Guid& sceneId)
{
const auto sceneAsset = Content::LoadAsync<JsonAsset>(sceneId);
return loadScene(sceneAsset, autoInitialize);
return loadScene(sceneAsset);
}
bool Level::loadScene(const String& scenePath, bool autoInitialize)
bool Level::loadScene(const String& scenePath)
{
LOG(Info, "Loading scene from file. Path: \'{0}\'", scenePath);
@@ -830,10 +816,10 @@ bool Level::loadScene(const String& scenePath, bool autoInitialize)
return true;
}
return loadScene(sceneData, autoInitialize);
return loadScene(sceneData);
}
bool Level::loadScene(JsonAsset* sceneAsset, bool autoInitialize)
bool Level::loadScene(JsonAsset* sceneAsset)
{
// Keep reference to the asset (prevent unloading during action)
AssetReference<JsonAsset> ref = sceneAsset;
@@ -845,10 +831,10 @@ bool Level::loadScene(JsonAsset* sceneAsset, bool autoInitialize)
return true;
}
return loadScene(*sceneAsset->Data, sceneAsset->DataEngineBuild, autoInitialize);
return loadScene(*sceneAsset->Data, sceneAsset->DataEngineBuild);
}
bool Level::loadScene(const BytesContainer& sceneData, bool autoInitialize, Scene** outScene)
bool Level::loadScene(const BytesContainer& sceneData, Scene** outScene)
{
if (sceneData.IsInvalid())
{
@@ -868,10 +854,10 @@ bool Level::loadScene(const BytesContainer& sceneData, bool autoInitialize, Scen
return true;
}
return loadScene(document, autoInitialize, outScene);
return loadScene(document, outScene);
}
bool Level::loadScene(rapidjson_flax::Document& document, bool autoInitialize, Scene** outScene)
bool Level::loadScene(rapidjson_flax::Document& document, Scene** outScene)
{
auto data = document.FindMember("Data");
if (data == document.MemberEnd())
@@ -880,10 +866,10 @@ bool Level::loadScene(rapidjson_flax::Document& document, bool autoInitialize, S
return true;
}
const int32 saveEngineBuild = JsonTools::GetInt(document, "EngineBuild", 0);
return loadScene(data->value, saveEngineBuild, autoInitialize, outScene);
return loadScene(data->value, saveEngineBuild, outScene);
}
bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, bool autoInitialize, Scene** outScene)
bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene)
{
PROFILE_CPU_NAMED("Level.LoadScene");
@@ -947,42 +933,35 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, bool autoI
// Fire event
CallSceneEvent(SceneEventType::OnSceneLoading, scene, sceneId);
// Maps the loaded actor object to the json data with the RemovedObjects array (used to skip restoring objects removed per prefab instance)
SceneObjectsFactory::ActorToRemovedObjectsDataLookup actorToRemovedObjectsData;
// Loaded scene objects list
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
sceneObjects->Resize(objectsCount);
sceneObjects->At(0) = scene;
SceneObjectsFactory::Context context(modifier.Value);
{
PROFILE_CPU_NAMED("Spawn");
// Spawn all scene objects
for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
{
auto& objData = data[i];
auto obj = SceneObjectsFactory::Spawn(objData, modifier.Value);
auto& stream = data[i];
auto obj = SceneObjectsFactory::Spawn(context, stream);
sceneObjects->At(i) = obj;
if (obj)
{
// Register object so it can be later referenced during deserialization (FindObject will work to link references between objects)
obj->RegisterObject();
// Special case for actors
if (auto actor = dynamic_cast<Actor*>(obj))
{
// Check for RemovedObjects listing
const auto removedObjects = SERIALIZE_FIND_MEMBER(objData, "RemovedObjects");
if (removedObjects != objData.MemberEnd())
{
actorToRemovedObjectsData.Add(actor, &removedObjects->value);
}
}
}
else
SceneObjectsFactory::HandleObjectDeserializationError(stream);
}
}
SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, data, modifier.Value);
SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData);
// TODO: resave and force sync scenes during game cooking so this step could be skipped in game
SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData);
// /\ all above this has to be done on an any thread
// \/ all below this has to be done on multiple threads at once
@@ -998,25 +977,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, bool autoI
auto& objData = data[i];
auto obj = sceneObjects->At(i);
if (obj)
{
SceneObjectsFactory::Deserialize(obj, objData, modifier.Value);
}
else
{
SceneObjectsFactory::HandleObjectDeserializationError(objData);
// Try to log some useful info about missing object (eg. it's parent name for faster fixing)
const auto parentIdMember = objData.FindMember("ParentID");
if (parentIdMember != objData.MemberEnd() && parentIdMember->value.IsString())
{
Guid parentId = JsonTools::GetGuid(parentIdMember->value);
Actor* parent = Scripting::FindObject<Actor>(parentId);
if (parent)
{
LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName());
}
}
}
SceneObjectsFactory::Deserialize(context, obj, objData);
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
}
@@ -1024,11 +985,15 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, bool autoI
// /\ all above this has to be done on multiple threads at once
// \/ all below this has to be done on an any thread
// Synchronize prefab instances (prefab may have objects removed or reordered so deserialized instances need to synchronize with it)
// TODO: resave and force sync scenes during game cooking so this step could be skipped in game
SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData);
// Call post load event to connect all scene actors
{
PROFILE_CPU_NAMED("Post Load");
for (int32 i = 0; i < objectsCount; i++)
for (int32 i = 0; i < sceneObjects->Count(); i++)
{
SceneObject* obj = sceneObjects->At(i);
if (obj)
@@ -1036,17 +1001,12 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, bool autoI
}
}
// Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it)
// TODO: resave and force sync scenes during game cooking so this step could be skipped in game
SceneObjectsFactory::SynchronizePrefabInstances(*sceneObjects.Value, actorToRemovedObjectsData, modifier.Value);
// Delete objects without parent
for (int32 i = 1; i < objectsCount; i++)
{
SceneObject* obj = sceneObjects->At(i);
if (obj && obj->GetParent() == nullptr)
{
sceneObjects->At(i) = nullptr;
LOG(Warning, "Scene object {0} {1} has missing parent object after load. Removing it.", obj->GetID(), obj->ToString());
obj->DeleteObject();
}
@@ -1066,20 +1026,11 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, bool autoI
{
PROFILE_CPU_NAMED("BeginPlay");
ScopeLock lock(ScenesLock);
Scenes.Add(scene);
if (autoInitialize)
{
SceneBeginData beginData;
scene->BeginPlay(&beginData);
beginData.OnDone();
}
else
{
// Send warning to log just in case (easier to track if scene will be loaded without init)
// Why? Because we can load collection of scenes and then call for all of them init so references between objects in a different scenes will be resolved without leaks.
//LOG(Warning, "Scene \'{0}:{1}\', has been loaded but not initialized. Remember to call OnBeginPlay().", scene->GetName(), scene->GetID());
}
SceneBeginData beginData;
scene->BeginPlay(&beginData);
beginData.OnDone();
}
// Fire event
@@ -1261,7 +1212,7 @@ void Level::SaveAllScenesAsync()
_sceneActions.Enqueue(New<SaveSceneAction>(Scenes[i]));
}
bool Level::LoadScene(const Guid& id, bool autoInitialize)
bool Level::LoadScene(const Guid& id)
{
// Check ID
if (!id.IsValid())
@@ -1297,7 +1248,7 @@ bool Level::LoadScene(const Guid& id, bool autoInitialize)
}
// Load scene
if (loadScene(sceneAsset, autoInitialize))
if (loadScene(sceneAsset))
{
LOG(Error, "Failed to deserialize scene {0}", id);
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, id);
@@ -1306,10 +1257,10 @@ bool Level::LoadScene(const Guid& id, bool autoInitialize)
return false;
}
Scene* Level::LoadSceneFromBytes(const BytesContainer& data, bool autoInitialize)
Scene* Level::LoadSceneFromBytes(const BytesContainer& data)
{
Scene* scene = nullptr;
if (loadScene(data, autoInitialize, &scene))
if (loadScene(data, &scene))
{
LOG(Error, "Failed to deserialize scene from bytes");
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, Guid::Empty);
@@ -1335,7 +1286,7 @@ bool Level::LoadSceneAsync(const Guid& id)
}
ScopeLock lock(_sceneActionsLocker);
_sceneActions.Enqueue(New<LoadSceneAction>(id, sceneAsset, true));
_sceneActions.Enqueue(New<LoadSceneAction>(id, sceneAsset));
return false;
}

View File

@@ -278,17 +278,15 @@ public:
/// Loads scene from the asset.
/// </summary>
/// <param name="id">Scene ID</param>
/// <param name="autoInitialize">Enable/disable auto scene initialization, otherwise user should do it (in that situation scene is registered but not in a gameplay, call OnBeginPlay to start logic for it; it will deserialize scripts and references to the other objects).</param>
/// <returns>True if loading cannot be done, otherwise false.</returns>
API_FUNCTION() static bool LoadScene(const Guid& id, bool autoInitialize = true);
API_FUNCTION() static bool LoadScene(const Guid& id);
/// <summary>
/// Loads scene from the bytes.
/// </summary>
/// <param name="data">The scene data to load.</param>
/// <param name="autoInitialize">Enable/disable auto scene initialization, otherwise user should do it (in that situation scene is registered but not in a gameplay, call OnBeginPlay to start logic for it; it will deserialize scripts and references to the other objects).</param>
/// <returns>Loaded scene object, otherwise null if cannot load data (then see log for more information).</returns>
API_FUNCTION() static Scene* LoadSceneFromBytes(const BytesContainer& data, bool autoInitialize = true);
API_FUNCTION() static Scene* LoadSceneFromBytes(const BytesContainer& data);
/// <summary>
/// Loads scene from the asset. Done in the background.
@@ -479,10 +477,10 @@ private:
};
static void callActorEvent(ActorEventType eventType, Actor* a, Actor* b);
static bool loadScene(const Guid& sceneId, bool autoInitialize);
static bool loadScene(const String& scenePath, bool autoInitialize);
static bool loadScene(JsonAsset* sceneAsset, bool autoInitialize);
static bool loadScene(const BytesContainer& sceneData, bool autoInitialize, Scene** outScene = nullptr);
static bool loadScene(rapidjson_flax::Document& document, bool autoInitialize, Scene** outScene = nullptr);
static bool loadScene(rapidjson_flax::Value& data, int32 engineBuild, bool autoInitialize, Scene** outScene = nullptr);
static bool loadScene(const Guid& sceneId);
static bool loadScene(const String& scenePath);
static bool loadScene(JsonAsset* sceneAsset);
static bool loadScene(const BytesContainer& sceneData, Scene** outScene = nullptr);
static bool loadScene(rapidjson_flax::Document& document, Scene** outScene = nullptr);
static bool loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene = nullptr);
};

View File

@@ -128,7 +128,7 @@ public:
rapidjson_flax::Document Data;
/// <summary>
/// The mapping from prefab instance object id to serialized objects array index (in Data)
/// The mapping from prefab instance object id to serialized objects array index (in Data).
/// </summary>
Dictionary<Guid, int32> PrefabInstanceIdToDataIndex;
@@ -322,6 +322,26 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array<PrefabInstanceData>& p
modifier->IdsMapping.Add(newPrefabObjectIds[i], Guid::New());
}
// Create new objects added to prefab
int32 deserializeSceneObjectIndex = sceneObjects->Count();
SceneObjectsFactory::Context context(modifier.Value);
for (int32 i = 0; i < newPrefabObjectIds.Count(); i++)
{
const Guid prefabObjectId = newPrefabObjectIds[i];
const ISerializable::DeserializeStream* data;
if (!prefabObjectIdToDiffData.TryGet(prefabObjectId, data))
{
LOG(Warning, "Missing object linkage to the prefab object diff data.");
continue;
}
SceneObject* obj = SceneObjectsFactory::Spawn(context, *(ISerializable::DeserializeStream*)data);
if (!obj)
continue;
obj->RegisterObject();
sceneObjects->Add(obj);
}
// Apply modifications
for (int32 i = existingObjectsCount - 1; i >= 0; i--)
{
@@ -349,25 +369,6 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array<PrefabInstanceData>& p
}
}
// Create new objects added to prefab
int32 deserializeSceneObjectIndex = sceneObjects->Count();
for (int32 i = 0; i < newPrefabObjectIds.Count(); i++)
{
const Guid prefabObjectId = newPrefabObjectIds[i];
const ISerializable::DeserializeStream* data;
if (!prefabObjectIdToDiffData.TryGet(prefabObjectId, data))
{
LOG(Warning, "Missing object linkage to the prefab object diff data.");
continue;
}
SceneObject* obj = SceneObjectsFactory::Spawn(*(ISerializable::DeserializeStream*)data, modifier.Value);
if (!obj)
continue;
obj->RegisterObject();
sceneObjects->Add(obj);
}
// Deserialize new objects added to prefab
for (int32 i = 0; i < newPrefabObjectIds.Count(); i++)
{
@@ -377,7 +378,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array<PrefabInstanceData>& p
continue;
SceneObject* obj = sceneObjects->At(deserializeSceneObjectIndex);
SceneObjectsFactory::Deserialize(obj, *(ISerializable::DeserializeStream*)data, modifier.Value);
SceneObjectsFactory::Deserialize(context, obj, *(ISerializable::DeserializeStream*)data);
// Link new prefab instance to prefab and prefab object
obj->LinkPrefab(prefabId, prefabObjectId);
@@ -492,6 +493,9 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array<PrefabInstanceData>& p
bool PrefabInstanceData::SynchronizePrefabInstances(Array<PrefabInstanceData>& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array<Guid>& oldObjectsIds, const Array<Guid>& newObjectIds)
{
if (prefabInstancesData.IsEmpty())
return false;
// Fully serialize default instance scene objects (accumulate all prefab and nested prefabs changes into a single linear list of objects)
rapidjson_flax::Document defaultInstanceData;
{
@@ -849,12 +853,13 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
}
}
// Deserialize prefab objects (apply modifications)
// Create prefab objects
auto& data = *Data;
sceneObjects->Resize(ObjectsCount);
sceneObjects->Resize(ObjectsCount + newPrefabInstanceIdToDataIndex.Count());
SceneObjectsFactory::Context context(modifier.Value);
for (int32 i = 0; i < ObjectsCount; i++)
{
SceneObject* obj = SceneObjectsFactory::Spawn(data[i], modifier.Value);
SceneObject* obj = SceneObjectsFactory::Spawn(context, data[i]);
sceneObjects->At(i) = obj;
if (!obj)
{
@@ -864,12 +869,31 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
}
obj->RegisterObject();
}
// Create new prefab objects
int32 newPrefabInstanceIdToDataIndexCounter = 0;
int32 newPrefabInstanceIdToDataIndexStart = ObjectsCount;
for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i)
{
const int32 dataIndex = i->Value;
SceneObject* obj = SceneObjectsFactory::Spawn(context, diffDataDocument[dataIndex]);
sceneObjects->At(newPrefabInstanceIdToDataIndexStart + newPrefabInstanceIdToDataIndexCounter++) = obj;
if (!obj)
{
// This should not happen but who knows
SceneObjectsFactory::HandleObjectDeserializationError(diffDataDocument[dataIndex]);
continue;
}
obj->RegisterObject();
}
// Deserialize prefab objects and apply modifications
for (int32 i = 0; i < ObjectsCount; i++)
{
SceneObject* obj = sceneObjects->At(i);
if (!obj)
continue;
SceneObjectsFactory::Deserialize(obj, data[i], modifier.Value);
SceneObjectsFactory::Deserialize(context, obj, data[i]);
int32 dataIndex;
if (diffPrefabObjectIdToDataIndex.TryGet(obj->GetSceneObjectId(), dataIndex))
@@ -897,31 +921,11 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
obj->DeleteObject();
obj->SetParent(nullptr);
sceneObjects->At(i) = nullptr;
}
}
for (int32 i = 0; i < sceneObjects->Count(); i++)
{
if (sceneObjects->At(i) == nullptr)
sceneObjects->RemoveAtKeepOrder(i);
}
// Deserialize new prefab objects
int32 newPrefabInstanceIdToDataIndexCounter = 0;
int32 newPrefabInstanceIdToDataIndexStart = sceneObjects->Count();
sceneObjects->Resize(sceneObjects->Count() + newPrefabInstanceIdToDataIndex.Count());
for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i)
{
const int32 dataIndex = i->Value;
SceneObject* obj = SceneObjectsFactory::Spawn(diffDataDocument[dataIndex], modifier.Value);
sceneObjects->At(newPrefabInstanceIdToDataIndexStart + newPrefabInstanceIdToDataIndexCounter++) = obj;
if (!obj)
{
// This should not happen but who knows
SceneObjectsFactory::HandleObjectDeserializationError(diffDataDocument[dataIndex]);
continue;
}
obj->RegisterObject();
}
newPrefabInstanceIdToDataIndexCounter = 0;
for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i)
{
@@ -929,7 +933,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
SceneObject* obj = sceneObjects->At(newPrefabInstanceIdToDataIndexStart + newPrefabInstanceIdToDataIndexCounter++);
if (!obj)
continue;
SceneObjectsFactory::Deserialize(obj, diffDataDocument[dataIndex], modifier.Value);
SceneObjectsFactory::Deserialize(context, obj, diffDataDocument[dataIndex]);
}
for (int32 j = 0; j < targetObjects->Count(); j++)
{
@@ -950,6 +954,12 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
}
}
}
for (int32 i = 0; i < sceneObjects->Count(); i++)
{
if (sceneObjects->At(i) == nullptr)
sceneObjects->RemoveAtKeepOrder(i);
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
if (sceneObjects.Value->IsEmpty())
{

View File

@@ -123,8 +123,6 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid
// Prepare
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
sceneObjects->Resize(objectsCount);
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache prefabDataIndexToSceneObject = ActorsCache::SceneObjectsListCache.Get();
prefabDataIndexToSceneObject->Resize(objectsCount);
CollectionPoolCache<ISerializeModifier, Cache::ISerializeModifierClearCallback>::ScopeCache modifier = Cache::ISerializeModifier.Get();
modifier->IdsMapping.EnsureCapacity(prefab->ObjectsIds.Count() * 4);
for (int32 i = 0; i < prefab->ObjectsIds.Count(); i++)
@@ -136,34 +134,35 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid
objectsCache->Clear();
objectsCache->SetCapacity(prefab->ObjectsDataCache.Capacity());
}
auto& data = *prefab->Data;
SceneObjectsFactory::Context context(modifier.Value);
// Deserialize prefab objects
auto& data = *prefab->Data;
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
for (int32 i = 0; i < objectsCount; i++)
{
auto& stream = data[i];
SceneObject* obj = SceneObjectsFactory::Spawn(stream, modifier.Value);
prefabDataIndexToSceneObject->operator[](i) = obj;
auto obj = SceneObjectsFactory::Spawn(context, stream);
sceneObjects->At(i) = obj;
if (obj)
{
obj->RegisterObject();
}
else
{
SceneObjectsFactory::HandleObjectDeserializationError(stream);
}
}
SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, data, modifier.Value);
if (withSynchronization)
{
// Synchronize new prefab instances (prefab may have new objects added so deserialized instances need to synchronize with it)
// TODO: resave and force sync prefabs during game cooking so this step could be skipped in game
SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData);
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
}
for (int32 i = 0; i < objectsCount; i++)
{
auto& stream = data[i];
SceneObject* obj = prefabDataIndexToSceneObject->At(i);
SceneObject* obj = sceneObjects->At(i);
if (obj)
{
SceneObjectsFactory::Deserialize(obj, stream, modifier.Value);
}
SceneObjectsFactory::Deserialize(context, obj, stream);
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
@@ -198,26 +197,8 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid
// Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it)
if (withSynchronization)
{
// Maps the loaded actor object to the json data with the RemovedObjects array (used to skip restoring objects removed per prefab instance)
SceneObjectsFactory::ActorToRemovedObjectsDataLookup actorToRemovedObjectsData;
for (int32 i = 0; i < objectsCount; i++)
{
auto& stream = data[i];
Actor* actor = dynamic_cast<Actor*>(prefabDataIndexToSceneObject->At(i));
if (!actor)
continue;
// Check for RemovedObjects listing
const auto removedObjects = stream.FindMember("RemovedObjects");
if (removedObjects != stream.MemberEnd())
{
actorToRemovedObjectsData.Add(actor, &removedObjects->value);
}
}
// TODO: consider caching actorToRemovedObjectsData per prefab
SceneObjectsFactory::SynchronizePrefabInstances(*sceneObjects.Value, actorToRemovedObjectsData, modifier.Value);
// TODO: resave and force sync scenes during game cooking so this step could be skipped in game
SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData);
}
// Delete objects without parent or with invalid linkage to the prefab
@@ -274,7 +255,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid
for (int32 i = 0; i < objectsCount; i++)
{
auto& stream = data[i];
SceneObject* obj = prefabDataIndexToSceneObject->At(i);
SceneObject* obj = sceneObjects->At(i);
if (!obj)
continue;

View File

@@ -8,15 +8,21 @@
#include "Engine/Scripting/Scripting.h"
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Serialization/ISerializeModifier.h"
#include "Engine/Serialization/SerializationFwd.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Threading/ThreadLocal.h"
SceneObject* SceneObjectsFactory::Spawn(ISerializable::DeserializeStream& stream, ISerializeModifier* modifier)
SceneObjectsFactory::Context::Context(ISerializeModifier* modifier)
: Modifier(modifier)
{
}
SceneObject* SceneObjectsFactory::Spawn(Context& context, ISerializable::DeserializeStream& stream)
{
// Get object id
Guid id = JsonTools::GetGuid(stream, "ID");
modifier->IdsMapping.TryGet(id, id);
context.Modifier->IdsMapping.TryGet(id, id);
if (!id.IsValid())
{
LOG(Warning, "Invalid object id.");
@@ -59,10 +65,10 @@ SceneObject* SceneObjectsFactory::Spawn(ISerializable::DeserializeStream& stream
}
// Map prefab object ID to the deserialized instance ID
modifier->IdsMapping[prefabObjectId] = id;
context.Modifier->IdsMapping[prefabObjectId] = id;
// Create prefab instance (recursive prefab loading to support nested prefabs)
obj = Spawn(*(ISerializable::DeserializeStream*)prefabData, modifier);
obj = Spawn(context, *(ISerializable::DeserializeStream*)prefabData);
}
else
{
@@ -139,7 +145,7 @@ SceneObject* SceneObjectsFactory::Spawn(ISerializable::DeserializeStream& stream
return obj;
}
void SceneObjectsFactory::Deserialize(SceneObject* obj, ISerializable::DeserializeStream& stream, ISerializeModifier* modifier)
void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISerializable::DeserializeStream& stream)
{
// Check for prefab instance
Guid prefabObjectId;
@@ -175,180 +181,42 @@ void SceneObjectsFactory::Deserialize(SceneObject* obj, ISerializable::Deseriali
}
// Deserialize prefab data (recursive prefab loading to support nested prefabs)
Deserialize(obj, *(ISerializable::DeserializeStream*)prefabData, modifier);
Deserialize(context, obj, *(ISerializable::DeserializeStream*)prefabData);
}
int32 instanceIndex;
if (context.ObjectToInstance.TryGet(obj->GetID(), instanceIndex) && instanceIndex != context.CurrentInstance)
{
// Apply the current prefab instance objects ids table to resolve references inside a prefab properly
context.CurrentInstance = instanceIndex;
auto& instance = context.Instances[instanceIndex];
for (auto& e : instance.IdsMapping)
context.Modifier->IdsMapping[e.Key] = e.Value;
}
// Load data
obj->Deserialize(stream, modifier);
}
bool Contains(Actor* actor, const SceneObjectsFactory::ActorToRemovedObjectsDataLookup& actorToRemovedObjectsData, const Guid& prefabObjectId)
{
// Check if actor has any removed objects registered
const rapidjson_flax::Value* data;
if (actorToRemovedObjectsData.TryGet(actor, data))
{
const int32 size = static_cast<int32>(data->Size());
for (int32 i = 0; i < size; i++)
{
if (JsonTools::GetGuid(data->operator[](i)) == prefabObjectId)
{
return true;
}
}
}
return false;
}
void SceneObjectsFactory::SynchronizePrefabInstances(Array<SceneObject*>& sceneObjects, const ActorToRemovedObjectsDataLookup& actorToRemovedObjectsData, ISerializeModifier* modifier)
{
PROFILE_CPU_NAMED("SynchronizePrefabInstances");
Scripting::ObjectsLookupIdMapping.Set(&modifier->IdsMapping);
// Check all objects with prefab linkage for moving to a proper parent
const int32 objectsToCheckCount = sceneObjects.Count();
for (int32 i = 0; i < objectsToCheckCount; i++)
{
SceneObject* obj = sceneObjects[i];
if (!obj)
continue;
SceneObject* parent = obj->GetParent();
const Guid prefabId = obj->GetPrefabID();
if (parent == nullptr || !obj->HasPrefabLink() || !parent->HasPrefabLink() || parent->GetPrefabID() != prefabId)
continue;
const Guid prefabObjectId = obj->GetPrefabObjectID();
const Guid parentPrefabObjectId = parent->GetPrefabObjectID();
// Load prefab
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
continue;
}
if (prefab->WaitForLoaded())
{
LOG(Warning, "Failed to load prefab {0}.", prefab->ToString());
continue;
}
// Get the actual parent object stored in the prefab data
const ISerializable::DeserializeStream* objData;
Guid actualParentPrefabId;
if (!prefab->ObjectsDataCache.TryGet(prefabObjectId, objData) || !JsonTools::GetGuidIfValid(actualParentPrefabId, *objData, "ParentID"))
continue;
// Validate
if (actualParentPrefabId != parentPrefabObjectId)
{
// Invalid connection object found!
LOG(Info, "Object {0} has invalid parent object {4} -> {5} (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", obj->GetSceneObjectId(), prefabObjectId, prefab->GetID(), prefab->GetPath(), parentPrefabObjectId, actualParentPrefabId);
// Map actual prefab object id to the current scene objects collection
modifier->IdsMapping.TryGet(actualParentPrefabId, actualParentPrefabId);
// Find parent
const auto actualParent = Scripting::FindObject<Actor>(actualParentPrefabId);
if (!actualParent)
{
LOG(Warning, "The actual parent is missing.");
continue;
}
// Reparent
obj->SetParent(actualParent, false);
}
// Preserve order in parent (values from prefab are used)
if (i != 0)
{
const auto defaultInstance = prefab->GetDefaultInstance(obj->GetPrefabObjectID());
if (defaultInstance)
{
obj->SetOrderInParent(defaultInstance->GetOrderInParent());
}
}
}
// Check all actors with prefab linkage for adding missing objects
for (int32 i = 0; i < objectsToCheckCount; i++)
{
Actor* actor = dynamic_cast<Actor*>(sceneObjects[i]);
if (!actor || !actor->HasPrefabLink())
continue;
const Guid actorId = actor->GetID();
const Guid prefabId = actor->GetPrefabID();
const Guid actorPrefabObjectId = actor->GetPrefabObjectID();
// Map prefab object id to this actor so the new objects gets added to it
modifier->IdsMapping[actorPrefabObjectId] = actorId;
// Load prefab
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
continue;
}
if (prefab->WaitForLoaded())
{
LOG(Warning, "Failed to load prefab {0}.", prefab->ToString());
continue;
}
// Check if the given actor has new children or scripts added (inside the prefab that it uses)
// TODO: consider caching prefab objects structure maybe to boost this logic?
for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it)
{
// Use only objects that are linked to the current actor
const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID");
if (parentId != actorPrefabObjectId)
continue;
// Use only objects that are missing
const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID");
if (actor->GetChildByPrefabObjectId(prefabObjectId) != nullptr ||
actor->GetScriptByPrefabObjectId(prefabObjectId) != nullptr)
continue;
// Skip if object was marked to be removed per instance
if (Contains(actor, actorToRemovedObjectsData, prefabObjectId))
continue;
// Create instance (including all children)
Scripting::ObjectsLookupIdMapping.Set(&modifier->IdsMapping);
SynchronizeNewPrefabInstance(prefab, actor, prefabObjectId, sceneObjects, modifier);
}
}
// Call post load event to the new objects
for (int32 i = objectsToCheckCount; i < sceneObjects.Count(); i++)
{
SceneObject* obj = sceneObjects[i];
// Preserve order in parent (values from prefab are used)
auto prefab = Content::LoadAsync<Prefab>(obj->GetPrefabID());
const auto defaultInstance = prefab && prefab->IsLoaded() ? prefab->GetDefaultInstance(obj->GetPrefabObjectID()) : nullptr;
if (defaultInstance)
{
obj->SetOrderInParent(defaultInstance->GetOrderInParent());
}
obj->PostLoad();
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
obj->Deserialize(stream, context.Modifier);
}
void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable::DeserializeStream& value)
{
// Print invalid object data contents
rapidjson_flax::StringBuffer buffer;
PrettyJsonWriter writer(buffer);
value.Accept(writer.GetWriter());
LOG(Warning, "Failed to deserialize scene object from data: {0}", String(buffer.GetString()));
// Try to log some useful info about missing object (eg. it's parent name for faster fixing)
const auto parentIdMember = value.FindMember("ParentID");
if (parentIdMember != value.MemberEnd() && parentIdMember->value.IsString())
{
const Guid parentId = JsonTools::GetGuid(parentIdMember->value);
Actor* parent = Scripting::FindObject<Actor>(parentId);
if (parent)
{
LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName());
}
}
}
Actor* SceneObjectsFactory::CreateActor(int32 typeId, const Guid& id)
@@ -422,7 +290,248 @@ Actor* SceneObjectsFactory::CreateActor(int32 typeId, const Guid& id)
return nullptr;
}
void SceneObjectsFactory::SynchronizeNewPrefabInstance(Prefab* prefab, Actor* actor, const Guid& prefabObjectId, Array<SceneObject*>& sceneObjects, ISerializeModifier* modifier)
SceneObjectsFactory::PrefabSyncData::PrefabSyncData(Array<SceneObject*>& sceneObjects, const ISerializable::DeserializeStream& data, ISerializeModifier* modifier)
: SceneObjects(sceneObjects)
, Data(data)
, Modifier(modifier)
, InitialCount(0)
{
}
void SceneObjectsFactory::SetupPrefabInstances(Context& context, PrefabSyncData& data)
{
PROFILE_CPU_NAMED("SetupPrefabInstances");
const int32 count = data.Data.Size();
ASSERT(count <= data.SceneObjects.Count())
for (int32 i = 0; i < count; i++)
{
SceneObject* obj = data.SceneObjects[i];
if (!obj)
continue;
const auto& stream = data.Data[i];
Guid prefabObjectId, prefabId;
if (!JsonTools::GetGuidIfValid(prefabObjectId, stream, "PrefabObjectID"))
continue;
if (!JsonTools::GetGuidIfValid(prefabId, stream, "PrefabID"))
continue;
const Guid parentId = JsonTools::GetGuid(stream, "ParentID");
const Guid id = obj->GetID();
auto prefab = Content::LoadAsync<Prefab>(prefabId);
// Check if it's parent is in the same prefab
int32 index;
if (context.ObjectToInstance.TryGet(parentId, index) && context.Instances[index].Prefab == prefab)
{
// Use parent object as prefab instance
}
else
{
// Use new prefab instance
index = context.Instances.Count();
auto& e = context.Instances.AddOne();
e.Prefab = prefab;
e.RootId = id;
}
context.ObjectToInstance[id] = index;
// Add to the prefab instance IDs mapping
auto& prefabInstance = context.Instances[index];
prefabInstance.IdsMapping[prefabObjectId] = id;
}
}
void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data)
{
PROFILE_CPU_NAMED("SynchronizeNewPrefabInstances");
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
data.InitialCount = data.SceneObjects.Count();
// Check all actors with prefab linkage for adding missing objects
for (int32 i = 0; i < data.InitialCount; i++)
{
Actor* actor = dynamic_cast<Actor*>(data.SceneObjects[i]);
if (!actor)
continue;
const auto& stream = data.Data[i];
Guid actorId, actorPrefabObjectId, prefabId;
if (!JsonTools::GetGuidIfValid(actorPrefabObjectId, stream, "PrefabObjectID"))
continue;
if (!JsonTools::GetGuidIfValid(prefabId, stream, "PrefabID"))
continue;
if (!JsonTools::GetGuidIfValid(actorId, stream, "ID"))
continue;
const Guid actorParentId = JsonTools::GetGuid(stream, "ParentID");
// Map prefab object id to this actor so the new objects gets added to it
data.Modifier->IdsMapping[actorPrefabObjectId] = actor->GetID();
// Load prefab
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
continue;
}
if (prefab->WaitForLoaded())
{
LOG(Warning, "Failed to load prefab {0}.", prefab->ToString());
continue;
}
// Check for RemovedObjects list
const auto removedObjects = SERIALIZE_FIND_MEMBER(stream, "RemovedObjects");
// Check if the given actor has new children or scripts added (inside the prefab that it uses)
// TODO: consider caching prefab objects structure maybe to boost this logic?
for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it)
{
// Use only objects that are linked to the current actor
const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID");
if (parentId != actorPrefabObjectId)
continue;
// Skip if object was marked to be removed per instance
const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID");
if (removedObjects != stream.MemberEnd())
{
auto& list = removedObjects->value;
const int32 size = static_cast<int32>(list.Size());
bool removed = false;
for (int32 j = 0; j < size; j++)
{
if (JsonTools::GetGuid(list[j]) == prefabObjectId)
{
removed = true;
break;
}
}
if (removed)
continue;
}
// Use only objects that are missing
bool spawned = false;
for (int32 j = i + 1; j < data.InitialCount; j++)
{
const auto& jData = data.Data[j];
const Guid jParentId = JsonTools::GetGuid(jData, "ParentID");
//if (jParentId == actorParentId)
// break;
if (jParentId != actorId)
continue;
const Guid jPrefabObjectId = JsonTools::GetGuid(jData, "PrefabObjectID");
if (jPrefabObjectId != prefabObjectId)
continue;
// This object exists in the saved scene objects list
spawned = true;
break;
}
if (spawned)
continue;
// Create instance (including all children)
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId);
}
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
}
void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyncData& data)
{
PROFILE_CPU_NAMED("SynchronizePrefabInstances");
// Check all objects with prefab linkage for moving to a proper parent
for (int32 i = 0; i < data.InitialCount; i++)
{
SceneObject* obj = data.SceneObjects[i];
if (!obj)
continue;
SceneObject* parent = obj->GetParent();
const Guid prefabId = obj->GetPrefabID();
if (parent == nullptr || !obj->HasPrefabLink() || !parent->HasPrefabLink() || parent->GetPrefabID() != prefabId)
continue;
const Guid prefabObjectId = obj->GetPrefabObjectID();
const Guid parentPrefabObjectId = parent->GetPrefabObjectID();
// Load prefab
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
continue;
}
if (prefab->WaitForLoaded())
{
LOG(Warning, "Failed to load prefab {0}.", prefab->ToString());
continue;
}
// Get the actual parent object stored in the prefab data
const ISerializable::DeserializeStream* objData;
Guid actualParentPrefabId;
if (!prefab->ObjectsDataCache.TryGet(prefabObjectId, objData) || !JsonTools::GetGuidIfValid(actualParentPrefabId, *objData, "ParentID"))
continue;
// Validate
if (actualParentPrefabId != parentPrefabObjectId)
{
// Invalid connection object found!
LOG(Info, "Object {0} has invalid parent object {4} -> {5} (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", obj->GetSceneObjectId(), prefabObjectId, prefab->GetID(), prefab->GetPath(), parentPrefabObjectId, actualParentPrefabId);
// Map actual prefab object id to the current scene objects collection
data.Modifier->IdsMapping.TryGet(actualParentPrefabId, actualParentPrefabId);
// Find parent
const auto actualParent = Scripting::FindObject<Actor>(actualParentPrefabId);
if (!actualParent)
{
LOG(Warning, "The actual parent is missing.");
continue;
}
// Reparent
obj->SetParent(actualParent, false);
}
// Preserve order in parent (values from prefab are used)
if (i != 0)
{
const auto defaultInstance = prefab->GetDefaultInstance(obj->GetPrefabObjectID());
if (defaultInstance)
{
obj->SetOrderInParent(defaultInstance->GetOrderInParent());
}
}
}
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
// Synchronize new prefab objects
for (int32 i = 0; i < data.NewObjects.Count(); i++)
{
SceneObject* obj = data.SceneObjects[data.InitialCount + i];
auto& newObj = data.NewObjects[i];
// Deserialize object with prefab data
Deserialize(context, obj, *(ISerializable::DeserializeStream*)newObj.PrefabData);
obj->LinkPrefab(newObj.Prefab->GetID(), newObj.PrefabObjectId);
// Preserve order in parent (values from prefab are used)
const auto defaultInstance = newObj.Prefab->GetDefaultInstance(newObj.PrefabObjectId);
if (defaultInstance)
{
obj->SetOrderInParent(defaultInstance->GetOrderInParent());
}
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
}
void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId)
{
PROFILE_CPU_NAMED("SynchronizeNewPrefabInstance");
@@ -438,22 +547,33 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstance(Prefab* prefab, Actor* ac
}
// Map prefab object ID to the new prefab object instance
modifier->IdsMapping[prefabObjectId] = Guid::New();
Scripting::ObjectsLookupIdMapping.Set(&modifier->IdsMapping);
Guid id = Guid::New();
data.Modifier->IdsMapping[prefabObjectId] = id;
// Create prefab instance (recursive prefab loading to support nested prefabs)
auto child = Spawn(*(ISerializable::DeserializeStream*)prefabData, modifier);
auto child = Spawn(context, *(ISerializable::DeserializeStream*)prefabData);
if (!child)
{
LOG(Warning, "Failed to create object {1} from prefab {0}.", prefab->ToString(), prefabObjectId);
return;
}
child->RegisterObject();
Deserialize(child, *(ISerializable::DeserializeStream*)prefabData, modifier);
// Register object
child->LinkPrefab(prefab->GetID(), prefabObjectId);
sceneObjects.Add(child);
child->RegisterObject();
data.SceneObjects.Add(child);
auto& newObj = data.NewObjects.AddOne();
newObj.Prefab = prefab;
newObj.PrefabData = prefabData;
newObj.PrefabObjectId = prefabObjectId;
newObj.Id = id;
int32 instanceIndex = -1;
if (context.ObjectToInstance.TryGet(actor->GetID(), instanceIndex) && context.Instances[instanceIndex].Prefab == prefab)
{
// Add to the prefab instance IDs mapping
context.ObjectToInstance[id] = instanceIndex;
auto& prefabInstance = context.Instances[instanceIndex];
prefabInstance.IdsMapping[prefabObjectId] = id;
}
// Use loop to add even more objects to added objects (prefab can have one new object that has another child, we need to add that child)
// TODO: prefab could cache lookup object id -> children ids
@@ -463,7 +583,7 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstance(Prefab* prefab, Actor* ac
if (JsonTools::GetGuidIfValid(qParentId, *q->Value, "ParentID") && qParentId == prefabObjectId)
{
const Guid qPrefabObjectId = JsonTools::GetGuid(*q->Value, "ID");
SynchronizeNewPrefabInstance(prefab, actor, qPrefabObjectId, sceneObjects, modifier);
SynchronizeNewPrefabInstance(context, data, prefab, actor, qPrefabObjectId);
}
}
}

View File

@@ -3,43 +3,46 @@
#pragma once
#include "SceneObject.h"
#include "Engine/Core/Collections/Dictionary.h"
/// <summary>
/// Helper class for scene objects creation and deserialization utilities.
/// </summary>
class SceneObjectsFactory
class FLAXENGINE_API SceneObjectsFactory
{
public:
typedef Dictionary<Actor*, const rapidjson_flax::Value*, HeapAllocation> ActorToRemovedObjectsDataLookup;
struct PrefabInstance
{
Guid RootId;
Prefab* Prefab;
Dictionary<Guid, Guid> IdsMapping;
};
public:
struct Context
{
ISerializeModifier* Modifier;
int32 CurrentInstance = -1;
Array<PrefabInstance> Instances;
Dictionary<Guid, int32> ObjectToInstance;
Context(ISerializeModifier* modifier);
};
/// <summary>
/// Creates the scene object from the specified data value. Does not perform deserialization.
/// </summary>
/// <param name="context">The serialization context.</param>
/// <param name="stream">The serialized data stream.</param>
/// <param name="modifier">The serialization modifier. Cannot be null.</param>
static SceneObject* Spawn(ISerializable::DeserializeStream& stream, ISerializeModifier* modifier);
static SceneObject* Spawn(Context& context, ISerializable::DeserializeStream& stream);
/// <summary>
/// Deserializes the scene object from the specified data value.
/// </summary>
/// <param name="context">The serialization context.</param>
/// <param name="obj">The instance to deserialize.</param>
/// <param name="stream">The serialized data stream.</param>
/// <param name="modifier">The serialization modifier. Cannot be null.</param>
static void Deserialize(SceneObject* obj, ISerializable::DeserializeStream& stream, ISerializeModifier* modifier);
/// <summary>
/// Synchronizes the prefab instances. Prefabs may have new objects added or some removed so deserialized instances need to synchronize with it. Handles also changing prefab object parent in the instance.
/// </summary>
/// <remarks>
/// Should be called after scene objects deserialization and PostLoad event when scene objects hierarchy is ready (parent-child relation exists). But call it before Init and BeginPlay events.
/// </remarks>
/// <param name="sceneObjects">The loaded scene objects. Collection will be modified after usage.</param>
/// <param name="actorToRemovedObjectsData">Maps the loaded actor object to the json data with the RemovedObjects array (used to skip restoring objects removed per prefab instance).</param>
/// <param name="modifier">The objects deserialization modifier. Collection will be modified after usage.</param>
static void SynchronizePrefabInstances(Array<SceneObject*>& sceneObjects, const ActorToRemovedObjectsDataLookup& actorToRemovedObjectsData, ISerializeModifier* modifier);
static void Deserialize(Context& context, SceneObject* obj, ISerializable::DeserializeStream& stream);
/// <summary>
/// Handles the object deserialization error.
@@ -56,7 +59,64 @@ public:
/// <returns>The created actor, or null if failed.</returns>
static Actor* CreateActor(int32 typeId, const Guid& id);
public:
struct PrefabSyncData
{
friend SceneObjectsFactory;
// The created scene objects. Collection can be modified (eg. for spawning missing objects).
Array<SceneObject*>& SceneObjects;
// The scene objects data.
const ISerializable::DeserializeStream& Data;
// The objects deserialization modifier. Collection will be modified (eg. for spawned objects mapping).
ISerializeModifier* Modifier;
PrefabSyncData(Array<SceneObject*>& sceneObjects, const ISerializable::DeserializeStream& data, ISerializeModifier* modifier);
private:
struct NewObj
{
Prefab* Prefab;
const ISerializable::DeserializeStream* PrefabData;
Guid PrefabObjectId;
Guid Id;
};
int32 InitialCount;
Array<NewObj> NewObjects;
};
/// <summary>
/// Initializes the prefab instances inside the scene objects for proper references deserialization.
/// </summary>
/// <remarks>
/// Should be called after spawning scene objects but before scene objects deserialization.
/// </remarks>
/// <param name="context">The serialization context.</param>
/// <param name="data">The sync data.</param>
static void SetupPrefabInstances(Context& context, PrefabSyncData& data);
/// <summary>
/// Synchronizes the new prefab instances by spawning missing objects that were added to prefab but were not saved with scene objects collection.
/// </summary>
/// <remarks>
/// Should be called after spawning scene objects but before scene objects deserialization and PostLoad event when scene objects hierarchy is ready (parent-child relation exists). But call it before Init and BeginPlay events.
/// </remarks>
/// <param name="context">The serialization context.</param>
/// <param name="data">The sync data.</param>
static void SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data);
/// <summary>
/// Synchronizes the prefab instances. Prefabs may have objects removed so deserialized instances need to synchronize with it. Handles also changing prefab object parent in the instance.
/// </summary>
/// <remarks>
/// Should be called after scene objects deserialization and PostLoad event when scene objects hierarchy is ready (parent-child relation exists). But call it before Init and BeginPlay events.
/// </remarks>
/// <param name="context">The serialization context.</param>
/// <param name="data">The sync data.</param>
static void SynchronizePrefabInstances(Context& context, PrefabSyncData& data);
private:
static void SynchronizeNewPrefabInstance(Prefab* prefab, Actor* actor, const Guid& prefabObjectId, Array<SceneObject*>& sceneObjects, ISerializeModifier* modifier);
static void SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId);
};

View File

@@ -294,7 +294,7 @@ String Localization::GetPluralString(const String& id, int32 n, const String& fa
return fallback;
CHECK_RETURN(n >= 1, fallback);
n--;
StringView result;
const String* result = nullptr;
for (auto& e : Instance.LocalizedStringTables)
{
const auto table = e.Get();
@@ -310,7 +310,7 @@ String Localization::GetPluralString(const String& id, int32 n, const String& fa
result = table->GetPluralString(id, n);
}
}
if (result.IsEmpty())
result = fallback;
return String::Format(result.GetText(), n);
if (!result)
result = &fallback;
return String::Format(result->GetText(), n);
}

View File

@@ -43,10 +43,10 @@ bool MeshCollider::CanAttach(RigidBody* rigidBody) const
CollisionDataType type = CollisionDataType::None;
if (CollisionData && CollisionData->IsLoaded())
type = CollisionData->GetOptions().Type;
#if USE_EDITOR
#if USE_EDITOR || !BUILD_RELEASE
if (type == CollisionDataType::TriangleMesh)
{
LOG(Warning, "Cannot attach {0} using Triangle Mesh collider {1} to RigidBody (not supported)", GetNamePath(), CollisionData->ToString());
LOG(Warning, "Cannot attach '{0}' using Triangle Mesh collider '{1}' to Rigid Body (not supported)", GetNamePath(), CollisionData->ToString());
}
#endif
return type != CollisionDataType::TriangleMesh;

View File

@@ -78,6 +78,21 @@ bool CollisionData::CookCollision(CollisionDataType type, const Span<Vector3>& v
return CookCollision(type, &modelData, convexFlags, convexVertexLimit);
}
bool CollisionData::CookCollision(CollisionDataType type, const Span<Vector3>& vertices, const Span<int32>& triangles, ConvexMeshGenerationFlags convexFlags, int32 convexVertexLimit)
{
CHECK_RETURN(vertices.Length() != 0, true);
CHECK_RETURN(triangles.Length() != 0 && triangles.Length() % 3 == 0, true);
ModelData modelData;
modelData.LODs.Resize(1);
auto meshData = New<MeshData>();
modelData.LODs[0].Meshes.Add(meshData);
meshData->Positions.Set(vertices.Get(), vertices.Length());
meshData->Indices.Resize(triangles.Length());
for (int32 i = 0; i < triangles.Length(); i++)
meshData->Indices.Get()[i] = triangles.Get()[i];
return CookCollision(type, &modelData, convexFlags, convexVertexLimit);
}
bool CollisionData::CookCollision(CollisionDataType type, ModelData* modelData, ConvexMeshGenerationFlags convexFlags, int32 convexVertexLimit)
{
// Validate state

View File

@@ -223,6 +223,19 @@ public:
/// <param name="convexVertexLimit">The convex mesh vertex limit. Use values in range [8;255]</param>
API_FUNCTION() bool CookCollision(CollisionDataType type, const Span<Vector3>& vertices, const Span<uint32>& triangles, ConvexMeshGenerationFlags convexFlags = ConvexMeshGenerationFlags::None, int32 convexVertexLimit = 255);
/// <summary>
/// Cooks the mesh collision data and updates the virtual asset. action cannot be performed on a main thread.
/// </summary>
/// <remarks>
/// Can be used only for virtual assets (see <see cref="Asset.IsVirtual"/> and <see cref="Content.CreateVirtualAsset{T}"/>).
/// </remarks>
/// <param name="type">The collision data type.</param>
/// <param name="vertices">The source geometry vertex buffer with vertices positions. Cannot be empty.</param>
/// <param name="triangles">The source data index buffer (triangles list). Uses 32-bit stride buffer. Cannot be empty. Length must be multiple of 3 (as 3 vertices build a triangle).</param>
/// <param name="convexFlags">The convex mesh generation flags.</param>
/// <param name="convexVertexLimit">The convex mesh vertex limit. Use values in range [8;255]</param>
API_FUNCTION() bool CookCollision(CollisionDataType type, const Span<Vector3>& vertices, const Span<int32>& triangles, ConvexMeshGenerationFlags convexFlags = ConvexMeshGenerationFlags::None, int32 convexVertexLimit = 255);
/// <summary>
/// Cooks the mesh collision data and updates the virtual asset. action cannot be performed on a main thread.
/// </summary>

View File

@@ -167,7 +167,7 @@ void PlatformBase::Exit()
#if COMPILE_WITH_PROFILER
#define TRACY_ENABLE_MEMORY (TRACY_ENABLE && !USE_EDITOR)
#define TRACY_ENABLE_MEMORY (TRACY_ENABLE)
void PlatformBase::OnMemoryAlloc(void* ptr, uint64 size)
{

View File

@@ -169,8 +169,8 @@ public:
/// <summary>
/// Copy memory region
/// </summary>
/// <param name="dst">Destination memory address</param>
/// <param name="src">Source memory address</param>
/// <param name="dst">Destination memory address. Must not be null, even if size is zero.</param>
/// <param name="src">Source memory address. Must not be null, even if size is zero.</param>
/// <param name="size">Size of the memory to copy in bytes</param>
FORCE_INLINE static void MemoryCopy(void* dst, const void* src, uint64 size)
{
@@ -180,7 +180,7 @@ public:
/// <summary>
/// Set memory region with given value
/// </summary>
/// <param name="dst">Destination memory address</param>
/// <param name="dst">Destination memory address. Must not be null, even if size is zero.</param>
/// <param name="size">Size of the memory to set in bytes</param>
/// <param name="value">Value to set</param>
FORCE_INLINE static void MemorySet(void* dst, uint64 size, int32 value)
@@ -191,7 +191,7 @@ public:
/// <summary>
/// Clear memory region with zeros
/// </summary>
/// <param name="dst">Destination memory address</param>
/// <param name="dst">Destination memory address. Must not be null, even if size is zero.</param>
/// <param name="size">Size of the memory to clear in bytes</param>
FORCE_INLINE static void MemoryClear(void* dst, uint64 size)
{
@@ -201,8 +201,8 @@ public:
/// <summary>
/// Compare two blocks of the memory.
/// </summary>
/// <param name="buf1">The first buffer address.</param>
/// <param name="buf2">The second buffer address.</param>
/// <param name="buf1">The first buffer address. Must not be null, even if size is zero.</param>
/// <param name="buf2">The second buffer address. Must not be null, even if size is zero.</param>
/// <param name="size">Size of the memory to compare in bytes.</param>
FORCE_INLINE static int32 MemoryCompare(const void* buf1, const void* buf2, uint64 size)
{

View File

@@ -1659,7 +1659,7 @@ void LinuxClipboard::SetText(const StringView& text)
return;
X11::Window window = (X11::Window)mainWindow->GetNativePtr();
Impl::ClipboardText.Set(text.GetText(), text.Length());
Impl::ClipboardText.Set(text.Get(), text.Length());
X11::XSetSelectionOwner(xDisplay, xAtomClipboard, window, CurrentTime); // CLIPBOARD
X11::XSetSelectionOwner(xDisplay, (X11::Atom)1, window, CurrentTime); // XA_PRIMARY
}

View File

@@ -120,36 +120,36 @@ public:
public:
// Compare two strings with case sensitive
// Compare two strings with case sensitive. Strings must not be null.
static int32 Compare(const Char* str1, const Char* str2);
// Compare two strings without case sensitive
// Compare two strings without case sensitive. Strings must not be null.
static int32 Compare(const Char* str1, const Char* str2, int32 maxCount);
// Compare two strings without case sensitive
// Compare two strings without case sensitive. Strings must not be null.
static int32 CompareIgnoreCase(const Char* str1, const Char* str2);
// Compare two strings without case sensitive
// Compare two strings without case sensitive. Strings must not be null.
static int32 CompareIgnoreCase(const Char* str1, const Char* str2, int32 maxCount);
// Compare two strings with case sensitive
// Compare two strings with case sensitive. Strings must not be null.
static int32 Compare(const char* str1, const char* str2);
// Compare two strings without case sensitive
// Compare two strings without case sensitive. Strings must not be null.
static int32 Compare(const char* str1, const char* str2, int32 maxCount);
// Compare two strings without case sensitive
// Compare two strings without case sensitive. Strings must not be null.
static int32 CompareIgnoreCase(const char* str1, const char* str2);
// Compare two strings without case sensitive
// Compare two strings without case sensitive. Strings must not be null.
static int32 CompareIgnoreCase(const char* str1, const char* str2, int32 maxCount);
public:
// Get string length
// Get string length. Returns 0 if str is null.
static int32 Length(const Char* str);
// Get string length
// Get string length. Returns 0 if str is null.
static int32 Length(const char* str);
// Copy string

View File

@@ -35,7 +35,7 @@ void RunUWP()
DialogResult MessageBox::Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon)
{
return (DialogResult)CUWPPlatform->ShowMessageDialog(parent ? parent->GetImpl() : nullptr, text.GetText(), caption.GetText(), (UWPPlatformImpl::MessageBoxButtons)buttons, (UWPPlatformImpl::MessageBoxIcon)icon);
return (DialogResult)CUWPPlatform->ShowMessageDialog(parent ? parent->GetImpl() : nullptr, String(text).GetText(), String(caption).GetText(), (UWPPlatformImpl::MessageBoxButtons)buttons, (UWPPlatformImpl::MessageBoxIcon)icon);
}
bool UWPPlatform::Init()

View File

@@ -23,9 +23,12 @@ void WindowsClipboard::Clear()
void WindowsClipboard::SetText(const StringView& text)
{
const int32 size = (text.Length() + 1) * sizeof(Char);
const HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size);
Platform::MemoryCopy(GlobalLock(hMem), text.GetText(), size);
const int32 sizeWithoutNull = text.Length() * sizeof(Char);
const HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, sizeWithoutNull + sizeof(Char));
Char* pMem = static_cast<Char*>(GlobalLock(hMem));
Platform::MemoryCopy(pMem, text.GetNonTerminatedText(), sizeWithoutNull);
Platform::MemorySet(pMem + text.Length(), sizeof(Char), 0);
GlobalUnlock(hMem);
OpenClipboard(nullptr);

View File

@@ -435,7 +435,7 @@ DialogResult MessageBox::Show(Window* parent, const StringView& text, const Stri
}
// Show dialog
int result = MessageBoxW(parent ? static_cast<HWND>(parent->GetNativePtr()) : nullptr, text.GetText(), caption.GetText(), flags);
int result = MessageBoxW(parent ? static_cast<HWND>(parent->GetNativePtr()) : nullptr, String(text).GetText(), String(caption).GetText(), flags);
// Translate result to dialog result
DialogResult dialogResult;
@@ -948,10 +948,12 @@ int32 WindowsPlatform::StartProcess(const StringView& filename, const StringView
LOG(Info, "Working directory: {0}", workingDir);
}
String filenameString(filename);
SHELLEXECUTEINFOW shExecInfo = { 0 };
shExecInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
shExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
shExecInfo.lpFile = filename.GetText();
shExecInfo.lpFile = filenameString.GetText();
shExecInfo.lpParameters = args.HasChars() ? args.Get() : nullptr;
shExecInfo.lpDirectory = workingDir.HasChars() ? workingDir.Get() : nullptr;
shExecInfo.nShow = hiddenWindow ? SW_HIDE : SW_SHOW;
@@ -1109,7 +1111,7 @@ int32 WindowsPlatform::RunProcess(const StringView& cmdLine, const StringView& w
// Create the process
PROCESS_INFORMATION procInfo;
if (!CreateProcessW(nullptr, const_cast<LPWSTR>(cmdLine.GetText()), nullptr, nullptr, TRUE, dwCreationFlags, (LPVOID)environmentStr, workingDir.HasChars() ? workingDir.Get() : nullptr, &startupInfoEx.StartupInfo, &procInfo))
if (!CreateProcessW(nullptr, const_cast<LPWSTR>(String(cmdLine).GetText()), nullptr, nullptr, TRUE, dwCreationFlags, (LPVOID)environmentStr, workingDir.HasChars() ? workingDir.Get() : nullptr, &startupInfoEx.StartupInfo, &procInfo))
{
LOG(Warning, "Cannot start process '{0}'. Error code: 0x{1:x}", cmdLine, static_cast<int64>(GetLastError()));
goto ERROR_EXIT;

View File

@@ -754,13 +754,20 @@ ScriptingObject* Scripting::FindObject(Guid id, MClass* type)
if (result)
{
// Check type
if (result->Is(type))
if (!type || result->Is(type))
return result;
LOG(Warning, "Found scripting object with ID={0} of type {1} that doesn't match type {2}.", id, String(result->GetType().Fullname), String(type->GetFullName()));
return nullptr;
}
// Check if object can be an asset and try to load it
if (!type)
{
result = Content::LoadAsync<Asset>(id);
if (!result)
LOG(Warning, "Unable to find scripting object with ID={0}", id);
return result;
}
if (type == ScriptingObject::GetStaticClass() || type->IsSubClassOf(Asset::GetStaticClass()))
{
Asset* asset = Content::LoadAsync(id, type);
@@ -774,6 +781,10 @@ ScriptingObject* Scripting::FindObject(Guid id, MClass* type)
ScriptingObject* Scripting::TryFindObject(Guid id, MClass* type)
{
if (!id.IsValid())
return nullptr;
PROFILE_CPU();
// Try to map object id
const auto idsMapping = ObjectsLookupIdMapping.Get();
if (idsMapping)
@@ -796,7 +807,7 @@ ScriptingObject* Scripting::TryFindObject(Guid id, MClass* type)
#endif
// Check type
if (result && !result->Is(type))
if (result && type && !result->Is(type))
{
result = nullptr;
}

View File

@@ -133,9 +133,9 @@ public:
/// Finds the object by the given identifier. Searches registered scene objects and optionally assets. Logs warning if fails.
/// </summary>
/// <param name="id">The object unique identifier.</param>
/// <param name="type">The type of the object to find.</param>
/// <param name="type">The type of the object to find (optional).</param>
/// <returns>The found object or null if missing.</returns>
static ScriptingObject* FindObject(Guid id, MClass* type);
static ScriptingObject* FindObject(Guid id, MClass* type = nullptr);
/// <summary>
/// Tries to find the object by the given identifier.
@@ -152,9 +152,9 @@ public:
/// Tries to find the object by the given identifier.
/// </summary>
/// <param name="id">The object unique identifier.</param>
/// <param name="type">The type of the object to find.</param>
/// <param name="type">The type of the object to find (optional).</param>
/// <returns>The found object or null if missing.</returns>
static ScriptingObject* TryFindObject(Guid id, MClass* type);
static ScriptingObject* TryFindObject(Guid id, MClass* type = nullptr);
/// <summary>
/// Finds the object by the given managed instance handle. Searches only registered scene objects.

View File

@@ -7,6 +7,8 @@
#include "Engine/Level/Actor.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include "Engine/Content/Asset.h"
#include "Engine/Content/Content.h"
#include "ManagedCLR/MAssembly.h"
#include "ManagedCLR/MClass.h"
#include "ManagedCLR/MUtils.h"
@@ -244,6 +246,20 @@ bool ScriptingObject::CanCast(MClass* from, MClass* to)
return from->IsSubClassOf(to);
}
bool ScriptingObject::CanCast(MClass* from, MonoClass* to)
{
if (!from && !to)
return true;
CHECK_RETURN(from && to, false);
#if PLATFORM_LINUX
// Cannot enter GC unsafe region if the thread is not attached
MCore::AttachThread();
#endif
return from->IsSubClassOf(to);
}
void ScriptingObject::OnDeleteObject()
{
// Cleanup managed object
@@ -542,13 +558,38 @@ public:
static MonoObject* FindObject(Guid* id, MonoReflectionType* type)
{
ScriptingObject* obj = Scripting::FindObject(*id, Scripting::FindClass(MUtils::GetClass(type)));
return obj ? obj->GetOrCreateManagedInstance() : nullptr;
if (!id->IsValid())
return nullptr;
auto klass = MUtils::GetClass(type);
ScriptingObject* obj = Scripting::TryFindObject(*id);
if (!obj)
{
if (!klass || klass == ScriptingObject::GetStaticClass()->GetNative() || mono_class_is_subclass_of(klass, Asset::GetStaticClass()->GetNative(), false) != 0)
{
obj = Content::LoadAsync<Asset>(*id);
}
}
if (obj)
{
if (klass && !obj->Is(klass))
{
LOG(Warning, "Found scripting object with ID={0} of type {1} that doesn't match type {2}.", *id, String(obj->GetType().Fullname), String(MUtils::GetClassFullname(klass)));
return nullptr;
}
return obj->GetOrCreateManagedInstance();
}
if (klass)
LOG(Warning, "Unable to find scripting object with ID={0}. Required type {1}.", *id, String(MUtils::GetClassFullname(klass)));
else
LOG(Warning, "Unable to find scripting object with ID={0}", *id);
return nullptr;
}
static MonoObject* TryFindObject(Guid* id, MonoReflectionType* type)
{
ScriptingObject* obj = Scripting::TryFindObject(*id, Scripting::FindClass(MUtils::GetClass(type)));
ScriptingObject* obj = Scripting::TryFindObject(*id);
if (obj && !obj->Is(MUtils::GetClass(type)))
obj = nullptr;
return obj ? obj->GetOrCreateManagedInstance() : nullptr;
}

View File

@@ -62,7 +62,6 @@ public:
/// <summary>
/// Gets the managed instance object.
/// </summary>
/// <returns>The Mono managed object or null if not created.</returns>
MonoObject* GetManagedInstance() const;
/// <summary>
@@ -131,6 +130,7 @@ public:
/// <param name="to">The destination class to the cast.</param>
/// <returns>True if can, otherwise false.</returns>
static bool CanCast(MClass* from, MClass* to);
static bool CanCast(MClass* from, MonoClass* to);
template<typename T>
static T* Cast(ScriptingObject* obj)
@@ -145,6 +145,11 @@ public:
return CanCast(GetClass(), type);
}
bool Is(MonoClass* klass) const
{
return CanCast(GetClass(), klass);
}
template<typename T>
bool Is() const
{

View File

@@ -73,7 +73,7 @@ namespace FlaxEngine.Json
ContractResolver = new ExtendedDefaultContractResolver(isManagedOnly),
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.Auto,
NullValueHandling = NullValueHandling.Ignore,
NullValueHandling = NullValueHandling.Include,
ObjectCreationHandling = ObjectCreationHandling.Auto,
};
if (ObjectConverter == null)
@@ -276,6 +276,11 @@ namespace FlaxEngine.Json
cache.IsDuringSerialization = false;
Current.Value = cache;
/*// Debug json string reading
cache.MemoryStream.Initialize(jsonBuffer, jsonLength);
cache.Reader.DiscardBufferedData();
string json = cache.Reader.ReadToEnd();*/
cache.MemoryStream.Initialize(jsonBuffer, jsonLength);
cache.Reader.DiscardBufferedData();
var jsonReader = new JsonTextReader(cache.Reader);

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#if PLATFORM_WINDOWS || PLATFORM_LINUX
#define CATCH_CONFIG_RUNNER
#include <ThirdParty/catch2/catch.hpp>
int main(int argc, char* argv[])
{
int result = Catch::Session().run(argc, argv);
return result;
}
#endif

View File

@@ -0,0 +1,331 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringView.h"
#include <ThirdParty/catch2/catch.hpp>
TEST_CASE("String Replace works") {
SECTION("Char, case sensitive") {
String str("hello HELLO");
CHECK(str.Replace('l', 'x', StringSearchCase::CaseSensitive) == 2);
CHECK(str == String("hexxo HELLO"));
}
SECTION("Char, ignore case") {
String str("hello HELLO");
CHECK(str.Replace('l', 'x', StringSearchCase::IgnoreCase) == 4);
CHECK(str == String("hexxo HExxO"));
}
SECTION("case sensitive") {
String str("hello HELLO this is me saying hello");
CHECK(str.Replace(TEXT("hello"), TEXT("hi"), StringSearchCase::CaseSensitive) == 2);
CHECK(str == String("hi HELLO this is me saying hi"));
}
SECTION("ignore case") {
String str("hello HELLO this is me saying hello");
CHECK(str.Replace(TEXT("hello"), TEXT("hi"), StringSearchCase::IgnoreCase) == 3);
CHECK(str == String("hi hi this is me saying hi"));
}
SECTION("case sensitive, search and replace texts identical") {
String str("hello HELLO this is me saying hello");
CHECK(str.Replace(TEXT("hello"), TEXT("hello"), StringSearchCase::CaseSensitive) == 2);
CHECK(str == String("hello HELLO this is me saying hello"));
}
SECTION("ignore case, search and replace texts identical") {
String str("hello HELLO this is me saying hello");
CHECK(str.Replace(TEXT("hello"), TEXT("hello"), StringSearchCase::IgnoreCase) == 3);
CHECK(str == String("hello hello this is me saying hello"));
}
SECTION("case sensitive, replace text empty") {
String str("hello HELLO this is me saying hello");
CHECK(str.Replace(TEXT("hello"), TEXT(""), StringSearchCase::CaseSensitive) == 2);
CHECK(str == String(" HELLO this is me saying "));
}
SECTION("ignore case, replace text empty") {
String str("hello HELLO this is me saying hello");
CHECK(str.Replace(TEXT("hello"), TEXT(""), StringSearchCase::IgnoreCase) == 3);
CHECK(str == String(" this is me saying "));
}
SECTION("no finds") {
String str("hello HELLO this is me saying hello");
CHECK(str.Replace(TEXT("bye"), TEXT("hi"), StringSearchCase::CaseSensitive) == 0);
CHECK(str.Replace(TEXT("bye"), TEXT("hi"), StringSearchCase::IgnoreCase) == 0);
CHECK(str == String("hello HELLO this is me saying hello"));
}
SECTION("empty input") {
String str("");
CHECK(str.Replace(TEXT("bye"), TEXT("hi"), StringSearchCase::CaseSensitive) == 0);
CHECK(str.Replace(TEXT("bye"), TEXT("hi"), StringSearchCase::IgnoreCase) == 0);
CHECK(str == String(""));
}
}
TEST_CASE("String Starts/EndsWith works") {
SECTION("StartsWith, case sensitive") {
SECTION("Char") {
CHECK(String("").StartsWith('h', StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").StartsWith('h', StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").StartsWith('H', StringSearchCase::CaseSensitive) == false);
}
SECTION("String") {
CHECK(String("hello HELLO").StartsWith(String(TEXT("hello")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").StartsWith(String(TEXT("HELLO")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").StartsWith(String(TEXT("")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").StartsWith(String(TEXT("xxx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("").StartsWith(String(TEXT("x")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").StartsWith(String(TEXT("hello HELLOx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").StartsWith(String(TEXT("xhello HELLO")), StringSearchCase::CaseSensitive) == false);
}
SECTION("StringView") {
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("hello")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("HELLO")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").StartsWith(StringView(), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("xxx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("").StartsWith(StringView(TEXT("x")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("hello HELLOx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("xhello HELLO")), StringSearchCase::CaseSensitive) == false);
}
}
SECTION("StartsWith, ignore case") {
SECTION("Char") {
CHECK(String("").StartsWith('h', StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").StartsWith('h', StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith('H', StringSearchCase::IgnoreCase) == true);
}
SECTION("String") {
CHECK(String("hello HELLO").StartsWith(String(TEXT("hello")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith(String(TEXT("HELLO")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith(String(TEXT("")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith(String(TEXT("xxx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("").StartsWith(String(TEXT("x")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").StartsWith(String(TEXT("hello HELLOx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").StartsWith(String(TEXT("xhello HELLO")), StringSearchCase::IgnoreCase) == false);
}
SECTION("StringView") {
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("hello")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("HELLO")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith(StringView(), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("xxx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("").StartsWith(StringView(TEXT("x")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("hello HELLOx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").StartsWith(StringView(TEXT("xhello HELLO")), StringSearchCase::IgnoreCase) == false);
}
}
SECTION("EndsWith, case sensitive") {
SECTION("Char") {
CHECK(String("").EndsWith('h', StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").EndsWith('O', StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").EndsWith('o', StringSearchCase::CaseSensitive) == false);
}
SECTION("String") {
CHECK(String("hello HELLO").EndsWith(String(TEXT("HELLO")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").EndsWith(String(TEXT("hello")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").EndsWith(String(TEXT("")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").EndsWith(String(TEXT("xxx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("").EndsWith(String(TEXT("x")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").EndsWith(String(TEXT("hello HELLOx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").EndsWith(String(TEXT("xhello HELLO")), StringSearchCase::CaseSensitive) == false);
}
SECTION("StringView") {
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("HELLO")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("hello")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").EndsWith(StringView(), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("")), StringSearchCase::CaseSensitive) == true);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("xxx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("").EndsWith(StringView(TEXT("x")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("hello HELLOx")), StringSearchCase::CaseSensitive) == false);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("xhello HELLO")), StringSearchCase::CaseSensitive) == false);
}
}
SECTION("EndsWith, ignore case") {
SECTION("Char") {
CHECK(String("").EndsWith('h', StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").EndsWith('O', StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith('o', StringSearchCase::IgnoreCase) == true);
}
SECTION("String") {
CHECK(String("hello HELLO").EndsWith(String(TEXT("HELLO")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith(String(TEXT("hello")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith(String(TEXT("")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith(String(TEXT("xxx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("").EndsWith(String(TEXT("x")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").EndsWith(String(TEXT("hello HELLOx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").EndsWith(String(TEXT("xhello HELLO")), StringSearchCase::IgnoreCase) == false);
}
SECTION("StringView") {
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("HELLO")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("hello")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith(StringView(), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("")), StringSearchCase::IgnoreCase) == true);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("xxx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("").EndsWith(StringView(TEXT("x")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("hello HELLOx")), StringSearchCase::IgnoreCase) == false);
CHECK(String("hello HELLO").EndsWith(StringView(TEXT("xhello HELLO")), StringSearchCase::IgnoreCase) == false);
}
}
}
TEST_CASE("String Compare works") {
SECTION("String") {
SECTION("case sensitive") {
// Empty strings
CHECK(String("").Compare(String(TEXT("")), StringSearchCase::CaseSensitive) == 0);
CHECK(String("").Compare(String(TEXT("xxx")), StringSearchCase::CaseSensitive) < 0);
CHECK(String("xxx").Compare(String(TEXT("")), StringSearchCase::CaseSensitive) > 0);
// Equal lengths, difference at end
CHECK(String("xxx").Compare(String(TEXT("xxx")), StringSearchCase::CaseSensitive) == 0);
CHECK(String("abc").Compare(String(TEXT("abd")), StringSearchCase::CaseSensitive) < 0);
CHECK(String("abd").Compare(String(TEXT("abc")), StringSearchCase::CaseSensitive) > 0);
// Equal lengths, difference in the middle
CHECK(String("abcx").Compare(String(TEXT("abdx")), StringSearchCase::CaseSensitive) < 0);
CHECK(String("abdx").Compare(String(TEXT("abcx")), StringSearchCase::CaseSensitive) > 0);
// Different lengths, same prefix
CHECK(String("abcxx").Compare(String(TEXT("abc")), StringSearchCase::CaseSensitive) > 0);
CHECK(String("abc").Compare(String(TEXT("abcxx")), StringSearchCase::CaseSensitive) < 0);
// Different lengths, different prefix
CHECK(String("abcx").Compare(String(TEXT("abd")), StringSearchCase::CaseSensitive) < 0);
CHECK(String("abd").Compare(String(TEXT("abcx")), StringSearchCase::CaseSensitive) > 0);
CHECK(String("abc").Compare(String(TEXT("abdx")), StringSearchCase::CaseSensitive) < 0);
CHECK(String("abdx").Compare(String(TEXT("abc")), StringSearchCase::CaseSensitive) > 0);
// Case differences
CHECK(String("a").Compare(String(TEXT("A")), StringSearchCase::CaseSensitive) > 0);
CHECK(String("A").Compare(String(TEXT("a")), StringSearchCase::CaseSensitive) < 0);
}
SECTION("ignore case") {
// Empty strings
CHECK(String("").Compare(String(TEXT("")), StringSearchCase::IgnoreCase) == 0);
CHECK(String("").Compare(String(TEXT("xxx")), StringSearchCase::IgnoreCase) < 0);
CHECK(String("xxx").Compare(String(TEXT("")), StringSearchCase::IgnoreCase) > 0);
// Equal lengths, difference at end
CHECK(String("xxx").Compare(String(TEXT("xxx")), StringSearchCase::IgnoreCase) == 0);
CHECK(String("abc").Compare(String(TEXT("abd")), StringSearchCase::IgnoreCase) < 0);
CHECK(String("abd").Compare(String(TEXT("abc")), StringSearchCase::IgnoreCase) > 0);
// Equal lengths, difference in the middle
CHECK(String("abcx").Compare(String(TEXT("abdx")), StringSearchCase::IgnoreCase) < 0);
CHECK(String("abdx").Compare(String(TEXT("abcx")), StringSearchCase::IgnoreCase) > 0);
// Different lengths, same prefix
CHECK(String("abcxx").Compare(String(TEXT("abc")), StringSearchCase::IgnoreCase) > 0);
CHECK(String("abc").Compare(String(TEXT("abcxx")), StringSearchCase::IgnoreCase) < 0);
// Different lengths, different prefix
CHECK(String("abcx").Compare(String(TEXT("abd")), StringSearchCase::IgnoreCase) < 0);
CHECK(String("abd").Compare(String(TEXT("abcx")), StringSearchCase::IgnoreCase) > 0);
CHECK(String("abc").Compare(String(TEXT("abdx")), StringSearchCase::IgnoreCase) < 0);
CHECK(String("abdx").Compare(String(TEXT("abc")), StringSearchCase::IgnoreCase) > 0);
// Case differences
CHECK(String("a").Compare(String(TEXT("A")), StringSearchCase::IgnoreCase) == 0);
CHECK(String("A").Compare(String(TEXT("a")), StringSearchCase::IgnoreCase) == 0);
}
}
SECTION("StringView") {
SECTION("case sensitive") {
// Null string views
CHECK(StringView().Compare(StringView(), StringSearchCase::CaseSensitive) == 0);
CHECK(StringView().Compare(StringView(TEXT("xxx")), StringSearchCase::CaseSensitive) < 0);
CHECK(StringView(TEXT("xxx")).Compare(StringView(), StringSearchCase::CaseSensitive) > 0);
// Empty strings
CHECK(StringView(TEXT("")).Compare(StringView(TEXT("")), StringSearchCase::CaseSensitive) == 0);
CHECK(StringView(TEXT("")).Compare(StringView(TEXT("xxx")), StringSearchCase::CaseSensitive) < 0);
CHECK(StringView(TEXT("xxx")).Compare(StringView(TEXT("")), StringSearchCase::CaseSensitive) > 0);
// Equal lengths, difference at end
CHECK(StringView(TEXT("xxx")).Compare(StringView(TEXT("xxx")), StringSearchCase::CaseSensitive) == 0);
CHECK(StringView(TEXT("abc")).Compare(StringView(TEXT("abd")), StringSearchCase::CaseSensitive) < 0);
CHECK(StringView(TEXT("abd")).Compare(StringView(TEXT("abc")), StringSearchCase::CaseSensitive) > 0);
// Equal lengths, difference in the middle
CHECK(StringView(TEXT("abcx")).Compare(StringView(TEXT("abdx")), StringSearchCase::CaseSensitive) < 0);
CHECK(StringView(TEXT("abdx")).Compare(StringView(TEXT("abcx")), StringSearchCase::CaseSensitive) > 0);
// Different lengths, same prefix
CHECK(StringView(TEXT("abcxx")).Compare(StringView(TEXT("abc")), StringSearchCase::CaseSensitive) > 0);
CHECK(StringView(TEXT("abc")).Compare(StringView(TEXT("abcxx")), StringSearchCase::CaseSensitive) < 0);
// Different lengths, different prefix
CHECK(StringView(TEXT("abcx")).Compare(StringView(TEXT("abd")), StringSearchCase::CaseSensitive) < 0);
CHECK(StringView(TEXT("abd")).Compare(StringView(TEXT("abcx")), StringSearchCase::CaseSensitive) > 0);
CHECK(StringView(TEXT("abc")).Compare(StringView(TEXT("abdx")), StringSearchCase::CaseSensitive) < 0);
CHECK(StringView(TEXT("abdx")).Compare(StringView(TEXT("abc")), StringSearchCase::CaseSensitive) > 0);
// Case differences
CHECK(StringView(TEXT("a")).Compare(StringView(TEXT("A")), StringSearchCase::CaseSensitive) > 0);
CHECK(StringView(TEXT("A")).Compare(StringView(TEXT("a")), StringSearchCase::CaseSensitive) < 0);
}
SECTION("ignore case") {
//Null string views
CHECK(StringView().Compare(StringView(), StringSearchCase::IgnoreCase) == 0);
CHECK(StringView().Compare(StringView(TEXT("xxx")), StringSearchCase::IgnoreCase) < 0);
CHECK(StringView(TEXT("xxx")).Compare(StringView(), StringSearchCase::IgnoreCase) > 0);
// Empty strings
CHECK(StringView(TEXT("")).Compare(StringView(TEXT("")), StringSearchCase::IgnoreCase) == 0);
CHECK(StringView(TEXT("")).Compare(StringView(TEXT("xxx")), StringSearchCase::IgnoreCase) < 0);
CHECK(StringView(TEXT("xxx")).Compare(StringView(TEXT("")), StringSearchCase::IgnoreCase) > 0);
// Equal lengths, difference at end
CHECK(StringView(TEXT("xxx")).Compare(StringView(TEXT("xxx")), StringSearchCase::IgnoreCase) == 0);
CHECK(StringView(TEXT("abc")).Compare(StringView(TEXT("abd")), StringSearchCase::IgnoreCase) < 0);
CHECK(StringView(TEXT("abd")).Compare(StringView(TEXT("abc")), StringSearchCase::IgnoreCase) > 0);
// Equal lengths, difference in the middle
CHECK(StringView(TEXT("abcx")).Compare(StringView(TEXT("abdx")), StringSearchCase::IgnoreCase) < 0);
CHECK(StringView(TEXT("abdx")).Compare(StringView(TEXT("abcx")), StringSearchCase::IgnoreCase) > 0);
// Different lengths, same prefix
CHECK(StringView(TEXT("abcxx")).Compare(StringView(TEXT("abc")), StringSearchCase::IgnoreCase) > 0);
CHECK(StringView(TEXT("abc")).Compare(StringView(TEXT("abcxx")), StringSearchCase::IgnoreCase) < 0);
// Different lengths, different prefix
CHECK(StringView(TEXT("abcx")).Compare(StringView(TEXT("abd")), StringSearchCase::IgnoreCase) < 0);
CHECK(StringView(TEXT("abd")).Compare(StringView(TEXT("abcx")), StringSearchCase::IgnoreCase) > 0);
CHECK(StringView(TEXT("abc")).Compare(StringView(TEXT("abdx")), StringSearchCase::IgnoreCase) < 0);
CHECK(StringView(TEXT("abdx")).Compare(StringView(TEXT("abc")), StringSearchCase::IgnoreCase) > 0);
// Case differences
CHECK(StringView(TEXT("a")).Compare(StringView(TEXT("A")), StringSearchCase::IgnoreCase) == 0);
CHECK(StringView(TEXT("A")).Compare(StringView(TEXT("a")), StringSearchCase::IgnoreCase) == 0);
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using Flax.Build;
/// <summary>
/// Engine tests module.
/// </summary>
public class Tests : EngineModule
{
/// <inheritdoc />
public Tests()
{
Deploy = false;
}
/// <inheritdoc />
public override void GetFilesToDeploy(List<string> files)
{
}
}

View File

@@ -4,6 +4,7 @@
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Mathd.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Platform/FileSystem.h"

View File

@@ -877,7 +877,7 @@ namespace FlaxEngine.GUI
/// <summary>
/// Gets or sets the tooltip text.
/// </summary>
[HideInEditor]
[HideInEditor, NoSerialize]
public string TooltipText
{
get => _tooltipText;
@@ -887,7 +887,7 @@ namespace FlaxEngine.GUI
/// <summary>
/// Gets or sets the custom tooltip control linked. Use null to show default shared tooltip from the current <see cref="Style"/>.
/// </summary>
[HideInEditor]
[HideInEditor, NoSerialize]
public Tooltip CustomTooltip
{
get => _tooltip;

View File

@@ -93,28 +93,28 @@ namespace FlaxEngine.GUI
int i = 0;
Vector2 upperLeft = Vector2.Zero;
float reamingHeight = Height;
float remainingHeight = Height;
for (int rowIndex = 0; rowIndex < _cellsV.Length; rowIndex++)
{
upperLeft.X = 0;
float cellHeight = _cellsV[rowIndex] * reamingHeight;
float cellHeight = _cellsV[rowIndex] * remainingHeight;
if (_cellsV[rowIndex] < 0)
{
cellHeight = -_cellsV[rowIndex];
reamingHeight -= cellHeight;
remainingHeight -= cellHeight;
}
float reamingWidth = Width;
float remainingWidth = Width;
for (int columnIndex = 0; columnIndex < _cellsH.Length; columnIndex++)
{
if (i >= ChildrenCount)
break;
float cellWidth = _cellsH[columnIndex] * reamingWidth;
float cellWidth = _cellsH[columnIndex] * remainingWidth;
if (_cellsH[columnIndex] < 0)
{
cellWidth = -_cellsH[columnIndex];
reamingWidth -= cellWidth;
remainingWidth -= cellWidth;
}
var slotBounds = new Rectangle(upperLeft, cellWidth, cellHeight);

View File

@@ -14,6 +14,7 @@ namespace FlaxEngine.GUI
private bool _alwaysShowScrollbars;
private int _layoutUpdateLock;
private ScrollBars _scrollBars;
private float _scrollBarsSize = ScrollBar.DefaultSize;
private Margin _scrollMargin;
/// <summary>
@@ -64,7 +65,7 @@ namespace FlaxEngine.GUI
VScrollBar = GetChild<VScrollBar>();
if (VScrollBar == null)
{
VScrollBar = new VScrollBar(this, Width - ScrollBar.DefaultSize, Height)
VScrollBar = new VScrollBar(this, Width - _scrollBarsSize, Height)
{
AnchorPreset = AnchorPresets.TopLeft
};
@@ -84,7 +85,7 @@ namespace FlaxEngine.GUI
HScrollBar = GetChild<HScrollBar>();
if (HScrollBar == null)
{
HScrollBar = new HScrollBar(this, Height - ScrollBar.DefaultSize, Width)
HScrollBar = new HScrollBar(this, Height - _scrollBarsSize, Width)
{
AnchorPreset = AnchorPresets.TopLeft
};
@@ -103,6 +104,22 @@ namespace FlaxEngine.GUI
}
}
/// <summary>
/// Gets or sets the size of the scroll bars.
/// </summary>
[EditorOrder(5), Tooltip("Scroll bars size.")]
public float ScrollBarsSize
{
get => _scrollBarsSize;
set
{
if (Mathf.NearEqual(_scrollBarsSize, value))
return;
_scrollBarsSize = value;
PerformLayout();
}
}
/// <summary>
/// Gets or sets a value indicating whether always show scrollbars. Otherwise show them only if scrolling is available.
/// </summary>
@@ -410,7 +427,7 @@ namespace FlaxEngine.GUI
if (VScrollBar != null)
{
float height = Height;
bool vScrollEnabled = (controlsBounds.Bottom > height + 0.01f || controlsBounds.Y < 0.0f) && height > ScrollBar.DefaultSize;
bool vScrollEnabled = (controlsBounds.Bottom > height + 0.01f || controlsBounds.Y < 0.0f) && height > _scrollBarsSize;
if (VScrollBar.Enabled != vScrollEnabled)
{
@@ -432,12 +449,12 @@ namespace FlaxEngine.GUI
{
VScrollBar.SetScrollRange(scrollBounds.Top, Mathf.Max(Mathf.Max(0, scrollBounds.Top), scrollBounds.Height - height));
}
VScrollBar.Bounds = new Rectangle(Width - ScrollBar.DefaultSize, 0, ScrollBar.DefaultSize, Height);
VScrollBar.Bounds = new Rectangle(Width - _scrollBarsSize, 0, _scrollBarsSize, Height);
}
if (HScrollBar != null)
{
float width = Width;
bool hScrollEnabled = (controlsBounds.Right > width + 0.01f || controlsBounds.X < 0.0f) && width > ScrollBar.DefaultSize;
bool hScrollEnabled = (controlsBounds.Right > width + 0.01f || controlsBounds.X < 0.0f) && width > _scrollBarsSize;
if (HScrollBar.Enabled != hScrollEnabled)
{
@@ -459,7 +476,7 @@ namespace FlaxEngine.GUI
{
HScrollBar.SetScrollRange(scrollBounds.Left, Mathf.Max(Mathf.Max(0, scrollBounds.Left), scrollBounds.Width - width));
}
HScrollBar.Bounds = new Rectangle(0, Height - ScrollBar.DefaultSize, Width - (VScrollBar != null && VScrollBar.Visible ? VScrollBar.Width : 0), ScrollBar.DefaultSize);
HScrollBar.Bounds = new Rectangle(0, Height - _scrollBarsSize, Width - (VScrollBar != null && VScrollBar.Visible ? VScrollBar.Width : 0), _scrollBarsSize);
}
}

View File

@@ -10,9 +10,27 @@ namespace FlaxEngine.GUI
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class TilesPanel : ContainerControl
{
private Margin _tileMargin;
private Vector2 _tileSize = new Vector2(64);
private bool _autoResize = false;
/// <summary>
/// Gets or sets the margin applied to each tile.
/// </summary>
[EditorOrder(0), Tooltip("The margin applied to each tile.")]
public Margin TileMargin
{
get => _tileMargin;
set
{
if (_tileMargin != value)
{
_tileMargin = value;
PerformLayout();
}
}
}
/// <summary>
/// Gets or sets the size of the tile.
/// </summary>
@@ -54,7 +72,17 @@ namespace FlaxEngine.GUI
/// Initializes a new instance of the <see cref="TilesPanel"/> class.
/// </summary>
public TilesPanel()
: this(0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TilesPanel"/> class.
/// </summary>
/// <param name="tileMargin">The tile margin.</param>
public TilesPanel(float tileMargin)
{
TileMargin = new Margin(tileMargin);
AutoFocus = false;
}
@@ -81,17 +109,17 @@ namespace FlaxEngine.GUI
{
var c = _children[i];
c.Bounds = new Rectangle(x, y, itemsWidth, itemsHeight);
c.Bounds = new Rectangle(x + TileMargin.Left, y + TileMargin.Top, itemsWidth, itemsHeight);
x += itemsWidth;
if (x + itemsWidth > width)
x += itemsWidth + TileMargin.Width;
if (x + itemsWidth + TileMargin.Width > width)
{
x = 0;
y += itemsHeight;
y += itemsHeight + TileMargin.Height;
}
}
if (x > 0)
y += itemsHeight;
y += itemsHeight + TileMargin.Height;
if (_autoResize)
{

View File

@@ -20,7 +20,7 @@ namespace FlaxEngine.Utilities
/// <summary>
/// The index buffer.
/// </summary>
public int[] IndexBuffer;
public uint[] IndexBuffer;
/// <summary>
/// The vertex buffer.

65
Source/FlaxTests.Build.cs Normal file
View File

@@ -0,0 +1,65 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.IO;
using System.Linq;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// Target that builds standalone, native tests.
/// </summary>
public class FlaxTestsTarget : EngineTarget
{
/// <inheritdoc />
public override void Init()
{
base.Init();
// Initialize
OutputName = "FlaxTests";
ConfigurationName = "Tests";
IsPreBuilt = false;
UseSymbolsExports = false;
Platforms = new[]
{
TargetPlatform.Windows,
TargetPlatform.Linux,
};
Architectures = new[]
{
TargetArchitecture.x64,
};
Configurations = new[]
{
TargetConfiguration.Development,
};
Modules.Remove("Main");
Modules.Add("Tests");
}
/// <inheritdoc />
public override void SetupTargetEnvironment(BuildOptions options)
{
base.SetupTargetEnvironment(options);
options.LinkEnv.LinkAsConsoleProgram = true;
// Setup output folder for Test binaries
var platformName = options.Platform.Target.ToString();
var architectureName = options.Architecture.ToString();
var configurationName = options.Configuration.ToString();
options.OutputFolder = Path.Combine(options.WorkingDirectory, "Binaries", "Tests", platformName, architectureName, configurationName);
}
/// <inheritdoc />
public override Target SelectReferencedTarget(ProjectInfo project, Target[] projectTargets)
{
var testTargetName = "FlaxNativeTests"; // Should this be added to .flaxproj, similarly as "GameTarget" and "EditorTarget"?
var result = projectTargets.FirstOrDefault(x => x.Name == testTargetName);
if (result == null)
throw new Exception(string.Format("Invalid or missing test target {0} specified in project {1} (referenced by project {2}).", testTargetName, project.Name, Project.Name));
return result;
}
}

23
Source/ThirdParty/catch2/LICENSE.txt vendored Normal file
View File

@@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

17959
Source/ThirdParty/catch2/catch.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.IO;
using Flax.Build;
/// <summary>
/// https://github.com/catchorg/Catch2
/// </summary>
public class catch2 : HeaderOnlyModule
{
/// <inheritdoc />
public override void Init()
{
base.Init();
LicenseType = LicenseTypes.BoostSoftwareLicense;
LicenseFilePath = "LICENSE.txt";
}
}

View File

@@ -166,7 +166,7 @@ namespace Flax.Build.Bindings
if (typeInfo.Type == "String")
return $"(StringView){value}";
if (typeInfo.IsPtr && typeInfo.IsConst && typeInfo.Type == "Char")
return $"((StringView){value}).GetText()";
return $"((StringView){value}).GetNonTerminatedText()"; // (StringView)Variant, if not empty, is guaranteed to point to a null-terminated buffer.
if (typeInfo.Type == "AssetReference" || typeInfo.Type == "WeakAssetReference")
return $"ScriptingObject::Cast<{typeInfo.GenericArgs[0].Type}>((Asset*){value})";
if (typeInfo.Type == "ScriptingObjectReference" || typeInfo.Type == "SoftObjectReference")

View File

@@ -42,6 +42,11 @@ namespace Flax.Build
/// </summary>
public bool BuildCSharp = true;
/// <summary>
/// True if module can be deployed.
/// </summary>
public bool Deploy = true;
/// <summary>
/// Initializes a new instance of the <see cref="Module"/> class.
/// </summary>

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
@@ -76,6 +76,11 @@ namespace Flax.Build.NativeCpp
/// </summary>
public bool GenerateWindowsMetadata = false;
/// <summary>
/// Use CONSOLE subsystem on Windows instead of the WINDOWS one.
/// </summary>
public bool LinkAsConsoleProgram = false;
/// <summary>
/// Enables documentation generation.
/// </summary>
@@ -114,6 +119,7 @@ namespace Flax.Build.NativeCpp
LinkTimeCodeGeneration = LinkTimeCodeGeneration,
UseIncrementalLinking = UseIncrementalLinking,
GenerateWindowsMetadata = GenerateWindowsMetadata,
LinkAsConsoleProgram = LinkAsConsoleProgram,
GenerateDocumentation = GenerateDocumentation
};
foreach (var e in InputFiles)

View File

@@ -86,6 +86,8 @@ namespace Flax.Deploy
var files = new List<string>();
foreach (var module in rules.Modules)
{
if (!module.Deploy)
continue;
module.GetFilesToDeploy(files);
files.Add(module.FilePath);
foreach (var file in files)

View File

@@ -675,7 +675,14 @@ namespace Flax.Build.Platforms
}
// Specify subsystem
args.Add("/SUBSYSTEM:WINDOWS");
if (linkEnvironment.LinkAsConsoleProgram)
{
args.Add("/SUBSYSTEM:CONSOLE");
}
else
{
args.Add("/SUBSYSTEM:WINDOWS");
}
// Generate Windows Metadata
if (linkEnvironment.GenerateWindowsMetadata)

View File

@@ -47,15 +47,15 @@ namespace FlaxEngine.Tests
{
ObjectOne obj = new ObjectOne();
Assert.AreEqual("{\n\t\"MyValue\": 0.0,\n\t\"MyVector\": {\n\t\t\"X\": 0.0,\n\t\t\"Y\": 0.0\n\t}\n}", FilterLineBreak(JsonSerializer.Serialize(obj)));
Assert.AreEqual("{\n\t\"MyValue\": 0.0,\n\t\"MyVector\": {\n\t\t\"X\": 0.0,\n\t\t\"Y\": 0.0\n\t},\n\t\"MyArray\": null\n}", FilterLineBreak(JsonSerializer.Serialize(obj)));
obj.MyValue = 1.2f;
Assert.AreEqual("{\n\t\"MyValue\": 1.2,\n\t\"MyVector\": {\n\t\t\"X\": 0.0,\n\t\t\"Y\": 0.0\n\t}\n}", FilterLineBreak(JsonSerializer.Serialize(obj)));
Assert.AreEqual("{\n\t\"MyValue\": 1.2,\n\t\"MyVector\": {\n\t\t\"X\": 0.0,\n\t\t\"Y\": 0.0\n\t},\n\t\"MyArray\": null\n}", FilterLineBreak(JsonSerializer.Serialize(obj)));
obj.MyVector.Y = 2.0f;
Assert.AreEqual("{\n\t\"MyValue\": 1.2,\n\t\"MyVector\": {\n\t\t\"X\": 0.0,\n\t\t\"Y\": 2.0\n\t}\n}", FilterLineBreak(JsonSerializer.Serialize(obj)));
Assert.AreEqual("{\n\t\"MyValue\": 1.2,\n\t\"MyVector\": {\n\t\t\"X\": 0.0,\n\t\t\"Y\": 2.0\n\t},\n\t\"MyArray\": null\n}", FilterLineBreak(JsonSerializer.Serialize(obj)));
obj.MyArray = new[]
{