Item/Ability system using scriptableobjects in Unity - c#

I'm new to Unity and I'm having some trouble creating scriptable objects for items to pick up and use in my project. I've created the initial set-up to make the items with variables that I can plug-and-play with.
What I'm trying to do is:
1 - plug in a monobehavior script for each new item's behavior when used.
2 - Allow the player to pick up the item on collision and use the item on key command. The player should only carry one item at a time.
Here is what I have so far.
the scriptable object (itemObject.cs)
[CreateAssetMenu(fileName = "New Item", menuName = "Item")]
public abstract class Item : ScriptableObject {
public new string name;
public string description;
public int damageAmount;
public int affectDelay;
public int affectDuration;
public int affectRange;
public float dropChance;
public float itemLife;
}
I have a script that will trigger if the player collides with the object (itemDisplay.cs)
Currently, this doesn't do anything but destroys the item.
public class itemDisplay : MonoBehaviour {
public Item item;
public static bool isActive = false;
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
var playerPickUp = other.gameObject;
var playerScript = playerPickUp.GetComponent<Player>();
var playerItem = playerScript.playerItem;
playerScript.pickUpItem();
bool isActive = true;
Object.Destroy(gameObject);
}
}
void Start () {
}
void Update(){
}
}
Currently, I have a pickup function and a use item function that's being handled by my player script(Player.cs). Currently, this just toggles a boolean saying that it did something when the item is collided with. My big question is how/where should I create and reference the item ability and how do I pass it to the player script here?
public void pickUpItem()
{
playerItem = true;
//itemInHand = itemInHand;
Debug.Log("You Picked Up An Item!");
// Debug.Log(playerItem);
if(playerItem == true){
Debug.Log("This is an item in your hands!");
}
}
public void useItem()
{
//use item and set
//playerItem to false
playerItem = false;
}

If your question is "how do I create an instance of an Item in my project", you do that by right clicking in your Project view, then selecting Create -> Item. Then drag the Item asset that you created from your Project view into the ItemDisplay scene object's item field in the Inspector.
Regarding how to pick up the item, you would pass the item to your Player script from your ItemDisplay script. You've got most of that already wired up.
public class ItemDisplay : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
var playerPickUp = other.gameObject;
var playerScript = playerPickUp.GetComponent<Player>();
var playerItem = playerScript.playerItem;
// This is where you would pass the item to the Player script
playerScript.pickUpItem(this.item);
bool isActive = true;
Object.Destroy(gameObject);
}
}
}
Then in your Player script...
public class Player : MonoBehaviour
{
public void PickUpItem(Item item)
{
this.playerItem = true;
this.itemInHand = item;
Debug.Log(string.Format("You picked up a {0}.", item.name));
}
public void UseItem()
{
if (this.itemInHand != null)
{
Debug.Log(string.Format("You used a {0}.", this.itemInHand.name));
}
}
}
You should also make your Item class non-abstract, or leave it abstract and create the appropriate non-abstract derived classes (Weapon, Potion, etc.). I put an example of that below.
You can't attach a MonoBehaviour to a ScriptableObject if that's what you're trying to do. A ScriptableObject is basically just a data/method container that isn't tied to a scene. A MonoBehaviour has to live on an object within a scene. What you could do however, is add a "use" method to your Item class, and call that from a MonoBehaviour within the scene when the player uses an item.
public abstract class Item : ScriptableObject
{
// Properties that are common for all types of items
public int weight;
public Sprite sprite;
public virtual void UseItem(Player player)
{
throw new NotImplementedException();
}
}
[CreateAssetMenu(menuName = "Items/Create Potion")]
public class Potion : Item
{
public int healingPower;
public int manaPower;
public override void UseItem(Player player)
{
Debug.Log(string.Format("You drank a {0}.", this.name));
player.health += this.healingPower;
player.mana += this.manaPower;
}
}
[CreateAssetMenu(menuName = "Items/Create Summon Orb")]
public class SummonOrb : Item
{
public GameObject summonedCreaturePrefab;
public override void UseItem(Player player)
{
Debug.Log(string.Format("You summoned a {0}", this.summonedCreaturePrefab.name));
Instantiate(this.summonedCreaturePrefab);
}
}
Then change your UseItem method in Player:
public class Player : MonoBehaviour
{
public void UseItem()
{
if (this.itemInHand != null)
{
this.itemInHand.UseItem(this);
}
}
}

