Vampire Survivors Clone

GitHub

A Vampire Survivors inspired mobile game made in Unity.

The player fends off hordes of monsters using a variety of weapons and abilities.

Key Features

  • 20+ upgradeable weapons and abilities
  • 4 unique enemy types and 2 bosses
  • Object pools for monsters, projectiles, items, chests, damage text, etc.
  • Infinite background shader
  • Mobile, gamepad, and keyboard input support
  • Enemy spawn chance/rate keyframe system
  • ScriptableObjects for straightforward character and level design
  • Level up system
  • Mandarin localization

Description

During a 3 month internship at Gamania in Taiwan I was asked to create a mobile game inspired by one of the most popular indie games of the year, Vampire Survivors.

More specifically, I was tasked with creating a game in which the player fights off hordes of increasingly difficult monsters using a collection of random, automatically attacking, upgradeable weapons and abilities. This involved creating a basic player controller, a level up system, various enemy types, and a whole bunch of unique items and abilities for the player to use.

The game is split into quick, 10 minute levels that are completed by surviving to the end of the level and defeating the final boss.

Challenges

During the development of this game I faced a number of interesting challenges.

Infinite Background

The first challenge I encountered was that of creating an infinite background for the player to walk across.

A classic approach to this problem would be to place several large, seamless tiles around the player's position and then as soon as the player nears the edge of one tile, bring a tile that had already moved entirely offscreen on the opposite side to the side in front of the player, thereby reusing tiles to give the illusion of an infinite background. While simple to implement, I chose not take this approach as it failed to meet a self-imposed requirement of mine: create an infinite background without the appearance of obviously repeating tiles.

Knowing that I wanted to remove any evidence of repeating tiles, for my actual solution to this problem I began by implementing a custom non-repeating texture shader, as discussed here. This allowed me to use any texture I wanted for the background image, seamless or not, without any obvious signs of repetition.

Example of standard, repetitive tiling.
Example of non-repetitive tiling achieved with custom shader.

To give the illusion of the player walking across an infinite background, I fixed a screen-sized quad with this custom shader applied to the player's position so that they moved together in tandem. I then made a slight modification to the shader so that instead of doing background texture samples based on preset vertex UVs, each fragment sampled the background texture based on its world position. This meant that as the player moved throughout the scene the coordinates used for background texture lookups changed based on the world position of the player. Just like that, this gave the appearance that the player was walking over an infinite, non-repeating background.

A behind the scenes demonstration of how the infinite background really works. A screen-sized quad with the infinite terrain shader applied follows the player (top), giving the illusion that the player is walking across an infinite background (bottom).

One issue with this approach is that as the player gets farther and farther from the origin, floating point imprecision causes artifacts to appear when sampling the texture since the lookup coordinates are derived from the player's world position.

Extreme example of texture sampling artifacts when the player moves very far away from the origin.

While it is rare that the player ever runs far enough away from the origin to encounter this issue, I made an attempt to combat it nevertheless.

Once the player has walked far enough away from the origin, an offset is applied to the world space position of each fragment inside the shader so that the texture lookup coordinates are once again near the origin, and the player's current position is saved as a new "origin." If the player walks far enough away from this new origin, this process will repeat itself. Of course, applying an instant offset to the texture lookup coordinates would lead to an abrupt and confusing sensation of teleportation for the player. Instead, once the player has walked far enough away from the origin, the shader samples the background texture at both the current world position and the new offset world position, blending from one to the other over time such that it is effectively imperceptible to the player that the background is being reset back to a position near the origin.

Demonstration of the background blending back to a point closer to the origin, slow enough that it is practically imperceptible to the player (try focusing on a single point in the background and watch as it slowly fades away into something different).

Enemy Spawning / Level Balancing

A key challenge to ensuring levels are enjoyable for the player is balancing enemy spawn rates and the distribution of which enemy types are spawned over the course of the level.

