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.
Related
I'm kinda new to unity, been spinning my head around it this couple of last days. I've encoutered a problem where when I'm spawning 2 or more players into my scene they just get launched out of the map. I've figured out that it's a problem with the spawn points. I did set up a range between some values where they will be spawned but seems that they still spawn into the same spot and get lauched across the map.
What I want to do is, create 4 spawnpoints(the maximum number of people that can play the game) in which they will get spawned based on the number of players.
I have a function called "SetPosition" with the code:
public void SetPosition()
{
transform.position = new Vector3(Random.Range(-1,11), 0.8f, Random.Range(-4,5));
}
and it is used here, if the scene is "Game"
private void Update()
{
if(SceneManager.GetActiveScene().name == "Game")
{
if(PlayerModel.activeSelf == false)
{
SetPosition();
PlayerModel.SetActive(true);
}
Any support is appreciated, been trying to find an answer but could find anything that would fit my need.
At early stage best solution will be create 4 empty game objects at the places where you want to keep spawn point.
Then take an array of Transform to store those in script and just pass the random index to get a point.
public Transform[] points;
public Vector3 GetRandomPoint()
{
return points[Random.Range(0,point.length)].position;
}
So I've done something. I've added 4 empty objects with a parent called "SpawnManager", also made a script for the spawn manager that is down below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnManager : MonoBehaviour
{
public static SpawnManager Instance;
Spawnpoint[] spawnpoints;
void Awake()
{
Instance = this;
spawnpoints = GetComponentsInChildren<Spawnpoint>();
}
public Transform GetSpawnPoint()
{
return spawnpoints[Random.Range(0, spawnpoints.Length)].transform;
}
}
In the PlayerModel script where I set the spawn position for the player I've added this function but it doesn't work. No errors, but it doesn't respect the spawnpoints
public void SetPosition()
{
Transform spawnpoint = SpawnManager.Instance.GetSpawnPoint();
transform.position = spawnpoint.position;
}
The SetPosition function was like this before I edited it:
public void SetPosition()
{
transform.position = new Vector3(Random.Range(-1,11), 0.8f, Random.Range(-4,5));
}
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
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);
}
}
I have a static Player class that I have a Jump method in it. It takes a Rigidbody2D parameter so I have to call it by typing
Player.Jump(GetComponent<Rigidbody2D>);
but I want to get the Rigidbody2D component in the Jump method and call it by typing
Player.Jump();
Is there any way to do this? Can I get the Rigidbody2D component from the Jump method?
My Jump code:
/// <summary>
/// Makes the given Rigidbody2D jump.
/// </summary>
public static void Jump(Rigidbody2D rb)
{
rb.velocity = new Vector2(rb.velocity.x, 0);
rb.AddForce(new Vector2(0, JumpHeight));
}
Class that I'm using the Jump method
if (Player.CanMove)
{
Player.Move(rb);
if (Input.GetKeyDown(KeyCode.Space) && canJump)
{
if (tempMaxJumps > 0)
{
Player.Jump(rb);
tempMaxJumps--;
}
else
{
canJump = false;
}
}
}
If you really need the player class to stay a static class, i suggest simply adding a
public static RigidBody2D Rigidbody;
to which you can assign to once in the Start of the class calling Jump
However this whole question seems like bad practice, a Player script should generally be inherit from MonoBehaviour and be attached to the player GameObject in your scene.
Then inside this Player script you can check for input and have a Jump-method
You could create a static member which would have a reference to the rigidbody and set the reference at some point prior to calling Jump, but that's a bad idea.
By using the static class to make the player object jump, you are breaking abstraction. Now you have code outside of your player object that is influencing the player, leading to code spaghetti.
Like this?
public static class Player
{
public static Rigidbody2D Jump()
{
return new Rigidbody2D();
}
}
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.