Updated a month ago
Offline multiplayer game for two with 40+ unique guns and power-ups.


This is beat`em up - like arena shooter game with dynamic gameplay and many features. The game was developed by one person (by me). The idea of game - try to grind your opponent with huge variety of guns, turrets and power-ups. See details and description down below.


A little changes in characters:

UPDATE (13.12.2018)

I decided to remake the levels and create new asset in flat design in Adobe Illustrator. Also I create script for sprite scrolling background (without quads/shader offset change or similar) For now there are 4 new vectorized levels.



All project contains 60+ scripts. Basic mechanics was realized under 4 days (main menu, player controller, gun system and health). All other stuff was realized since August 2018. Game contains breakable objects and explosive barrels, gun upgrades (accuracy, fire rate, damage, bullets), three types of shields (lite, medium, heavy), bosses (The Grenader - with super grenade laucher and 500 HP; Rocketman - with super fast rocket launcher and 400 HP and mini-bosses that have 125-250 HP), 4 types of turrets, that can fire an opponent`s turret, air strike and grenade rain. There are: health vending machine, so you can grab some health points, and if it is empty it will be refill after several seconds; basic item spawn system with random factor (± some constant value), that spawns guns, upgrades, health, boss packs, turrets, crates with items, etc.; weapon spawn system that spawns only weapons. Also, there are advanced camera system thats tracking players position and calculate camera trajectory and zoom factor. Here is the main part of code:
void Update () { if (Player1 != null && Player2 != null) // if two players are in game { distance = Vector2.Distance(Player1.transform.position, Player2.transform.position); //calculate the distance between players Zoom(distance,3);//zoom if (distance < 10f) //check the distance between players { // ZoomIn = false; // and zoom posX = (Player1.transform.position.x + Player2.transform.position.x) / 2.5f; //calculating the average point betweeen two players on x axis posY = (Player1.transform.position.y + Player2.transform.position.y) / 3; } else { // ZoomIn = true; posX = (Player1.transform.position.x + Player2.transform.position.x) / 5; //calculating the average point betweeen two players on x axis posY = (Player1.transform.position.y + Player2.transform.position.y) / 4; //division is nessecary because of camera move too fast } Vector3 position = new Vector3(posX, posY, cam.transform.position.z); //calculating the average position between players cam.transform.position = Vector3.MoveTowards(cam.transform.position, position, 3f * Time.deltaTime); // move camera to specific Vector3 } if (Player1 == null || Player2 == null) //if somebody is dead { //ZoomIn = true; //zoom cam.transform.position = Vector3.MoveTowards(cam.transform.position, startPos, 9f * Time.deltaTime); // calculate trajectory to start position and move camera to it Zoom(20,3);//zoom // calling function "FindPlayers" in RespawnSystem after respawning instead of finding by name or tags; } } void Zoom(float distance, float speed) //distance = distance betweeen players, speed = camera zoom speed { float z = Mathf.Lerp(camFOV, zoom, distance/20f); //20f - camera speed limit cam.orthographicSize = Mathf.Lerp(cam.orthographicSize, z, Time.deltaTime * speed); }


Weapons have two basic mechanics : raycast and projectile launcher. Raycasts used by SMG`s, machineguns, laser guns, shotguns. Projectile launchers used by rocket launchers, grenade launchers, BFG, etc.


Projectiles are detonative. So I create script "Detonator" and attach it to projectile. This script have the following features:
  • Radial damage - damage depends on distance between player and projectile
  • It can be used to reproduce "shrapnel" effect for explosion particle with childed Rigidbodies to it
  • Explosion force 2D
  • Camera shake after explosion with custom shake amount and durability
  • Several detonation modes - idle: projectile don`t detonate until player touch it; with timer: timer activates after first collide to anything or after start; player collide - should the projectile detonates imediately after player collide or not
Here is the part of "Detonator" script:
void OnCollisionEnter2D(Collision2D coll) { if (ExplosionOnCollide) { StartCoroutine(Explode()); } if (DetonateOnPlayerTouch) { if (coll.gameObject.CompareTag("Player")) { StopCoroutine(Explode()); TimeToExplode = 0; StartCoroutine(Explode()); } } } IEnumerator Explode() //explosion ienumerator { float time; if (TimeToExplode < 0.3f) { time = TimeToExplode; } else time = Random.Range(TimeToExplode - 0.3f, TimeToExplode + 0.6f); yield return new WaitForSeconds(time); Collider2D[] coll = Physics2D.OverlapCircleAll(transform.position, Radius); GameObject audio = GameObject.FindGameObjectWithTag("Audio"); //audio.GetComponent<AudioSource>().pitch = Random.Range(0.9f, 1.1f); audio.GetComponent<AudioSource>().PlayOneShot(ExplosionSound); if (ExplosionParticle != null) { GameObject g = (GameObject)Instantiate(ExplosionParticle, transform.position, Quaternion.identity); foreach (Transform rb in g.transform) { if (rb != null) { if (rb.GetComponent<Rigidbody2D>() != null) { rb.GetComponent<Rigidbody2D>().AddRelativeForce(new Vector2(Random.Range(-15f, 15f), Random.Range(-4f, 10f)), ForceMode2D.Impulse); } } } } foreach(Collider2D colliders in coll) { float proximity = (transform.position - colliders.transform.position).magnitude; float radialDamage = 1 - (proximity / Radius); int damage = (int)(Damage*radialDamage); //player health damage if (colliders.gameObject.GetComponent<Health>() != null) { if (colliders.transform.GetComponent<Health>().m_Health > 0) { colliders.transform.GetComponent<Health>().TakeDamage(damage, AlternativeDamage); } } //object damage and destroying if (colliders.gameObject.GetComponent<ObjectHealth>() != null) { colliders.transform.GetComponent<ObjectHealth>().TakeDamage(damage); } //explosive barrel if (colliders.gameObject.GetComponent<BarrelExplosiveHealth>() != null) { colliders.transform.GetComponent<BarrelExplosiveHealth>().TakeDamage(damage); } //camera shake if (Camera.main.GetComponent<CameraShake>() != null) { Camera.main.GetComponent<CameraShake>().Shake(CameraShakeAmount, 1f); } //explosion force 2d if (colliders.GetComponent<Rigidbody2D>() != null) { ExplosionForce2D.AddExplosionForce(colliders.GetComponent<Rigidbody2D>(), 1000 * Radius, transform.position, Radius); } if (colliders.GetComponent<BulletImpact>() != null && damage <= 65) { colliders.GetComponent<BulletImpact>().PlayBulletImpactSound(); } } } Destroy(gameObject); } void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, Radius); }

Respawning, scoring and final results


Respawning is simple : after death the player must wait 2 seconds for respawn on specific spawn place. The first second player is undamagable.


There are two types of scoring:
  • If you kill the opponent - you will have one kill point and the opponent will have one death point.
  • If you died by water/lava/air strike/laser or some other obstacle - you will have one death point and nobody haves kill points.

Final result

Final result - it is your kill points divided by death points (aka K/D Ratio).
gm = FindObjectOfType<GameManager>(); // game manager stores all kills and deaths points float redRatio = (float)gm.redScore / (float)gm.redDeaths; float blueRatio = (float)gm.blueScore / (float)gm.blueDeaths;

Main menu

Main menu contains:
  • Map choosing (or use random toggle to launch random map)
  • Player`s names (defalut names are "Player 1" and "Player 2" if all characters are the same or text field is empty)
  • Game time
  • Players` color (changes UI elements color, spawn points color, player`s labels colors)
  • Settings (quality and sound volume)
  • Help/Credits - brief information about how to play and credits
  • Quit button
  • Game version label


Vector characters

There are 15 body skins and 17 head skins. On each player spawn skins are randomly generated. So for now there are 255 unrepeatable player skins. By the way, hand color depends on body skin color (hand is separated from skin and by default have white color with black kontur). For example if the body color is red - color of hand is the same red.
Implementation of skin and hand color generator:
void Start () { heads = HeadSkins.Length; bodies = BodySkins.Length; GenerateSkin(); } public void GenerateSkin() { int i = Random.Range(0, heads); int j = Random.Range(0, bodies); Head.sprite = HeadSkins[i]; Body.sprite = BodySkins[j]; Hand.color = HandColor(BodySkins[j]); //generates color from sprite rect } Color HandColor(Sprite texture) { var croppedTexture = textureFromSprite(texture); Color color = croppedTexture.GetPixel(10,8); return new Color(color.r, color.g, color.b,1f); } public static Texture2D textureFromSprite(Sprite sprite) { if (sprite.rect.width != sprite.texture.width) { Texture2D newText = new Texture2D((int)sprite.rect.width, (int)sprite.rect.height); Color[] newColors = sprite.texture.GetPixels((int)sprite.textureRect.x, (int)sprite.textureRect.y, (int)sprite.textureRect.width, (int)sprite.textureRect.height); newText.SetPixels(newColors); newText.Apply(); return newText; } else return sprite.texture; }

Perfomance and optimization

RAM usage : 120-140 Mb;
VRAM usage : fastest quality - 80 Mb, ultra quality - 250 Mb;
Trees: 7.0k, Verts; 7.5k (average on each map);
FPS : 75 frames per second (with V-Sync) with no lagging after 20 projectiles explosion or intensive physics and over 200 FPS - without V-Sync;
All code are optimized to use less garbage collecting and do not store local variables in Update() function;
Next optimizing target - use object pooling instead instantiating and destroying gameobjects and texture compressing;

Yuriy Diachyshyn
Programmer - Student
Yuriy Diachyshyn
a month ago
Programmer - Student
MatthewThis style is really cool, it has a CRT feel to it. Good job!
Thanlk you very much!
a month ago
Senior Game Developer - Designer
This style is really cool, it has a CRT feel to it. Good job!