Add script inheriting from scriptableobject to game object - c#

I have a script called Weapon.cs, which is a scriptable object.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class Weapon : ScriptableObject {
protected Camera _mainCamera;
protected Transform _weaponTransform;
protected int _damage;
protected int _fireRatePerSecond;
protected bool _isAutomaticWeapon;
protected void FireWeapon()
{
//if the weapon is automatic
if (_isAutomaticWeapon)
{
ShootRapidFireOnMouseHold();
}
//if the weapon is semi automatic
else
{
ShootSingleBulletOnMouseClick();
}
}
protected void MoveWeaponWithCamera(Transform weaponTransform)
{
_weaponTransform.rotation = _mainCamera.transform.rotation; //temporary way of making sure the gun moves with the camera
}
protected void ShootSingleBulletOnMouseClick()
{
if (Input.GetKeyDown(KeyCode.Mouse0)) //if left mouse button is clicked
{
CastRay();
}
}
protected void ShootRapidFireOnMouseHold()
{
if (Input.GetKey(KeyCode.Mouse0)) //if left mouse button is held down
{
CastRay(); //rapid fire
//PlayShootAnimation();
}
}
protected void CastRay()
{
RaycastHit hit;
if (Physics.Raycast(_mainCamera.transform.position, _mainCamera.transform.forward, out hit, 100))
{
Debug.Log(hit.transform.name);
}
}
//protected abstract void PlayShootAnimation();
}
I have a second script called MachineGun.cs, which inherits from Weapon.cs, and thus indirectly from scriptable object.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MachineGun : Weapon{
private GameObject _machineGunBarrel;
//private float _animationVelocity;
// Use this for initialization
void Start () {
//initialize the basic properties of a weapon
_damage = 7;
_fireRatePerSecond = 10;
_isAutomaticWeapon = true;
_weaponTransform = GameObject.Find("Weapon_MachineGun").transform;
_machineGunBarrel = GameObject.Find("machinegun_p1");
_mainCamera = Camera.main;
}
// Update is called once per frame
void Update () {
MoveWeaponWithCamera(_weaponTransform);
FireWeapon();
}
//protected override void Shoot()
//{
// RaycastHit hit;
// if(Physics.Raycast(_mainCamera.transform.position, _mainCamera.transform.forward, out hit, 100))
// {
// Debug.Log(hit.transform.name);
// }
//}
void PlayShootAnimation()
{
_machineGunBarrel.transform.RotateAround(_machineGunBarrel.transform.up, _fireRatePerSecond * Time.deltaTime);
PlayShootAnimation(); //play the shoot animation of the gun
}
}
It's currently impossible, since MachineGun.cs doesn't inherit from monobehaviour anymore.
I have a weapon gameobject in my scene, and so here's my question:
How do I go about adding the MachineGun.cs script as a component to my weapon gameobject? Or since this is impossible,
How should I build a weapon system with a general Weapon.cs script from which all weapons can inherit basic functions and fields/variables?
EDIT 1: provided code to my post and added the "why I wanna do this".
Thanks in advance!

ScriptableObjects should mainly be used in a data oriented way, they are very convenient and quite efficient for the task. Also, plaguing your project of MonoBehaviour is a very bad (and wide-spread) practice.
IMO, you should have a MonoBehaviour with weapon logic management and your ScriptableObjects should be your weapon data (which is interpreted by loading them up in your Weapon MonoBehaviour), such that you have Minigun, Glock, Katana.. Scriptable objects which have data like, attack speed, reload speed, charger size, weapon model/textures, reference to model hitbox, yadayadayada. (You might have a generic Weapon MonoBehaviour, but derive a Gun one, a Blade etc.. for very specific management, but which will still need data from ScriptableObjects)
In short your MonoBehaviours define usage and interaction, while your ScriptableObjects define characteristics

ScriptableObject says:
Description
A class you can derive from if you want to create objects that don't
need to be attached to game objects.
So either you should not attach this script to game object or derive your object from "MonoBehaviour" if you want to attach it to game object.
Why did you derive it from ScriptableObject ?

Related

Instantiated gameObject's self-written component is not working

