From 420ec2433af0e2fbeb3543eddbd4d54e7e3a55aa Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 7 Apr 2023 01:21:56 +0100 Subject: [PATCH] feat: add Component move/copy As usual, experimental API - subject to change. --- CHANGELOG.md | 2 + .../Assets/Tests/ComponentTests.cs | 132 ++++++++++- .../Assets/Tests/GameObjectTests.cs | 197 ++++++++++++++++ X10D.Unity/X10D.Unity.csproj | 15 ++ X10D.Unity/src/ComponentExtensions.cs | 214 +++++++++++++++++- X10D.Unity/src/ExceptionMessages.Designer.cs | 80 +++++++ X10D.Unity/src/ExceptionMessages.resx | 34 +++ X10D.Unity/src/GameObjectExtensions.cs | 180 +++++++++++++++ 8 files changed, 852 insertions(+), 2 deletions(-) create mode 100644 X10D.Unity/src/ExceptionMessages.Designer.cs create mode 100644 X10D.Unity/src/ExceptionMessages.resx diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de5308..343b6dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - X10D: Added `TextWriter.WriteLineNoAlloc(uint[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(long[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan[, IFormatProvider]])`. +- X10D.Unity: Added `Component.CopyTo(GameObject)` and `Component.MoveTo(GameObject)`. +- X10D.Unity: Added `GameObject.CopyComponent(GameObject)` and `GameObject.MoveComponent(GameObject)`. ### Changed - X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`. diff --git a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs index e6c1431..615f16e 100644 --- a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs @@ -1,14 +1,77 @@ #nullable enable +using System; using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using Object = UnityEngine.Object; namespace X10D.Unity.Tests { public class ComponentTests { + [UnityTest] + public IEnumerator CopyTo_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10.0f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + sourceComponent.CopyTo(target); + + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyTo_ShouldThrowArgumentNullException_GivenNullComponent() + { + var target = new GameObject(); + Rigidbody rigidbody = null!; + + Assert.Throws(() => rigidbody.CopyTo(target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyTo_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + GameObject target = null!; + + Assert.Throws(() => rigidbody.CopyTo(target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator CopyTo_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => rigidbody.CopyTo(target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + [UnityTest] public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() { @@ -16,13 +79,80 @@ namespace X10D.Unity.Tests var rigidbody = parent.AddComponent(); var child = new GameObject(); - child.transform.SetParent(parent.transform); child.AddComponent(); + yield return null; + Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly(); Assert.That(components.Length, Is.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); + Object.Destroy(parent); + Object.Destroy(child); + } + + [UnityTest] + public IEnumerator MoveTo_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + sourceComponent.MoveTo(target); + + // effects of Destroy only take place at end of frame + yield return null; + + Assert.That(sourceComponent == null); + Assert.That(source.TryGetComponent(out Rigidbody _), Is.False); + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + } + + [UnityTest] + public IEnumerator MoveTo_ShouldThrowArgumentNullException_GivenNullComponent() + { + var target = new GameObject(); + Rigidbody rigidbody = null!; + + Assert.Throws(() => rigidbody.MoveTo(target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveTo_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + GameObject target = null!; + + Assert.Throws(() => rigidbody.MoveTo(target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator MoveTo_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => rigidbody.MoveTo(target)); + + Object.Destroy(source); + Object.Destroy(target); yield break; } } diff --git a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs index fef5a66..10ab455 100644 --- a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs @@ -1,14 +1,107 @@ #nullable enable +using System; using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using Object = UnityEngine.Object; namespace X10D.Unity.Tests { public class GameObjectTests { + [UnityTest] + public IEnumerator CopyComponent_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10.0f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + source.CopyComponent(target); + + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullComponentType() + { + var source = new GameObject(); + var target = new GameObject(); + Type componentType = null!; + + Assert.Throws(() => source.CopyComponent(componentType, target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullGameObject() + { + var target = new GameObject(); + GameObject source = null!; + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + GameObject target = null!; + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowInvalidOperationException_GivenInvalidComponent() + { + var source = new GameObject(); + var target = new GameObject(); + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + [UnityTest] public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() { @@ -23,6 +116,8 @@ namespace X10D.Unity.Tests Assert.That(components.Length, Is.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); + Object.Destroy(parent); + Object.Destroy(child); yield break; } @@ -58,6 +153,103 @@ namespace X10D.Unity.Tests first.LookAt(Vector3.right); Assert.That(firstTransform.rotation, Is.EqualTo(expected)); + Object.Destroy(first); + Object.Destroy(second); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10.0f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + source.MoveComponent(target); + + // effects of Destroy only take place at end of frame + yield return null; + + Assert.That(sourceComponent == null); + Assert.That(source.TryGetComponent(out Rigidbody _), Is.False); + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullComponentType() + { + var source = new GameObject(); + var target = new GameObject(); + Type componentType = null!; + + Assert.Throws(() => source.MoveComponent(componentType, target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullGameObject() + { + var target = new GameObject(); + GameObject source = null!; + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + GameObject target = null!; + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowInvalidOperationException_GivenInvalidComponent() + { + var source = new GameObject(); + var target = new GameObject(); + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); yield break; } @@ -82,6 +274,9 @@ namespace X10D.Unity.Tests Assert.That(child.layer, Is.EqualTo(layer)); Assert.That(grandChild.layer, Is.EqualTo(layer)); + Object.Destroy(parent); + Object.Destroy(child); + Object.Destroy(grandChild); yield break; } @@ -103,6 +298,8 @@ namespace X10D.Unity.Tests second.SetParent(first); Assert.That(second.transform.parent, Is.EqualTo(first.transform)); + Object.Destroy(first); + Object.Destroy(second); yield break; } } diff --git a/X10D.Unity/X10D.Unity.csproj b/X10D.Unity/X10D.Unity.csproj index f99bf2e..90caf27 100644 --- a/X10D.Unity/X10D.Unity.csproj +++ b/X10D.Unity/X10D.Unity.csproj @@ -70,4 +70,19 @@ + + + ResXFileCodeGenerator + ExceptionMessages.Designer.cs + + + + + + True + True + ExceptionMessages.resx + + + diff --git a/X10D.Unity/src/ComponentExtensions.cs b/X10D.Unity/src/ComponentExtensions.cs index 6ad7388..93afe4d 100644 --- a/X10D.Unity/src/ComponentExtensions.cs +++ b/X10D.Unity/src/ComponentExtensions.cs @@ -1,4 +1,7 @@ -using UnityEngine; +using System.Globalization; +using System.Reflection; +using UnityEngine; +using Object = UnityEngine.Object; namespace X10D.Unity; @@ -7,6 +10,95 @@ namespace X10D.Unity; /// public static class ComponentExtensions { + /// + /// Copies the component to another game object. + /// + /// The component to copy. + /// The game object to which the component will be copied. + /// The type of the component to copy. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void CopyTo(this T component, GameObject target) + where T : Component + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (target.TryGetComponent(out T targetComponent)) + { + string message = ExceptionMessages.ComponentAlreadyExists; + message = string.Format(CultureInfo.CurrentCulture, message, target.name, typeof(T).Name); + throw new InvalidOperationException(message); + } + + targetComponent = target.AddComponent(); + + var typeInfo = typeof(T).GetTypeInfo(); + CopyFields(typeInfo, component, targetComponent); + CopyProperties(typeInfo, component, targetComponent); + } + + /// + /// Copies the component to another game object. + /// + /// The component to copy. + /// The game object to which the component will be copied. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of the same type. + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void CopyTo(this Component component, GameObject target) + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + var componentType = component.GetType(); + if (target.TryGetComponent(componentType, out Component targetComponent)) + { + string message = ExceptionMessages.ComponentAlreadyExists; + message = string.Format(CultureInfo.CurrentCulture, message, target.name, componentType.Name); + throw new InvalidOperationException(message); + } + + targetComponent = target.AddComponent(componentType); + + var typeInfo = componentType.GetTypeInfo(); + CopyFields(typeInfo, component, targetComponent); + CopyProperties(typeInfo, component, targetComponent); + } + /// /// Returns an array of components of the specified type, excluding components that live on the object to which this /// component is attached. @@ -18,4 +110,124 @@ public static class ComponentExtensions { return component.gameObject.GetComponentsInChildrenOnly(); } + + /// + /// Moves the component to another game object. + /// + /// The component to move. + /// The game object to which the component will be moved. + /// The type of the component to move. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveTo(this T component, GameObject target) + where T : Component + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + component.CopyTo(target); + Object.Destroy(component); + } + + /// + /// Moves the component to another game object. + /// + /// The component to move. + /// The game object to which the component will be moved. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of the same type. + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveTo(this Component component, GameObject target) + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + component.CopyTo(target); + Object.Destroy(component); + } + + private static void CopyFields(TypeInfo typeInfo, T component, T targetComponent) + where T : Component + { + foreach (FieldInfo field in typeInfo.DeclaredFields) + { + if (field.IsStatic) + { + continue; + } + + object fieldValue = GetNewReferences(component, targetComponent, field.GetValue(component)); + field.SetValue(targetComponent, fieldValue); + } + } + + private static void CopyProperties(TypeInfo typeInfo, T component, T targetComponent) + where T : Component + { + foreach (PropertyInfo property in typeInfo.DeclaredProperties) + { + if (!property.CanRead || !property.CanWrite) + { + continue; + } + + MethodInfo getMethod = property.GetMethod; + MethodInfo setMethod = property.SetMethod; + if (getMethod.IsStatic || setMethod.IsStatic) + { + continue; + } + + object propertyValue = GetNewReferences(component, targetComponent, property.GetValue(component)); + property.SetValue(targetComponent, propertyValue); + } + } + + private static object GetNewReferences(T component, T targetComponent, object value) + where T : Component + { + if (ReferenceEquals(value, component)) + { + value = targetComponent; + } + else if (ReferenceEquals(value, component.gameObject)) + { + value = targetComponent.gameObject; + } + + return value; + } } diff --git a/X10D.Unity/src/ExceptionMessages.Designer.cs b/X10D.Unity/src/ExceptionMessages.Designer.cs new file mode 100644 index 0000000..954a74a --- /dev/null +++ b/X10D.Unity/src/ExceptionMessages.Designer.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace X10D.Unity { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ExceptionMessages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ExceptionMessages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("X10D.Unity.src.ExceptionMessages", typeof(ExceptionMessages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The game object {0} already has a component of type {1}.. + /// + internal static string ComponentAlreadyExists { + get { + return ResourceManager.GetString("ComponentAlreadyExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The game object {0} does not have a component of type {1}.. + /// + internal static string ComponentDoesNotExist { + get { + return ResourceManager.GetString("ComponentDoesNotExist", resourceCulture); + } + } + } +} diff --git a/X10D.Unity/src/ExceptionMessages.resx b/X10D.Unity/src/ExceptionMessages.resx new file mode 100644 index 0000000..ec27743 --- /dev/null +++ b/X10D.Unity/src/ExceptionMessages.resx @@ -0,0 +1,34 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + + The game object {0} does not have a component of type {1}. + + + + The game object {0} already has a component of type {1}. + + \ No newline at end of file diff --git a/X10D.Unity/src/GameObjectExtensions.cs b/X10D.Unity/src/GameObjectExtensions.cs index 97d03d3..1ede330 100644 --- a/X10D.Unity/src/GameObjectExtensions.cs +++ b/X10D.Unity/src/GameObjectExtensions.cs @@ -1,4 +1,6 @@ +using System.Globalization; using UnityEngine; +using Object = UnityEngine.Object; namespace X10D.Unity; @@ -7,6 +9,90 @@ namespace X10D.Unity; /// public static class GameObjectExtensions { + /// + /// Copies the component of the specified type from one game object to another. + /// + /// The game object from which to copy the component. + /// The game object to which the component will be copied. + /// The type of the component to copy. + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + public static void CopyComponent(this GameObject gameObject, GameObject target) + where T : Component + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(out T sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, typeof(T).Name); + throw new InvalidOperationException(message); + } + + sourceComponent.CopyTo(target); + } + + /// + /// Copies the component of the specified type from one game object to another. + /// + /// The game object from which to copy the component. + /// The type of the component to copy. + /// The game object to which the component will be copied. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + public static void CopyComponent(this GameObject gameObject, Type componentType, GameObject target) + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(componentType, out Component sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, componentType.Name); + throw new InvalidOperationException(message); + } + + sourceComponent.CopyTo(target); + } + /// /// Returns an array of components of the specified type, excluding components that live on this game object. /// @@ -171,6 +257,100 @@ public static class GameObjectExtensions gameObject.transform.LookAt(target, worldUp); } + /// + /// Moves the component of the specified type from one game object to another. + /// + /// The game object from which to move the component. + /// The game object to which the component will be moved. + /// The type of the component to copy. + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveComponent(this GameObject gameObject, GameObject target) + where T : Component + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(out T sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, typeof(T).Name); + throw new InvalidOperationException(message); + } + + sourceComponent.MoveTo(target); + Object.Destroy(sourceComponent); + } + + /// + /// Moves the component of the specified type from one game object to another. + /// + /// The game object from which to move the component. + /// The type of the component to copy. + /// The game object to which the component will be moved. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveComponent(this GameObject gameObject, Type componentType, GameObject target) + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(componentType, out Component sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, componentType.Name); + throw new InvalidOperationException(message); + } + + sourceComponent.MoveTo(target); + Object.Destroy(sourceComponent); + } + /// /// Sets the new layer of this game object and its children, recursively. ///