pacing code execution in a Unity based Roguelike - c#

[UPDATE] I tried using the suggestion below but am still having an issue where the enemies appear to move simultaneously and they each move twice when first seen if there is already an enemy in line of sight. All I really want to do is have a delay between the actions of enemies in the player s line of sight so they do not simultaneously move, play sounds etc...
The updated code is:
private IEnumerator takeAllEnemyActions()
{
// Cycle through all enemies and have them perform their actions
if (Enemies != null && Enemies.Length > 0)
{
bool foundEnemyInLoS = false;
Enemy lastEnemy = null;
foreach (GameObject enemy in Enemies)
{
// Cache the enemies script
Enemy thisEnemy = enemy.GetComponent<Enemy>();
// If it is not this enemies turn to move then move on to the next enemy
if (thisEnemy.NextMoveTick > GameManager.Instance.CurrentTick)
{
continue;
}
// Check to see is this enemy is the first enemy in the players line of sight (If there is one)
if (!foundEnemyInLoS && thisEnemy.isInLineOfSightOfPlayer())
{
// If so then we save it for last to ensure that if any enemy is called with a delay that the last one does not make the player wait to move
foundEnemyInLoS = true;
lastEnemy = thisEnemy;
continue;
}
// At this point only enemies whose turn it is and who are not being saved for later can act
thisEnemy.TakeEnemyActions();
// If the enemy was in the players line of sight wait 200ms before continuing
if (thisEnemy.isInLineOfSightOfPlayer())
{
yield return new WaitForSeconds(0.5f);
}
}
// if there were enemies in the players line of sight then one was saved for last
// Take its action without a delay so the player can go next
if (foundEnemyInLoS)
{
lastEnemy.TakeEnemyActions();
}
}
Original Question:
In a top down, 2d, turn based roguelike in Unity3d 5 I am having an issue with game pacing. I am using a very simple state machine to control execution but I want enemies that all move in the same turn to do so with a pause between each one if they are in the players line of sight so that animations and sound have time to occur without overlapping (Especially sounds). The only issue I am having is getting the actual pause to occur without affecting animations or camera movement (The camera may be sliding to follow a player movement and it was stopping). I only need about a 100ms pause before moving to the next enemy.
The enemies are taking their actions in a foreach loop in an EnemyManager script
private void takeAllEnemyActions()
{
// Cycle through all enemies and have them perform their actions
if (Enemies != null && Enemies.Length > 0)
{
foreach (GameObject enemy in Enemies)
if (enemy.GetComponent<Enemy>().NextMoveTick <= GameManager.Instance.CurrentTick)
enemy.GetComponent<Enemy>().TakeEnemyActions();
}
GameManager.States.NextPlayState();
}
I tried creating a routine to invoke each enemy to act but the issue there was that execution of other enemies continued.
I tried using a coroutine but again the issue was the game would move on into the next state.
I even tried a while-do loop using Time.RealTimeSinceStartup just to see what it would do (I know, a very VERY bad idea)
I'm certain this is simple and I am just having a brain cramp but I've been trying things and google-binging for hours with no progress. Thank goddness for git to roll back.
Does anyone have any suggestions on the best way to go? I'm not needing someone to write my code, I just need pointed in the right direction.
Thanks

Use a co-routine and yield for each statement. Here's some modified code.
private IEnumerator TakeAllEnemyActions() {
// Cycle through all enemies and have them perform their actions
if (enemies != null && enemies.Length > 0) {
foreach (var enemy in enemies) {
if (enemy.GetComponent<Enemy>().NextMoveTick <= GameManager.Instance.CurrentTick) {
enemy.GetComponent<Enemy>().TakeEnemyActions();
yield return new WaitForSeconds (0.1f);
}
}
}
GameManager.States.NextPlayState();
}
Call this method with
StartCoroutine(TakeAllEnemyActions());

Related

How can I execute code if something is true, continue executing the code indefinitely, but stop evaluating the if statement?

I'm just starting out please excuse vast ignorance.
I'm writing a c# script in unity as part of the essentials training. I'm doing the 3d audio module and I thought I'd try and get a little bit fancier than the scope of this particular lesson which is supposed to be having an object fly through a window in a pre-built scene and make a 3d sound as it moves.
I wanted to make the movement of the object conditional upon a player moving close to it in 3d space. I figured out how to trigger the movement of an object in a script with an if statement that changes the transform parameters of the object the script is attached to when a 'distanceFromObject' variable is < 2. It works, however the script runs in the update section of the script which runs once every frame. This means that the object's transform parameters are changed every frame as expected but of course stops doing so when the distance between the object that's moving and the player exceeds 2.
I see the mistake I've made because if the object moves away when the player gets close then it will inevitably eventually move far enough away that the distanceFromObject variable will grow bigger than 2 whereupon it stops and just hovers in place. I don't know how to fix it though.
I need the script to check the distance between the object and the player every frame so that it will trigger the instance the player gets close enough, and when they get close enough, I need the object to move away, however once it has been triggered to move, I need the object to continue moving, but the script to stop checking what the distance is anymore.
The script looks like this
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyOff : MonoBehaviour
{
public Vector3 rotateChange;
public Vector3 positionChange;
public float distanceFromObject;
public GameObject character;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
distanceFromObject = Vector3.Distance(character.transform.position, this.gameObject.transform.position);
print (distanceFromObject);
if (distanceFromObject < 2)
{
transform.Rotate (rotateChange);
transform.position += positionChange;
}
}
}
Use flags instead of writing your logic in the if statement :
public class FlyOff : MonoBehaviour
{
// fields removed for more readability
// use a flag that's set to true/false
private bool isCloseEnough = false;
void Update()
{
distanceFromObject = Vector3.Distance(character.transform.position, this.gameObject.transform.position);
print (distanceFromObject);
// set the flag to true when player is close enough
if (distanceFromObject < 2)
{
isCloseEnough = true;
}
// even if the player gets far, the flag will remain true
if (isCloseEnough)
{
transform.Rotate (rotateChange);
transform.position += positionChange;
}
}
}
You can even apply the opposite logic to stop the object to move away when it has reach a certain distance :
if (distanceFromObject < 2)
{
isCloseEnough = true;
}
else if (distanceFromObject > SomeValue)
{
isCloseEnough = false;
}
If I understand correctly you could just add a bool flag and set it once you are close enough. Then you can start moving and skip further distance checks but keep moving forever.
private bool flyAway;
void Update()
{
if(!flyAway)
{
distanceFromObject = Vector3.Distance(character.transform.position, transform.position);
print (distanceFromObject);
if (distanceFromObject < 2)
{
flyAway = true;
}
}
else
{
transform.Rotate (rotateChange);
transform.position += positionChange;
}
}
In general: Avoid using print every frame! Even if you user doesn't see the log in a built app it is still causing overhead!

