Dynamically add a Component from a list of components - c#

I will break it down to give an understanding.
I have a parent class Item.
I have child classes for each type of Item (Weapon, Armor, etc).
I have a list of all the items.
I'm trying to randomly grab an item from that list, and then add it as a component.
My items each randomly generate their own names, stats, etc, but they are all specific to the type of item they are and so I needed them to be generated in their own classes and not just generating "Item".
So here is what I have so far.
public Item RandomList(List<Item> list)
{
return list[random.Next(list.Count)];
}
This returns a random item from my list.
public void GenerateLoot(GameObject gameObject, List<Item> list)
{
gameObject.AddComponent(randomManager.RandomList(list));
}
This is where I'm running into problems. It says : "cannot conver from Item to System.Type". I've tried various things such as using typeof, etc.
This is something I'm fairly new to.

AddComponent seems to not support parameters other than type of a script, and if I understood you correctly, your items are all customizable in many aspects that should be applied when you add component in runtime. I advise adding method AddToGameobject in class Item:
abstract class Item : Monobehaviour {
public abstract void AddToGameObject (GameObject obj);
}
class Weapon : Item {
int damage;
int cooldown;
public override void AddToGameObject (GameObject obj) {
var copy = obj.AddComponent <Weapon> ();
copy.damage = damage;
copy.cooldown = cooldown;
}
}
class Armor : Item {
int protection;
public override void AddToGameObject (GameObject obj) {
var copy = obj.AddComponent <Armor> ();
copy.protection = protection;
}
}
And your loot generation function will become
public void GenerateLoot (GameObject gameObject, List <Item> list) {
randomManager.RandomList (list).AddToGameObject (gameObject);
}
If you want to attach existing component itself instead of a copy, you can't (explanations: 1, 2).

As MSL says, you need to give it a Type argument to AddComponent, as the documentation suggest:
https://docs.unity3d.com/ScriptReference/GameObject.AddComponent.html
So my suggestion is to do it that way:
1.First you call and add the component (similar as you do)
gameObject.AddComponent(typeof(Item)) as Item;
or my prefered way:
gameObject.AddComponent<Item>();
2.Then add the item values and properties:
gameObject.GetComponent<Item>().value = item.value;

