I have a real brain teaser right here and I can't figure out what I am doing wrong since the code looks absolutely normal.
Little background information:
Since I've started working in the XNA/MonoGame environment for about 2 days now I wanted to start off with something easy. I'm currently building an astroid defender like game, and I'm working on a collision check. Before I even got to the collision, I found that I got a rather odd null reference while im using a for loop to loop through all enemies inside a list. The list however is in another class.
Upon startup I initizalize a SpawnManager class that immediately creates 20 enemies when the class has been made like so:
class SpawnManager
{
//Draw
ContentManager myContentManager;
//Enemies
public List<Enemy> enemy = new List<Enemy>();
public void LoadContent(ContentManager theContentManager)
{
myContentManager = theContentManager;
for (int i = 0; i < 20; i++)
{
Enemy newEnemy = new Enemy(new Vector2((800 / (20 + 1) * i + 16.5f), 20), 500, 50, 25, 10, 2500, 100, 1, 3, 2, false);
newEnemy.LoadContent(myContentManager);
enemy.Add(newEnemy);
}
}
Then whenever I fire a bullet with my spaceship, it will create a bulletclass like so:
public class Bullet : PlayerScript
{
//Enemy enemy;
SpawnManager spawnManager;
PlayerScript playerScript;
private Texture2D _bullet;
private Rectangle _hitBox;
public float xPos = 0;
public float yPos = 0;
public bool isActive = false;
public void LoadContent(ContentManager theContentManager)
{
_bullet = theContentManager.Load<Texture2D>("Graphics/citem");
_hitBox = new Rectangle(0, 0, _bullet.Width, _bullet.Height);
}
public void Update(GameTime gameTime)
{
//Movement.
yPos -= 13.5f;
//Collision.
for (int i = 0; i < spawnManager.enemy.Count; i++)
{
//Console.WriteLine(spawnManager.enemy.Count);
}
}
When while im holding space to fire bullets, I immediately get a null reference error, hightlighting the for loop part. I've read some posts on the internet saying that the Enemy List might be null or empty, but even if that is the case, that shouldn't really matter as my code structure goes as follows:
Game1 Initialize -> add SpawnManager -> add Enemies + push them in the Enemy list.
So when the enemies are on the screen, they are basically already added to the Enemy list and the List shouldn't be null or empty.
Also, might there be an easier or more efficient way to check for collision? Because everytime I'm creating a bullet, a bullet.Update script will start looping through all enemies like a madman until it reaches an enemy, checking which enemy it is, applying changes to it and adding itself to a pool. (Ofcourse when its in the pool I wont update the Bullet script). Since this astroid defender is more like a little bullet hell, shooting 100 bullets per few seconds, will cause a lot of looping in total. I just came over to XNA / MonoGame C# after spending 1.5 years in Unity, and in Unity everything seems so simple, OnCollisionEnter ftw! :)
Still, I'm pretty sure its a total ID-10-T error on my side, but I just cant wrap my head around it, any help would be gratefully appreciated :)
P.S: If anything is stated unclear, I'd be glad to edit this question!
Related
So, I've been trying to learn Unity these past couple of weeks. As a starter project I decided to try and replicate the mobile game Pop the Lock. Right now, I'm having some problems with my keyboard inputs that I just can't figure out how to solve.
This is how my game screen looks: GameScreen. The red bar is the Player and the yellow circle is the Target. As the game progresses, the Player rotates around the ring towards the Target.
Basically, if the Player and the Target are touching AND the Player presses the Space Key at the exact same time, the Player is supposed to gain a point. The Target is also supposed to be destroyed, and a new Target is supposed to randomly be spawned in somewhere on the game screen. At the moment, this system works ... most of the time. About 80% of the time my code operates as it should, but around 20% of the time my code doesn't register when player presses the space key as the two collide. Here's my code:
public class Target: MonoBehaviour {
public GameObject target;
void Update () {
if (Input.GetKeyDown("space")) {
Debug.Log("SPACE PRESSED!!");
}
}
private void OnTriggerEnter2D (Collider2D collision) {
Debug.Log("Collision!");
}
private void OnTriggerStay2D(Collider2D other) {
// This is the part that sometimes isn't registering:
if (Input.GetKeyDown("space")) {
Debug.Log("HIT!!");
Score.score++;
// Code to spawn new Target on random place in the ring:
// Seems to be working as intended:
float distance = 2.034822f;
float x = Random.Range(-2f, 2f);
float y = Mathf.Pow(distance,2) - Mathf.Pow(x,2);
y = Mathf.Sqrt(y);
float[] options = {y, -y};
int randomIndex = Random.Range(0, 2);
y = options[randomIndex];
Vector3 vector = new Vector3(x, y, 0);
GameObject newTarget = Instantiate(target, vector, Quaternion.identity);
Destroy(gameObject);
}
}
}
As you can see I have Log statements that print something every time the player and the target are touching, every time the space key is pressed, and every time the space key is pressed while they are touching. This is how the console looks like when everything is working : Image One. This is what the console looks like when my code isn't working : Image Two.
So even when it isn't working, the collision and the key press are still registered at the exact same time. But for some reason the hit itself isn't registered (so the if condition isn't passed). Because of this I'm quite confident that it's not just input delay or me pressing the key at the wrong time. As I mentioned above, this only happens about 20% of the time, which makes it even more confusing to me. The Target has a trigger collider2D and it also has a dynamic RigidBody2D with gravity scale set to 0 (as I was told it should). Any help would be greatly appreciated.
(How my collider and rigidbody look: Image)
Something you can do is to set a flag to becomes true while you are pressing the key in the update loop, so the update loop will convert to something like:
private bool isSpacePressed = false;
update() {
isSpacePressed = false;
if (Input.GetKeyDown(KeyCode.Space)){
isSpacePressed = true;
}
}
so every loop the flag will be set to false except if you are pressing the space bar and the OnTriggerStay2D while become something like
OnTriggerStay2D () {
if(isSpacePressed){
.. do magic..
}
}
Look that I replace the Input.GetKeyDown('space') to Input.GetKeyDown(KeyCode.Space) I recommend using this one to avoid typing errors
so I'm wanting to pause the game once the amount of enemies hits 0. So I'm using GameObject.FindGameObjectsWithTag("Enemy").Length to find the number of enemies. I put this in a function that's called right when the enemies are instantiated so I can see the length go to 4, as there's 4 enemies spawning. When an enemy is killed the function is called again where the length is printed to console again. For some reason, on the first enemy killed the count repeats with a 4 again despite there only being 3 enemies. Once another enemy is killed it reports 3 when there's actually 2 and so on until I get to 1 when there's 0 enemies.
Here's the first snippet of code:
public class EnemyList : MonoBehaviour
{
public List<GameObject> weakMobs = new List<GameObject>();
public List<GameObject> mediumMobs = new List<GameObject>();
public List<GameObject> bossMobs = new List<GameObject>();
public List<Transform> spawningChildren = new List<Transform>();
public static int mobCount;
void Start()
{
for (int i = 0; i < spawningChildren.Count; i++)
{
GameObject newWeakMob = Instantiate(weakMobs[0], spawningChildren[Random.Range(0, 4)]) as GameObject;
}
CheckMobCount();
}
public void CheckMobCount()
{
mobCount = GameObject.FindGameObjectsWithTag("Enemy").Length;
print(mobCount);
}
The next piece of code is where the enemy is killed and the CheckMobCount() is called again.
public void TakeDamage()
{
enemyCurrentHealth -= 25;
enemyHealthBar.SetHealth(enemyCurrentHealth);
if (enemyCurrentHealth == 0)
{
Destroy(this.gameObject);
enemyList.CheckMobCount();
//needs death animations
}
}
Here's the console messages:
Console of printed lengths
I'm self taught so I apologize if this is elementary. I've tried doing this several different ways and this is the closest I've been but I'm open to new ideas as well.
Thank you!!
As noted in this answer, the object is not actually destroyed in the current frame.
From the documentation:
The object obj is destroyed immediately after the current Update loop… Actual object destruction is always delayed until after the current Update loop, but is always done before rendering.
I also agree that using DestroyImmediate() is a bad idea.
Ultimately, your question seems to really be about pausing the game when the enemy count reaches 0, which unfortunately hasn't actually been answered yet.
In fact, you don't really need to do anything different except move the check for the enemy count to the beginning of the Update() method, and pause the game there if it's 0. Then you'll find that the component for the enemy has been destroyed at that point.
Presumably enemies are spawned before the update loop starts (i.e. before the first frame), but if not then you can use whatever logic you're already using to decide that new enemies need to be spawned, to detect the fact that you haven't spawned any yet and avoid pausing before the enemies have spawned.
Here you have attached your script to your enemy instances. And they are still alive when you are querying for the number of enemies left.
You should do the following:
public class Enemy: MonoBehaviour
{
public static int EnemyCount = 0;
private void Start()
{
EnemyCount++;
}
private void OnDestroy()
{
EnemyCount--;
}
}
And then you can query the enemy count from anywhere but just excessing the EnemyCount by Enemy.EnemyCount.
If you want to get a more difficult example then you can check out this Game Dev tutorial: https://www.youtube.com/watch?v=LPBRLg4c5F8&t=134s
Destroy is actually executed at the end of the frame. There is DestroyImmediate that is executed immidiatelly but it's not recommended to be used. What I would do is to add a field or a property to identify whether the enemy is still alive and then to check against it. Something like:
class Enemy : MonoBehaviour
{
public bool IsAlive { get; set; } = true;
}
public class EnemyList : MonoBehaviour
{
//...
public void CheckMobCount()
{
mobCount = GameObject.FindGameObjectsWithTag("Enemy").Select(x => x.GetComponent<Enemy>()).Count(x => x.IsAlive);
print(mobCount);
}
}
And then:
public void TakeDamage()
{
enemyCurrentHealth -= 25;
enemyHealthBar.SetHealth(enemyCurrentHealth);
if (enemyCurrentHealth == 0)
{
Destroy(this.gameObject);
this.GetComponent<Enemy>().IsAlive = false;
enemyList.CheckMobCount();
//needs death animations
}
}
This can be further optimized to store the Enemy somewhere and not use GetComponent every time but you get the idea.
As already mentioned by others the issue is that Destroy is executed delayed.
Actual object destruction is always delayed until after the current Update loop, but is always done before rendering.
You could simply count only the GameObjects that are still alive, those for which the bool operator is true.
Does the object exist?
It will be false for objects destroyed in that same frame.
E.g. using Linq Count
using System.Linq;
....
mobCount = GameObject.FindGameObjectsWithTag("Enemy").Count(e => e);
which basically equals doing
mobCount = 0;
foreach(e in GameObject.FindGameObjectsWithTag("Enemy"))
{
if(e) mobCount++;
}
There is no need for an additional property or Component.
I am suggesting you to use “DestroyImmediate” instead of “Destroy”,Then look at the result.
I have a better idea, why not just use static variables when spawning enemies?
void Start()
{
for (int i = 0; i < spawningChildren.Count; i++)
{
GameObject newWeakMob = Instantiate(weakMobs[0],
spawningChildren[Random.Range(0, 4)]) as GameObject;
mobCount++;
}
}
Do not use Linq
Do not use DestroyImmediate (it will freeze and bug your game, probably)
Avoid FindGameObjectsWithTag in loops, only in initialization.
Track your enemies in an array or list
When you destroy an enemy, remove it's reference from the list
Use the list count/length to get the real actual number.
I'm trying to make a simple game where there's a zombie horde coming from the top of the screen to the bottom. I want that a new zombie appears every 5 seconds, and it does, but every time a new zombie appears the previous one stops moving and the collision doesn't work on that one. Can someone help me understand this behaviour and what's the best way to make it work the way its supposed to? :)
Here is my where I create the zombie:
private void CreateZombie()
{
zombieSprite = new CCSprite ("zombie");
zombieSprite.PositionX = CCRandom.GetRandomFloat (10, 600);
zombieSprite.PositionY = 1055;
AddChild (zombieSprite);
}
and here's the code inside my gamelogic method:
void GameLogic (float frameTImeInSeconds) {
zombieYVelocity += frameTImeInSeconds * -gravity;
zombieSprite.PositionY += zombieYVelocity * frameTImeInSeconds;
if (timer % 5 == 0) {
CreateZombie ();
zombieYVelocity = 0;
}
}
I attached a screenshot that shows whats happening
Every 5 seconds when a new one is added the previous one stops, and the collision detection is no longer working with the ones that are stoped.
I'm trying to create boundingBoxes (Collision) between an enemy and the bullets that it's struck with. Unfortunately, I'm being presented with the error System.ArgumentOutOfRangeException every time a enemy leaves the screen, or more than 1 bullet is spawned at a time.
Both bounding boxes are created in the Update functions of the bullet & enemy classes as such:
boundingBox = new Rectangle((int)position.X, (int)position.Y, texture.Width, texture.Height);
This is the relevant code being used in my Game1 update function:
for (int i = bullets.Count - 1; i >= 0; i--)
{
bullets[i].Update(gameTime);
ERROR OCCURS HERE -> if (bullets[i].boundingBox.Intersects(enemies[i].boundingBox))
{
score += 1;
bullets.RemoveAt(i);
}
//Bullets being destroyed upon leaving
if (bullets[i].position.Y < 0)
bullets.RemoveAt(i);
}
Any help would be appreciated.
Also, Here is how I implemented the bullet list into the Game1 class if that helps at all?:
List<Bullet> bullets = new List<Bullet>();
public void Shoot (Vector2 pos, Vector2 dir, float speed)
{
Bullet bullet = new Bullet(bulletTexture);
bullet.position = pos;
bullet.direction = dir;
bullet.speed = speed;
if (bullets.Count() < 40) //Maximum amount of bullets
bullets.Add(bullet);
}
If I have to guess, the error comes from the if (bullets[i].boundingBox.Intersects(enemies[i].boundingBox)) statement.
Basically, do you have the same amount of enemies and bullets all the time? If not, then it will fail whenever the numbers are different.
Why is this? Well, you are using two different collections, bullets and enemies, and taking the element at position i.
So if you have 5 bullets but only 1 enemy in the collection, the first iteration of the for will have i = 4, a position that does not exist in the enemies collection (it only has the enemies[0] position).
One way to solve the problem would be:
foreach (enemy in enemies)
foreach (bullet in bullets)
if (bullet.boundingBox.Intersects(enemy.boundingBox))
{
// do your logic here.
}
You go through every enemy, and for each one you check if any bullet is intersecting with it. This is not the best solution performance wise, but I think it's a good start.
I have made a game similar to asteroids and I have made an array of Asteroids controlled by an int count which i have made spawn 10 asteroids to the screen when the game starts.
What I'm wondering is how can I get the asteroids to spawn infinitely. I'm thinking of using a loop and have tried:
if (asteroidcount <= 5)
{
asteroidcount += 10;
}
But that doesn't seem to work. I am also using Visual Studio Express C# 2010
I think you need to try a different approach. First you will need an asteroid class where you can store positions and other variables you may need.
public class Asteroid
{
public Vector2 Velocity;
public Vector2 Position;
public Asteroid(Vector2 velocity, vector2 position)
{
Velocity = velocity;
Position = position;
}
}
Now add this List to your game, it will store all the asteroids. The reason I chose this over an array is it is much easier to change the size depending on how many asteroids you have.
List<Asteroid> Asteroids = new List<Asteroid>();
Now you can spawn 10 asteroids like this at the begining of your game
for (int i = 0; i<10;i++)
{
Asteroids.Add(new Asteroid(new Vector2(0,10), new Vector2(50,50)));
}
That will make an asteroid at position 50,50 with velocity of 10, so if you use my update code below it will move down with that velocity.
Now, for your actual problem, we need to spawn more when their are not enough (Player destroyed them I assume)
So, in your update method:
while (Asteroids.Count <5) //If there are less than 5 asteroids, add more
{
Asteroids.Add(new Asteroid(new Vector2(0,10), new Vector2(50,50)));
//Same thing as before, add asteroid
}
And there you go!
Here are some extra tips
If you want to draw all your asteroids, you need to make a method for it
public void DrawAsteroid(Asteroid a)
{
spriteBatch.Draw(ASTEROID TEXTURE, a.Position, Color.White);
spriteBatch.End();
}
now in your Draw() method you can add this
spriteBatch.Begin();
foreach (Asteroid a in Asteroids) //Draw each astroid
{
DrawAsteroid(a);
}
spriteBatch.End();
And you can use a simlar approach if you want to update all of the asteroids.
In Update(),
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
foreach (Asteroid a in Asteroids) //Update each astroid
{
UpdateAsteroid(a, elapsed);
}
And the method,
public void UpdateAsteroid(Asteroid a, float elapsed)
{
a.Position += a.Velocity * elapsed;
}
It's hard to answer without more code or info about how your game works, but a guess would be this: You probably want to put more logic into a larger game loop and add more asteroids in every frame or time step in the game if the amount drops below a certain amount.
bool running = true;
while (running)
{
// Handle input etc
// Handle game logic
// Spawn more asteroids if there are too few of them!
if (asteroidcount <= 5)
{
asteroidcount += 10;
}
// Render
}