Skip to content

Commit

Permalink
feat(BehaviourStateRequirement): allow returning early in Behaviours
Browse files Browse the repository at this point in the history
In a `UnityEngine.Behaviour` or a type derived from it it is often
useful to return early while the Behaviour or the GameObject the
Behaviour is on are disabled/inactive. This new weaver takes care of
adding the necessary instructions to a method to return early based
on those two states.
  • Loading branch information
Christopher-Marcel Böddecker committed Jan 21, 2019
1 parent d11cc84 commit ee9955a
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 11 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Please follow these steps to install the package using a local location until Un
<Malimbe.FodyRunner>
<LogLevel>Error, Warning</LogLevel>
</Malimbe.FodyRunner>
<Malimbe.BehaviourStateRequirementMethod/>
<Malimbe.MemberClearanceMethod/>
<Malimbe.PropertySerializationAttribute/>
<Malimbe.PropertySetterMethod/>
Expand Down Expand Up @@ -74,6 +75,13 @@ Weaves assemblies using `FodyRunner` in the Unity Editor after Unity compiled th
* There is no need to manually run the weaving process. The library just needs to be part of a Unity project (it's configured to only run in the Editor) to be used. It hooks into the various callbacks Unity offers and automatically weaves any assembly on startup as well as when they change.
* Once the library is loaded in the Editor a menu item `Tools/Malimbe/Weave All Assemblies` allows to manually trigger the weaving process for all assemblies in the current project. This is useful when a `FodyWeavers.xml` file was changed.

### `BehaviourStateRequirementMethod`

A Unity-specific weaver. Changes a method to return early if a combination of the GameObject's active state and the Behaviour's enabled state doesn't match the configured state.

* Annotate a method with `[RequiresBehaviourState]` to use this. The method needs to be defined in a type that derives from `UnityEngine.Behaviour`, e.g. a `MonoBehaviour`.
* Use the attribute constructor's parameters to configure the specific state you need the GameObject and the Behaviour to be in.

### `MemberClearanceMethod.Fody`

A generic weaver. Creates `ClearMemberName()` methods for any member `MemberName` that is of reference type. Sets the member to `null` in this method.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>Malimbe.BehaviourStateRequirementMethod.Fody</AssemblyName>
<RootNamespace>Malimbe.BehaviourStateRequirementMethod.Fody</RootNamespace>
<LangVersion>latest</LangVersion>
<AttributeProjectName>$([MSBuild]::ValueOrDefault('$(MSBuildProjectName)', '').Replace('.Fody', ''))</AttributeProjectName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FodyHelpers" Version="3.3.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\$(AttributeProjectName)\$(AttributeProjectName).csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>

</Project>
211 changes: 211 additions & 0 deletions Sources/BehaviourStateRequirementMethod.Fody/ModuleWeaver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
namespace Malimbe.BehaviourStateRequirementMethod.Fody
{
using System.Collections.Generic;
using System.Linq;
using global::Fody;
using Malimbe.Shared;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Mono.Collections.Generic;

// ReSharper disable once UnusedMember.Global
public sealed class ModuleWeaver : BaseModuleWeaver
{
private static readonly string _fullAttributeName = typeof(RequiresBehaviourStateAttribute).FullName;

private TypeReference _behaviourTypeReference;
private MethodReference _getGameObjectMethodReference;
private MethodReference _getActiveSelfMethodReference;
private MethodReference _getActiveInHierarchyMethodReference;
private MethodReference _getIsActiveAndEnabledMethodReference;
private MethodReference _getEnabledMethodReference;

public override bool ShouldCleanReference =>
true;

public override void Execute()
{
FindReferences();

IEnumerable<MethodDefinition> methodDefinitions =
ModuleDefinition.Types.SelectMany(definition => definition.Methods);
foreach (MethodDefinition methodDefinition in methodDefinitions)
{
if (!FindAndRemoveAttribute(methodDefinition, out CustomAttribute attribute))
{
continue;
}

GameObjectActivity gameObjectActivity = (GameObjectActivity)attribute.ConstructorArguments[0].Value;
bool behaviourNeedsToBeEnabled = (bool)attribute.ConstructorArguments[1].Value;

if (gameObjectActivity == GameObjectActivity.None && !behaviourNeedsToBeEnabled)
{
LogWarning(
$"The method '{methodDefinition.FullName}' is annotated to require a Behaviour state"
+ " but the attribute constructor arguments result in no action being taken.");
continue;
}

MethodBody body = methodDefinition.Body;
Collection<Instruction> instructions = body.Instructions;
if (instructions.Count == 0)
{
LogWarning(
$"The method '{methodDefinition.FullName}' is annotated to require a Behaviour state"
+ " but the method has no instructions in its body and thus no action is being taken.");
continue;
}

body.SimplifyMacros();
InsertInstructions(
body,
methodDefinition,
instructions,
gameObjectActivity,
behaviourNeedsToBeEnabled);
body.OptimizeMacros();
}
}

public override IEnumerable<string> GetAssembliesForScanning()
{
yield return "UnityEngine";
}

private void FindReferences()
{
MethodReference ImportPropertyGetter(TypeDefinition typeDefinition, string propertyName) =>
ModuleDefinition.ImportReference(
typeDefinition.Properties.Single(definition => definition.Name == propertyName).GetMethod);

TypeDefinition behaviourTypeDefinition = FindType("UnityEngine.Behaviour");
TypeDefinition gameObjectTypeDefinition = FindType("UnityEngine.GameObject");

_behaviourTypeReference = ModuleDefinition.ImportReference(behaviourTypeDefinition);
_getGameObjectMethodReference = ImportPropertyGetter(FindType("UnityEngine.Component"), "gameObject");
_getActiveSelfMethodReference = ImportPropertyGetter(gameObjectTypeDefinition, "activeSelf");
_getActiveInHierarchyMethodReference = ImportPropertyGetter(gameObjectTypeDefinition, "activeInHierarchy");
_getIsActiveAndEnabledMethodReference =
ImportPropertyGetter(behaviourTypeDefinition, "isActiveAndEnabled");
_getEnabledMethodReference = ImportPropertyGetter(behaviourTypeDefinition, "enabled");
}

private bool FindAndRemoveAttribute(MethodDefinition methodDefinition, out CustomAttribute foundAttribute)
{
foundAttribute = methodDefinition.CustomAttributes.SingleOrDefault(
attribute => attribute.AttributeType.FullName == _fullAttributeName);
if (foundAttribute == null)
{
return false;
}

methodDefinition.CustomAttributes.Remove(foundAttribute);
LogInfo($"Removed the attribute '{_fullAttributeName}' from the method '{methodDefinition.FullName}'.");

if (methodDefinition.DeclaringType.IsSubclassOf(_behaviourTypeReference))
{
return true;
}

LogError(
$"The method '{methodDefinition.FullName}' is annotated to require a Behaviour state"
+ $" but the declaring type doesn't derive from '{_behaviourTypeReference.FullName}'.");
return false;
}

private void InsertInstructions(
MethodBody body,
MethodReference methodDefinition,
IList<Instruction> instructions,
GameObjectActivity gameObjectActivity,
bool behaviourNeedsToBeEnabled)
{
Instruction earlyReturnInstruction;

if (methodDefinition.ReturnType.FullName != TypeSystem.VoidReference.FullName)
{
// Create new variable to return a value
VariableDefinition variableDefinition = new VariableDefinition(methodDefinition.ReturnType);
body.Variables.Add(variableDefinition);
// Set variable to default value
body.InitLocals = true;

// Load variable
Instruction loadInstruction = Instruction.Create(OpCodes.Ldloc, variableDefinition);
instructions.Add(loadInstruction);
// Return
instructions.Add(Instruction.Create(OpCodes.Ret));

earlyReturnInstruction = loadInstruction;
}
else
{
earlyReturnInstruction = instructions.Last(instruction => instruction.OpCode == OpCodes.Ret);
}

int index = -1;

if (gameObjectActivity == GameObjectActivity.InHierarchy && behaviourNeedsToBeEnabled)
{
// Load this (for isActiveAndEnabled getter call)
instructions.Insert(++index, Instruction.Create(OpCodes.Ldarg_0));
// Call isActiveAndEnabled getter
instructions.Insert(
++index,
Instruction.Create(OpCodes.Callvirt, _getIsActiveAndEnabledMethodReference));

AddEarlyReturnInstruction(instructions, ref index, earlyReturnInstruction);
}
else
{
if (gameObjectActivity != GameObjectActivity.None)
{
// Load this (for gameObject getter call)
instructions.Insert(++index, Instruction.Create(OpCodes.Ldarg_0));
// Call gameObject getter
instructions.Insert(++index, Instruction.Create(OpCodes.Callvirt, _getGameObjectMethodReference));

// ReSharper disable once SwitchStatementMissingSomeCases
switch (gameObjectActivity)
{
case GameObjectActivity.Self:
// Call activeSelf getter
instructions.Insert(
++index,
Instruction.Create(OpCodes.Callvirt, _getActiveSelfMethodReference));
break;
case GameObjectActivity.InHierarchy:
// Call activeInHierarchy getter
instructions.Insert(
++index,
Instruction.Create(OpCodes.Callvirt, _getActiveInHierarchyMethodReference));
break;
}

AddEarlyReturnInstruction(instructions, ref index, earlyReturnInstruction);
}

if (behaviourNeedsToBeEnabled)
{
// Load this (for enabled getter call)
instructions.Insert(++index, Instruction.Create(OpCodes.Ldarg_0));
// Call enabled getter
instructions.Insert(++index, Instruction.Create(OpCodes.Callvirt, _getEnabledMethodReference));

AddEarlyReturnInstruction(instructions, ref index, earlyReturnInstruction);
}
}

LogInfo($"Added (an) early return(s) to the method '{methodDefinition.FullName}'.");
}

private static void AddEarlyReturnInstruction(
IList<Instruction> instructions,
ref int index,
Instruction earlyReturnInstruction) =>
// Return early if false
instructions.Insert(++index, Instruction.Create(OpCodes.Brfalse, earlyReturnInstruction));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>Malimbe.BehaviourStateRequirementMethod</AssemblyName>
<RootNamespace>Malimbe.BehaviourStateRequirementMethod</RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>

</Project>
21 changes: 21 additions & 0 deletions Sources/BehaviourStateRequirementMethod/GameObjectActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Malimbe.BehaviourStateRequirementMethod
{
/// <summary>
/// The active state of a GameObject.
/// </summary>
public enum GameObjectActivity
{
/// <summary>
/// The GameObject active state is of no interest.
/// </summary>
None = 0,
/// <summary>
/// The GameObject itself needs to be active, the state of parent GameObjects is ignored.
/// </summary>
Self,
/// <summary>
/// The GameObject is active in the scene because it is active itself and all parent GameObjects are, too.
/// </summary>
InHierarchy
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Malimbe.BehaviourStateRequirementMethod
{
using System;

/// <summary>
/// Indicates that the method returns early in case a specific GameObject state or Behaviour state isn't matched.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class RequiresBehaviourStateAttribute : Attribute
{
// ReSharper disable MemberCanBePrivate.Global
/// <summary>
/// The required active state of the GameObject that the component the method is on is added to.
/// </summary>
public readonly GameObjectActivity GameObjectActivity;
/// <summary>
/// The required state of the Behaviour.
/// </summary>
public readonly bool BehaviourNeedsToBeEnabled;
// ReSharper restore MemberCanBePrivate.Global

/// <summary>
/// Indicates that the method returns early in case a specific GameObject state or Behaviour state isn't matched.
/// </summary>
/// <param name="gameObjectActivity">The required active state of the GameObject that the component the method is on is added to.</param>
/// <param name="behaviourNeedsToBeEnabled">The required state of the Behaviour.</param>
public RequiresBehaviourStateAttribute(
GameObjectActivity gameObjectActivity = GameObjectActivity.InHierarchy,
bool behaviourNeedsToBeEnabled = true)
{
GameObjectActivity = gameObjectActivity;
BehaviourNeedsToBeEnabled = behaviourNeedsToBeEnabled;
}
}
}
14 changes: 14 additions & 0 deletions Sources/Malimbe.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XmlDocumentationAttribute",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Weavers", "Weavers", "{335FA460-758B-4432-80DC-4A953749E164}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BehaviourStateRequirementMethod", "BehaviourStateRequirementMethod\BehaviourStateRequirementMethod.csproj", "{C5BB6975-64A3-4182-BA20-EAC052EBE6EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BehaviourStateRequirementMethod.Fody", "BehaviourStateRequirementMethod.Fody\BehaviourStateRequirementMethod.Fody.csproj", "{4F7AC6C0-619C-4EB7-AD4D-CDEEFA3FFB60}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -95,6 +99,14 @@ Global
{BB10FF4A-EA8A-42F2-A750-4D3234EFD581}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB10FF4A-EA8A-42F2-A750-4D3234EFD581}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB10FF4A-EA8A-42F2-A750-4D3234EFD581}.Release|Any CPU.Build.0 = Release|Any CPU
{C5BB6975-64A3-4182-BA20-EAC052EBE6EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5BB6975-64A3-4182-BA20-EAC052EBE6EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5BB6975-64A3-4182-BA20-EAC052EBE6EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5BB6975-64A3-4182-BA20-EAC052EBE6EB}.Release|Any CPU.Build.0 = Release|Any CPU
{4F7AC6C0-619C-4EB7-AD4D-CDEEFA3FFB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F7AC6C0-619C-4EB7-AD4D-CDEEFA3FFB60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F7AC6C0-619C-4EB7-AD4D-CDEEFA3FFB60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F7AC6C0-619C-4EB7-AD4D-CDEEFA3FFB60}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -110,6 +122,8 @@ Global
{9C55B0CE-82D8-49BA-9DE5-805DFC2527C4} = {335FA460-758B-4432-80DC-4A953749E164}
{D5280C62-A165-421A-AEA9-DCBEE7AE8C52} = {335FA460-758B-4432-80DC-4A953749E164}
{BB10FF4A-EA8A-42F2-A750-4D3234EFD581} = {335FA460-758B-4432-80DC-4A953749E164}
{C5BB6975-64A3-4182-BA20-EAC052EBE6EB} = {335FA460-758B-4432-80DC-4A953749E164}
{4F7AC6C0-619C-4EB7-AD4D-CDEEFA3FFB60} = {335FA460-758B-4432-80DC-4A953749E164}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3BF0472E-2BB1-4136-A25A-A8B63E1935C2}
Expand Down
11 changes: 11 additions & 0 deletions Sources/Shared/CecilExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,16 @@ public static FieldReference GetBackingField(this PropertyDefinition propertyDef
FieldReferenceComparer.Instance)
.FirstOrDefault();
}

public static bool IsSubclassOf(this TypeDefinition typeDefinition, TypeReference superTypeReference)
{
TypeDefinition baseTypeDefinition = typeDefinition.BaseType?.Resolve();
while (baseTypeDefinition != null && baseTypeDefinition.FullName != superTypeReference.FullName)
{
baseTypeDefinition = baseTypeDefinition.BaseType?.Resolve();
}

return baseTypeDefinition?.FullName == superTypeReference.FullName;
}
}
}
12 changes: 1 addition & 11 deletions Sources/UnityPackaging/UnityPackaging.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\MemberClearanceMethod.Fody\MemberClearanceMethod.Fody.csproj" />
<ProjectReference Include="..\MemberClearanceMethod\MemberClearanceMethod.csproj" />
<ProjectReference Include="..\FodyRunner.UnityIntegration\FodyRunner.UnityIntegration.csproj" />
<ProjectReference Include="..\PropertySerializationAttribute.Fody\PropertySerializationAttribute.Fody.csproj" />
<ProjectReference Include="..\PropertySerializationAttribute\PropertySerializationAttribute.csproj" />
<ProjectReference Include="..\PropertySetterMethod.Fody\PropertySetterMethod.Fody.csproj" />
<ProjectReference Include="..\PropertySetterMethod\PropertySetterMethod.csproj" />
<ProjectReference Include="..\PropertyValidationMethod.Fody\PropertyValidationMethod.Fody.csproj" />
<ProjectReference Include="..\PropertyValidationMethod\PropertyValidationMethod.csproj" />
<ProjectReference Include="..\XmlDocumentationAttribute.Fody\XmlDocumentationAttribute.Fody.csproj" />
<ProjectReference Include="..\XmlDocumentationAttribute\XmlDocumentationAttribute.csproj" />
<ProjectReference Include="..\**\*.csproj" />
<ProjectReference Remove="..\**\FodyRunner.csproj" />
<ProjectReference Remove="..\**\Shared.csproj" />
<ProjectReference Remove="..\**\UnityPackaging.csproj" />
Expand Down

0 comments on commit ee9955a

Please sign in to comment.