Im working on a AI combat system where each AI has a secondary collider with "Trigger" enabled. here is my script so far
public float health = 100;
public int isrunning = 1;
public GameObject currenttarget;
public int attackspeed;
public int damage;
public int newdamage = 0;
void Start()
{
StartCoroutine(DoDamage());
}
public void TakeDamage(float x)
{
this.health = this.health - x;
}
public IEnumerator DoDamage()
{
isrunning = 0;
yield return new WaitForSeconds(attackspeed);
Debug.Log("loop");
newdamage = damage;
isrunning = 1;
}
private void OnTriggerStay(Collider other)
{
if ( other.gameObject.CompareTag("AI"))
{
other.GetComponent<Framework>().TakeDamage(newdamage);
newdamage = 0;
}
}
private void Update()
{
if (isrunning==1)
{
StartCoroutine(DoDamage());
}
}
// Update is called once per frame
}
When I place three objects with this script where there damage is set to 5 and attack rate to 1, The result that I want out of this would be: A.100 B.100 C.100 (1 Second) A.80 B.80 C.80 However what I find is that the other.GetComponent().TakeDamage is only applying to one object at a time rather then being applied to the other two objects as I want. is this how the OnTriggerStay should be working? and if so are there any workarounds for this?
In my way, I suggest you to use a List of object, whenever the eneny attack(OnTriggerEnter) the player, you can push them to the list, then use the foreach loop in the Update Func to do some Function like take damage,... , after the enemy stop attack(OnTriggerExit) you can Pop its out of the List.
Related
I am making a script for a unity project to destroy an instantiated clone if it collides with another clone, but since the Game Object (the clone) is declared in the start method and I cannot put it at the top I need to figure out how to destroy the clone if it collides with something else.
This is the error I get if I put it on top:
A field initializer cannot reference the non-static field, method, or property
Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class topbigspike : MonoBehaviour
{
public GameObject Flame;
// Start is called before the first frame update
void Start()
{
int a = 30;
int i = 0;
while (0 < a)
{
a--;
i++;
GameObject FlameClone = Instantiate(Flame);
FlameClone.transform.position = new Vector3(Random.Range(10, 2000), -3, 0);
}
}
void OnCollisionEnter2D(Collision2D col)
{
Destroy(FlameClone);
}
}
As the error message says, you cannot use Flame as field initializer. But you can still declare FlameClone as field (at the top, as you say) ) and initialize it in the Start method:
public class topbigspike : MonoBehaviour
{
public GameObject Flame;
public GameObject FlameClone; // <=== Declare here, as class field.
// Start is called before the first frame update
void Start()
{
int a = 30;
int i = 0;
while (0 < a)
{
a--;
i++;
FlameClone = Instantiate(Flame); // <=== Initialize here.
FlameClone.transform.position = new Vector3(Random.Range(10, 2000), -3, 0);
}
}
void OnCollisionEnter2D(Collision2D col)
{
Destroy(FlameClone);
}
}
As already mentioned you need to store it a field in order to access it from other methods.
However, seeing a while loop there instantiating multiple (30) instances a single field isn't enough anyway except you really want to only destroy the last created instance.
It should probably rather be e.g.
public class topbigspike : MonoBehaviour
{
public int amount = 30;
public GameObject Flame;
private GameObject[] flameInstances;
void Start()
{
flameInstances = new GameObject[amount];
for(var i = 0; i < amount; i++)
{
var flameInstance = Instantiate(Flame);
flameInsance.transform.position = new Vector3(Random.Range(10, 2000), -3, 0);
flameInstances[i] = flameInstance;
}
}
void OnCollisionEnter2D(Collision2D col)
{
foreach(var flameInstance in flameInstances)
{
Destroy(flameInstance);
}
}
}
I am trying to call a method from the script Dice and after that method is executed the value of the variable diceValue will change. This is the value that I want to take and use in the method from the script Levizja.
Levizja.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Levizja : MonoBehaviour
{
public Transform[] lojtaret;
public GameObject zari;
private int numer = 0;
public Dice vleraEzarit;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
GetComponent<Rigidbody>().AddForceAtPosition(new Vector3(Random.Range(0, 500), Random.Range(0, 500) * 10, Random.Range(0, 500)), new Vector3(0, 0, 0), ForceMode.Force);
Debug.Log("U hodh zari me numer: " + Dice.getValue());
}
}
}
Dice.cs
using System.Collections.Generic;
using UnityEngine;
public class Dice : MonoBehaviour
{
Rigidbody rb;
bool hasLanded, thrown;
Vector3 initPosition;
[SerializeField] private static int diceValue;
private static int numriLojtareve;
//public int diceValue;
public DiceSides[] diceSides;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Reset();
RollDice();
}
if (rb.IsSleeping() && !hasLanded && thrown)
{
hasLanded = true;
rb.useGravity = false;
rb.isKinematic = true;
SideValueCheck();
}
else if (rb.IsSleeping() && hasLanded && diceValue == 0)
RollAgain();
}
void SideValueCheck()
{
diceValue = 0;
foreach(DiceSides side in diceSides)
{
if (side.OnGround())
{
diceValue = side.getValue();
Debug.Log(diceValue + " has been rolled!");
}
}
}
public static int getValue()
{
return diceValue;
}
}
Some of the methods are not included just to address only the issue.
I want to execute the Update method in Dice.cs which will call SideValueCheck method. This way the variable diceValue will be updated. After that I want the Update method in Levizja.cs to execute this way the new value will be stored there.
What happens is the first time I get the value 0 and the next run I get the last value that dice had. So if first time it landed 3 it shows 0. Next time it lands 2 it shows 3 and so on.
You could adjust this in the Script Execution Order and force a certain order of the executions of the same event message type (Update in this case).
However, this won't be enough. You rather want to wait until the dice has a result.
So before/instead of touching the execution order I would rather rethink the code structure and do something else. E.g. why do both your scripts need to check the user input individually?
Rather have one script call the methods of the other one event based. There is also no reason to have things static here as you already have a reference to an instance of a Dice anyway in vleraEzarit
public class Dice : MonoBehaviour
{
[Header("References")]
[SerializeField] private Rigidbody _rigidbody;
[SerializeField] private DiceSides[] diceSides;
[Header("Debug")]
[SerializeField] private int diceValue;
private bool hasLanded, thrown;
private Vector3 initPosition;
private int numriLojtareve;
// if you still also want to also provide a read-only access
// to the current value
public int DiceValue => diceValue;
// Event to be invoked everytime there is a new valid dice result
public event Action<int> OnHasResult;
private void Awake()
{
if(!_rigidbody)
{
_rigidbody = GetComponent<Rigidbody>();
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Reset();
RollDice();
}
if (_rigidbody.IsSleeping() && !hasLanded && thrown)
{
hasLanded = true;
_rigidbody.useGravity = false;
_rigidbody.isKinematic = true;
SideValueCheck();
}
else if (_rigidbody.IsSleeping() && hasLanded && diceValue == 0)
{
RollAgain();
}
}
void SideValueCheck()
{
foreach(DiceSides side in diceSides)
{
if (side.OnGround())
{
diceValue = side.getValue();
Debug.Log(diceValue + " has been rolled!");
// Invoke the event and whoever is listening to it will be informed
OnHasResult?.Invoke(diceValue);
// also I would return here since i makes no sense to continue
// checking the other sides if you already have a result
return;
}
}
// I would move this here
// In my eyes it is clearer now that this is the fallback case
// and only happening if nothing in the loop matches
diceValue = 0;
}
}
And then make your Levizja listen to this event and only act once it is invoked like e.g.
public class Levizja : MonoBehaviour
{
[Header("References")]
[SerializeField] private Rigidbody _rigidbody;
[SerializeField] private Transform[] lojtaret;
[SerializeField] private GameObject zari;
[SerializeField] private Dice vleraEzarit;
private int numer = 0;
private void Awake()
{
if(!_rigidbody)
{
_rigidbody = GetComponent<Rigidbody>();
}
// Attach a callback/listener to the event
// just out of a habit I usually remove it first to make sure it can definitely only be added once
vleraEzarit.OnHasResult -= HandleDiceResult;
vleraEzarit.OnHasResult += HandleDiceResult;
}
private void OnDestroy()
{
// make sure to remove callbacks once not needed anymore
// to avoid exceptions
vleraEzarit.OnHasResult -= HandleDiceResult;
}
// This is called ONCE everytime the dice has found a new result
private void HandleDiceResult(int diceValue)
{
_rigidbody.AddForceAtPosition(new Vector3(Random.Range(0, 500), Random.Range(0, 500) * 10, Random.Range(0, 500)), new Vector3(0, 0, 0), ForceMode.Force);
Debug.Log("U hodh zari me numer: " + Dice.getValue());
}
}
Ideally the diceValue should be returned by the method and Dice should not have a state. If you absolutely need Dice to have a state then it should not be static.
I feel like I'm missing some easy solution here, but I'm stuck on this. I'm calculating a score based on how far the player travels until they hit a building at the end of the course. The destruction score is separate from the distance score, and it increments until all of the building pieces have come to a rest.
I have an animation I want to play to add the distance score to the total destruction score and give the player's overall score, but I need the animation to trigger once the destruction score has stopped increasing. Right now, each piece of the building has code that checks if its moving and increments the score while true.
public class SkiLodgeScoreTracker : MonoBehaviour
{
Rigidbody rb;
private GameObject[] score;
private void Start()
{
rb = gameObject.GetComponent<Rigidbody>();
score = GameObject.FindGameObjectsWithTag("Score");
}
private void Update()
{
//check if lodgePiece is moving, while it is, add to Score object
if(rb.velocity.magnitude >= 2f && !rb.isKinematic)
{
score[0].GetComponent<SkiScore>().addToSkiScore(2f);
}
}
}
Here's where I want to have the animation trigger once that score has stopped (this was another attempt I made, the logic doesn't work)
public Animator moveScore;
...
if(skiScore > 0)
{
previousScore2 = previousScore1;
previousScore1 = skiScore;
if(previousScore1 == previousScore2 && !scoreMoved)
{
moveScore.SetTrigger("EndCourse");
addScoreToSkiScore();
}
}
public void addScoreToSkiScore()
{
scoreMoved = true;
for(float i = score; i>0; i--)
{
skiScore += 1;
}
}
I wanted to grab the score on one frame and see if it equals the score on the next frame and, if so, then trigger the animation, but I feel like that's not a valid option.
Any ideas?
In SkiScore, keep track of how many pieces are moving, and when a piece stops and the count becomes 0, do your thing:
public class SkiScore : MonoBehaviour
{
int movingCount;
void Start()
{
ResetMovingCount();
/* ... */
}
public void ResetMovingCount() {movingCount = 0;} // call as needed
public void OnStartedMoving() {++movingCount;}
public void OnStoppedMoving()
{
if (--movingCount == 0) OnNoneMoving();
}
void OnNoneMoving() {/* do the thing */}
/* ... */
}
For each piece, use a flag to remember if it has moved recently and if it has, and it's no longer moving, let your score manager know:
public class SkiLodgeScoreTracker : MonoBehaviour
{
Rigidbody rb;
private GameObject[] score;
// flag used to recognize newly stopped movement
bool recentlyMoving;
// cache for GetComponent
SkiScore mySkiScore
private void Start()
{
rb = gameObject.GetComponent<Rigidbody>();
score = GameObject.FindGameObjectsWithTag("Score");
// GetComponent is expensive, try not to call it in Update unless necessary
mySkiScore = score[0].GetComponent<SkiScore>();
}
private void Update()
{
//check if lodgePiece is moving, while it is, add to Score object
if(rb.velocity.magnitude >= 2f && !rb.isKinematic)
{
mySkiScore.addToSkiScore(2f);
recentlyMoving = true;
mySkiScore.OnStartedMoving();
}
else if (recentlyMoving)
{
recentlyMoving = false;
mySkiScore.OnStoppedMoving();
}
}
}
I have made a game manager making sure my data gets passed on from the first scene on to the next scene. Within the game manager, I have added certain scripts to ensure it gets passed. As you can see in the picture underneath.
The problem is that the score adds up at the first level, let's say I have 5 points. I go to level 2 and the UI shows my score as 0 (even tho I have nothing put as text within the score text) I kill 1 monster and the UI shows 6. So how can I put the UI to be showing it at all times? (Constant refresh?)
The second problem is that while the score manager does work. The health script cancels everything out when switching levels.
The user starts with 10 health. Takes damage in the first scene, but in the second scene, the user still has 10 health for some reason?
Game Manager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour {
public ActionButton PopupPrefab;
private ActionButton currentlySpawnedPopup;
public static GameManager instance = null;
void Awake () {
if (instance == null) {
instance = this;
} else if (instance != this) {
Destroy (gameObject);
}
Application.targetFrameRate = 60;
}
void Update () {
//if (Input.GetKeyDown(KeyCode.R)) {
// RestartScene ();
//}
}
public void InvokeRestartScene (float time) {
Invoke ("RestartScene", time);
}
public void RestartScene () {
SceneManager.LoadScene (0);
}
public void SpawnPopup (Vector2 position) {
DespawnPopup ();
currentlySpawnedPopup = Instantiate (PopupPrefab, position, Quaternion.identity) as ActionButton;
}
public void DespawnPopup () {
if (currentlySpawnedPopup != null) {
currentlySpawnedPopup.DestroySelf();
currentlySpawnedPopup = null;
}
}
public void FadePopup () {
if (currentlySpawnedPopup != null) {
currentlySpawnedPopup.FadeMe ();
currentlySpawnedPopup = null;
}
}
}
Score Manager
using UnityEngine;
using System;
using System.Collections;
public class ScoreManager : MonoBehaviour
{
public static ScoreManager Instance { get; private set; }
public int Score { get; private set; }
public int HighScore { get; private set; }
public bool HasNewHighScore { get; private set; }
public static event Action<int> ScoreUpdated = delegate {};
public static event Action<int> HighscoreUpdated = delegate {};
private const string HIGHSCORE = "HIGHSCORE";
// key name to store high score in PlayerPrefs
void Awake()
{
if (Instance)
{
DestroyImmediate(gameObject);
}
else
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
void Start()
{
Reset();
}
public void Reset()
{
// Initialize score
Score = 0;
// Initialize highscore
HighScore = PlayerPrefs.GetInt(HIGHSCORE, 0);
HasNewHighScore = false;
}
public void AddScore(int amount)
{
Score += amount;
// Fire event
ScoreUpdated(Score);
if (Score > HighScore)
{
UpdateHighScore(Score);
HasNewHighScore = true;
}
else
{
HasNewHighScore = false;
}
}
public void UpdateHighScore(int newHighScore)
{
// Update highscore if player has made a new one
if (newHighScore > HighScore)
{
HighScore = newHighScore;
PlayerPrefs.SetInt(HIGHSCORE, HighScore);
HighscoreUpdated(HighScore);
}
}
}
Health Script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class Health : MonoBehaviour {
public UnityEvent OnTakeDamageEvent;
public UnityEvent OnTakeHealEvent;
public UnityEvent OnDeathEvent;
[Header ("Max/Starting Health")]
public int maxHealth;
[Header ("Current Health")]
public int health;
[Header ("IsDeathOrNot")]
public bool dead = false;
[Header ("Invincible")]
public bool invincible = false;
public bool becomeInvincibleOnHit = false;
public float invincibleTimeOnHit = 1f;
private float invincibleTimer = 0f;
[Header ("Perform Dead Events after x time")]
public float DieEventsAfterTime = 1f;
void Start () {
health = maxHealth;
}
void Update () {
if (invincibleTimer > 0f) {
invincibleTimer -= Time.deltaTime;
if (invincibleTimer <= 0f) {
if (invincible)
invincible = false;
}
}
}
public bool TakeDamage (int amount) {
if (dead || invincible)
return false;
health = Mathf.Max (0, health - amount);
if (OnTakeDamageEvent != null)
OnTakeDamageEvent.Invoke();
if (health <= 0) {
Die ();
} else {
if (becomeInvincibleOnHit) {
invincible = true;
invincibleTimer = invincibleTimeOnHit;
}
}
return true;
}
public bool TakeHeal (int amount) {
if (dead || health == maxHealth)
return false;
health = Mathf.Min (maxHealth, health + amount);
if (OnTakeHealEvent != null)
OnTakeHealEvent.Invoke();
return true;
}
public void Die () {
dead = true;
if (CameraShaker.instance != null) {
CameraShaker.instance.InitShake(0.2f, 1f);
}
StartCoroutine (DeathEventsRoutine (DieEventsAfterTime));
}
IEnumerator DeathEventsRoutine (float time) {
yield return new WaitForSeconds (time);
if (OnDeathEvent != null)
OnDeathEvent.Invoke();
}
public void SetUIHealthBar () {
if (UIHeartsHealthBar.instance != null) {
UIHeartsHealthBar.instance.SetHearts (health);
}
}
}
I have thought of adding the following script on to my Health Script
But then I get the following error messages:
" Cannot implicitly convert type 'int' to 'bool'"
"The left-hand side of an assignment must be a variable, property or indexer"
void Awake()
{
if (health)
{
DestroyImmediate(gameObject);
}
else
{
(int)health = this;
DontDestroyOnLoad(gameObject);
}
}
The problem is that the score adds up at the first level, let's say I have 5 points. I go to level 2 and the UI shows my score as 0 (even tho I have nothing put as text within the score text) I kill 1 monster and the UI shows 6. So how can I put the UI to be showing it at all times? (Constant refresh?)
You can make one of the scripts set the UI text score when the scene is loaded.
void Start(){
// Loads the scoreText on start
scoreText.text = yourCurrentScore.ToString();
// Will work unless it has a "DontDestroyOnLoad" applied to it
}
The second problem is that while the score manager does work. The
health script cancels everything out when switching levels. The user
starts with 10 health. Takes damage in the first scene, but in the
second scene, the user still has 10 health for some reason?
In your health script, you had this:
void Start () {
health = maxHealth;
}
This resets your health everytime the scene loads and starts (Aka when you load to the next level). This causes the issue.
" Cannot implicitly convert type 'int' to 'bool'"
if (health)
The () for the if statement should be a condition (a question).
For example, doing health < 0 is valid since its saying "Is health less than 0?"
Doing health is not, since its just saying "10" (or some number).
"The left-hand side of an assignment must be a variable, property or
indexer"
(int)health = this;
If you wanted to change the value of health, just do health = 10 or health = some_Variable_That_Is_An_Integer
I have been doing a RPG game in Unity with C # and when doing a system of quests, specifically those of killing a certain number of enemies, I found the problem of having 3 enemies in the scene and being the target of the quest: Kill 3 enemies. If I kill them before activating the quest and later active the quest does not give me the reward (in this case experience). How can I tell the enemies and make that if the quest detects that I have already killed the necessary enemies to get the quest give me the reward equally?
Here the two needed scripts i think:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class QuestObject : MonoBehaviour {
public int questNumber;
public QuestManager qManager;
public string startText;
public string endText;
public bool isItemQuest;
public string targetItem;
public bool isEnemyQuest;
public string targetEnemy;
public int enemiesToKill;
private int enemyKillCount;
private PlayerStats playerStats;
public int EXPToGive;
void Start () {
playerStats = FindObjectOfType <PlayerStats> ();
}
void Update () {
if (isItemQuest) {
if (qManager.itemCollected == targetItem) {
qManager.itemCollected = null;
EndQuest ();
}
}
if (isEnemyQuest) {
if (qManager.enemyKilled == targetEnemy) {
qManager.enemyKilled = null;
enemyKillCount++;
}
if (enemyKillCount >= enemiesToKill) {
EndQuest ();
}
}
}
public void StartQuest (){
qManager.ShowQuestText (startText);
}
public void EndQuest (){
qManager.ShowQuestText (endText);
playerStats.AddEXP (EXPToGive);
qManager.questCompleted [questNumber] = true;
gameObject.SetActive (false);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyHealth : MonoBehaviour {
public int startingHealth;
public int currentHealth;
public GameObject damageBurst;
private PlayerStats playerStats;
public int EXPToGive;
public string enemyQuestName;
private QuestManager qManager;
void Start ()
{
// Setting up the references.
//anim = GetComponent <Animator> ();
//enemyAudio = GetComponent <AudioSource> ();
//enemyMovement = GetComponent <EnemyMovement> ();
//enemyAttacking = GetComponentInChildren <EnemyAttack> ();
// Set the initial health of the player.
currentHealth = startingHealth;
playerStats = FindObjectOfType <PlayerStats> ();
qManager = FindObjectOfType <QuestManager> ();
}
void Update ()
{
if (currentHealth <= 0) {
qManager.enemyKilled = enemyQuestName;
Destroy (gameObject);
playerStats.AddEXP (EXPToGive);
}
}
public void TakeDamage (int amountDamage)
{
// Reduce the current health by the damage amount.
currentHealth -= amountDamage;
Instantiate (damageBurst, transform.position, transform.rotation);
}
public void SetMaxHelth () {
currentHealth = startingHealth;
}
}
One Aproach would be to create some type of "WorldManager" which counts every Enemy which has been slain. And when Starting a quest this quest could check the WorldManagers kill count and add it to it's own count.
public void StartQuest (){
qManager.ShowQuestText (startText);
this.enemyKillCount += worldManager.GetKillCount();
}
In your enemy class you have to add a kill to your worldManager.
void Update ()
{
if (currentHealth <= 0) {
qManager.enemyKilled = enemyQuestName;
this.worldManager.AddKill(this)
Destroy (gameObject);
playerStats.AddEXP (EXPToGive);
}
}
Alternative:
Make your QManager be aware of every kill in a Scene.
You can achieve this through many ways.
One of them is passing your EnemyObject an reference of your Qmanager and do the same as with the "WorldManager" provided above, or you use Messaging and fire a Message targeting the QManager when an enemy is slain.
Alternative 2:
Throw an Event when an enemy has been slain and subscribe to it on your QManager/WorldManager. This way u can reuse your enemy class in every game. From my point of view static dependencies are evil, but there are many discussions and SO and everywhere on the internet about that.
You can several approach. The most straight-forward is to use static.
The purpose of static is for the variable/method to belong to the class instead of an instance of the class.
In your case, you want each enemy to have its own health, this cannot be static.
And you want to count how many instances there are in the scene from the class. So static is fine.
public class Enemy:MonoBehaviour
{
private static int enemyCount = 0;
public static int EnemyCount {get{ return enemyCount;} }
public event Action<int> RaiseEnemyDeath;
public static void ResetEnemyCount(){
enemyCount = 0;
}
private int health;
public void Damage(int damage)
{
CheckForDamage(); // here you check that damage is not neg or too big...
this.health -= damage;
if(this.health <= 0)
{
OnDeath();
}
}
void OnActivate()
{
enemyCount++;
this.health = 20;
}
void OnDeath()
{
enemyCount--;
RaiseEnemyDeath(enemyCount); // Should check for nullity...
}
}
This one is fairly simple. The first part is all static and is relevant to the class. The second part is relevant to the instance. If you use a pool of enemy and then reuse the same instance multiple times, the OnActivate method is called when you make the enemy alive in the scene (it may have been there for a while as inactive). Then when the health is down, kill the enemy (there are not all the required actions there...) and trigger the event.
Using the public static property, you can know what is the enemy count from a GameManager (Enemy should not affect the gameplay, only takes care of the enemy).
public class GameManager:MonoBehaviour
{
void Start()
{
Enemy.RaiseEnemyDeath += Enemy_RaiseEnemyDeath;
}
void Enemy_RaiseEnemyDeath(int count)
{
if(count < 0){ // End of level }
// You can also access enemyCount
int count = Enemy.EnemyCount;
}
}
The good point of using this principle is that Enemy has no clue about GameManager and can be reused in another game without any modification. The GameManager is a higher level entity and knows about it.