You are trying to set a GameObject Component of type Item. If you try to Add Component Item in the Inspector of any GameObject, there is none (unless your script is called Item, but that won't do what you're trying to do).
I'm assuming in your example, you may want to generate a random item upon opening a chest. There is a couple of steps to create a fully functional item, so I can't really give it all to you, so I'll give you an idea of how you'd do it.
The following will randomly change the image of the GameObject by Adding Component SpriteRenderer and randomly change the Sprite value of this Component.
public class RandomSpawn : MonoBehaviour {
GameObject gameObject;
public Sprite[] weapons; // assign an array of weapon sprites in Inspector
void Start() {
gameObject = new GameObject();
SpriteRenderer gameObjectSR = // add SpriteRenderer component
gameObject.AddComponent(typeof(SpriteRenderer)) as SpriteRenderer;
int randomIndex = Random.Range(0, weapons.Length); // random index within weapons
gameObjectSR.sprite = weapons[randomIndex]; // assign Sprite from weapons
// instantiate object
Instantiate(gameObject, transform.position, transform.rotation);
}
}
So you begin with Sprite[] weapons, an array of weapon images (basically). In the Start() function, create a new GameObject, and add a SpriteRenderer (this doesn't have to be random because you want to change what the item looks like). The random part begins with obtaining a random int (the index which is inside the length of the weapons). Then assigning a random weapons Sprite onto the GameObject, and instantiating it. This would work as compared to your attempt because SpriteRenderer is a Unity GameObject Component.
For this GameObject, you may want to attach your Weapon script (since it's derived from the Item class) and assuming you have a Weapon function SetDamage, like this:
Weapon w = gameObject.AddComponent<Weapon>();
w.SetDamage(Random.Range(5,10)); // set weapon damage between 5 and 10
This will then change the value of the Weapon damage, which can be accessed by script for when you are damaging an enemy, for example.
Note: There is probably a way to create your own components, if the normal library of components doesn't satisfy your needs.

Related

How do I find all Scriptable Objects in a folder and then load a variable from one?

I have a folder in my unity project under "Resources/ScriptableObjects/Skins". I need to get all the objects in the folder, generate a random number for the index, then assign the sprite to an existing game object that the script is attached to. The current error I'm getting is "NullReferenceException: Object reference not set to an instance of an object" on line 151 but I'm creating an instance of the object on line 149. What gives? Here is my function to assign the sprite from a random scriptableobject in the folder to the game object the script is tied to:
void AssignRandomSkin(){
// Load all skins into memory
Object[] skinObjects = Resources.LoadAll("ScriptableObjects/Skins");
// Get length of list
int amountOfSkins = skinObjects.Length;
// Get random index of skin to assign
int skinToAssignIndex = Random.Range(0, amountOfSkins);
GameObject thisSkin = Instantiate(skinObjects[skinToAssignIndex]) as GameObject;
// Assign it to game object
gameObject.GetComponent<SpriteRenderer>().sprite = thisSkin.GetComponent<Sprite>();
}
Here is the Scriptable Object:
using UnityEngine;
[CreateAssetMenu(fileName = "Skin", menuName = "ScriptableObjects/SkinObject", order = 1)]
public class SkinObject : ScriptableObject
{
public string spriteName; // Name of sprite
public Sprite sprite;
public float xPos;
public float yPos;
public float zPos;
public float xScale;
public float yScale;
public float zScale;
public float fallSpeed; //AKA Weight
public string tier; //Category that skin can be assigned in
}
So what happens here?
Your objects are ScriptableObject of type SkinObject! => They are not GameObject prefabs!
Everything in your code should work until the
GameObject thisSkin = Instantiate(skinObjects[skinToAssignIndex]) as GameObject;
first of all it is unnecessary to instantiate a ScriptableObject. This would only create a clone of the asset but you don't need this.
And second you are trying to cast it to GameObject. As said this is a type mismatch and therefore thisSkin will be null!
And finally Sprite is no component. You are rather trying to access the field .sprite of your SkinObject type.
I'm pretty sure it should rather be
// You can directly tell LoadAll to only load assets of the correct type
// even if there would be other assets in the same folder
SkinObject[] skinObjects = Resources.LoadAll<SkinObject>("ScriptableObjects/Skins");
var thisSkin = skinObjects[Random.Range(0, skinObjects.Length)];
// Assign it to game object
gameObject.GetComponent<SpriteRenderer>().sprite = thisSkin.sprite;
However, as said before, from the Best Practices - Resource Folder Don't use it!
Why not simply reference these ScriptableObjects the usual way via the Inspector in a field like
public SkinObject[] availableSkins;

How do I access a prefab and all of its instantiated clones in c# and unity?

I need a little help again please. I'm currently working on powerups and it's setup so that the powerup affectsthe prefab thats put into its prefab slot, so this is means it is currently only affecting one object (in my case, enemy). When the game starts I spawn about 20 enemies and if i pickup the powerup I want it to affect all those spawned enemies and not only the one that is dragged onto the script, does anyone have a solution or alternate method to do this please?
When instantiate them store them in a list and then iterate through all and update their value
In your case e.g.
// if enemies have a certain Component rather use that type here instead of GameObject
public GameObject enemyPrefab;
// and use the same type here instead of GameObject
private List<GameObject> enemyInstances = new List<GameObject> enemyInstances;
private void Start()
{
for(int i = 0; i < 20; i++)
{
var newEnemy = Instantiate(enemyPrefab);
enemyInstances.Add(newEnemy);
}
}
and then later
public void UpdateEnemies()
{
foreach(var enemy in enemyInstances)
{
// skip already destroyed objects
if(!enemy) continue;
// whatever to be done to the enemies
}
}
To get all game objects to an array you can use the Tag.
Just add an 'Enemy' tag to the prefab and use the code below:
public GameObject[] enemys;
void Start()
{
respawns = GameObject.FindGameObjectsWithTag("Enemy");
}
Full Unity documentation HERE

Own Class and List<> problems Unity C#

I need your help with things that are going to blow up my mind...(Don't know why but I can't write "Hello everybody!" at the start of the post. Forum hides it:)
I can't figure out why my List <'My Tile Class> is cleared by itself.
I have few prefabs (with box-collider) with no scripts attached to them and one script on the scene. The idea is to take random prefab, attach random value to it and initiate it. After that player clicks on prefab on the scene and gets its value.
public class Generate_Field : MonoBehaviour {
public class Tiles {
public GameObject My_Tile;
public int My_Value;
}
public List<Tiles> My_List;
private GameObject EmptyObj;
// ... other variables
void Start (){
List<Tiles> My_List = new List<Tiles> ();
// ... below some calculation (about 100 rows)
//Instantiate obj and give its reference to Emptyobj
EmptyObj = Instantiate (My_GameObj_To_Inst, new Vector3(My_X, My_Y, 0f), Quaternion.identity);
Tiles Tile1 = new Tiles ();
Tile1.My_Tile = EmptyObj;
Tile1.My_Value = 1; //
My_List.Add (Tile1); // Add Tile1 (with reference to Gameobj and value to List)
// If I put code inside Void Start () it works ok and print all values
foreach (Tiles SS in My_List) {
print (SS.My_Value);
}
The problem is when I put it to Void Update ().
My_List somehow "Suicide" to zero.count although it is public List. In that case I get error:
"NullReferenceException: Object reference not set to an instance of an object..."
void Update ()
{
if (Input.GetKeyDown (KeyCode.Mouse1)) {
print (My_List.Count);
}
}
You declare a new local list in Start and populate that one. If you mean to initialise the class member, remove the type from the line doing the initialisation.
void Start (){
My_List = new List<Tiles> (); // class member, not new variable.
As a usual guideline, it's better to initialise lists at the declaration point. There's usually no reason to replace the list at a later time.

Using foreach loop to find GameObjects that are children of something else (Unity3d)

I am making a video game, and while I have an existing code in generic C#, I now have to move it to Unity. I have some basic knowledge with generic C#, but I just started to learn the Unity way of coding.
For starters, I want to write a code that positions all game areas to correct positions, then turn them invisible. Yes, don't be surprised, they need to be all in same places.
Areas can have three size options, I called them Small, Medium and Large. Small and large areas have to be written manually.
List <GameObject> SmallAreas = new List<GameObject>();
void DefineSmallAreas()
{
SmallAreas.Add(areaConfirmLoad);
SmallAreas.Add(areaConfirmQuit);
SmallAreas.Add(areaConfirmSave);
SmallAreas.Add(areaGameSaved);
SmallAreas.Add(areaSave);
SmallAreas.Add(areaLoad);
}
Same with large areas.
Now, all other areas, are medium, and there is a large number of them.
So, I want to go through all game objects that are children of "areaContainer", check if their names start with "area", and if they do, I want to add them to MediumAreas list.
That's how I tried it:
void DefineMediumAreas()
{
GameObject areaContainer = GameObject.Find("areaContainer");
foreach (GameObject thisObject in areaContainer)
{
char[] a = thisObject.Name.ToCharArray();
if (a.Length >= 4)
{
char[] b = { a[0], a[1], a[2], a[3] };
string thisObjectType = new string(b);
(if (thisObjectType == "area")&&(!(SmallAreas.Contains(thisObject))
&&(!(LargeAreas.Contains(thisObject)))
{
MediumAreas.Add(thisObject);
}
}
}
This however shows an error, that "areaContainer" can't be used that way, I don't have access to Unity now, so can't copy exact message. I think that it's something like "Gameobject doesn't have IEnumerator".
I did try to google for the better approach, and found something called "transform".
foreach(Transform child in transform)
{
Something(child.gameObject);
}
What I don't understand, is how to use this "transform" in my specific situation.
Please don't get angry at me if this question is silly, I am very new to Unity, and have to learn it from scratch.
And a small second question. Will this work of turning object invisible work:
foreach(GameObject thisObject in MediumAreas)
{
thisObject.position = MediumVector;
thisObject.GetComponent<Renderer>().enabled = false;
}
MediumVector is location where the object must be moved to, and it seems to be working.
You can do this: foreach(Transform child in transform)
because the Transform class implements IEnumerable and have some mechanism that enables you to access the child GameObjects with the foreach loop.
Unfortunately, you can't do this: foreach (GameObject thisObject in areaContainer)
because areaContainer is a GameObject and this implementation is not done for the GameObject class. That's why you are getting this error:
foreach statement cannot operate on variables of type
'UnityEngine.GameObject' because 'UnityEngine.GameObject' does not
contain a public definition for 'GetEnumerator'
To fix it, change your loop to use Transform after finding the GameObject:
GameObject areaContainer = GameObject.Find("areaContainer");
foreach (Transform thisObject in areaContainer.transform){}
The complete code:
List<GameObject> MediumAreas = new List<GameObject>();
void DefineMediumAreas()
{
GameObject areaContainer = GameObject.Find("areaContainer");
foreach (Transform thisObject in areaContainer.transform)
{
//Check if it contains area
if (thisObject.name.StartsWith("area"))
{
//Add to MediumAreas List
MediumAreas.Add(thisObject.gameObject);
}
}
}
There is multiple approaches to your problem. One of them is using Tags. Simply mark your MediumArea prefab with some Tag and then you can find all tagged GameObjects with FindGameObjectsWithTag(string) (Unity Docs). Then you can populate your collection like that:
MediumAreas.AddRange(FindGameObjectsWithTag("MediumArea"));
Second approach could be finding all objects with same attached script FindObjectsOfType<T>() (Unity Docs). This is usefull when you are searching for instances of same type, like Medium Area.
Lets say that you have an Area script
public class Area : MonoBehaviour {
public AreaSize Size; // AreaSize is Enum
}
Then you can simply find your areas like:
var allAreas = FindGameObjectsOfType<Area>();
var mediumAreas = allAreas.Where(e => e.Size == AreaSize.Medium); // using System.Linq;
I created a project to answer your question, the final result will be like this :
As you can see I have created a game object called "areaContainer" and added 3 children with respective names : "area01", "area02" and "anotherObject".
The script that manage to get all "areaContainer" children that start with "area" looks like :
public GameObject areaContainer;
public List<GameObject> MediumAreas = new List<GameObject>();
private void Start()
{
DefineMediumAreas();
}
void DefineMediumAreas()
{
for (int i = 0; i < areaContainer.transform.childCount; i++)
{
var childGameObject = areaContainer.transform.GetChild(i).gameObject;
if (childGameObject.name.StartsWith("area"))
MediumAreas.Add(childGameObject);
}
}
1- I ended up referencing the areaContainer object in a script rather than using GameObject.Find because it's more performant.
2- To get a child of a game object you need to access to its transform and call GetChild(index). So by iterating through the parent container which is "areaContainer" we are getting its childCount.
3- To check if the name start with "area", simply use .StartsWith("area") which return true or false.
For your second question, you can hide object disabling the Renderer or by deactivating it (thisObject.SetActive(false);
I hope this help you; Happy coding!
You want to access all game objects that are children of "areaContainer"
In void DefineMediumAreas() function, you need to Transform[] to get an array of childeren. Use this:
Transform[] areaContainer = GameObject.Find("areaContainer").GetComponentsInChildren<Transform>();
foreach(Transform thisTransform in areaContainer)
{
...
}
I hope it helps you

Unity 3D spawning a prefab from a List

I made a script where it loads the prefabs from my Resources/ai folder and loop up to five times and generate a random index number. It successfully loaded my prefabs as GameObjects and does generate random index based on the count of the List.
However, I am having trouble upon spawning my prefab to my scene since Instatiate command does return an error while I'm coding.
I have tried methods to grab the name of the prefab through list[index].name or even list[index].toString() but it wont work. (Note: list stands for a variable of a list index is just an index to grab the game object on the list. This is not the actual code).
How to spawn a loaded GameObject prefab to my scene with a list?
My current code:
public List<GameObject> ai;
public GameObject[] spawn;
int indexAI;
int indexSpawn;
private void Start()
{
var res = Resources.LoadAll<GameObject>("ai/");
foreach (GameObject obj in res)
{
ai.Add(obj);
}
for (int x=0; x<5; x++)
{
indexAI = UnityEngine.Random.Range(0,ai.Count);
indexSpawn = UnityEngine.Random.Range(0, spawn.Length);
string name = ai[indexAI].name;
Debug.Log(name);
//I am currently using this kind of format since this is what I know for now.
Instantiate(name,spawn[indexSpawn].transform.position,spawn[indexSpawn].transform.rotation);
}
The error looks like this
Thanks!
You are attempting to instantiate a string - you need to instantiate the gameobject in your ai list instead.
Instantiate(ai[indexAI] ,spawn[indexSpawn].transform.position, spawn[indexSpawn].transform.rotation);

Categories

Resources