OnTriggerStay2D performance/alternatives

I got a simple "OnTouch" script on my enemies, which knocks back the player if they come in contact. The player then gets invincible for a short time. Something like this:
void OnTriggerEnter2D(Collider2D collider) {
if (collider.gameObject.CompareTag("Player")) {
if (Time.time > isInvincible) {
isInvincible = Time.time + invincibleTimer;
if (enemy.IsFacingRight) {
player.SetVelocity(knockback * Vector2.right);
} else {
player.SetVelocity(knockback * Vector2.left);
}
}
}
}
(SetVelocity is just a method i use to set.. velocity)
The problem with this is when the player gets invincible after been pushed away. While invincible you can then run on top of an enemy and stay there, even after the invincible timer runs out. Which i guess makes sense since it only triggers on enter.
Using the same code but inside a "OnTriggerStay2D", works as expected. You get pushed away, go invincible, run on top of an enemy, invincible runs out and you then get pushed away out of the enemy.
But having multiple enemies running around with OnTriggerStay colliding with different objects feels like it would be bad performance wise? Is there any more efficient way to do this? Or is TriggerStay the way to go?
The way I found is manually tracking the collisions like this:
List<Collider2D> hitColliders = new List<Collider2D>();
void OnTriggerEnter2D(Collider2D collision) {
if (hitColliders.Contains(collision)) { return; }
hitColliders.Add(collision);
}
void OnTriggerExit2D(Collider2D collision) {
hitColliders.Remove(collision);
}
// Perform operations to the colliders.
void Update() {
foreach (var col in hitColliders) { DoStuff(col); }
}
Although you may not run into performance problems even with the solution you have now, if you do in the future you can try using Physics.IgnoreLayerCollision:
At the start of your invincibleTimer call:
IgnoreLayerCollision(playerLayer, enemyLayer, true);
And at the end of your timer call:
IgnoreLayerCollision(playerLayer, enemyLayer, false);
And acording to the docs:
IgnoreLayerCollision will reset the trigger state of affected
colliders, so you might receive OnTriggerExit and OnTriggerEnter
messages in response to calling this.
This means that when you call IgnoreLayerCollision(false), OnTriggerEnter will be called again, even if you are already on top of an enemy. This is exactly the behaviour you are after.

