Pixel Invaders is a cute but gory boomer shooter combining 2D and 3D aesthetics. Set in the late 1990s, the player must fight and destroy the invasion.
My role in our team was the programmer, developing the mechanics of the game such as player movement, attacks and enemy pathfinding.
Boss with different attacking and movement phases
Combat with a variety of unique attacks against enemies with a range of behaviours
Progression through the map via opening barriers
Dialogue between the player and companion character
I started making the player script by creating first person movement and adding a projectile gun. When the player shoots, a bullet is instantiated and given forward force and deals damage when it collides with an enemy. I later changed this to a raycast gun to improve the feel of shooting based on feedback.
Original shooting code for the projectile gun.
Updated shooting using raycasts.
I added a grenade that can kill many enemies in a radius to give the player more attacks to deal with hordes. The grenade is instantiated with forward and upward force to simulate throwing physics. The grenade explodes when colliding with another object after a short delay, creating a blast radius object which kills any enemy within range.
After adding the grenade, I found that it would occasionally fall through the ground as it was travelling fast enough that it skipped the collision detection between frames, so I changed the collision detection to continuous.
Enemies ran towards the player regardless of location and converged around the player. This caused the player to get stuck in groups of enemies and was not able to easily escape by sprinting. I implemented a forward dash with a coroutine, which rapidly moves the player for the duration of the dash. This also gives the player the opportunity to use a wider variety of movement when exploring and fighting.
The goal with enemy pathfinding was to have 3 enemy types with unique pathing and attacks to create variety and increase engagement for the player. I started with a raycast check that moved the enemy towards the player if the path was clear and then added pathfinding to the enemy if it lost sight of the player. I then added navmesh modifiers to low obstacles so that the flying enemy could pass over them to create variation in how different enemies moved.
Enemy raycast checks
Flying enemy navmesh modifier
This resulted in simple movement for both ground and flying enemies, which I then adapted to include more advanced movement so the enemies were more fun and engaging to fight. I added a flying enemy function that chose a random nearby point to fly towards to evade the player. I later changed this to choose a random angle behind the enemy so it always moves away from the player to improve the game feel as evading is now always a defensive move.
The ground enemy uses a function to run to the nearest cover position where it is hidden after attacking. This was then expanded to searching for the closest five positions and choosing one at random, so enemies went to different cover positions to create more variety in gameplay.
One major bug occurred where enemies became stuck in place after several had spawned in. I tested whether it was an issue with the navmesh, enemies interacting with each other or problems with enemy pathfinding, and found that the pathfinding took too much processing power as the path for each agent was recalculated every frame.
I fixed this by only recalculating the path when the current path was finished, removing most of the path calculations and allowing more agents to use the navmesh without issues. This resulted in the target position of the enemies being inaccurate as they moved to the old player position, which made the enemies feel like less of a threat. To fix this I added a check that updated the position when the player position was too far from the target location so the enemies remained an active threat.
Each enemy switches between idle, attack, hurt and death sprites. The status of the enemy determines which sprite is active. As shown here, the sprite changes back to the idle sprite which is done by using a counter. When the enemy dies, this counter is also used to destroy the enemy. This conveys information to the player, allowing them to understand the enemies.
Gibs prefabs of the enemies’ body parts explode outwards upon death. Once they hit the floor the rigidbody is set to kinematic so the body parts stay in place on the ground, creating a gory atmosphere.
A blood prefab is instantiated when the enemy takes damage or dies. After the effect disappears, the blood prefab remains in the scene, so I used a script to destroy them and clean the scene to reduce processing power. I integrated the blood prefab with the body parts using raycast gun detection. This adds to the gore as they repeatedly spray blood when shot.
I added dialogue between the player and the companion Foldie to help the player and add humour to the game. When the player completes dialogue criteria such as entering a trigger area, the target dialogue number is stored in a global variable for the UI script to access.
The UI script has an array of dialogue numbers that activate the dialogue box. This number increments by 1 every time the key is pressed to progress through a dialogue segment. When the end of each segment is reached, the dialogue box is deactivated, and the game is unpaused. To display the dialogue sprites, I enabled the correct sprite from an array depending on the dialogue number.
I implemented different types of pickups using trigger colliders. Health packs aid the player and add strategy. As seen here, collecting a pack gives health, up to a maximum of 10.
Areas are blocked off by barriers that are destroyed by completing criteria. Two barriers are destroyed by breaking all the TVs in that area. Each barrier has an array containing the TVs required to break. If every TV in the array has been destroyed the barrier is removed. Originally, I destroyed the barrier as soon as the TVs were destroyed, but changed this to destroy after the barrier removal animation had finished.
Barriers can also be removed by collecting blueprint pieces. When the player collects a blueprint, the blueprint count increments by 1. I only tracked how many were collected as they are in a set order. Collecting each blueprint removes the associated barrier in the same way as the TV barriers.