I'm trying to implement chest mechanic in my game. When a user touches to chest, chest disappears and heart shows up. When player touches the instantiated heart, it must disappear and add 1 to life value, but the script is not working.
Chest.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Chest : MonoBehaviour
{
public GameObject heart;
public GameObject _heart; //Instantiated heart.
void OnTriggerEnter2D(Collider2D col){
if (col.gameObject.tag == "Player"){
Vector3 insPos = transform.position;
_heart = Instantiate(heart,new Vector3(insPos.x,insPos.y+0.5f,insPos.z),Quaternion.identity);
_heart.AddComponent<Heart>();
_heart.GetComponent<Heart>().ls = GameObject.FindGameObjectWithTag("Player").GetComponent<LifeSystem>();
Destroy(gameObject);
}
}
}
Heart.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Heart : MonoBehaviour
{
public LifeSystem ls;
void OnTriggerEnter2D(Collider2D col){
if (col.gameObject.tag == "Player"){
SoundManager.PlaySound("CollectCoinSound");
ls.lifes += 1;
Destroy(gameObject);
}
}
}
I check the components of the instantiated heart when player touches the chest and I can see that life system reference is added and so heart script is.
Thanks for your time.
You need to meet these conditions in order for OnTriggerEnter2D to be called on the spawned Hearth:
At least one between Player and Hearth need to have a Rigidbody2D component
Both Player and Hearth need to have a collider with IsTrigger = true
Player and Hearth GameObjects need to be on two interacting layers in the collision matrix
The Hearth component needs to be either on the same GameObject with a collider or on the same GameObject with the RigidBody2D
I actually simplified rules 1 and 2 a bit to show the most common use-case: if you want to know the actual rules for collisions, you can find them here (see the matrix at the bottom).
Besides the general collision rules I would make sure your fields have the correct type in the first place and already attach the Heart component to your prefab.
Also there is no need for find .. You already know the reference to the player you are colliding with
public class Chest : MonoBehaviour
{
public Heart heartPrefab;
void OnTriggerEnter2D(Collider2D col)
{
if (col.CompareTag("Player"))
{
Instantiate(heart, transform.position + Vector3.up * 0.5f), Quaternion.identity);
Destroy(gameObject);
}
}
}
and then simply do
public class Heart : MonoBehaviour
{
private void OnTriggerEnter2D(Collider2D col)
{
if (col.TryGetComponent<LifeSystem>(out var ls))
{
SoundManager.PlaySound("CollectCoinSound");
ls.lifes += 1;
Destroy(gameObject);
}
}
}
and rather have the LifeSystem attached to your player object.
In general I would kind of expect both eventually triggered at the same time since the heart might be already triggered if spawning right where the player is

How can I use inheritance in Unity effectively?