SetDestination and active agent/NavMesh issue with Unity Game

I am working in unity 5 on a game and I am currently getting this error:
"SetDestination" can only be called on an active agent that has been placed on a NavMesh.
UnityEngine.NavMeshAgent:SetDestination(Vector3)
EnemyMovement:Update() (at Assets/Scripts/Enemy/EnemyMovement.cs:25)
My code for the enemyMovement script is:
using UnityEngine;
using System.Collections;
public class EnemyMovement : MonoBehaviour
{
Transform player;
PlayerHealth playerHealth;
EnemyHealth enemyHealth;
NavMeshAgent nav;
void Awake ()
{
player = GameObject.FindGameObjectWithTag ("Player").transform;
playerHealth = player.GetComponent <PlayerHealth> ();
enemyHealth = GetComponent <EnemyHealth> ();
nav = GetComponent <NavMeshAgent> ();
}
void Update ()
{
if(enemyHealth.currentHealth > 0 && playerHealth.currentHealth > 0)
{
nav.SetDestination (player.position);
}
else
{
nav.enabled = false;
}
}
}
Now I know that there is almost an identical question here: Unity - "SetDestination" can only be called on an active agent that has been placed on a NavMesh. UnityEngine.NavMeshAgent:SetDestination(Vector3) However, the question does not seem to have a firm response and I have tried doing some of the responses and nothing appears to work. I will say that my game does work fine. However, I have added an ability that if the player presses 'B' then all of the enemies will die on the screen. When I press 'B' the enemies die but the system 'crashes' and I get that error message.
EnemyManager.cs:
public void Kill(int InstanceID)
{
if (EnemyManager.Instance.EnemyList.ContainsKey(InstanceID))
EnemyManager.Instance.EnemyList.Remove(InstanceID);
}
public void DeathToAll()
{
Dictionary<int, Object> TempEnemyList = new Dictionary<int, Object>(EnemyManager.Instance.EnemyList);
foreach (KeyValuePair<int, Object> kvp in TempEnemyList)
{
// kvp.Key; // = InstanceID
// kvp.Value; // = GameObject
GameObject go = (GameObject)kvp.Value;
go.GetComponent<EnemyHealth>().Death();
}
}
Enemyhealth.cs
public void Death ()
{
// The enemy is dead.
isDead = true;
// Turn the collider into a trigger so shots can pass through it.
capsuleCollider.isTrigger = true;
// Tell the animator that the enemy is dead.
anim.SetTrigger ("Dead");
// Change the audio clip of the audio source to the death clip and play it (this will stop the hurt clip playing).
enemyAudio.clip = deathClip;
enemyAudio.Play ();
// Kill the Enemy (remove from EnemyList)
// Get the game object associated with "this" EneymHealth script
// Then get the InstanceID of that game object.
// That is the game object that needs to be killed.
EnemyManager.Instance.Kill(this.gameObject.GetInstanceID());
}
The problem here is that while you're checking if enemyHealth.currentHealth > 0 before calling nav.SetDestination(), that value doesn't get changed when the enemies are killed by the player's ability. Instead, it seems that the flag isDead is set on the enemy, without affecting the actual health.
To ensure nav.SetDestination() doesn't get called when the enemy is dead (because it seems at that point, the enemy is no longer a valid active agent), just add an additional condition to your if statement:
if(enemyHealth.currentHealth > 0 && !enemyHealth.isDead && playerHealth.currentHealth > 0)
{
nav.SetDestination (player.position);
}
Hope this helps! Let me know if you have any questions. (You may need to make isDead public for this to work.)
An alternative solution is to make a property IsDead which returns whether enemyHealth.currentHealth > 0, and kill enemies by setting their health to 0. Then you don't need to have a health tracker and a separate isDead flag that must be explicitly set. That's more of a design decision though as it won't affect the functionality.
I was having a similar issue where my navmesh agent was not able to reach the setdestination() point ,the navmesh agent just stopped so what i was doing wrong was that my setdestination() point was bit high from navmesh and that point was becoming out of range the agent.
So I created a foot empty gameobject that was near to the navmesh "in height".
and my agent was then able to reach the point

XNA 2D Top Down game - FOREACH didn't work for checking Enemy and Switch-Tile

