Quest: Wack-A-Mole Quest
Challenge: Tricky Patterns
Feel free to share your solutions, ideas and creations below. If you get stuck, you can find some ideas here for completing this challenge.
Quest: Wack-A-Mole Quest
Challenge: Tricky Patterns
Feel free to share your solutions, ideas and creations below. If you get stuck, you can find some ideas here for completing this challenge.
Using the animation editor, you can move the Body (not Enemy) to a third location. When you click the record button and slide the white line past the end of the animation and move the body, it will add that movement to the animation. I only moved mine at a right angle and made it pop up at the third location. You have to keep in mind that the box may end up out of the play field.
this was a major headache for me and ended up taking about 3 hours. First I remade the animation (had to review how that stuff works together) so that all it does is pop the enemy up and then back down. Then I created a new EnemyMover class to deal with the hidden movement. I had some trouble with getting the animation to run properly but eventually got it working just the way I intended
using UnityEngine;
public class EnemyMover : MonoBehaviour
{
[SerializeField] Animator animator;
[SerializeField] Vector2 feildSize;
[SerializeField] float moveSpeed = 1f;
[SerializeField] float moveDistance = 1;
Vector3 nextPosition;
bool animationIsActive = false;
public Vector3 GetNextPosition() { return nextPosition; }
void Start()
{
AssignNextPosition();
PopUp();
}
void Update() { if (!animationIsActive) MoveToNewPosition(); }
void MoveToNewPosition()
{
transform.position = Vector3.MoveTowards(transform.position, nextPosition, moveSpeed);
if(transform.position == nextPosition) PopUp();
}
void PopUp()
{
animationIsActive = true;
animator.SetTrigger("StartAnimation");
AssignNextPosition();
}
void ResetAnimationFlag() { animationIsActive = false; }
void AssignNextPosition()
{
bool isTooClose = true;
Vector3 newPosition = new Vector3(0,0,0);
while(isTooClose)
{
newPosition = CalculatePosition(transform.position.y);
if(Vector3.Distance(transform.position, newPosition) > moveDistance) isTooClose = false;
}
nextPosition = newPosition;
Vector3 CalculatePosition(float yPos)
{
float xAxisValue = ((Random.value - .5f) * feildSize.x);
float yAxisValue = transform.position.y;
float zAxisValue = ((Random.value - .5f) * feildSize.y);
return new Vector3(xAxisValue, yAxisValue, zAxisValue);
}
}
}
I took me more than a day to find a solution for this task, and I’m still not proud of what I came with but it’s a good start for now, and maybe later I’ll refactor this solution.
I created a bunch of spawn points, and a new class
In the new class first I find all the enemies in the level and add it to a list, and also an array of spawn points. In the SpawnTimer I check that there are elements in the list and if so I set the state of the enemies to true. The other methods are helper methods to give me random spawn point and set the game object to true or false
public class SpawnCubesAtRandom : MonoBehaviour
{
[HideInInspector] public int enemiesCount;
[SerializeField] private Transform[] spawnPoints;
public List<GameObject> enemies;
[SerializeField] float waitTime = 2f;
float startTime = 0;
WaitForSecondsRealtime WaitForSeconds;
[SerializeField] float waitTimeForCoroutine = 1.5f;
private void Start()
{
WaitForSeconds = new WaitForSecondsRealtime(waitTimeForCoroutine);
for (int i = 0; i < enemies.Count; i++)
{
SetEnemyActiveState(enemies[i].gameObject, false);
}
enemiesCount = enemies.Count;
}
private void Update()
{
SpawnTimer();
}
private void SpawnTimer()
{
startTime += Time.deltaTime;
if (startTime >= waitTime)
{
if (enemiesCount >= 0)
{
foreach (var enemy in enemies)
{
Vector3 offset = new Vector3(transform.position.x, .5f, transform.position.z);
SetEnemyActiveState(enemy.gameObject, true, enemy.transform.position + offset, enemy.transform.rotation);
startTime = 0;
StartCoroutine(TurnOffGameObject(enemy.gameObject));
}
}
}
}
private Transform ReturnRandSpawnPoint()
{
int r = Random.Range(0, spawnPoints.Length);
return spawnPoints[r];
}
private void SetEnemyActiveState(GameObject go, bool isActive)
{
go.SetActive(isActive);
}
private void SetEnemyActiveState(GameObject go, bool isActive, Vector3 pos, Quaternion quaternion)
{
go.SetActive(isActive);
go.transform.position = ReturnRandSpawnPoint().position;
go.transform.rotation = quaternion;
}
IEnumerator TurnOffGameObject(GameObject go)
{
yield return WaitForSeconds;
SetEnemyActiveState(go, false);
}
}
This is the first time I’ve heard of [HideInInspector]. You sent me down a rabbit hole.
As you shouldnt really have public fields in a class you shouldnt really need it …
I actually enjoyed this challenge, as it took me a little out of my comfort zone, and I had to do some real digging, but Im really happy with what Ive come up with… I created two new classes:
the first is a grid of way points:
{
[Header("Grid Variables")]
[SerializeField] int width = 20;
[SerializeField] int height = 20;
[SerializeField] GameObject tilePrefab;
[Header("Grid Visualizer/Debugging")]
[SerializeField] bool visualizeGrid = true; //Serialized to activate and Deactivate Grid visualization;
[SerializeField] List<GameObject> tiles = new List<GameObject>();//serialized for debug purposes
[SerializeField] List<Vector3> waypoints; //serialized for debug purposes
public List<Vector3> Waypoints { get => waypoints; } //public access to Get waypoints
private void Awake()
{
GenerateGrid();
}
private void Update()
{
//Checks if Visualization has been activated and enables/disables meshRenderers for tilePrefabs
if (visualizeGrid == true) { VisualizeGrid(); }
else { InvisibleGrid(); }
}
public void GenerateGrid()
{
//Creates a list of Vector3 grid waypoint coordinates, based on the size of the grid (Default is 20x20 to match the size of the Arena).
for (int x = 0; x < width; x++)
{
for (int z = 0; z < height; z++)
{
waypoints.Add(new Vector3(x, -.47f, z));
}
}
//creates a grid of cubes, 1 for each waypoint (Default is 400); This is inefficient, and is only being used for visualization purposes, should probably be done with a sprite renderer
//instead
for (int i = 0; i < waypoints.Count; i++)
{
var tile = Instantiate(tilePrefab, waypoints[i], Quaternion.identity, this.transform);
tile.name = $"Waypoint {i} = {waypoints[i]}";
tile.GetComponent<MeshRenderer>().enabled = false;
tiles.Add(tile);
}
}
private void VisualizeGrid()
{
//enables MeshRenderer on grid cubes, for vizualization;
foreach (GameObject tilePrefab in tiles)
{
tilePrefab.GetComponent<MeshRenderer>().enabled = true;
}
}
private void InvisibleGrid()
{
//disables MeshRenderer on grid cubes, when vizualiztion of the grid is not needed.
foreach (GameObject tilePrefab in tiles)
{
tilePrefab.GetComponent<MeshRenderer>().enabled = false;
}
}
}
The second is an EnemyMover, it decides a random waypoint on the grid, and moves to that location, pops up and down, decides a new random location on the grid, and repeats…
{
[Header("Variable Setting")]
[SerializeField] float locationDelay = 2.0f;
WaypointGrid grid;
Animator animator;
List<Vector3> waypoints;
Vector3 nextLocation;
void Start()
{
animator = GetComponent<Animator>();
grid = FindObjectOfType<WaypointGrid>();
//Gets the list of waypoints Generated by the WaypointGrid;
waypoints = grid.Waypoints;
//Chooses a random location within the grid to move to
StartCoroutine(ChangeLocation());
}
public IEnumerator ChangeLocation()
{
//chooses a Random number based on the number of waypoints created, then uses that number to determine its next spawning location.
int randomWaypoint = Random.Range(0, waypoints.Count);
nextLocation = new Vector3(waypoints[randomWaypoint].x, waypoints[randomWaypoint].y + 1f, waypoints[randomWaypoint].z) ;
transform.position = nextLocation;
//plays the "Pop up" animation
animator.Play("Pop Up");
//waits for animation to finish and restarts the process.
yield return new WaitForSeconds(locationDelay);
StartCoroutine(ChangeLocation());
}
}
The grid of waypoints also generated a Grid of cubes to help with visualization purposes, that can be turned on or off in the inspector. That helped me get the arena and all other assets aligned with the grid coordinates.
A nice option.
However from a gameplay point of view it may become annoying for the player if the mole keeps (randomly) appearing on the far side of the grid and there is no chance of getting to it.
May be an option is to choose a random waypoint at a limited distance from where the mole is.
See what happens when you play it…
Enemies pop up at a random location at a constrained distance. Relies on the animator to move the cubes up and down, code does the rest. Great exercise that got me to do a lot of experimenting!
public class Enemy : MonoBehaviour
{
// configs
[SerializeField] float maxTravelDistance = 4.75f;
[SerializeField] float minimumXOfArena = -9.5f;
[SerializeField] float maximumXOfArena = 9.5f;
[SerializeField] float minimumZOfArena = -9.5f;
[SerializeField] float maximumZOfArena = 9.5f;
// cache
Animator myAnimator;
// state
float animationDuration;
// Unity messages
void Awake()
{
myAnimator = GetComponent<Animator>();
animationDuration = myAnimator.runtimeAnimatorController.animationClips[0].length;
}
void Start()
{
StartCoroutine(MoleAround());
}
// private methods
IEnumerator MoleAround()
{
while (true)
{
myAnimator.SetTrigger("popUpThenDown");
yield return new WaitForSeconds(animationDuration);
MoveToRandomLocation();
}
}
private void MoveToRandomLocation()
{
float randomX = Random.Range(transform.position.x - maxTravelDistance, transform.position.x + maxTravelDistance);
float randomZ = Random.Range(transform.position.z - maxTravelDistance, transform.position.z + maxTravelDistance);
while (randomX > maximumXOfArena || randomX < minimumXOfArena)
{
randomX = Random.Range(transform.position.x - maxTravelDistance, transform.position.x + maxTravelDistance);
}
while (randomZ > maximumZOfArena || randomZ < minimumZOfArena)
{
randomZ = Random.Range(transform.position.z - maxTravelDistance, transform.position.z + maxTravelDistance);
}
Vector3 targetLocation = new Vector3(randomX, transform.position.y, randomZ);
transform.position = targetLocation;
}
}
Here is an implementation that just uses code (no animator). As before, enemies pop up at a random location at a constrained distance.
public class Enemy : MonoBehaviour
{
// configs
[SerializeField] float moveSpeed = 10f;
[SerializeField] float underGroundDistance = -.51f;
[SerializeField] float aboveGroundDistance = .51f;
[SerializeField] float secondsUnderGround = 2f;
[SerializeField] float secondsAboveGround = 2f;
[SerializeField] float maxTravelDistance = 4.75f;
[SerializeField] float minimumXOfArena = -9.5f;
[SerializeField] float maximumXOfArena = 9.5f;
[SerializeField] float minimumZOfArena = -9.5f;
[SerializeField] float maximumZOfArena = 9.5f;
// cache
Rigidbody myRigidbody;
// Unity messages
void Awake()
{
myRigidbody = GetComponent<Rigidbody>();
}
void Start()
{
StartCoroutine(MoleAround());
}
// private methods
IEnumerator MoleAround()
{
while (true)
{
yield return new WaitUntil(PopDown);
yield return new WaitForSeconds(secondsUnderGround);
MoveToRandomLocation();
yield return new WaitUntil(PopUp);
yield return new WaitForSeconds(secondsAboveGround);
}
}
bool PopUp()
{
Vector3 direction = new Vector3(
myRigidbody.position.x,
myRigidbody.position.y + 1 * moveSpeed * Time.deltaTime,
myRigidbody.position.z);
myRigidbody.MovePosition(direction);
if (myRigidbody.position.y >= aboveGroundDistance)
{
return true;
}
return false;
}
bool PopDown()
{
Vector3 direction = new Vector3(
myRigidbody.position.x,
myRigidbody.position.y - 1 * moveSpeed * Time.deltaTime,
myRigidbody.position.z);
myRigidbody.MovePosition(direction);
if (myRigidbody.position.y <= underGroundDistance)
{
return true;
}
return false;
}
private void MoveToRandomLocation()
{
float randomX = Random.Range(transform.position.x - maxTravelDistance, transform.position.x + maxTravelDistance);
float randomZ = Random.Range(transform.position.z - maxTravelDistance, transform.position.z + maxTravelDistance);
while (randomX > maximumXOfArena || randomX < minimumXOfArena)
{
randomX = Random.Range(transform.position.x - maxTravelDistance, transform.position.x + maxTravelDistance);
}
while (randomZ > maximumZOfArena || randomZ < minimumZOfArena)
{
randomZ = Random.Range(transform.position.z - maxTravelDistance, transform.position.z + maxTravelDistance);
}
Vector3 targetLocation = new Vector3(randomX, transform.position.y, randomZ);
myRigidbody.position = targetLocation;
}
}
I removed the X/Z moving part of the animation and then used an Animation Event when the block is below ground to run this code to move the enemy a max distance in a random direction from where it is while checking to make sure it doesn’t go out of the arena.
public class EnemyMover : MonoBehaviour
{
[SerializeField] float maxMoveDistance = 6f;
[SerializeField] float areaBounds = 10f;
[SerializeField] Transform block;
public void MoveBlock()
{
float newX = block.transform.position.x + Random.Range(-maxMoveDistance, maxMoveDistance);
float newZ = block.transform.position.z + Random.Range(-maxMoveDistance, maxMoveDistance);
while (!enemyLocationCheck(newX))
{
newX = block.transform.position.x + Random.Range(-maxMoveDistance, maxMoveDistance);
}
while (!enemyLocationCheck(newZ))
{
newZ = block.transform.position.z + Random.Range(-maxMoveDistance, maxMoveDistance);
}
gameObject.transform.position = new Vector3(newX, transform.position.y, newZ);
}
bool enemyLocationCheck(float newCoordinate)
{
if (newCoordinate > -areaBounds && newCoordinate < areaBounds) { return true; }
else { return false; }
}
}
Here is my solution.
I’ve created a new class called EnemyIA.cs and added it the following code:
public class EnemyIA : MonoBehaviour
{
[SerializeField] float randomValue;
private Vector3 newPosition;
private float randomX;
private float randomZ;
private List<Vector3> occupiedPositions = new List<Vector3>();
[SerializeField] private bool positionOccuppied = true;
//x = 9 a -9
// Z = -3 a -9.5 // 0.5 a 9.5
void Start()
{
SetPosition();
}
public void SetPosition()
{
while (positionOccuppied)
{
randomX = Random.Range(-8, 8);
randomValue = Random.Range(0f, 1f);
if (randomValue <= .5f)
{
randomZ = Random.Range(-3, -8f);
}
else
{
randomZ = Random.Range(1f, 9f);
}
newPosition = new Vector3(randomX, transform.position.y, randomZ);
positionOccuppied = occupiedPositions.Exists(pos => Mathf.Approximately(pos.x, newPosition.x)
&& Mathf.Approximately(pos.z, newPosition.z));
}
occupiedPositions.Add(newPosition);
transform.position = newPosition;
occupiedPositions.Remove(newPosition);
positionOccuppied = true;
}
Basically I set up the ranges where a cube can appear. I check if the new position is available (not occupied for other enemy) and then I change the transform position, clean the position in the list and call the function through the end of the animEvent again