I am making an editor tool that allows me to add parts to instances of scriptable-object class I've made. I use a generic method to add the new parts:
[CreateAssetMenu]
class Whole : ScriptableObject {
List<PartBase> parts = new();
public void AddPart<T>() where T:PartBase, new() { parts.Add(new T()); }
}
class Foo {
//insert selection statement that goes through each Child class of PartBase
}
The way I'm currently doing what I intend to do looks like this:
switch (EPartType)
{
case EPartType.Bar:
AddPart<BarPart>();
case EPartType.Baz:
AddPart<BazPart>();
case EPartType.Bor:
AddPart<BorPart>();
default:
break;
}
I am wondering if there's a way to do this that doesn't require me to switch on every single sub-class of PartBase (so that users can add their own custom parts just by making a new script inheriting from PartBase and not having to additionally tinker with my safekept enum), while still providing some sort of enumerated selection that can be used as a drop-down in my tool.
Thank you in advance!
I know this goes a bit beyond the scope of your question but here you go.
The comments in the code should hopefully explain every step
[CreateAssetMenu]
public class Whole : ScriptableObject
{
public List<PartBase> parts = new();
#if UNITY_EDITOR
// A custom Inspector for this type to extend it with an additional button
[CustomEditor(typeof(Whole))]
private class WholeEditor : Editor
{
// serialized property for the "parts"
private SerializedProperty m_PartsProperty;
// tiny hack to simulate a dropdown (see below)
private Rect _rect;
private void OnEnable()
{
// link up serialized property
m_PartsProperty = serializedObject.FindProperty(nameof(parts));
}
public override void OnInspectorGUI()
{
// draw the default inspector
base.OnInspectorGUI();
EditorGUILayot.Space();
var buttonClicked = GUILayout.Button("Add Part");
if (Event.current.type == EventType.Repaint)
{
// little hack to get the button rect in order to place
// our popup window here to kinda simulate a dropdown
_rect = GUIUtility.GUIToScreenRect(GUILayoutUtility.GetLastRect());
}
if (buttonClicked)
{
// Get the FOLDER where the current main asset is placed
// we will place created part assets here as well
var mainAssetPath = AssetDatabase.GetAssetPath(target);
var parts = mainAssetPath.Split('/').ToList();
parts.RemoveAt(parts.Count - 1);
mainAssetPath = string.Join('/', parts);
// shift the target position lower to start the window right under the "Add Part" button
_rect.y += _rect.height;
WholeEditorAddPopup.OpenPopup(_rect, m_PartsProperty, mainAssetPath);
}
}
private class WholeEditorAddPopup : EditorWindow
{
// The property where to finally add the created part
private SerializedProperty m_ListProperty;
// available Types and their according display names
private Type[] m_AvailableTypes;
private GUIContent[] m_DisplayOptions;
// just the label for the dropdown
private readonly GUIContent m_Label = new("Part to add");
// Folder where to create assets
private string m_MainAssetPath;
// currently selected type index
private int m_Selected = -1;
public static void OpenPopup(Rect buttonRect, SerializedProperty listProperty, string mainAssetPath)
{
// create a new instance of this window
var window = GetWindow<WholeEditorAddPopup>(true, "Add Part");
// assign the fields
window.m_ListProperty = listProperty;
window.m_MainAssetPath = mainAssetPath;
// get all assemblies
window.m_AvailableTypes = AppDomain.CurrentDomain.GetAssemblies()
// get all Types
.SelectMany(assembly => assembly.GetTypes())
// Filter to only have non-abstract child classes of "PartBase"
.Where(type => type.IsSubclassOf(typeof(PartBase)) && !type.IsAbstract)
// order by "FullName" (=> including namespaces)
.OrderBy(type => type.FullName)
.ToArray();
// For the display names replace all "." by "/"
// => Unity treats those as nested folders in the popup (see demo below)
window.m_DisplayOptions = window.m_AvailableTypes.Select(type => new GUIContent(type.FullName.Replace('.', '/'))).ToArray();
// show as Dropdown -> clicking outside automatically closes window
window.ShowAsDropDown(buttonRect, new Vector2(buttonRect.width, EditorGUIUtility.singleLineHeight * 4));
// [optional] set position again since "ShowAsDropDown" might have hanged it
window.position = new Rect(buttonRect.x, buttonRect.y, buttonRect.width, EditorGUIUtility.singleLineHeight * 4);
}
private void OnGUI()
{
// Draw a dropdown button containing all the available types
// grouped by namespaces and ordered alphabetically
m_Selected = EditorGUILayout.Popup(m_Label, m_Selected, m_DisplayOptions);
// only enable the "Add" button if valid index selected
var blockAdd = m_Selected < 0;
EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(blockAdd))
{
if (GUILayout.Button("Add"))
{
// get selected type by selected index
var selectedType = m_AvailableTypes[m_Selected];
// create runtime ScriptableObject instance by selected type
var part = CreateInstance(selectedType);
// Set its initial name
part.name = $"new {selectedType.Name}";
// Get a unique path for this asset
// => if already an asset with same name Unity adds an auto-incremented index
var path = AssetDatabase.GenerateUniqueAssetPath($"{m_MainAssetPath}/{part.name}.asset");
// Create the asset, save and refresh
AssetDatabase.CreateAsset(part, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// not sure anymore but from an old experience I think you need to re-load the asset
var loadedPart = AssetDatabase.LoadAssetAtPath<PartBase>(path);
// add the loaded asset to the "parts" list
m_ListProperty.arraySize += 1;
var elementProperty = m_ListProperty.GetArrayElementAtIndex(m_ListProperty.arraySize - 1);
elementProperty.objectReferenceValue = loadedPart;
// finally make the modified SerializedProperties persistent in the actual "Whole" asset
m_ListProperty.serializedObject.ApplyModifiedProperties();
// [optional] "Ping" the created asset => get highlighted in the Assets folder
EditorGUIUtility.PingObject(loadedPart);
// Close the popup window
Close();
}
}
}
}
}
#endif
}
And here a little demo how this would look like
For the demo I created the following types - all in their individual script files of course
public class PartBase : ScriptableObject { }
public class ExamplePart : PartBase { }
namespace NamespaceA
{
public class PartA : PartBase { }
}
namespace NamespaceA
{
public class PartAExtended : PartA { }
}
namespace NamespaceB
{
public class PartB : PartBase { }
}
namespace NamespaceB
{
public class PartBExtended : PartB { }
}
Related
I have two classes:
public class Stats : MonoBehaviour
{
// Primary Stats
public int strength;
public int agility;
public int intellect;
public int stamina;
public int spirit;
}
and
public class EquipmentProperties : ItemProperties
{
public Stats stats;
}
public enum Stats
{//variables from "Stats" class to be in this enum
}
I am trying to get all the variables from the Stats class to be in the stats enum without having to manually enter them..
"I am trying to get all the variables from the Stats class to be in the stats enum without having to manually enter them"
Enums must be specified at compile time, you can't dynamically add enums during run-time. If you would like to stablish the fields of your enum dynamically with your class variables, guess that because the Stats class might change along the app's development, you would need to store that enum somewhere, because if not you would need to access the fields of the dynamic enum according to that generic way of setting the enumeration, in a kind of meta-programming templated way that would not make much sense.
So along with your question comes the question of how to store that enum to be used afterwards I guess. For that you can check EnumBuilder class.
Extending that example, you can build the enum according to the specific Stats class like this:
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
public class Stats
{
// Primary Stats
public int strength;
public int agility;
public int intellect;
public int stamina;
public int spirit;
}
class Example
{
public static List<string> getFields(Type type) {
var propertyValues = type.GetFields();
var result = new Stats[propertyValues.Length];
var retStr = new List<string>();
for (int i = 0; i < propertyValues.Length; i++) {
retStr.Add(propertyValues[i].Name);
}
return retStr;
}
public static void Main() {
// Get the current application domain for the current thread.
AppDomain currentDomain = AppDomain.CurrentDomain;
// Create a dynamic assembly in the current application domain,
// and allow it to be executed and saved to disk.
AssemblyName aName = new AssemblyName("TempAssembly");
AssemblyBuilder ab = currentDomain.DefineDynamicAssembly(
aName, AssemblyBuilderAccess.RunAndSave);
// Define a dynamic module in "TempAssembly" assembly. For a single-
// module assembly, the module has the same name as the assembly.
ModuleBuilder mb = ab.DefineDynamicModule(aName.Name, aName.Name + ".dll");
// Define a public enumeration with the name "Elevation" and an
// underlying type of Integer.
EnumBuilder eb = mb.DefineEnum("Stats", TypeAttributes.Public, typeof(int));
int fieldCount = 0;
getProperties(typeof(Stats)).ForEach(field => {
eb.DefineLiteral(field, fieldCount);
fieldCount++;
});
// Define two members, "High" and "Low".
//eb.DefineLiteral("Low", 0);
//eb.DefineLiteral("High", 1);
// Create the type and save the assembly.
Type finished = eb.CreateType();
ab.Save(aName.Name + ".dll");
foreach (object o in Enum.GetValues(finished)) {
Console.WriteLine("{0}.{1} = {2}", finished, o, ((int)o));
}
Console.ReadLine();
}
}
Output:
Stats.strength = 0
Stats.agility = 1
Stats.intellect = 2
Stats.stamina = 3
Stats.spirit = 4
Prolog
This is of course not what you are asking directly since it is not automatic but I would suggest a Dictionary<Stats, int> and do e.g.
public class StatsComponent : MonoBehaviour
{
// Make these only assignable via the Inspector
[SerializeField] private int strength;
[SerializeField] private int agility;
[SerializeField] private int intellect;
[SerializeField] private int stamina;
[SerializeField] private int spirit;
public readonly Dictionary<Stats, int> stats = new Dictionary<Stats, int>();
private void Awake ()
{
// Initialize once with values from the Inspector
stats.Add(Stats.Strength, strength);
stats.Add(Stats.Agility, agility);
stats.Add(Stats.Intellect, intellect);
stats.Add(Stats.Stamina, stamina);
stats.Add(Stats.Spirit, spirit);
}
}
public enum Stats
{
Strength,
Agility,
Intellect,
Stamina,
Spirit
}
Of course there are ways to automatize that via reflection but I'm sure it will bring you more headaches and issues then it is solving - that's only an opinion of course.
Intermediate Solution
If you don't want to type things twice you could instead of an enum rather go by index or strings e.g. using SerializedDictionary you could simply have a
public SerializedDictionary<string, int> stats;
and fill it in the Inspector and not have your fields at all.
Enum Generator Window
However, if you still want at least automation with a minimal effort to build on top of this answer I made this a tool EditorWindow you can directly use within Unity.
Just place this script anywhere in your project.
#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
public class EnumGeneratorWindow : EditorWindow
{
// This is of course optional but I thought it makes sense to filer for a specific field type
private enum FieldType
{
Int,
Float,
Bool,
// more could of course be added
}
private MonoScript sourceScript;
private MonoScript targetScript;
private FieldType fieldType;
private Type GetFieldType()
{
return fieldType switch
{
FieldType.Int => typeof(int),
FieldType.Float => typeof(float),
FieldType.Bool => typeof(bool),
// according to the enum add more cases
_ => null
};
}
[MenuItem("Window/ENUM GENERATOR")]
private static void Init()
{
var window = GetWindow<EnumGeneratorWindow>();
window.Show();
}
private void OnGUI()
{
EditorGUILayout.LabelField("ENUM GENERATOR", EditorStyles.boldLabel);
sourceScript = EditorGUILayout.ObjectField("Source", sourceScript, typeof(MonoScript), false) as MonoScript;
if (!sourceScript)
{
EditorGUILayout.HelpBox("Reference the script where to fetch the fields from", MessageType.None, true);
return;
}
var sourceType = sourceScript.GetClass();
if (sourceType == null)
{
EditorGUILayout.HelpBox("Could not get Type from source file!", MessageType.Error, true);
return;
}
targetScript = EditorGUILayout.ObjectField("Target", targetScript, typeof(MonoScript), false) as MonoScript;
if (!targetScript)
{
EditorGUILayout.HelpBox("Reference the script where write the generated enum to", MessageType.None, true);
return;
}
if (targetScript == sourceScript)
{
EditorGUILayout.HelpBox("The source and target script should probably rather not be the same file ;)", MessageType.Error, true);
return;
}
var targetType = targetScript.GetClass();
if (targetType == null)
{
EditorGUILayout.HelpBox("Could not get Type from target file!", MessageType.Error, true);
return;
}
fieldType = (FieldType)EditorGUILayout.EnumPopup("Field Type", fieldType);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
var fields = sourceType.GetFields().Where(f => f.FieldType == GetFieldType()).Select(f => f.Name).ToArray();
var fileContent = new StringBuilder("public enum ").Append(targetType.Name).Append(" { ");
for (var i = 0; i < fields.Length; i++)
{
if (i != 0)
{
fileContent.Append(", ");
}
fileContent.Append(fields[i]);
}
fileContent.Append(" }");
EditorGUILayout.LabelField(fileContent.ToString());
var color = GUI.color;
GUI.color = Color.red;
GUILayout.BeginVertical();
{
EditorGUILayout.LabelField("! DANGER ZONE !", EditorStyles.boldLabel);
EditorGUILayout.Space();
if (GUILayout.Button("GENERATE ENUM"))
{
var targetID = targetScript.GetInstanceID();
// e.g. Assets/SomeFolder/MyStats.cs
var targetAssetPath = AssetDatabase.GetAssetPath(targetID);
// just as a safety net
if (EditorUtility.DisplayDialog("Generate and overwrite with enum?", $"Attention\n\nThis will overwrite any content of {targetAssetPath} with the new content.\n\nAre you sure?", "Yes generate", "OMG NO! Cancel this!"))
{
// a bit of a hack but we need to convert the Unity asset path into a valid system path by erasing one duplicate "Assets"
var pathParts = targetAssetPath.Split('/').ToArray();
// overwrite the "Assets" with the full path to Assets
pathParts[0] = Application.dataPath;
// re-combine all path parts but this time use the according system file path separator char
var targetSystemPath = Path.Combine(pathParts);
// Write the content into the file via the normal file IO
File.WriteAllText(targetSystemPath, fileContent.ToString());
// trigger a refresh so unity re-loads and re-compiles
AssetDatabase.Refresh();
}
}
}
GUILayout.EndVertical();
GUI.color = color;
}
}
#endif
How it works:
Open the window via the header menu -> Window -> ENUM GENERATOR
Drag in your MonoBehaviour script into "Source"
Create a new empty script with the enum name you like
Drag your enum target script into "Target"
Select the type of fields we are looking for
Finally hit generate
Here is a little demo ;)
I just start of using Example.cs
public class Example : MonoBehaviour
{
public int someInt, anotherInt;
public float someFloat, anotherFloat, andOnMore;
public bool someBool, yetAnotherBool;
}
and ExampleEnum.cs
public enum ExampleEnum
{
}
how can I collect the name of all of my Script(Generic) (in a list or array?)
Like below?
public class inputdata
{
public Component com;
public int index;
public inputdata(Component newcom,int newindex)
{
com = newcom;
index = newindex;
}
}
inputdata[] data = {new inputdata(component1, 1),inputdata(component2, 1),inputdata(component3, 1)}
foreach (inputdata ft in data)
{
movefunc <ft.com> (ft.index);
}
void movefunc <T> (int index){
gameobject.GetComponent<T>()
}
it 's shows 'component1' is a type, which is not valid in the given context.
component1 is my c# script, I use it for detecting raycast.
like this
hitInfo.collider.gameObject.GetComponent <component1 > ()
It seems not clear what you are trying to achieve. In case it helps you can achive a list of the components in a gameObject like this:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class GetComponets : MonoBehaviour {
public GameObject goWithComponets; //attach in the inspector
void Start() {
getComponets().ToList().ForEach(component => {
Debug.Log(component.GetType().ToString());
});
}
private IEnumerable<Component> getComponets() {
return goWithComponets.GetComponents<Component>().ToList(); ;
}
}
Your components wont be generic, they for sure will inherit from the Component type, so in case its needed you can grab all the components from the gameObject getting all the components from the base class Component, and if you need to handle specifics types, handle that later on with some code.
Sounds to me like you don't want a generic but rather something like e.g
public class inputdata
{
public Type Type;
public int Index;
public inputdata(Type type, int index)
{
Type = type;
Index = index;
}
}
inputdata[] data = new []
{
new inputdata(typeof(component1), 1),
new inputdata(typeof(component2), 1),
new inputdata(typeof(component3), 1)
};
and then
foreach (inputdata ft in data)
{
movefunc (ft.type, ft.index);
}
and
void movefunc (Type type, int index)
{
// ... whatever happens before and where you get the hitInfo from
if(hitInfo.collider.TryGetComponent(type, out var component))
{
// component of given type exists on the hit object
// "component" will contain a reference to it in the base type "Component" -> if you need it in its actual type you will need to cast
}
else
{
// there is no component of given type on the hit object
}
}
Working with the android studio Bottom Navigation activity template, want my HomeFragment / HomeViewModel page text to change each time the Home is accessed from the bottom navigation. When I click it the first time the text it displays should be different from the text displayed the 2nd time.
I'm able to create a variable that holds the text output, and I've created a function that should be able to update that variable but I'm unable to call it up within the MainActivity class. Any ideas?
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(setOf(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications))
navView.setupWithNavController(navController)
}
HomeFragment
class HomeFragment : Fragment() {
private lateinit var homeViewModel: HomeViewModel
var chosenOutput:String = "primary output";
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//using update here crashes the app
homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_home, container, false)
val textView: TextView = root.findViewById(R.id.text_home)
homeViewModel.text.observe(viewLifecycleOwner, Observer {
textView.text = chosenOutput; //changing 'chosenOutput' to 'it' means that text update must take place within HomeViewModel instead of fragment.
})
return root
}
fun update(result: String)
{
chosenOutput = result;
}
}
HomeViewModel
class HomeViewModel : ViewModel() {
var temp:String = "demo"
//constructor()
private val _text = MutableLiveData<String>().apply {
value = temp;
}
fun update(result: String) //function may be obsolete if text change can be done in fragment.
{
temp = result;
}
val text: LiveData<String> = _text
}
What is the correct way to inject a string into either the Fragment or the ViewModel from the MainActivity class I've given?
You could change your approach and store your chosenOutput variable in the MainActivity
and update it everytime you select the HomeFragment
class MainActivity : AppCompatActivity()
{
var chosenOutput = "primary output"
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(setOf(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications))
navView.apply {
setupWithNavController(navController)
setOnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
R.id.navigation_home -> {
chosenOutput = "new value" // <-- Update chosen output here
// Add this method to call the `onResume` method in the fragment
showFragment(HomeFragment.newInstance())
}
else { }
}
true
}
}
private fun showFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.replace(R.id.nav_host_fragment, fragment)
.addToBackStack(null)
.commit()
}
}
Then you can retrieve the updated string in your Fragment with this code.
Note that I added the companion object to avoid the creation of multiple instances of the Fragment.
class HomeFragment : Fragment()
{
companion object
{
fun newInstance() : HomeFragment
{
return HomeFragment()
}
}
override fun onResume() {
super.onResume()
val newChosenOutput = (activity as MainActivity).chosenOutput
}
}
I'm building a card game and I'd like to have a clean architecture for card abilities.
I've got a CardData ScriptableObject with properties for a card. I want card abilities to compose together to describe what a card does, like a card called DrawAndHealCard that draws 2 cards and heals 5 health when played.
I realized right away that this means I'll need a concrete asset for each variation of a CardAbility. So DrawAndHealCard has a reference to two assets: DrawCards2 and HealPlayer5. That's ridiculous, I want all the data to feel like it's on a single DrawAndHealCard.
So I learned about AssetDatabase.AddObjectToAsset(), this seems like the right idea, I can have the abilities as sub-assets of a CardData asset and not have deal with the organization of all these separate assets. So now I'm trying to build an Editor to manage this and it is painful.
I've read so much stuff about Unity serialization, SOs, Editor scripts, ... seriously hitting a wall with this and about to downgrade to something that feels less elegant architecturally. If there's a better way to do this, I'm open to suggestions about totally different routes too.
Code below is stripped down, but it's the gist of what I'm trying to figure out.
Where I'm at right now is onAddCallback seems to be adding a sub-asset correctly, but onRemoveCallback does not remove it. My Clear All Abilities button does work however. I can't find any good docs or guides about this stuff, so I'm pretty lost currently.
// CardData.cs
[CreateAssetMenu(fileName = "CardData", menuName = "Card Game/CardData", order = 1)]
public class CardData : ScriptableObject
{
public Sprite image;
public string description;
public CardAbility[] onPlayed;
}
// CardAbility.cs
public class CardAbility : ScriptableObject
{
public abstract void Resolve();
}
// DrawCards.cs
public class DrawCards : CardAbility
{
public int numCards = 1;
public override void Resolve()
{
Deck.instance.DrawCards(numCards);
}
}
// HealPlayer.cs
public class HealPlayer : CardAbility
{
public int healAmt = 10;
public override void Resolve()
{
Player.instance.Heal(healAmt);
}
}
// CardDataEditor.cs
[CustomEditor(typeof(CardData))]
public class CardDataEditor : Editor
{
private ReorderableList abilityList;
public void OnEnable()
{
abilityList = new ReorderableList(
serializedObject,
serializedObject.FindProperty("onPlayed"),
draggable: true,
displayHeader: true,
displayAddButton: true,
displayRemoveButton: true);
abilityList.onRemoveCallback = (ReorderableList l) => {
l.serializedProperty.serializedObject.Update();
var obj = l.serializedProperty.GetArrayElementAtIndex(l.index).objectReferenceValue;
DestroyImmediate(obj, true);
AssetDatabase.SaveAssets();
l.serializedProperty.DeleteArrayElementAtIndex(l.index);
l.serializedProperty.serializedObject.ApplyModifiedProperties();
};
abilityList.onAddCallback = (ReorderableList l) => {
var index = l.serializedProperty.arraySize;
l.serializedProperty.arraySize++;
l.index = index;
var element = l.serializedProperty.GetArrayElementAtIndex(index);
// Hard coding a specific ability for now
var cardData = (CardData)target;
var newAbility = ScriptableObject.CreateInstance<DrawCards>();
newAbility.name = "test";
newAbility.numCards = 22;
element.objectReferenceValue = newAbility;
AssetDatabase.AddObjectToAsset(newAbility, cardData);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
serializedObject.ApplyModifiedProperties();
};
// Will use this to provide a menu of abilities to choose from.
/*
abilityList.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => {
var menu = new GenericMenu();
var guids = AssetDatabase.FindAssets("", new[]{"Assets/CardAbility"});
foreach (var guid in guids) {
var path = AssetDatabase.GUIDToAssetPath(guid);
menu.AddItem(new GUIContent("Mobs/" + Path.GetFileNameWithoutExtension(path)), false, clickHandler, new WaveCreationParams() {Type = MobWave.WaveType.Mobs, Path = path});
}
menu.ShowAsContext();
};
*/
// Will use this to render CardAbility properties
/*
abilityList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
};
*/
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
abilityList.DoLayoutList();
// XXX: Ultimately don't expect to use these, experimenting with
// other ways of adding/deleting.
if (GUILayout.Button("Add Ability")) {
var cardData = (CardData)target;
var newAbility = ScriptableObject.CreateInstance<CardAbility>();
AssetDatabase.AddObjectToAsset(newAbility, cardData);
AssetDatabase.SaveAssets();
}
if (GUILayout.Button("Clear All Abilities")) {
var path = AssetDatabase.GetAssetPath(target);
Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);
for (int i = 0; i < assets.Length; i++) {
if (assets[i] is CardAbility) {
Object.DestroyImmediate(assets[i], true);
}
}
AssetDatabase.SaveAssets();
}
serializedObject.ApplyModifiedProperties();
}
}
Ok I finally figured this out. I read a hundred stack overflow and forum posts trying to understand this, so I'm paying it forward, hopefully this helps someone else navigate this. This produces an Editor like the image below, where OnPlayed is an array of polymorphic ScriptableObjects. These CardAbility SO's are stored as sub-assets on the owning ScriptableObject (CardData). Still more to clean up here, and it could be made more generic, but should be a good start for someone else trying to do this.
The [+] button produces a list of all CardAbility SO's that are available to add.
And the properties for a concrete CardAbility are rendered dynamically.
One of the weirdest thing about all this is that you can't render the contents of an objectReferenceValue using PropertyField, you have to construct a SerializedObject first like this:
SerializedObject nestedObject = new SerializedObject(element.objectReferenceValue);
Thanks to Unity: Inspector can't find field of ScriptableObject for that tip.
Some other great resources for ReorderableList:
https://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/
https://sites.google.com/site/tuxnots/gamming/unity3d/unitymakeyourlistsfunctionalwithreorderablelist
https://sandordaemen.nl/blog/unity-3d-extending-the-editor-part-3/
// CardData.cs
[CreateAssetMenu(fileName = "CardData", menuName = "Card Game/CardData", order = 1)]
public class CardData : ScriptableObject
{
public enum CardType
{
Attack,
Skill
}
public CardType type;
public Sprite image;
public string description;
// XXX: Hidden in inspector because it will be drawn by custom Editor.
[HideInInspector]
public CardAbility[] onPlayed;
}
// CardAbility.cs
public abstract class CardAbility : ScriptableObject
{
public abstract void Resolve();
}
// DrawCards.cs
public class DrawCards : CardAbility
{
public int numCards = 1;
public override void Resolve()
{
Deck.instance.DrawCards(numCards);
}
}
// HealPlayer.cs
public class HealPlayer : CardAbility
{
public int healAmount = 10;
public override void Resolve()
{
Player.instance.Heal(healAmount);
}
}
// CardDataEditor.cs
[CustomEditor(typeof(CardData))]
[CanEditMultipleObjects]
public class CardDataEditor : Editor
{
private ReorderableList abilityList;
private SerializedProperty onPlayedProp;
private struct AbilityCreationParams {
public string Path;
}
public void OnEnable()
{
onPlayedProp = serializedObject.FindProperty("onPlayed");
abilityList = new ReorderableList(
serializedObject,
onPlayedProp,
draggable: true,
displayHeader: true,
displayAddButton: true,
displayRemoveButton: true);
abilityList.drawHeaderCallback = (Rect rect) => {
EditorGUI.LabelField(rect, "OnPlayed Abilities");
};
abilityList.onRemoveCallback = (ReorderableList l) => {
var element = l.serializedProperty.GetArrayElementAtIndex(l.index);
var obj = element.objectReferenceValue;
AssetDatabase.RemoveObjectFromAsset(obj);
DestroyImmediate(obj, true);
l.serializedProperty.DeleteArrayElementAtIndex(l.index);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
ReorderableList.defaultBehaviours.DoRemoveButton(l);
};
abilityList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
SerializedProperty element = onPlayedProp.GetArrayElementAtIndex(index);
rect.y += 2;
rect.width -= 10;
rect.height = EditorGUIUtility.singleLineHeight;
if (element.objectReferenceValue == null) {
return;
}
string label = element.objectReferenceValue.name;
EditorGUI.LabelField(rect, label, EditorStyles.boldLabel);
// Convert this element's data to a SerializedObject so we can iterate
// through each SerializedProperty and render a PropertyField.
SerializedObject nestedObject = new SerializedObject(element.objectReferenceValue);
// Loop over all properties and render them
SerializedProperty prop = nestedObject.GetIterator();
float y = rect.y;
while (prop.NextVisible(true)) {
if (prop.name == "m_Script") {
continue;
}
rect.y += EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(rect, prop);
}
nestedObject.ApplyModifiedProperties();
// Mark edits for saving
if (GUI.changed) {
EditorUtility.SetDirty(target);
}
};
abilityList.elementHeightCallback = (int index) => {
float baseProp = EditorGUI.GetPropertyHeight(
abilityList.serializedProperty.GetArrayElementAtIndex(index), true);
float additionalProps = 0;
SerializedProperty element = onPlayedProp.GetArrayElementAtIndex(index);
if (element.objectReferenceValue != null) {
SerializedObject ability = new SerializedObject(element.objectReferenceValue);
SerializedProperty prop = ability.GetIterator();
while (prop.NextVisible(true)) {
// XXX: This logic stays in sync with loop in drawElementCallback.
if (prop.name == "m_Script") {
continue;
}
additionalProps += EditorGUIUtility.singleLineHeight;
}
}
float spacingBetweenElements = EditorGUIUtility.singleLineHeight / 2;
return baseProp + spacingBetweenElements + additionalProps;
};
abilityList.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => {
var menu = new GenericMenu();
var guids = AssetDatabase.FindAssets("", new[]{"Assets/CardAbility"});
foreach (var guid in guids) {
var path = AssetDatabase.GUIDToAssetPath(guid);
var type = AssetDatabase.LoadAssetAtPath(path, typeof(UnityEngine.Object));
if (type.name == "CardAbility") {
continue;
}
menu.AddItem(
new GUIContent(Path.GetFileNameWithoutExtension(path)),
false,
addClickHandler,
new AbilityCreationParams() {Path = path});
}
menu.ShowAsContext();
};
}
private void addClickHandler(object dataObj) {
// Make room in list
var data = (AbilityCreationParams)dataObj;
var index = abilityList.serializedProperty.arraySize;
abilityList.serializedProperty.arraySize++;
abilityList.index = index;
var element = abilityList.serializedProperty.GetArrayElementAtIndex(index);
// Create the new Ability
var type = AssetDatabase.LoadAssetAtPath(data.Path, typeof(UnityEngine.Object));
var newAbility = ScriptableObject.CreateInstance(type.name);
newAbility.name = type.name;
// Add it to CardData
var cardData = (CardData)target;
AssetDatabase.AddObjectToAsset(newAbility, cardData);
AssetDatabase.SaveAssets();
element.objectReferenceValue = newAbility;
serializedObject.ApplyModifiedProperties();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
abilityList.DoLayoutList();
if (GUILayout.Button("Delete All Abilities")) {
var path = AssetDatabase.GetAssetPath(target);
Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);
for (int i = 0; i < assets.Length; i++) {
if (assets[i] is CardAbility) {
Object.DestroyImmediate(assets[i], true);
}
}
AssetDatabase.SaveAssets();
}
serializedObject.ApplyModifiedProperties();
}
}
Not sure since Editor scripting is always quite tricky if you don't have a full project in front of you.
On first glance I would say you are adding the assets to the asset but you do not remove them using AssetDatabase.RemoveObjectFromAsset
You should probably do something like
abilityList.onRemoveCallback = (ReorderableList l) =>
{
// Update should be redundant here since you already call it anyway on beginning of the draw loop
// Are you also sure the `l.index` is the correct value to use here?
var obj = l.serializedProperty.GetArrayElementAtIndex(l.index).objectReferenceValue;
AssetDatabase.RemoveObjectFromAsset(obj);
DestroyImmediate(obj, true);
l.serializedProperty.DeleteArrayElementAtIndex(l.index);
// If you do save assets also refresh
// Not sure if you should even do that though to be honest
AssetDatabase.SaveAssets();
// Also refresh here
AssetDatabase.Refresh();
// Also ApplyModifiedProperties should be redundant
};
also as commented I think you wouldn't even have to use
AssetDatabase.SaveAssets();
without it the assets would simply be marked as dirty and be saved along with the scene on the next CTRL+S press.
However, afaik, if you do it you should always combine it with AssetDatabase.Refresh(); to actually see the changes made reflected in the assets view
I want add search logic for my application (IOS8). I have simple MvxTableViewController and display my data by UITableViewSource. Here is:
...controller:
MvxViewFor(typeof(MainViewModel))]
partial class MainController : MvxTableViewController
{
public MainController(IntPtr handle) : base(handle) { }
public override void ViewDidLoad()
{
base.ViewDidLoad();
// make background trasnsparent page
this.View.BackgroundColor = UIColor.Clear;
this.TableView.BackgroundColor = UIColor.Clear;
this.NavigationController.NavigationBar.BarStyle = UIBarStyle.Black;
this.SetBackground ();
(this.DataContext as MainViewModel).PropertyChanged += this.ViewModelPropertyChanged;
}
private void SetBackground()
{
// set blured bg image
}
private void ViewModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var viewModel = this.ViewModel as MainViewModel;
if (e.PropertyName == "Title")
{
this.Title = viewModel.Title;
}
else if (e.PropertyName == "Topics")
{
var tableSource = new TopicTableViewSource(viewModel.Topics);
tableSource.TappedCommand = viewModel.NavigateToChildrenPageCommand;
this.TableView.Source = tableSource;
this.TableView.ReloadData();
}
}
I read about search in IOS and choosed UISearchController for IOS8 app. But I don't understand, how I can add this controller to my view :(
I found sample from Xamarin (TableSearch) - but they don't use UITableViewSource and I don't understand what I should do with this.
I tried add controller:
this.searchController = new UISearchController (this.searchTableController)
{
WeakDelegate = this,
DimsBackgroundDuringPresentation = false,
WeakSearchResultsUpdater = this,
};
this.searchController.SearchBar.SizeToFit ();
this.TableView.TableHeaderView = searchController.SearchBar;
this.TableView.WeakDelegate = this;
this.searchController.SearchBar.WeakDelegate = this;
what should I do in this.searchTableController? Do I need move my display logic there?
Yes. The "searchTableController" should be responsible for the presentation of search results.
Here is the test project (native, not xmarin) which help you understand.
The searchController manages a "searchBar" and "searchResultPresenter". His not need add to a view-hierarchy of the carrier controller. When user starts typing a text in the "searchBar" the "SearchController" automatically shows your SearchResultPresenter for you.
Steps:
1) Instantiate search controller with the SearchResultsPresenterController.
2) When user inputs text in the search-bar you should invoke a your own service for the search. Below a sample of code..
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
NSString *searchString = searchController.searchBar.text;
if (searchString.length > 1)
{
// TODO - call your service for the search by string
// this may be async or sync
// When a data was found - set it to presenter
[self.searchResultPresenter dataFound:<found data>];
}
}
3) In the search presenter need to reload a table in the method "dataFound:"
- (void)dataFound:(NSArray *)searchResults
{
_searchResults = searchResults;
[self.tableView reloadData];
}
Here are some advice on how to use the UISearchController with Xamarin.iOS.
Create a new class for the results table view subclassing UITableViewSource. This is gonna be the view responsible of displaying the results. You need to make the items list of that table view public.
public List<string> SearchedItems { get; set; }
In your main UIViewController, create your UISearchController and pass your result table view as an argument. I added some extra setup.
public UISearchController SearchController { get; set; }
public override void ViewDidLoad ()
{
SearchController = new UISearchController (resultsTableController) {
WeakDelegate = this,
DimsBackgroundDuringPresentation = false,
WeakSearchResultsUpdater = this,
};
SearchController.SearchBar.SizeToFit ();
SearchController.SearchBar.WeakDelegate = this;
SearchController.HidesNavigationBarDuringPresentation = false;
DefinesPresentationContext = true;
}
The best way to add the search bar to your UI in term of user experience, in my opinion, is to add it as a NavigationItem to a NavigationBarController.
NavigationItem.TitleView = SearchController.SearchBar;
Add methods to perform the search in the main UIViewController:
[Export ("updateSearchResultsForSearchController:")]
public virtual void UpdateSearchResultsForSearchController (UISearchController searchController)
{
var tableController = (UITableViewController)searchController.SearchResultsController;
var resultsSource = (ResultsTableSource)tableController.TableView.Source;
resultsSource.SearchedItems = PerformSearch (searchController.SearchBar.Text);
tableController.TableView.ReloadData ();
}
static List<string> PerformSearch (string searchString)
{
// Return a list of elements that correspond to the search or
// parse an existing list.
}
I really hope this will help you, good luck.