Here is the gameplay. There is three condition.
The player step on a Switch-Tile and it became false.
1) When the Enemy step on it (trapped) AND the player step on it too, the Enemy will be destroyed.
2) But when the Enemy step on it AND the player DIDN'T step on it too, the Enemy will be escaped.
3) If the Switch-Tile condition is true then nothing happened. The effect is activated when the Switch tile is false (player step on the Switch-Tile).
Because there are a lot of Enemy and a lot of Switch-Tile, I have to use foreach loop.
The problem is after the Enemy is ESCAPED (case 2) and step on another Switch-Tile again, nothing happened to the enemy!
I didn't know what's wrong. The effect should be the same, but the Enemy pass the Switch tile like nothing happened (They should be trapped)
Can someone tell me what's wrong?
Here is the code :
public static void switchUpdate(GameTime gameTime)
{
foreach (SwitchTile switch in switchTiles)
{
foreach (Enemy enemy in EnemyManager.Enemies)
{
if (switch.Active == false)
{
if (!enemy.Destroyed)
{
if (switch.IsCircleColliding(enemy.EnemyBase.WorldCenter,
enemy.EnemyBase.CollisionRadius))
{
enemy.EnemySpeed = 10; //reducing Enemy Speed if it enemy is step on the Tile (for about two seconds)
enemy.Trapped = true;
float elapsed = (float)gameTime.ElapsedGameTime.Milliseconds;
moveCounter += elapsed;
if (moveCounter> minMoveTime)
{
//After two seconds, if the player didn't step on Switch-Tile.
//The Enemy escaped and its speed back to normal
enemy.EnemySpeed = 60f;
enemy.Trapped = false;
}
}
}
}
else if (switch.Active == true && enemy.Trapped == true
&& switch.IsCircleColliding(enemy.EnemyBase.WorldCenter,
enemy.EnemyBase.CollisionRadius)
)
{
//When the Player step on Switch-Tile and
//there is an enemy too on this tile which was trapped = Destroy Enemy
enemy.Destroyed = true;
}
}
}
}
else if (switch.Active == true && enemy.Trapped == true
&& switch.IsCircleColliding(enemy.EnemyBase.WorldCenter,
enemy.EnemyBase.CollisionRadius)
)
{
//When the Player step on Switch-Tile and
//there is an enemy too on this tile which was trapped = Destroy Enemy
enemy.Destroyed = true;
}
This code will never be true since you set whether the enemy is trapped or not only if the switch is not active. Once you set it to true you should really break out the loop and then test again to see whether you should destroy the enemy or not.
Rethink the logic you want to do, and ensure that at each stage you can gain access to the correct information for the enemy etc.
P.S You do NOT need to use a foreach loop. You could easily use a for loop and iterate over the container or if you were really sadistic you could simple hardcode for every enemy yourself. Foreach loops are simply one way to solve this kind of issue, just because they can be used, doesnt mean you need to use them ;)

Detecting collisions of enemies

I need to detect the collisions of the same type of enemies in XNA.
If a collision is detected, I wanted the enemies to turn around and walk in the other direction.
If i save all instances to a List how can I detect if different(But same) type of enemies collide?
This is in C# XNA.
EDIT: I have enemies that are in a class called "Enemy", all enemies in my game are created from this class, I need to be able to check if the enemies have collided.
EDIT 2:
Here is a code sample:
// Fields.
private List<Enemy> enemies = new List<Enemy>();
// Add our enemies when we need to.
enemies.Add(new Enemy(this, position, spriteSet));
// Here is it's update method.
private void UpdateEnemies(GameTime gameTime)
{
foreach (Enemy enemy in enemies)
{
enemy.Update(gameTime);
// This code works because it's comparing the player.
if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
{
OnPlayerKilled(enemy);
}
// This code is always ture because enemy is enemy I can't figure out how to solve this.
if (enemy.BoundingRectangle.Intersects(enemy.BoundingRectangle))
{
// Make enemies turn... This if statement is the problem.
}
}
To determine if two enemies have collided, you need to find the Rectangle each of them is occupying. Then you can use Rectangle.Intersects(Rectangle) to find out if they are overlapping.
Re: Edit:
Every enemy is always checking for collision with itself. Not what you want :(|)
Something like this is what you want:
foreach (Enemy enemy1 in enemies)
{
foreach (Enemy enemy2 in enemies)
{
if (enemy1 != enemy2 && enemy1.BoundingRectangle.Intersects(enemy2.BoundingRectangle))
{
// enemy1 is colliding!
}
}
}

Categories

Resources