Related

How do I pass a variable between scripts in Unity 2d C#

For example, I have a variable "Wisps" that I want to change when the player picks up an object. But I don't know how to do it. I tried to add a WispDisplay object to call the classes, like in Java, but it doesn't seem to work.
public class WispCode : MonoBehaviour
{
WispDisplay wd = new WispDisplay();
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Player")
{
wd.setWisp(wd.getWisp()+1);
Destroy(gameObject);
}
}
}
public class WispDisplay : MonoBehaviour
{
public int Wisp = 5;
public Text WispText;
void Start()
{
}
void Update()
{
WispText.text = "Wisp: " + Wisp.ToString();
}
public int getWisp()
{
return Wisp;
}
public void setWisp(int newWisp)
{
Wisp = newWisp;
}
}
Easiest (a tiny bit dirty) way is to use a static variable. Downside: you can only have exactly ONE.
Example:
public class MyClass: MonoBehaviour {
public static int wisps;
}
Then, in ANY class, just use this to access it:
MyClass.wisps = 1234;
The more elegant way, working with multiple class instances, is using references.
Example:
public class PlayerClass: MonoBehaviour {
public int wisps = 0;
}
public class MyClass: MonoBehaviour {
public PlayerClass player;
void Update(){
player.wisps += 1;
}
}
Then, you need to drag-drop (aka "assign") the "PlayerClass" Component (attached to the player) to the the Gameobject that should increase the Wisps count. You can duplicate these objects after assigning the reference.
Now, if you actually want to have some sort of collectible, I'd suggest this approach:
You Have a Player "PlayerClass" and some Objects that are collectible, which have Trigger Colliders.
The objects have this code:
public class Example : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
// probably a good idea to check for player tag:
// other.compareTag("Player");
// but you need to create the "Player" Tag and assign it to Player Collider Object.
if(TryGetComponent(out PlayerClass player))
{
player.wisps += 1;
}
}
}

How do I store the same types of classes that have a different generic inside of a list?