I have basic understanding of inheritance but since every script I want as a component on a game object has to inherit from MonoBehaviour things get tricky.
Let's say I want to have a base class called Character and I want Player and Enemy to derive from Character. How am I suppose to add Player and Enemy script to Player and Enemy objects without getting an error?
I also tried to create empty game object and add CharacterStats to it then populate this class with different character objects with different stats. Guess what, you can't use new keyword to create objects if the script derives from Monobehaviour.
Is there some tutorials about this topic to make it more clear?
Probably the thing to understand is that MonoBehaviour itself inherits Component - A MonoBehaviour IS a Component.
When you look at it as if Components are a part of a thing, it makes at least some sense that you can't just new a Component - could I make a new Arm? If I did, where would it got? But I could make a new Body body and then dobody.AddComponent<Arm>, right? Because then it's clear what happens to the Arm - it gets attached to the body.
Similarly you can't just new a Component because they're supposed to be a part of a GameObject. What you can do is to make a new GameObject and .AddComponent<>() to that object. Again, now it's clear where that Component is going.
I generally agree with UnholySheep's comment that you should prefer composition over inheritance because this generally makes your code more modular. That said, I definitely do also use inheritance.
The question you should be asking yourself when you're outlining your class is, "Is this new class a KIND of _____ or does this new class HAVE a _____." And it's easy I guess to say a Player is a Character and an Enemy is a Character, but do you need to subclass? Or would a Player just be a Character that also has a PlayerController? Maybe you could have an interface like IPlayerController and have a LocalPlayerController for the user and an AIPlayerController for the enemies? Lots of ways to problem-solve with programming.
Or maybe all Characters have a PlayerController and just the .activeControl bit is false for non-player characters. Then you could do things like ghost/spectate NPCs, etc.
But to your question specifically:
Let's say I want to have a base class called Character and I want Player and Enemy to derive from Character. How am I suppose to add Player and Enemy script to Player and Enemy objects without getting an error?
You would start with a Character script that inherits MonoBehaviour and implements any of the logic that is common to both classes:
public class Character : MonoBehaviour
{
// Your common character logic
}
Then you would have subclasses inherit Character:
public class Player : Character
{
// Your Player-specific logic here
}
and
public class Enemy : Character
{
// Your Enemy-specific logic here
}
Then you either add the Enemy and Player scripts to a GameObject in the editor or you can do it programmatically by getting a reference to the target GameObject (by setting the reference in editor or by creating a new GameObject in a script) and then you call targetGameObject.AddComponent<YourComponentToAdd>();.
Using inheritance well comes from planning and iterating. Perhaps your Character class doesn't work well as a class for your enemies. I tend to have the player in their own class (unless its multiplayer) with enemies using their own base class. Below is an example of some inheritance I use in a 2D game I'm working on.
Base Class:
Enemy.cs
public abstract class Enemy : MonoBehaviour
{
public Vector2 flyInPos;
public Vector2 startPos;
public bool isFlyingIn;
public float flyInDuration = 5000f;
public float flyInTime = 0f;
public abstract void Destroy();
public abstract void DestroyWithoutScore();
public abstract void Attack();
public abstract void Attack2();
public abstract void Attack3();
public abstract void FlyOut();
public virtual void FlyIn()
{
isFlyingIn = true;
}
}
Every enemy needs to have these elements. You'll notice most of the methods in my base class are marked abstract so that each class that inherits can have a unique set of Attacks, and Flying patterns. One thing to notice is that the base class is inheriting from MonoBehaviour. Here is how I implemented inheritance in one of my enemies. Sorry its still a WIP.
Inherited Class:
ShootingDrone.cs
public class ShootingDrone : Enemy
{
public GameObject projectileSpawner;
public GameObject projectile;
public void Start()
{
startPos = transform.position;
Scheduler.Invoke(Attack, 5f, gameObject);
}
public void Update()
{
if(isFlyingIn)
{
transform.position = Vector3.Lerp(startPos, flyInPos, flyInTime / flyInDuration);
flyInTime += Time.deltaTime;
}
}
public override void Attack()
{
Shoot();
Scheduler.Invoke(Shoot, 0.25f, gameObject);
Scheduler.Invoke(Shoot, 0.5f, gameObject);
}
public override void Attack2()
{
Attack();
}
public override void Attack3()
{
Attack();
}
public void Shoot()
{
GameObject newProjectile = Instantiate(projectile, projectileSpawner.transform);
newProjectile.transform.parent = null;
}
public override void FlyOut()
{
throw new System.NotImplementedException();
}
public override void DestroyWithoutScore()
{
throw new System.NotImplementedException();
}
public override void Destroy()
{
Destroy(gameObject);
}
}
The base class of Enemy inherits from MonoBehaviour. So when Shooting Drone inherits from Enemy it also is inheriting from MonoBehaviour.
For your problem of trying to add a new MonoBehaviour. The reason for this is that a MonoBehavior HAS to be attached to something. Imagine a Rigidbody with no GameObject, it doesn't work. If you wanted to make a "new" CharacterStats the way you would do that is:
CharacterStats stats = gameObject.AddComponent<CharacterStats>();
If you don't want your CharacterStats as a component, then simply remove the MonoBehavior inheritance from the class and instantiate it as new CharacterStats.
There are a variety of tutorials that cover inheritance, but depending on how new you are to the subject I would start with the Unity official inheritance tutorial. I do think this tutorial is too brief but I also like CircutStreams tutorial since it also mentions implementing Interfaces which can be a better solution to inheritance in many cases.