At first I considered taking a numerical approach to this problem, perhaps estimating the player's expected DPS (damage per second) over time for a given level, and precomputing a corresponding variable spawn rate for the enemies. Or even better, computing those values in real-time and actively scaling the spawn rate and health of the enemies accordingly to ensure that the player is always facing a challenging yet winnable scenario. However, since a level should ideally play out the same from run to run, a real-time difficulty scaling approach did seem suitable, and as for the precomputed approach, such a model would also be difficult to tune as estimating the player's DPS given all of the potential combinations of items and abilities is not trivial — not to mention other confounding factors such as enemy DPS!

Knowing that balancing each level was going to require at least some playtesting no matter what, I instead opted for an approach that would allow me to efficiently iterate and test different enemy configurations and spawn rates. I did so using a simple keyframe based spawning system that works as follows:

  • For a given level, the developer first specifies which enemies are allowed to spawn in that level.
  • Then, the developer creates "spawn chance keyframes" at select times throughout level. Each keyframe specifies the spawn chance for each of enemy at that given point in time.
  • Next, the developer does the same thing, but for "spawn rate keyframes," which specify the overall spawn rate of all the enemies at given points in time.
  • Finally, when the level is played, the EnemySpawner script interpolates between keyframes to determine the exact spawn chance of every enemy at the current point in time, as well as the exact spawn rate, and spawns enemies accordingly.
Enemy spawn chance keyframes for the first level. Crab enemies have a 100% spawn chance for the first 5% of the level, but at 35% through the level crabs only have a 15% chance to spawn, aliens have a 60% chance to spawn, and ghosts have a 25% chance to spawn. The large spike in crab spawn chance near the middle of the level corresponds to when the level's first miniboss appears, at which point limiting the types of enemies that can spawn to only crabs (the weakest enemy) allows the player to fully focus on the boss fight.
Enemy spawn rate keyframes for the first level. The enemy spawn rate generally increases over time, with exceptions at the middle and end of the level when bosses are spawned. This was done to make the boss fights more fair for the player.

I found this system to be extremely effective in allowing me to iteratively balance levels. For example, I was able to build and balance the first level of the game in less than half an hour using this system by simply setting up some preliminary keyframes that followed a rough outline for how I thought the level should play out, and then playing through the level a few times to tweak those values and add additional keyframes where needed.

Demo

Following are demonstrations of some of the core systems in the game.

Level Up

The player level ups by collecting XP gems dropped from slain enemies. When leveling up, the player is given a choice between three options consisting of new abilities as well as upgrades to abilities they already own.

The player levels up and is given the choice between 3 random abilities: throwing stars, dagger, and attack cooldown decrease.

Abilities

The game contains 20+ unique weapons and abilities.

Each ability has a likelihood of appearing in the level up screen based on its rarity, where rarity is generally correlated with how powerful the ability is.

Abilities are also upgradeable, meaning that once they have been acquired for the first time they have a chance of appearing in the level up screen again, giving the player the opportunity to further improve them.

Example of a level up screen that includes upgrades for already owned abilities. For instance, the machine gun can be upgraded from level 1 to level 2, increasing the number of bullets it fires before reloading by 5, its attack damage by 100%, and decreasing its cooldown/reload by 10%.

A key challenge I encountered while developing the abilities was making them not only visually distinct, but also mechanically distinct from one another. For example, I was asked to add three projectile-based weapons to the game -- a bazooka, throwing stars, and a machine gun. Initially, I had all projectile-based abilities inheriting from the same "Projectile Ability" class that simply launched projectiles in the direction of the player's movement. Naturally, when more than one projectile-based ability was in use at the same time, this lead to a visually messy and very one-note gameplay experience. To resolve this specific issue, I gave each weapon its own distinct firing mode: the bazooka locks on to the closest enemy and launches an explosive projectile at it, whereas the machine gun orbits the player and fires outward in all directions, while the throwing star is simply launched in the direction the player is moving. In general, for any weapons of similar type (e.g. all melee weapons), time was taken to ensure that despite being similar, each weapon attacks slightly differently, or occupies a different part of the screen than its counterparts. This went a long way into making each ability feel unique.