I've been tinkering with this and I have a 'RespawnManager' that I want to use to manage my multiple 'SpawnPoint' classes with different generics but it ended up forcing me to use generics for my 'RespawnManager' which I don't want.
Let's say I had a SpawnPoint<T> class and I made a SpawnPoint<Enemy1>, SpawnPoint<Enemy2>, and SpawnPoint<Enemy3>. Is there any way I can make a list that can just manage multiple 'SpawnPoint's of any generic?
Base class:
public abstract class SpawnPoint<T> : MonoBehaviour
{
//how big the range of the spawn protection is
public int spawnProtectionRadius = 20;
public bool Occupied { get; set; }
public bool IsInSpawn(Transform target)
{
Debug.Log((target.position - transform.position).magnitude);
if ((target.position - transform.position).magnitude <= spawnProtectionRadius)
{
return true;
}
return false;
}
public abstract T Get();
}
Class that Inherits this
public class SeaMineSpawnPoint : SpawnPoint<Seamine>
{
public override Seamine Get()
{
return SeaMineObjectPool.PoolInstance.Get();
}
private void Start()
{
RespawnManager<Seamine>.respawnManager.AddSpawn(this);
}
}
Respawn manager:
public class RespawnManager<T> : MonoBehaviour where T : Component
{
public static RespawnManager<T> respawnManager;
[SerializeField]
private List<Transform> playerList;
[SerializeField]
private List<SpawnPoint<T>> spawnpoints;
private float respawnCounter;
private void Awake()
{
respawnManager = this;
}
private void Start()
{
foreach (SpawnPoint<T> sp in spawnpoints)
{
Debug.Log(sp.transform.position);
}
}
public void AddSpawn(SpawnPoint<T> spawnPoint)
{
spawnpoints.Add(spawnPoint);
}
public void RespawnSeaMines()
{
if (respawnCounter > 5)
{
respawnCounter = 0;
foreach (SpawnPoint<T> sp in spawnpoints)
{
foreach (Transform playerT in playerList)
{
if (sp.Occupied == false && !sp.IsInSpawn(playerT))
{
Component ourGameObj = sp.Get();
ourGameObj.transform.position = sp.transform.position;
ourGameObj.gameObject.SetActive(true);
sp.Occupied = true;
return;
}
}
}
}
}
private void Update()
{
respawnCounter += Time.deltaTime;
Debug.Log(respawnCounter);
RespawnSeaMines();
}
}
ObjectPool
//Class that's used for object pooling of different types.
//'T' must be a Unity component or it will error.
public abstract class ObjectPool<T> : MonoBehaviour where T : Component
{
//An object with this specific component that we use to copy.
[SerializeField]
private T prefab;
//Makes sure that only 1 coroutine runs at a time
private bool coroutineIsRunning;
//The singleton instance to our object pool.
public static ObjectPool<T> PoolInstance { get; private set; }
//A queue is used to organize plus activate and deactivate objects which
//have this component.
protected Queue<T> objects = new Queue<T>();
private void Awake()
{
//Set the instance of this pool to this class instance. Only one of these can be set.
if (PoolInstance != null)
{
throw new System.Exception("Singleton already exists. Cannot make another copy of this");
}
PoolInstance = this;
}
public T Get()
{
//If the queue happens to be empty, then add a brand new component.
if (objects.Count == 0) AddObjects(1);
//Returns the generic component and removes it from the queue.
return objects.Dequeue();
}
public void ReturnToPool(T objectToReturn)
{
//Disables the game object that the T component is attached to.
objectToReturn.gameObject.SetActive(false);
//Stores the T component in the queue.
objects.Enqueue(objectToReturn);
}
public void AddObjects(int count)
{
for (int i = 0; i < count; i++)
{
//Create a new copy of the prefab.
//The prefab is a game object with the T component attached to it.
T newObject = Instantiate(prefab);
//Disable the game object.
newObject.gameObject.SetActive(false);
//Add the T component to the queue.
//The T component is attached to the game object we created earlier.
objects.Enqueue(newObject);
}
}
public T GetWithDelay(int time)
{
T genericToReturn = null;
if (!coroutineIsRunning)
{
coroutineIsRunning = true;
StartCoroutine(GetCoroutine(time, genericToReturn));
}
return genericToReturn;
}
private IEnumerator GetCoroutine(int time, T generic)
{
float counter = 0;
while (counter < time)
{
counter += Time.deltaTime;
yield return null;
}
generic = Get();
generic.gameObject.SetActive(true);
coroutineIsRunning = false;
}
}
You should be able to declare your spawnpoints property in RespawnManager as a List<SpawnPoint<Component>> instead of List<SpawnPoint<T>>. That will allow you to get rid of the <T> type parameter entirely from RespawnManager and make it non-generic.

OnMouseDown() null reference exception