Finding a way for script to detect a child in a playerContainer game object

I'm currently making a 3D endless runner game with the choice to select different skins for the player. Everything's going smoothly until I come across a problem, in which I try to assign two collision game objects to each character prefab in the container that can detect the player's collision box when it collides with a powerup.
It only detects the first playerContainer's (Milo) 'Coin Detect' game object (eventhough it's been deactivated) and does not recognize the 'Coin Detect' collision game object in the Baddie playerContainer (which the player has chosen to play)
So my question is, how am I able to get the script to recognize the child that's active in the third playerContainer game object instead of it automatically detecting the first playerContainer game object's child?
From the Magnet powerup script I've made, the script detects the first playerContainer's 'Coin Detect' game object only. As shown in the attached picture below:
Here's my current script where the player is able to select their preferred characters.
using System.Collections.Generic;
using UnityEngine;
public class EnablePlayerSelector : MonoBehaviour
{
public GameObject[] players;
public int currPlayerCount;
void Start()
{
currPlayerCount = PlayerPrefs.GetInt("SelectedPlayer", 0);
foreach (GameObject player in players)
player.SetActive(false);
players[currPlayerCount].SetActive(true);
}
}
and here's the script where I deactivate the 'Coin Detect' collision game Object and activate it when the player collides with the powerup.
using System.Collections.Generic;
using UnityEngine;
public class Magnet : MonoBehaviour
{
public GameObject coinDetect;
void Start()
{
coinDetect = GameObject.FindGameObjectWithTag("CoinDetect");
coinDetect.SetActive(false);
Debug.Log("False");
}
private void OnTriggerEnter(Collider other)
{
if(other.gameObject.tag == "Player")
{
StartCoroutine(coinActivate());
Destroy(transform.GetChild(0).gameObject);
}
}
IEnumerator coinActivate()
{
Debug.Log("Hi");
coinDetect.SetActive(true);
yield return new WaitForSeconds(5f);
coinDetect.SetActive(false);
}
}
Well your issue is rather that you are doing GameObject.FindGameObjectWithTag("CoinDetect") which will return whatever is the first encountered CoinDetect in your scene in this moment. So unless EnablePlayerSelector.Start is executed before the Magnet.Start it will always be the first one since they are still all active by then.
You could probably already solve your issue by following the general thumb-rule:
Do things where you do not depend on other components already in Awake
Do things where you o depend on other components in Start
This way you already cover most of the use cases and are sure that components are self-initialized (Awake) before anything is accessing them in Start.
So simply change the EnablePlayerSelector.Start into Awake and you should be fine.
In very specific cases you might still have to adjust the Script Execution Order or use events.
Instead of doing this at all though I would rather in the moment of the collision request the current one from the PlayerContainer directly:
// put this on each player root object
public class Player : MonoBehaviour
{
// reference these via the Inspector
[SerializeField] private GameObject coinDetector;
[SerializeField] private GameObject playerrange;
// ... and in general any information bout this Player that is relevant for other scripts
// Readonly accessor property
public GameObject CoinDetector => coinDetector;
}
and then rather have
public class EnablePlayerSelector : MonoBehaviour
{
[SerializeField] private Player[] players;
[SerializeField] private int currPlayerCount;
public Player CurrentPlayer => players[currPlayerCount];
// In general do things where you don't depend on other scripts already in Awake
// This way you could actually already solve your issue by keeping things where you do
// depend on others in Start
private void Awake()
{
currPlayerCount = PlayerPrefs.GetInt("SelectedPlayer", 0);
for(var i = 0; i < players.Length; i++)
{
players[i].gameObject.SetActive(i == currPlayerCount);
}
}
}
and then finally do e.g.
public class Magnet : MonoBehaviour
{
public float effectDuration = 5f;
private IEnumerator OnTriggerEnter(Collider other)
{
var playerSelector = other.GetComponentInParent<EnablePlayerSelector>();
if(!playerSelector) yield break;
Debug.Log("Hi");
Destroy(transform.GetChild(0).gameObject);
var coinDetect = playerSelector.CurrentPlayer.CoinDetector;
coinDetect.SetActive(true);
yield return new WaitForSeconds(effectDuration);
coinDetect.SetActive(false);
}
}
or alternatively if the other in this case refers to the object with the Player component anyway you could also directly do
public class Magnet : MonoBehaviour
{
public float effectDuration = 5f;
private IEnumerator OnTriggerEnter(Collider other)
{
if(!other.TryGetComponent<Player>(out var player)) yield break;
Debug.Log("Hi");
Destroy(transform.GetChild(0).gameObject);
var coinDetect = player.CoinDetector;
coinDetect.SetActive(true);
yield return new WaitForSeconds(effectDuration);
coinDetect.SetActive(false);
}
}