Below are demonstrations of a handful of abilities from the game:

One of three "Boomerang" type abilities, the lightsaber is automatically thrown in the direction of the nearest enemy (found using a custom grid-based spatial data structure), damaging all enemies along its path before returning to the player. As this is an upgraded version of the ability, two lightsabers are thrown instead of one.
The bat performs a sweeping melee attack in the direction of the player's movement, dealing light damange and applying significant knockback to any hit enemies.
The dagger performs a stabbing melee attack in the direction of the player's movement, dealing bleed damage over time to any enemy that it hits.
The bazooka launches explosive projectiles in the direction of the nearest enemy, dealing AOE damage to all enemies in the explosion's radius.
The machine gun rapidly fires projectiles outward is it orbits the player.
Grenades are thrown in random directions, exploding into a burst of damaging projectiles after a short delay.
Molotovs are thrown toward a random enemy near the player, leaving a flaming puddle on the ground that deals damage over time.

Not all abilities grant weapons. Instead, some abilities grant global upgrades which apply to all of the player's weapons/abilities. For example, there's an ability that increases the area of effect (AOE) of all abilities that have an AOE (such as bazooka, molotov, etc.), an ability that increases the damage of all damaging abilities, and most fun of all, an ability that increases the projectile count of all projectile-based weapons (including grenades, molotovs, axes and more).

Projectile count upgrade demonstrated on a variety of weapons. In the grenade's case, note how not only are more grenades thrown, but also every grenade splits into more projectiles.

In many ways, the heart of the game lies in using these global upgrades along with each ability's unique upgrade path to create exponentially powerful, game-breaking builds.

Enemies

There are currently 4 unique basic enemy types as well as 2 boss enemies in the game.

The basic enemy types are melee, ranged projectile, boomerang, and grenade throwing enemies, where each type includes enemies of varying strength, speed, health, etc.

Three melee enemy variants, organized weakest to strongest from left to right. Melee enemies automatically deal damage to the player on collision, but have varying walk speeds, attack rates, and health points.
The ranged projectile enemy type launches damaging projectiles in the direction of the player.
The boomerang enemy type throws damaging boomerangs towards the player, which return back to the enemy to be thrown again.
The grenade enemy type lobs grenades in the direction the player is running. The enemy shown here throws a gravity-well producing grenade that pulls the player and any nearby enemies into it.

Bosses are made up of a collection of modular abilities, such that new bosses can be quickly created by simply utilizing a different combination of pre-existing boss abilities. The three abilities that have been created so far are the charge ability, the shotgun ability, and the grenade throw ability. The boss chooses which ability to use at any given time via the strategy design pattern, in which each ability is scored based on how useful it is to the boss at that moment, and the boss chooses the ability with the highest score.

The boss throws grenades, charges, and shoots a shotgun-like spread of projectiles towards the player to finally finish them off.

Items

Another difference between this game and Vampire Survivors is the inclusion of items — such as healing potions, bombs, and magnets — which the player can activate using the corresponding button on the right hand side of the screen. These items are found in chests which appear randomly throughout the map during each level.

The bomb item deals 300 damage to every on-screen enemy.
The health potion item heals the player by 50 HP.
The magnet item collects every non-collected XP gem in the level. When used in combination with the bomb item, this can cause the player to level up several times in a row.

Game Over

The game ends in defeat when the player's health reaches zero, and in victory if the player manages to survive past the allotted time and defeat the final boss. The game over screen displays stats about the number of enemies killed, damage dealt, etc.

The player is defeated when their health drops to zero.
The player wins when they defeat the final boss.

Tools Used

Languages: C#, HLSL

Software: Unity, Aseprite, Git, Creature Mixer