public class TowerNode : MonoBehaviour {
public bool IsShopOpen=false;
//Z position
public Vector3 positionOffSet;
//colors
public Color hoverColor;
private Color startColor;
//GameObjects
public GameObject turret;
//shop
public Shop shop;
//Build Manager
BuildManager buildManager;
void Start()
{
rend=GetComponent<Renderer>();
startColor = rend.material.color;
buildManager = BuildManager.instance;
shop = GetComponent<Shop>();
}
//When mouse is on the turret node
public void OnMouseDown()
{
Debug.Log("Mouse is Down");
bool IsShopOpen = true;
if (IsShopOpen == true)
{
Debug.Log("shop is open");
shop.onEnable();
}
if (EventSystem.current.IsPointerOverGameObject())
{
return;
}
if (!buildManager.CanBuild)
{
return;
}
if (turret != null)
{
Debug.Log("Cant Build Here!!!");
return;
}
buildManager.BuildTurretOn(this);
}
the other script of the shop is this:
public class Shop : MonoBehaviour
{
public TurretBlueprint Archery;
public TurretBlueprint Treb;
public TurretBlueprint Workamp;
public TurretBlueprint Barracks;
public Button Archeryy;
public Button Trebb;
public Button WorkCampp;
public Button Barrackss;
BuildManager buildManager;
void Start()
{
buildManager = BuildManager.instance;
disableAllButtons();
//OnEnable();
}
public void SelectArchery()
{
buildManager.SelectTurretToBuild(Archery);
Debug.Log("archery!!!!");
}
public void SelectTreb()
{
buildManager.SelectTurretToBuild(Treb);
Debug.Log("Treb!!!!");
}
public void SelectWorkamp()
{
buildManager.SelectTurretToBuild(Workamp);
Debug.Log("Work Camp!!!!");
}
public void SelectBarracks()
{
buildManager.SelectTurretToBuild(Barracks);
Debug.Log("Barracks!!!!");
}
public void onEnable()
{
Archeryy.gameObject.SetActive(true);
Trebb.gameObject.SetActive(true);
WorkCampp.gameObject.SetActive(true);
Barrackss.gameObject.SetActive(true);
}
public void disableAllButtons()
{
Archeryy.gameObject.SetActive(false);
Trebb.gameObject.SetActive(false);
WorkCampp.gameObject.SetActive(false);
Barrackss.gameObject.SetActive(false);
}
}
I need when i click on the game object that the shop will open the sop got few buttons under it, but it gives me:
"NullReferenceException: Object reference not set to an instance of an object
TowerNode.OnMouseDown () (at Assets/Scripts/TowerNode.cs:51)
UnityEngine.SendMouseEvents:DoSendMouseEvents(Int32)"
and I don't understand why.
The error you are getting implies that an object in the OnMouseDown function is null when executed:
NullReferenceException: Object reference not set to an instance of an object TowerNode.OnMouseDown () (at Assets/Scripts/TowerNode.cs:51)
You should look at ine 51 of TowerNode.cs and ascertain what can be null on that line. Could shop or buildManager be null? Perhaps this event is firing before you have initialised your TowerNode object.
It seems you are trying to access a component (Shop) from a completely unrelated GameObject. If you want all of your TowerNode objects to reference the Shop then you could use GameObject.Find. This isn't really recommended though; it seems like there is a better way to link these two objects but I am unsure of your use case.

Unity 2D project structure: How to create Player dynamically based on script

I need a little help for my inheritance of my player in my little 2D Game.
In fact, I don't know how I can create a new player, and add it to a list.
This is my project structure :
As you can see, in my scripts, I have a GameBase script, and PlayerBase script with PlayerLife, PlayerController and PlayerAimManager as child of PlayerBase.
In my prefabs, I have my camera with GameBase script, and PlayerPrefab, with player scripts.
My PlayerBase is a script witch define a player, like his life, speed, color etc... :
public class PlayerBase : MonoBehaviour
{
public int Id { get; set; }
public string Name { get; set; }
protected float Life { get; set; }
public Color Color { get; set; }
protected WeaponBase PrimaryWeapon { get; set; }
protected WeaponBase SecondaryWeapon { get; set; }
protected WeaponBase SelectedWeapon { get; set; }
protected ScoreBase Score { get; set; }
protected bool IsDead { get; set; }
protected float Speed { get; set; }
protected float JumpForce { get; set; }
protected virtual void Start()
{
IsDead = false;
//Reset rotation
transform.rotation = Quaternion.identity;
Life = 100;
Speed = 20;
JumpForce = 4000;
PrimaryWeapon = gameObject.AddComponent<SMG>();
SecondaryWeapon = gameObject.AddComponent<Shotgun>();
SelectedWeapon = PrimaryWeapon;
}
}
(And my child works like this :)
public class PlayerLife : PlayerBase
{
// Use this for initialization
protected override void Start()
{
base.Start();
var color = base.Color();
}
}
Ok, now this is my problem :
How can I instantiate a new player and save it in list.
In my GameBase I do something like this:
public class GameBase : MonoBehaviour
{
public List<PlayerBase> Players { get; set; }
void Start()
{
var playerObj1 = Instantiate(Resources.Load("Prefabs/Players/PlayerPrefab")) as GameObject;
PlayerBase player1 = playerObj1.GetComponent<PlayerBase>();
player1.Color = Color.red;
player1.Name = "Naografix Red";
var playerObj2 = Instantiate(Resources.Load("Prefabs/Players/PlayerPrefab")) as GameObject;
PlayerBase player2 = playerObj2.GetComponent<PlayerBase>();
player2.Color = Color.blue;
player2.Name = "Foo Blue";
Players = new List<PlayerBase>{
player1,
player2
}
}
}
If I do this, my color in my child player scripts don't have my color because is not static. But if I put Color to static prop, I can't change my color in my GameBase.
Well, I don't know if my inheritance is good or not.
I just want to create a player dynamically with child script based on PlayerBase to get his own variable.
EDIT
In fact, I don't know how I can create dynamically a player with custom properties.
I want to do this :
PlayerBase player1 = new PlayerBase();
player1.Name = "Nao";
player.Color = Color.Red;
And store it in my PlayerList. But I can't do this, because, no prefab is created.
So I instantiate a new GameObject with my PlayerPrefab.
-Ok, cool, my player is spawned, I'm going to apply some properties. Huh? How can I get my PlayerBase script?
Then, I get my PlayerBase with GetComponent, but still not working because I got my child (I don't have PlayerBase on my PlayerPrefab).
And if I put PlayerBase on it, every child have a different instance of my PlayerBase.
To conclude : How can I create a new Player based on PlayerBase, share all properties to my childs and spawn a PlayerPrefab with their properties ?
ENDEDIT
EDIT With simple example
public class Game : MonoBehaviour
{
private List<PlayerBase> _players;
public GameObject Player;
void Start () {
_players = new List<PlayerBase>();
var obj = Instantiate(Player);
var playerBase = obj.GetComponent<PlayerBase>();
playerBase.Color = Color.blue;
}
void Update () {
}
}
public class PlayerBase : MonoBehaviour {
public Color Color { get; set; }
// Use this for initialization
protected virtual void Start () {
gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color);
}
// Update is called once per frame
protected virtual void Update () {
}
}
public class PlayerLife : PlayerBase {
// Use this for initialization
protected override void Start () {
gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color);
}
// Update is called once per frame
void Update () {
}
}
My playerLife color is null. This is my first problem.
ENDEDIT
Ask me if you want more details.
Thanks
When you define a public member (not a property) in a MonoBehaviour, the value set by the inspector is automatically serialized for that game object. So that when you instantiate from that game object it will be automatically restored into the instantiated game object.
This also can be done without public accessor and with [SerializeField] attribute.
Make sure that only your PlayerLife script inherits from PlayerBase. The rest of your scripts(PlayerController and PlayerAimManager) should inherit from MonoBehaviour.
As, for your color problem, it is not showing because when you do player2.Color = Color.blue;, you modifying a struct which is a copy not a reference. You have to manually assign that color to gameObject.GetComponent<Renderer>().material.SetColor.
Your new PlayerBase script:
public class PlayerBase : MonoBehaviour
{
public Color Color;
public Color matColor
{
get
{
Color = gameObject.GetComponent<Renderer>().material.GetColor("_Color");
return Color;
}
set
{
Color = value;
gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color);
}
}
// Use this for initialization
protected virtual void Start()
{
matColor = Color;
}
// Update is called once per frame
protected virtual void Update()
{
}
}
PlayerLife:
public class PlayerLife : PlayerBase
{
public PlayerBase pBase;
void Awake()
{
pBase = this;
}
// Use this for initialization
protected override void Start()
{
matColor = Color;
}
// Update is called once per frame
protected override void Update()
{
}
}
To use it from your game scriot:
public class Game : MonoBehaviour
{
private List<PlayerLife> _players;
public GameObject Player;
void Start()
{
_players = new List<PlayerLife>();
GameObject obj = Instantiate(Player);
PlayerLife playerLife = obj.GetComponent<PlayerLife>();
playerLife.matColor = Color.blue;
_players.Add(playerLife);
GameObject obj2 = Instantiate(Player) as GameObject;
PlayerLife playerLife2 = obj2.GetComponent<PlayerLife>();
playerLife2.Color = Color.red;
_players.Add(playerLife2);
}
void Update()
{
}
}
Now, if you decide to access PlayerBase from your PlayerController and PlayerAimManager script,
PlayerBase playerBase = gameObject.GetComponent<PlayerBase>();
OR
PlayerBase playerBase = gameObject.GetComponent<PlayerLife>().pBase;

Disable instantiate and button click

There is issue that i facing with two objects and one button. One is cube second is ground when we click on button cube is collide with ground destroy and instantiate again. On Cube collision score is decrement.Also in hierarchy there is Empty game object which name is controller which has method of text score.Score is working fine but i want that when score is 0 then button click does not work and cube is not instantiate.
Cube :
Ground :
Controller :
CubeScript:
public class Cube : MonoBehaviour {
Rigidbody2D body;
void Start () {
body = GetComponent<Rigidbody2D>();
body.isKinematic = true;
}
}
Ground Script:
public class Ground : MonoBehaviour {
private Button button;
private BoxCollider2D collide;
public GameObject object1Clone;
void Start () {
collide = GetComponent<BoxCollider2D>();
collide.isTrigger = true;
button = GameObject.FindGameObjectWithTag ("Button").GetComponent<Button> ();
button.onClick.AddListener (() => Magnetic ());
}
void OnTriggerEnter2D(Collider2D target) {
Destroy (target.gameObject);
Instantiate (object1Clone, new Vector3 (0f, 4.12f, 0f), Quaternion.identity);
}
public void Magnetic(){
GameObject.FindGameObjectWithTag ("Player").GetComponent<Rigidbody2D> ().isKinematic = false;
}
}
ScoreScript:
public class ScoreScript : MonoBehaviour {
public static int Score=1;
void OnTriggerEnter2D(Collider2D target) {
if (Score <=0) {
} else {
Score--;
Controller.instance.SetScore(Score);
}
}
}
Controller:
public class Controller : MonoBehaviour {
public static Controller instance;
public Text scoreText;
void Start () {
scoreText.text = ""+1;
if(instance==null){
instance=this;
}
}
public void SetScore(int score){
scoreText.text =""+score;
}
}
First change the listener registration to this:
button.onClick.AddListener (Magnetic);
this will make it easier to remove the listener.
I will show you two ways of doing it, an easy one and a proper one a bit harder to grasp. So if you don't quite get it, use the first and learn about the second.
Every time you decrease the score, check for it and call for the appropriate action:
public class ScoreScript : MonoBehaviour {
public static int Score=1;
void OnTriggerEnter2D(Collider2D target)
{
Score--;
Controller.instance.SetScore(Score);
if(Score <= 0){
GameObject.Find("ground").GetComponent<Ground>().ClearButtonListener();
}
}
}
And in the Ground component:
public void ClearButtonListener()
{
button.onClick.RemoveListener (Magnetic);
}
Now the second more appropriate way would be to use event and listener
public class ScoreScript : MonoBehaviour, IScoreHandler {
public static int Score=1;
public event Action OnScoreZero = () => {};
void OnTriggerEnter2D(Collider2D target)
{
Score--;
Controller.instance.SetScore(Score);
if(Score <= 0){
OnScoreZero();
}
}
}
public interface IScoreHandler{ event Action OnScoreZero; }
And your listeners listens.
public class Ground : MonoBehaviour {
private Button button;
private BoxCollider2D collide;
public GameObject object1Clone;
private IScoreHandler scoreHandler = null;
void Start () {
scoreHandler = GameObject.Find("Score").GetComponent<IScoreHandler>();
if(scoreHandler != null){
scoreHandler.OnScoreZero += ClearButtonListener;
}
collide = GetComponent<BoxCollider2D>();
collide.isTrigger = true;
button = GameObject.FindGameObjectWithTag ("Button").GetComponent<Button> ();
button.onClick.AddListener (Magnetic);
}
void OnDestroy(){
if(scoreHandler != null){
scoreHandler.OnScoreZero -= ClearButtonListener;
}
}
}
Thanks to interface and event, your class is no more relying on another class but on an interface which makes it more flexible and scalable.
You need to set the field interactable of the UnityEngine.UI.Button object to false, see http://docs.unity3d.com/ScriptReference/UI.Button.html, i.e. use
void OnTriggerEnter2D(Collider2D target) {
if (Score <=0) {
/* disable the button */
GameObject.FindGameObjectWithTag ("Button").GetComponent<Button>().interactable = false;
}
in your ScoreScript.cs.

Categories