Can't figure out what i've done wrong

I'm new to programming and I'm currently trying to do a simple game in Unity.
The code makes some things dissapear when they touch the ground and it works well but the "Evaporated" variable does not update in the Unity Inspector. When something touch the ground, evaporated should be incremented, but it stays at 0.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
public class EnemyEvaporate : MonoBehaviour
{
public GameObject Enemy;
public int Evaporated;
void OnCollisionEnter(Collision CollisionInfo)
{
if (CollisionInfo.gameObject.name == "Map" || CollisionInfo.gameObject.name == "Player")
{
Thread.Sleep(15);
gameObject.SetActive(false);
Evaporated++;
}
}
}
I'm not really sure what's the behavior you're trying to get in this script. If Evaporated is some kind of 'Global counter' for score, it need to be written different and not store inside script for this gameObject.
It might be related to your gameObject.SetActive(false) which will deactivate your object, so in fact it will disables also the scripts connected with it (and especially this one. SetActive documentation) and removes them from Update() for a given gameObject (so given enemy. If you have 4 Enemy objects on the Scene, in fact you have 4 instances of this script and each has own Evaporated variable)
Why won't you move the void OnCollisionEnter(Collision CollisionInfo) to your Enemy scipt? I think Enemy should now if it collided with anything, not some external script.
You can also use tags instead of names when detecting Collisions.
Also, gameObject.SetActive(false) - if you set your gameObject to disabled, nothing more from its script will happen unless you set it to active again. It e.g. cancels all Coroutine. The gameObject is just "sleeping", I would say. It's not destroyed - you still can see it in your Hierarchy window - but it can't do anything. (By the way, in this case you're setting EnemyEvaporate gameObject as disabled, not Enemy gameObject.)
Also, you can use Coroutine instead of Thread.Sleep(15). It is more common to use Coroutines than Threads in Unity.
Now, Evaporation. If you want to count how many Enemies evaporated (I suppose, looking at Evaporated++;) you should have some external object to count it. The simplest way I can propose fow now is creating a GameObject with EvaporationCounter : MonoBehaviour script attached to it. You can also use Singleton pattern to be sure there's only one object of this type.
public class EvaporationCounter : MonoBehaviour
{
private int evaporates;
public int Evaporates {
get { return evaporates; }
private set
{
if (value >= 0) evaporates = value;
}
}
public void AddEvaporation()
{
Evaporates++;
}
}
Your Enemy class could look like:
public class Enemy : MonoBehaviour
{
private IEnumerator coroutine;
private EvaporationCounter evaporationCounter;
private void Start()
{
evaporationCounter = FindObjectOfType<EvaporationCounter>();
}
private IEnumerator WaitAndSetInactive(float waitTime)
{
yield return new WaitForSeconds(waitTime);
gameObject.SetActive(false);
}
private void OnCollisionEnter(Collision CollisionInfo)
{
if (!CollisionInfo.gameObject.CompareTag("Map") && !CollisionInfo.gameObject.CompareTag("Player")) return;
if (evaporationCounter != null)
evaporationCounter.AddEvaporation();
StartCoroutine(WaitAndSetInactive(15f));
}
}
Calling evaporationCounter.AddEvaporation() from the Enemy script is not the best solution, since it does not apply to the Dependency Inversion principle but I would say it's good for the beginning with Unity.

Go to next level when Key is grabbed by Player Unity 2d

I need some help with a feature I'm implementing for a game made in Unity 2D.
The player must take a key in order to unlock a door (maybe showing an animation) and when the player go in front of that door if he has the key, he will automatically go to the next level.
I need help, cause the door is not letting go to the next level.
Here is the KEY code/script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class KeyScript : MonoBehaviour {
//public AudioSource coinSoundEffect;
public AudioClip key1;
void Awake () {
//source = GetComponent<AudioSource>();
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
void OnCollisionEnter2D (Collision2D other) {
Debug.Log("Chiave Presa");
if(other.gameObject.tag =="Player")
GameObject.Find("KeyDoor").SendMessage("HitKey");
SoundManager2D.playOneShotSound(key1);
}
}
Here is the DOOR code/script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class DoorScript : MonoBehaviour {
public bool key = false;
public string nextLevelName;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
void HitKey (){
Debug.Log("La porta รจ sbloccata");
key = true;
Destroy (GameObject.Find ("Key"));
}
void OnCollisionEnter2D (Collision2D other){
if(other.gameObject.tag == "Player"){
if (key == true){
Application.LoadLevel(nextLevelName);
//Destroy(gameObject);
//GameObject.Find("Key").SendMessage("DestroyKey"); //If you also want to destroy the key
//GoToNextLevel ();
}
}
}
void OnTriggerEnter2D (Collision2D other)
{
Application.LoadLevel(nextLevelName);
}
public virtual void GoToNextLevel()
{
//loadingImage.SetActive(true);
Application.LoadLevel(nextLevelName);
}
}
The code works but when the player goes in front of the door, he is not passing to the next level.
Any help or hint appreciated.
Cheers
First of all, you're missing one thing:
unity is Component based engine
This means you should make interactions between Components, not GameObjects.
In your script there's a public field public bool key which should not be accessible from the outside. But if you're looking for simple and wrong answer that will work then you can just replace this line :
GameObject.Find("KeyDoor").SendMessage("HitKey");
Into this one :
GameObject.Find("KeyDoor").GetComponent<DoorScript>().key = true;
In that case you'll end up with messy code and unstable game. What I can recommend to you as an good alternative is to rewrite your logic a bit.
Instead of making a MonoBehaviour which is not needed you can just create a Component :
public class KeyComponent : Component
{
[SerializeField]
AudioClip m_KeyPickupSound;
[SerializeField]
bool m_IsPickedUp;
public bool PickedUp
{
get { return m_IsPickedUp; }
}
public void PickUp()
{
m_IsPickedUp = true;
SoundManager2D.playOneShotSound(m_KeyPickupSound);
}
}
Now attach this into your Player's Components list and in your door script do:
void OnCollisionEnter2D (Collision2D other)
{
if(other.GetComponent<KeyComponent>() != null && other.GetComponent<KeyComponent>().PickedUp)
{
SceneManager.LoadScene(2);
}
}
Now only thing left is to update your Player's MonoBehaviour and add simple collision check :
void OnCollisionEnter2D(Collision2D other)
{
if(other.tag == "TAG_FOR_KEY")
GetComponent<DoorScript>().PickUp();
}
Now you're interacting with Components and not GameObject which then require less effort changing some scripts.
First off, you should change Application.LoadLevel to SceneManager.LoadScene. The first is obsolete in the newer versions of unity.
Second you need to make sure that your scenes are actually registered in File -> BuildSettings and the parameter you pass to LoadScene matches either the index in that list or the name as string.
Make sure your player needs to have a Rigidbody2D and a Collider2D and the door has a Collider2D component
If your doors Collider2D is a trigger you must use void OnTriggerEnter2D(Collider2D other).
Another problem may be that "Level 2" isn't added to your build settings, make sure that it is added to the level's list.
Answering your last comment as it seemed the problem was that the Collider2D on the door, you can make this in a lot of ways, the easiest way is this:
Add a public string variable called nextLevelName in your Door's script, then when calling LoadLevel, use this variable. You can change the value from the inspector in each level. The problem with this is that if you rearrange the levels then you need to change the strings in each level.
The best solution in that case is this one:
int currentLevelNum = SceneManager.GetActiveScene().buildIndex;
SceneManager.LoadScene(currentLevelNum+1);
This way if you have your levels in order in your build settings you don't need to do anything else.

Categories