diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..98ed710 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 05b5b68dce3744da595c577a5ddd9c6e +folderAsset: yes +timeCreated: 1518172502 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/EcsEntity.cs b/Editor/EcsEntity.cs new file mode 100644 index 0000000..3496a16 --- /dev/null +++ b/Editor/EcsEntity.cs @@ -0,0 +1,123 @@ +// ---------------------------------------------------------------------------- +// The MIT License +// UnityEditor integration https://github.com/Leopotam/ecslite-unityeditor +// for LeoECS Lite https://github.com/Leopotam/ecslite +// Copyright (c) 2021 Leopotam +// ---------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace Leopotam.EcsLite.UnityEditor { + [CustomEditor (typeof (EcsEntityObserver))] + sealed class EcsEntityObserverInspector : Editor { + const int MaxFieldToStringLength = 128; + + static object[] _componentsCache = new object[32]; + + EcsEntityObserver _observer; + + public override void OnInspectorGUI () { + if (_observer.World != null) { + var guiEnabled = GUI.enabled; + GUI.enabled = true; + DrawComponents (); + GUI.enabled = guiEnabled; + EditorUtility.SetDirty (target); + } + } + + void OnEnable () { + _observer = target as EcsEntityObserver; + } + + void OnDisable () { + _observer = null; + } + + void DrawComponents () { + if (_observer.gameObject.activeSelf) { + var count = _observer.World.GetComponents (_observer.Entity, ref _componentsCache); + for (var i = 0; i < count; i++) { + var component = _componentsCache[i]; + _componentsCache[i] = null; + var type = component.GetType (); + GUILayout.BeginVertical (GUI.skin.box); + var typeName = EditorHelpers.GetCleanGenericTypeName (type); + if (!EcsComponentInspectors.Render (typeName, type, component, _observer)) { + EditorGUILayout.LabelField (typeName, EditorStyles.boldLabel); + var indent = EditorGUI.indentLevel; + EditorGUI.indentLevel++; + foreach (var field in type.GetFields (BindingFlags.Instance | BindingFlags.Public)) { + DrawTypeField (component, field, _observer); + } + EditorGUI.indentLevel = indent; + } + GUILayout.EndVertical (); + EditorGUILayout.Space (); + } + } + } + + void DrawTypeField (object instance, FieldInfo field, EcsEntityObserver entity) { + var fieldValue = field.GetValue (instance); + var fieldType = field.FieldType; + if (!EcsComponentInspectors.Render (field.Name, fieldType, fieldValue, entity)) { + if (fieldType == typeof (UnityEngine.Object) || fieldType.IsSubclassOf (typeof (UnityEngine.Object))) { + GUILayout.BeginHorizontal (); + EditorGUILayout.LabelField (field.Name, GUILayout.MaxWidth (EditorGUIUtility.labelWidth - 16)); + var guiEnabled = GUI.enabled; + GUI.enabled = false; + EditorGUILayout.ObjectField (fieldValue as UnityEngine.Object, fieldType, false); + GUI.enabled = guiEnabled; + GUILayout.EndHorizontal (); + return; + } + var strVal = fieldValue != null ? string.Format (System.Globalization.CultureInfo.InvariantCulture, "{0}", fieldValue) : "null"; + if (strVal.Length > MaxFieldToStringLength) { + strVal = strVal.Substring (0, MaxFieldToStringLength); + } + GUILayout.BeginHorizontal (); + EditorGUILayout.LabelField (field.Name, GUILayout.MaxWidth (EditorGUIUtility.labelWidth - 16)); + EditorGUILayout.SelectableLabel (strVal, GUILayout.MaxHeight (EditorGUIUtility.singleLineHeight)); + GUILayout.EndHorizontal (); + } + } + } + + static class EcsComponentInspectors { + static readonly Dictionary Inspectors = new Dictionary (); + + static EcsComponentInspectors () { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies ()) { + foreach (var type in assembly.GetTypes ()) { + if (typeof (IEcsComponentInspector).IsAssignableFrom (type) && !type.IsInterface) { + if (Activator.CreateInstance (type) is IEcsComponentInspector inspector) { + var componentType = inspector.GetFieldType (); + if (Inspectors.ContainsKey (componentType)) { + Debug.LogWarningFormat ("Inspector for \"{0}\" already exists, new inspector will be used instead.", componentType.Name); + } + Inspectors[componentType] = inspector; + } + } + } + } + } + + public static bool Render (string label, Type type, object value, EcsEntityObserver observer) { + if (Inspectors.TryGetValue (type, out var inspector)) { + inspector.OnGUI (label, value, observer.World, observer.Entity); + return true; + } + return false; + } + } + + public interface IEcsComponentInspector { + Type GetFieldType (); + void OnGUI (string label, object value, EcsWorld world, int entityId); + } +} \ No newline at end of file diff --git a/Editor/EcsEntity.cs.meta b/Editor/EcsEntity.cs.meta new file mode 100644 index 0000000..768f0e6 --- /dev/null +++ b/Editor/EcsEntity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3702a75f6bda8480db007e5744ca520d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/EcsWorld.cs b/Editor/EcsWorld.cs new file mode 100644 index 0000000..276b768 --- /dev/null +++ b/Editor/EcsWorld.cs @@ -0,0 +1,28 @@ +// ---------------------------------------------------------------------------- +// The MIT License +// UnityEditor integration https://github.com/Leopotam/ecslite-unityeditor +// for LeoECS Lite https://github.com/Leopotam/ecslite +// Copyright (c) 2021 Leopotam +// ---------------------------------------------------------------------------- + +using UnityEditor; +using UnityEngine; + +namespace Leopotam.EcsLite.UnityEditor { + [CustomEditor (typeof (EcsWorldObserver))] + sealed class EcsWorldObserverInspector : Editor { + public override void OnInspectorGUI () { + // var observer = (Runtime.EcsWorldObserver) target; + // var stats = observer.GetStats (); + var guiEnabled = GUI.enabled; + GUI.enabled = true; + GUILayout.BeginVertical (GUI.skin.box); + // EditorGUILayout.LabelField ("Components", stats.Components.ToString ()); + // EditorGUILayout.LabelField ("Filters", stats.Filters.ToString ()); + // EditorGUILayout.LabelField ("Active entities", stats.ActiveEntities.ToString ()); + // EditorGUILayout.LabelField ("Reserved entities", stats.ReservedEntities.ToString ()); + GUILayout.EndVertical (); + GUI.enabled = guiEnabled; + } + } +} \ No newline at end of file diff --git a/Editor/EcsWorld.cs.meta b/Editor/EcsWorld.cs.meta new file mode 100644 index 0000000..d5973ba --- /dev/null +++ b/Editor/EcsWorld.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8f89072c4ea643e3bc3fc36cb0c1d41 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Inspectors.meta b/Editor/Inspectors.meta new file mode 100644 index 0000000..dcaf95d --- /dev/null +++ b/Editor/Inspectors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e152c3772dfd34cb69092047cf0e5ab6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Inspectors/Quaternion.cs b/Editor/Inspectors/Quaternion.cs new file mode 100644 index 0000000..c3ee647 --- /dev/null +++ b/Editor/Inspectors/Quaternion.cs @@ -0,0 +1,22 @@ +// ---------------------------------------------------------------------------- +// The MIT License +// UnityEditor integration https://github.com/Leopotam/ecslite-unityeditor +// for LeoECS Lite https://github.com/Leopotam/ecslite +// Copyright (c) 2021 Leopotam +// ---------------------------------------------------------------------------- + +using System; +using UnityEditor; +using UnityEngine; + +namespace Leopotam.EcsLite.UnityEditor.Inspectors { + sealed class QuaternionInspector : IEcsComponentInspector { + public Type GetFieldType () { + return typeof (Quaternion); + } + + public void OnGUI (string label, object value, EcsWorld world, int entityId) { + EditorGUILayout.Vector3Field (label, ((Quaternion) value).eulerAngles); + } + } +} \ No newline at end of file diff --git a/Editor/Inspectors/Quaternion.cs.meta b/Editor/Inspectors/Quaternion.cs.meta new file mode 100644 index 0000000..22c4585 --- /dev/null +++ b/Editor/Inspectors/Quaternion.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 270e8b51370ab4160bc5abfda6daa6d7 +timeCreated: 1519547455 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Inspectors/Vector3.cs b/Editor/Inspectors/Vector3.cs new file mode 100644 index 0000000..8c7b96d --- /dev/null +++ b/Editor/Inspectors/Vector3.cs @@ -0,0 +1,22 @@ +// ---------------------------------------------------------------------------- +// The MIT License +// UnityEditor integration https://github.com/Leopotam/ecslite-unityeditor +// for LeoECS Lite https://github.com/Leopotam/ecslite +// Copyright (c) 2021 Leopotam +// ---------------------------------------------------------------------------- + +using System; +using UnityEditor; +using UnityEngine; + +namespace Leopotam.EcsLite.UnityEditor.Inspectors { + sealed class Vector3Inspector : IEcsComponentInspector { + public Type GetFieldType () { + return typeof (Vector3); + } + + public void OnGUI (string label, object value, EcsWorld world, int entityId) { + EditorGUILayout.Vector3Field (label, (Vector3) value); + } + } +} \ No newline at end of file diff --git a/Editor/Inspectors/Vector3.cs.meta b/Editor/Inspectors/Vector3.cs.meta new file mode 100644 index 0000000..8cc792a --- /dev/null +++ b/Editor/Inspectors/Vector3.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 9936e55c585ee4a63829034ebba25436 +timeCreated: 1519547455 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Leopotam.EcsLite.UnityEditor.asmdef b/Editor/Leopotam.EcsLite.UnityEditor.asmdef new file mode 100644 index 0000000..5c1bf87 --- /dev/null +++ b/Editor/Leopotam.EcsLite.UnityEditor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "Leopotam.EcsLite.UnityEditor", + "rootNamespace": "Leopotam.EcsLite.UnityEditor", + "references": [ + "Leopotam.EcsLite", + "Leopotam.EcsLite.UnityEditor.Runtime" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/Leopotam.EcsLite.UnityEditor.asmdef.meta b/Editor/Leopotam.EcsLite.UnityEditor.asmdef.meta new file mode 100644 index 0000000..c433088 --- /dev/null +++ b/Editor/Leopotam.EcsLite.UnityEditor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 206779980ac58453ea65e900299ccf71 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Templates.meta b/Editor/Templates.meta new file mode 100644 index 0000000..c7bcb81 --- /dev/null +++ b/Editor/Templates.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b502615d7194c4cd7aa5e212cba5bd14 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Templates/Component.cs.txt b/Editor/Templates/Component.cs.txt new file mode 100644 index 0000000..aba1f3f --- /dev/null +++ b/Editor/Templates/Component.cs.txt @@ -0,0 +1,5 @@ +namespace #NS# { + struct #SCRIPTNAME# { + // add your data here. + } +} \ No newline at end of file diff --git a/Editor/Templates/Component.cs.txt.meta b/Editor/Templates/Component.cs.txt.meta new file mode 100644 index 0000000..e12b712 --- /dev/null +++ b/Editor/Templates/Component.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a8d4f71016aa84921ae340348b049655 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Templates/InitSystem.cs.txt b/Editor/Templates/InitSystem.cs.txt new file mode 100644 index 0000000..ef75c8a --- /dev/null +++ b/Editor/Templates/InitSystem.cs.txt @@ -0,0 +1,9 @@ +using Leopotam.EcsLite; + +namespace #NS# { + sealed class #SCRIPTNAME# : IEcsInitSystem { + public void Init (EcsSystems systems) { + // add your initialize code here. + } + } +} \ No newline at end of file diff --git a/Editor/Templates/InitSystem.cs.txt.meta b/Editor/Templates/InitSystem.cs.txt.meta new file mode 100644 index 0000000..c2f33db --- /dev/null +++ b/Editor/Templates/InitSystem.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b5173d52d29e24dcf906a03bf9b2ba04 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Templates/RunSystem.cs.txt b/Editor/Templates/RunSystem.cs.txt new file mode 100644 index 0000000..bc40701 --- /dev/null +++ b/Editor/Templates/RunSystem.cs.txt @@ -0,0 +1,9 @@ +using Leopotam.EcsLite; + +namespace #NS# { + sealed class #SCRIPTNAME# : IEcsRunSystem { + public void Run (EcsSystems systems) { + // add your run code here. + } + } +} \ No newline at end of file diff --git a/Editor/Templates/RunSystem.cs.txt.meta b/Editor/Templates/RunSystem.cs.txt.meta new file mode 100644 index 0000000..ea8b266 --- /dev/null +++ b/Editor/Templates/RunSystem.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d21889d687d534eb18075425156ae69c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Templates/Startup.cs.txt b/Editor/Templates/Startup.cs.txt new file mode 100644 index 0000000..509931e --- /dev/null +++ b/Editor/Templates/Startup.cs.txt @@ -0,0 +1,49 @@ +using Leopotam.EcsLite; +using UnityEngine; + +namespace #NS# { + sealed class #SCRIPTNAME# : MonoBehaviour { + EcsWorld _world; + EcsSystems _systems; + + void Start () { + _world = new EcsWorld (); + // register your shared data here, for example: + // var shared = new Shared (); + // systems = new EcsSystems (world, shared); + _systems = new EcsSystems (_world); +#if UNITY_EDITOR + Leopotam.EcsLite.UnityEditor.EcsWorldObserver.Create (_world); +#endif + _systems + // register your systems here, for example: + // .Add (new TestSystem1 ()) + // .Add (new TestSystem2 ()) + + // register additional worlds here, for example: + // .AddWorld (new EcsWorld (), "events") + + // register "delete-here" helpers here + // for components cleanup (order is important), for example: + // .DelHere () + // .DelHere () + + .Init (); + } + + void Update () { + _systems?.Run (); + } + + void OnDestroy () { + if (_systems != null) { + _systems.Destroy (); + _systems = null; + } + if (_world != null) { + _world.Destroy (); + _world = null; + } + } + } +} \ No newline at end of file diff --git a/Editor/Templates/Startup.cs.txt.meta b/Editor/Templates/Startup.cs.txt.meta new file mode 100644 index 0000000..4905ba8 --- /dev/null +++ b/Editor/Templates/Startup.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9c5b2b3dee7e241559a07978175eca75 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Templates/TemplateGenerator.cs b/Editor/Templates/TemplateGenerator.cs new file mode 100644 index 0000000..aef57b0 --- /dev/null +++ b/Editor/Templates/TemplateGenerator.cs @@ -0,0 +1,152 @@ +// ---------------------------------------------------------------------------- +// The MIT License +// UnityEditor integration https://github.com/Leopotam/ecslite-unityeditor +// for LeoECS Lite https://github.com/Leopotam/ecslite +// Copyright (c) 2021 Leopotam +// ---------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEditor.ProjectWindowCallback; +using UnityEngine; + +namespace Leopotam.EcsLite.UnityEditor.Templates { + sealed class TemplateGenerator : ScriptableObject { + const string Title = "LeoECS Lite template generator"; + + const string StartupTemplate = "Startup.cs.txt"; + const string InitSystemTemplate = "InitSystem.cs.txt"; + const string RunSystemTemplate = "RunSystem.cs.txt"; + const string ComponentTemplate = "Component.cs.txt"; + + [MenuItem ("Assets/Create/LeoECS Lite/Create Startup from template", false, -200)] + static void CreateStartupTpl () { + var assetPath = GetAssetPath (); + CreateAndRenameAsset ($"{assetPath}/EcsStartup.cs", GetIcon (), (name) => { + if (CreateTemplateInternal (GetTemplateContent (StartupTemplate), name) == null) { + if (EditorUtility.DisplayDialog (Title, "Create data folders?", "Yes", "No")) { + CreateEmptyFolder ($"{assetPath}/Components"); + CreateEmptyFolder ($"{assetPath}/Systems"); + CreateEmptyFolder ($"{assetPath}/Views"); + CreateEmptyFolder ($"{assetPath}/Services"); + AssetDatabase.Refresh (); + } + } + }); + } + + static void CreateEmptyFolder (string folderPath) { + if (!Directory.Exists (folderPath)) { + try { + Directory.CreateDirectory (folderPath); + File.Create ($"{folderPath}/.gitkeep"); + } catch { + // ignored + } + } + } + + [MenuItem ("Assets/Create/LeoECS Lite/Systems/Create InitSystem from template", false, -199)] + static void CreateInitSystemTpl () { + CreateAndRenameAsset ($"{GetAssetPath ()}/EcsInitSystem.cs", GetIcon (), + (name) => CreateTemplateInternal (GetTemplateContent (InitSystemTemplate), name)); + } + + [MenuItem ("Assets/Create/LeoECS Lite/Systems/Create RunSystem from template", false, -198)] + static void CreateRunSystemTpl () { + CreateAndRenameAsset ($"{GetAssetPath ()}/EcsRunSystem.cs", GetIcon (), + (name) => CreateTemplateInternal (GetTemplateContent (RunSystemTemplate), name)); + } + + [MenuItem ("Assets/Create/LeoECS Lite/Components/Create Component from template", false, -197)] + static void CreateComponentTpl () { + CreateAndRenameAsset ($"{GetAssetPath ()}/EcsComponent.cs", GetIcon (), + (name) => CreateTemplateInternal (GetTemplateContent (ComponentTemplate), name)); + } + + public static string CreateTemplate (string proto, string fileName) { + if (string.IsNullOrEmpty (fileName)) { + return "Invalid filename"; + } + var ns = EditorSettings.projectGenerationRootNamespace.Trim (); + if (string.IsNullOrEmpty (EditorSettings.projectGenerationRootNamespace)) { + ns = "Client"; + } + proto = proto.Replace ("#NS#", ns); + proto = proto.Replace ("#SCRIPTNAME#", SanitizeClassName (Path.GetFileNameWithoutExtension (fileName))); + try { + File.WriteAllText (AssetDatabase.GenerateUniqueAssetPath (fileName), proto); + } catch (Exception ex) { + return ex.Message; + } + AssetDatabase.Refresh (); + return null; + } + + static string SanitizeClassName (string className) { + var sb = new StringBuilder (); + var needUp = true; + foreach (var c in className) { + if (char.IsLetterOrDigit (c)) { + sb.Append (needUp ? char.ToUpperInvariant (c) : c); + needUp = false; + } else { + needUp = true; + } + } + return sb.ToString (); + } + + static string CreateTemplateInternal (string proto, string fileName) { + var res = CreateTemplate (proto, fileName); + if (res != null) { + EditorUtility.DisplayDialog (Title, res, "Close"); + } + return res; + } + + static string GetTemplateContent (string proto) { + // hack: its only one way to get current editor script path. :( + var pathHelper = CreateInstance (); + var path = Path.GetDirectoryName (AssetDatabase.GetAssetPath (MonoScript.FromScriptableObject (pathHelper))); + DestroyImmediate (pathHelper); + try { + return File.ReadAllText (Path.Combine (path ?? "", proto)); + } catch { + return null; + } + } + + static string GetAssetPath () { + var path = AssetDatabase.GetAssetPath (Selection.activeObject); + if (!string.IsNullOrEmpty (path) && AssetDatabase.Contains (Selection.activeObject)) { + if (!AssetDatabase.IsValidFolder (path)) { + path = Path.GetDirectoryName (path); + } + } else { + path = "Assets"; + } + return path; + } + + static Texture2D GetIcon () { + return EditorGUIUtility.IconContent ("cs Script Icon").image as Texture2D; + } + + static void CreateAndRenameAsset (string fileName, Texture2D icon, Action onSuccess) { + var action = CreateInstance (); + action.Callback = onSuccess; + ProjectWindowUtil.StartNameEditingIfProjectWindowExists (0, action, fileName, icon, null); + } + + sealed class CustomEndNameAction : EndNameEditAction { + [NonSerialized] public Action Callback; + + public override void Action (int instanceId, string pathName, string resourceFile) { + Callback?.Invoke (pathName); + } + } + } +} \ No newline at end of file diff --git a/Editor/Templates/TemplateGenerator.cs.meta b/Editor/Templates/TemplateGenerator.cs.meta new file mode 100644 index 0000000..52d750f --- /dev/null +++ b/Editor/Templates/TemplateGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 03f7a60fd4716447e8daf44a95dcc230 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..387f290 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Leopotam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md.meta b/LICENSE.md.meta new file mode 100644 index 0000000..b1e0e80 --- /dev/null +++ b/LICENSE.md.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 3e6d50dd016d943f798f7a767ae00c20 +timeCreated: 1518307847 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa617a0 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# UnityEditor integration for LeoEcsLite C# Entity Component System framework +Unity editor integration for [LeoECS Lite](https://github.com/Leopotam/ecslite). + +> Tested on unity 2020.3 (dependent on it) and contains assembly definition for compiling to separate assembly file for performance reason. + +> **Important!** Don't forget that this module developed to work only inside unity editor and under `UNITY_EDITOR` definition. + +# Table of content +* [Socials](#socials) +* [Installation](#installation) + * [As unity module](#as-unity-module) + * [As source](#as-source) +* [Integration](#integration) + * [From code](#from-code) + * [From UI](#from-ui) +* [License](#license) + +# Socials +[![discord](https://img.shields.io/discord/404358247621853185.svg?label=enter%20to%20discord%20server&style=for-the-badge&logo=discord)](https://discord.gg/5GZVde6) + +# Installation + +## As unity module +This repository can be installed as unity module directly from git url. In this way new line should be added to `Packages/manifest.json`: +``` +"com.leopotam.ecslite.unityeditor": "https://github.com/Leopotam/ecslite-unityeditor.git", +``` +By default last released version will be used. If you need trunk / developing version then `develop` name of branch should be added after hash: +``` +"com.leopotam.ecslite.unityeditor": "https://github.com/Leopotam/ecslite-unityeditor.git#develop", +``` + +## As source +If you can't / don't want to use unity modules, code can be cloned or downloaded as archive from `releases` page. + +# Integration + +## From code +```csharp +// ecs-startup code: +void Start () { + _world = new EcsWorld (); + _systems = new EcsSystems (_world); +#if UNITY_EDITOR + Leopotam.EcsLite.UnityEditor.EcsWorldObserver.Create (_world); +#endif + _systems + .Add (new TestSystem1 ()) + .Init (); +} +``` + +## From UI +Some code can be generated automatically from unity editor main menu "Assets / Create / LeoECS Lite". + +# License +The software is released under the terms of the [MIT license](./LICENSE.md). + +No personal support or any guarantees. diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..0702c3e --- /dev/null +++ b/README.md.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 00d71e0a512d84bd9b4cf770c76c0af3 +timeCreated: 1518307851 +licenseType: Free +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..3b0225d --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 1220013c14e314da8a23f4bf3e504394 +folderAsset: yes +timeCreated: 1518435832 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/EditorHelpers.cs b/Runtime/EditorHelpers.cs new file mode 100644 index 0000000..3b3bc64 --- /dev/null +++ b/Runtime/EditorHelpers.cs @@ -0,0 +1,189 @@ +// ---------------------------------------------------------------------------- +// The MIT License +// Unity integration https://github.com/Leopotam/ecs-unityintegration +// for ECS framework https://github.com/Leopotam/ecs +// Copyright (c) 2017-2021 Leopotam +// ---------------------------------------------------------------------------- + +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using UnityEngine; + +// ReSharper disable UnusedMethodReturnValue.Global +// ReSharper disable InconsistentNaming + +namespace Leopotam.EcsLite.UnityEditor { + public static class EditorHelpers { + public static string GetCleanGenericTypeName (Type type) { + if (!type.IsGenericType) { + return type.Name; + } + var constraints = ""; + foreach (var constraint in type.GetGenericArguments ()) { + constraints += constraints.Length > 0 ? $", {GetCleanGenericTypeName (constraint)}" : constraint.Name; + } + return $"{type.Name.Substring (0, type.Name.LastIndexOf ("`", StringComparison.Ordinal))}<{constraints}>"; + } + } + + public sealed class EcsEntityObserver : MonoBehaviour { + public EcsWorld World; + public int Entity; + } + + // public sealed class EcsSystemsObserver : MonoBehaviour, IEcsSystemsDebugListener { + // EcsSystems _systems; + // + // public static GameObject Create (EcsSystems systems) { + // if (systems == null) { throw new ArgumentNullException (nameof (systems)); } + // var go = new GameObject (systems.Name != null ? $"[ECS-SYSTEMS {systems.Name}]" : "[ECS-SYSTEMS]"); + // DontDestroyOnLoad (go); + // go.hideFlags = HideFlags.NotEditable; + // var observer = go.AddComponent (); + // observer._systems = systems; + // systems.AddDebugListener (observer); + // return go; + // } + // + // public EcsSystems GetSystems () { + // return _systems; + // } + // + // void OnDestroy () { + // if (_systems != null) { + // _systems.RemoveDebugListener (this); + // _systems = null; + // } + // } + // + // void IEcsSystemsDebugListener.OnSystemsDestroyed (EcsSystems systems) { + // // for immediate unregistering this MonoBehaviour from ECS. + // OnDestroy (); + // // for delayed destroying GameObject. + // Destroy (gameObject); + // } + // } + + public sealed class EcsWorldObserver : MonoBehaviour, IEcsWorldEventListener { + EcsWorld _world; + public readonly Dictionary EntityGameObjects = new Dictionary (1024); + static Type[] _componentTypesCache = new Type[32]; + + Transform _entitiesRoot; + // Transform _filtersRoot; + + public static GameObject Create (EcsWorld world, string name = null) { + if (world == null) { throw new ArgumentNullException (nameof (world)); } + var go = new GameObject (name != null ? $"[ECS-WORLD {name}]" : "[ECS-WORLD]"); + DontDestroyOnLoad (go); + go.hideFlags = HideFlags.NotEditable; + var observer = go.AddComponent (); + observer._world = world; + var worldTr = observer.transform; + // entities root. + observer._entitiesRoot = new GameObject ("Entities").transform; + observer._entitiesRoot.gameObject.hideFlags = HideFlags.NotEditable; + observer._entitiesRoot.SetParent (worldTr, false); + // filters root. + // observer._filtersRoot = new GameObject ("Filters").transform; + // observer._filtersRoot.gameObject.hideFlags = HideFlags.NotEditable; + // observer._filtersRoot.SetParent (worldTr, false); + // subscription to events. + world.AddEventListener (observer); + return go; + } + + // public EcsWorldStats GetStats () { + // return _world.GetStats (); + // } + + public void OnEntityCreated (int entity) { + if (!EntityGameObjects.TryGetValue (entity, out var go)) { + go = new GameObject (); + go.transform.SetParent (_entitiesRoot, false); + go.hideFlags = HideFlags.NotEditable; + var unityEntity = go.AddComponent (); + unityEntity.World = _world; + unityEntity.Entity = entity; + EntityGameObjects[entity] = go; + UpdateEntityName (entity, false); + } else { + // need to update cached entity generation. + go.GetComponent ().Entity = entity; + } + go.SetActive (true); + } + + public void OnEntityChanged (int entity) { + UpdateEntityName (entity, true); + } + + public void OnEntityDestroyed (int entity) { + if (!EntityGameObjects.TryGetValue (entity, out var go)) { + throw new Exception ("Unity visualization not exists, looks like a bug"); + } + UpdateEntityName (entity, false); + go.SetActive (false); + } + + public void OnFilterCreated (EcsFilter filter) { + // var go = new GameObject (); + // go.transform.SetParent (_filtersRoot); + // go.hideFlags = HideFlags.NotEditable; + // var observer = go.AddComponent (); + // observer.World = this; + // observer.Filter = filter; + // + // // included components. + // var goName = $"Inc<{filter.IncludedTypes[0].Name}"; + // for (var i = 1; i < filter.IncludedTypes.Length; i++) { + // goName += $",{filter.IncludedTypes[i].Name}"; + // } + // goName += ">"; + // // excluded components. + // if (filter.ExcludedTypes != null) { + // goName += $".Exc<{filter.ExcludedTypes[0].Name}"; + // for (var i = 1; i < filter.ExcludedTypes.Length; i++) { + // goName += $",{filter.ExcludedTypes[i].Name}"; + // } + // goName += ">"; + // } + // go.name = goName; + } + + public void OnWorldResized (int newSize) { } + + public void OnWorldDestroyed (EcsWorld world) { + // for immediate unregistering this MonoBehaviour from ECS. + OnDestroy (); + // for delayed destroying GameObject. + Destroy (gameObject); + } + + void UpdateEntityName (int entity, bool requestComponents) { + var entityName = entity.ToString ("X8"); + if (_world.GetEntityGen (entity) > 0 && requestComponents) { + var count = _world.GetComponentTypes (entity, ref _componentTypesCache); + for (var i = 0; i < count; i++) { + entityName = $"{entityName}:{EditorHelpers.GetCleanGenericTypeName (_componentTypesCache[i])}"; + _componentTypesCache[i] = null; + } + } + EntityGameObjects[entity].name = entityName; + } + + void OnDestroy () { + if (_world != null) { + _world.RemoveEventListener (this); + _world = null; + } + } + } + + // public sealed class EcsFilterObserver : MonoBehaviour { + // public EcsWorldObserver World; + // public EcsFilter Filter; + // } +} +#endif \ No newline at end of file diff --git a/Runtime/EditorHelpers.cs.meta b/Runtime/EditorHelpers.cs.meta new file mode 100644 index 0000000..e9f3bf1 --- /dev/null +++ b/Runtime/EditorHelpers.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 24d43f345c6cb4440964c581f6d2f589 +timeCreated: 1519891912 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Leopotam.EcsLite.UnityEditor.Runtime.asmdef b/Runtime/Leopotam.EcsLite.UnityEditor.Runtime.asmdef new file mode 100644 index 0000000..7a792d2 --- /dev/null +++ b/Runtime/Leopotam.EcsLite.UnityEditor.Runtime.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Leopotam.EcsLite.UnityEditor.Runtime", + "rootNamespace": "Leopotam.EcsLite.UnityEditor.Runtime", + "references": [ + "Leopotam.EcsLite" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Leopotam.EcsLite.UnityEditor.Runtime.asmdef.meta b/Runtime/Leopotam.EcsLite.UnityEditor.Runtime.asmdef.meta new file mode 100644 index 0000000..eb1afef --- /dev/null +++ b/Runtime/Leopotam.EcsLite.UnityEditor.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a22362b82293644e1bbe51356c008d63 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..bd01481 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "com.leopotam.ecslite.unityeditor", + "author": "Leopotam", + "displayName": "LeoECS Lite Unity Integration", + "description": "UnityEditor integration for LeoECS Lite.", + "unity": "2020.3", + "version": "2021.6.22-preview", + "keywords": [ + "leoecs", + "ecs", + "performance", + "unity", + "integration" + ], + "dependencies": {}, + "repository": { + "type": "git", + "url": "https://github.com/Leopotam/ecslite-unityeditor.git" + } +} \ No newline at end of file diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..8b9cfa0 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: df8491d880ea24c36bfc4af49b4421ee +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: