Platform: PC, 2 players Co-op game
Technology: Unity, C#
Team size: Solo
Project Duration: 2 months (Aug. 2021- Oct. 2021)
Design And Gameplay
In this game, two players will take on the role of a mage and a warrior to defeat an enemy.But they can’t get through the level if they just fight on their own, they have to effectively combine their skills with those of the other to maximize the effect of their skills.
Inspiration
When my friends and I play DOTA, we always get much pleasure from the skills unleashing in group combat. When picking heroes, we tend to choose those who can maximize damage by matching their skills with each other’s, and control + damage is a never-ending combination. During the battle, we also need to be able to release these skills that require coordination, and my friends and I always have a tacit understanding of how to release these skills. So I want to create a game that lets players feel the thrill of working with their partners to release their skills.
Core Experience
In addition to using skill damage to wipe out normal enemies, players also need to use skills with specific attributes to deal damage to enemies with different attributes. Also, each skill has its own cooldown, so the player needs to have a strategy for releasing skills.
Because everyone’s point of view is fixed, it’s common to overlook the dangers behind you, and it’s up to your partner to remind you.
The mage takes time to cast spells, and some enemies will only attack the mage and ignore the warrior. So the mage must pay special attention to his position, and the warrior must also take care to protect the mage.
Notable Features
1.Based on the behavior designer to write and produce different enemy AI, so that different enemies can carry out different chase, attack behavior.
2. Designed and implemented a two-player cooperative skill tree system that allows two people to have separate skill trees, but every two skills can be combined to produce a reasonable effect.
Realtime Screenshots
Code Sample – Map Generation
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Dungeon : MonoBehaviour
{
GameObject cube;
GameObject map3D;
GameObject enemy3D;
Camera Mcamera;
int enemyPercent;
public int numRoomTries;
public int extraConnectorChance = 20;
public int roomExtraSize = 0;
public int windingPercent = 0;
public int dimensionX, dimensionY;
public int maxRoomX=5;
public int maxRoomY=5;
public int minRoomX=2;
public int minRoomY=2;
public GameObject floor;
public GameObject[] wall;
public GameObject door;
public GameObject[] enemys;
public GameObject[] UI;
//wall-white floor-red openDoor-yellow closedDoor-black
enum Tiles { wall,floor,openDoor,closedDoor}
Tiles[,] TileType;
GameObject[,] cubes;
Vector2[] direction = new Vector2[] { new Vector2(-1, 0), new Vector2(1, 0), new Vector2(0, -1), new Vector2(0, 1) };
List<Rect> _rooms;
int[,] _regions;
int _currentRegion = -1;
Color floorColor=Color.red;
private void Start()
{
cube = GameObject.Find("_Cube");
map3D = GameObject.Find("3DMap");
enemy3D = GameObject.Find("3DEnemy");
Mcamera = Camera.main;
}
private void Update()
{
GameObject.Find("3DCamera").transform.Rotate(new Vector3(0,1,0),0.1f,Space.World);
}
void Inis()
{
if (cubes != null)
{
foreach (var cube in cubes)
{
Destroy(cube);
}
}
floorColor = Color.red;
_currentRegion = -1;
int i = 0;
while (i < map3D.transform.childCount)
{
Destroy(map3D.transform.GetChild(i++).gameObject);
}
i = 0;
while (i < enemy3D.transform.childCount)
{
Destroy(enemy3D.transform.GetChild(i++).gameObject);
}
Mcamera.gameObject.SetActive(true);
if (UI[0].GetComponent<InputField>().text != null)
{
numRoomTries = int.Parse(UI[0].GetComponent<InputField>().text);
}
if (UI[1].GetComponent<InputField>().text != null)
{
extraConnectorChance = int.Parse(UI[1].GetComponent<InputField>().text);
}
if (UI[2].GetComponent<InputField>().text != null)
{
dimensionX = int.Parse(UI[2].GetComponent<InputField>().text);
}
if (UI[3].GetComponent<InputField>().text != null)
{
dimensionY = int.Parse(UI[3].GetComponent<InputField>().text);
}
if (UI[4].GetComponent<InputField>().text != null)
{
maxRoomX = int.Parse(UI[4].GetComponent<InputField>().text);
}
if (UI[5].GetComponent<InputField>().text != null)
{
minRoomX = int.Parse(UI[5].GetComponent<InputField>().text);
}
if (UI[6].GetComponent<InputField>().text != null)
{
maxRoomY = int.Parse(UI[6].GetComponent<InputField>().text);
}
if (UI[7].GetComponent<InputField>().text != null)
{
minRoomY = int.Parse(UI[7].GetComponent<InputField>().text);
}
if (UI[8]!= null)
{
windingPercent = (int)UI[8].GetComponent<Slider>().value;
}
if (UI[9]!= null)
{
enemyPercent = (int)UI[9].GetComponent<Slider>().value;
}
}
public void generate()
{
Inis();
//Generating Empty Map
_rooms = new List<Rect>();
cubes=new GameObject[dimensionX * 2 + 1, dimensionY * 2 + 1];
_regions = new int[dimensionX*2+1,dimensionY*2+1];
TileType=new Tiles[dimensionX*2+1, dimensionY*2+1];
for(int i = 0; i < dimensionX * 2 + 1; i++)
{
for(int j = 0; j < dimensionY * 2 + 1; j++)
{
TileType[i, j] = Tiles.wall;
_regions[i, j] = -1;
}
}
generateStage();
_addRooms();
floorColor = Color.green;
//Fill a blank area with a maze
for (int y = 1; y<dimensionY * 2 + 1; y += 2)
{
for(int x = 1; x < dimensionX * 2 + 1; x += 2)
{
var pos = new Vector2(x, y);
if (getTile(pos) != Tiles.wall) continue;
_growMaze(pos);
}
}
_connectRegions();
_removeDeadEnds();
}
private void _generateEnemy(Rect room)
{
for (int m = (int)room.x; m < (int)room.x + room.width; m++)
{
for (int n = (int)room.y; n < (int)room.y + room.height; n++)
{
if (UnityEngine.Random.Range(0, 100) < enemyPercent)
{
Instantiate(enemys[UnityEngine.Random.Range(0, enemys.Length)],cubes[m,n].transform.position+new Vector3(-1.5f,0.2f,1.5f),Quaternion.identity);
}
}
}
}
public void generate3D()
{
GameObject.Find("3DCamera").transform.position = Camera.main.transform.position;
Mcamera.gameObject.SetActive(false);
for (int i = 0; i < dimensionX * 2 + 1; i++)
{
for (int j = 0; j < dimensionY * 2 + 1; j++)
{
switch (TileType[i, j])
{
case Tiles.floor:
Instantiate(floor, cubes[i, j].transform.position, Quaternion.identity,map3D.transform);
if (UnityEngine.Random.Range(0, 100) < enemyPercent&& cubes[i, j].GetComponent<Renderer>().material.color ==Color.red)
{
Instantiate(enemys[UnityEngine.Random.Range(0, enemys.Length)], cubes[i, j].transform.position + new Vector3(-1.5f, 0.2f, 1.5f), Quaternion.identity, enemy3D.transform);
}
break;
case Tiles.closedDoor:
Instantiate(door, cubes[i, j].transform.position + new Vector3(0f, 0, 1f), Quaternion.identity, map3D.transform);
break;
case Tiles.openDoor:
Instantiate(door, cubes[i, j].transform.position + new Vector3(0f, 0, 1f), Quaternion.identity, map3D.transform);
break;
case Tiles.wall:
Instantiate(wall[UnityEngine.Random.Range(0,wall.Length)], cubes[i, j].transform.position+new Vector3(-0.5f,0,0.5f), Quaternion.identity, map3D.transform);
break;
default:
break;
}
Destroy(cubes[i, j]);
}
}
//foreach (Rect room in _rooms)
//{
// _generateEnemy(room);
// //onDecorateRoom(room);
//}
}
private void onDecorateRoom(Rect room)
{
throw new NotImplementedException();
}
private void _growMaze(Vector2 start)
{
var cells = new List<Vector2>();
Vector2 lastDirection=new Vector2(0,0);
_startRegion();
_carve(start);
cells.Add(start);
while (cells.Count > 0)
{
var cell = cells[cells.Count-1];
var unmadeCells = new List<Vector2>();
foreach(var dir in direction)
{
if (_canCarve(cell, dir))
{
unmadeCells.Add(dir);
}
}
if (unmadeCells.Count > 0)
{
Vector2 dir;
if (unmadeCells.Contains(lastDirection) && UnityEngine.Random.Range(0, 100) > windingPercent)
{
dir = lastDirection;
}
else
{
dir = unmadeCells[UnityEngine.Random.Range(0, unmadeCells.Count)];
}
_carve(cell + dir);
_carve(cell + dir*2);
cells.Add(cell + dir * 2);
lastDirection = dir;
}
else
{
//No adjacent suitable cell
cells.RemoveAt(cells.Count-1);
//The end of the road
lastDirection = new Vector2(0, 0);
}
}
}
private bool _canCarve(Vector2 pos, Vector2 direction)
{
//Determining whether the boundary is out of bounds
Vector2 newPoint = pos + direction * 3;
if (newPoint.x < 0 || newPoint.x > dimensionX * 2 + 1 || newPoint.y < 0 || newPoint.y > dimensionY * 2 + 1)
{
return false;
}
return getTile(pos + direction * 2) == Tiles.wall;
}
private void _startRegion()
{
_currentRegion++;
}
private void _removeDeadEnds()
{
bool done = false;
while (!done)
{
done = true;
for (int m = 1; m < dimensionX * 2; m++)
{
for (int n = 1; n < dimensionY * 2; n++)
{
var pos = new Vector2(m, n);
if (getTile(pos) == Tiles.wall) continue;
int exits = 0;
foreach(var dir in direction)
{
if (getTile(pos + dir) != Tiles.wall) exits++;
}
if (exits != 1) continue;
done = false;
TileType[(int)pos.x, (int)pos.y] = Tiles.wall;
cubes[(int)pos.x, (int)pos.y].GetComponent<Renderer>().material.color = Color.white;
}
}
}
}
private void _connectRegions()
{
Dictionary<Vector2, List<int>> connectorRegions = new Dictionary<Vector2, List<int>>();
for (int m = 1; m < dimensionX * 2; m++)
{
for (int n = 1; n < dimensionY * 2; n++)
{
var pos = new Vector2(m, n);
if (getTile(pos) != Tiles.wall) continue;
var regions = new List<int>();
foreach (var dir in direction)
{
var region = _regions[(int)(pos + dir).x, (int)(pos + dir).y];
if (region != -1 && !regions.Contains(region))
{
regions.Add(region);
}
}
if (regions.Count < 2) continue;
connectorRegions[pos] = regions;
}
}
var connectors = new List<Vector2>();
foreach(var c in connectorRegions)
{
connectors.Add(c.Key);
}
//Monitor merged areas
var merged = new int[_currentRegion+100];
var openRegions = new List<int>();
for(int i = 0; i <= _currentRegion; i++)
{
merged[i] = i;
if (!openRegions.Contains(i))
{
openRegions.Add(i);
}
}
//Continuously connects areas until all areas become one
while (openRegions.Count > 1)
{
var connector = connectors[UnityEngine.Random.Range(0,connectors.Count)];
_addJunction(connector);
//Continuous consolidation of regions
var regions = connectorRegions[connector];
for (int region = 0; region < regions.Count; region++)
{
regions[region] = merged[regions[region]];
}
var dest = regions[0];
var sources = new List<int>();
for (int i = 1; i < regions.Count; i++)
{
sources.Add(regions[i]);
}
for(int i = 0; i <= _currentRegion; i++)
{
if (sources.Contains(merged[i]))
{
merged[i] = dest;
}
}
for (int i = 0; i < sources.Count; i++)
{
openRegions.Remove(sources[i]);
}
//Remove unwanted connectors
int length = connectors.Count;
for(int i = 0; i < length; i++)
{
var pos = connectors[i];
if (Vector2.Distance(connector , pos) < 2)
{
connectors.Remove(pos);
length--;
//Debug.Log("1 " + i + " " + length);
continue;
}
var _regions = connectorRegions[pos];
for(int j = 0; j < _regions.Count; j++)
{
_regions[j] = merged[_regions[j]];
}
var _region = _regions[0];
bool repeat = true;
for (int j = 1; j < _regions.Count; j++)
{
if(_regions[j] != _region)
{
//Debug.Log("2 " + i + " " + length);
repeat = false;
}
}
if (!repeat) continue;
//Add some imperfect connections
if (UnityEngine.Random.Range(0, extraConnectorChance) == 0)
{
_addJunction(pos);
}
connectors.Remove(pos);
length--;
//Debug.Log("3 "+i+" "+length);
continue;
}
}
}
private void _addJunction(Vector2 pos)
{
if (UnityEngine.Random.Range(0, 4) == 0)
{
if (UnityEngine.Random.Range(0, 3) == 0)
{
TileType[(int)pos.x, (int)pos.y] = Tiles.openDoor;
cubes[(int)pos.x, (int)pos.y].GetComponent<Renderer>().material.color = Color.black;
}
else
{
TileType[(int)pos.x, (int)pos.y] = Tiles.floor;
cubes[(int)pos.x, (int)pos.y].GetComponent<Renderer>().material.color = floorColor;
}
}
else
{
TileType[(int)pos.x, (int)pos.y] = Tiles.closedDoor;
cubes[(int)pos.x, (int)pos.y].GetComponent<Renderer>().material.color = Color.black;
}
}
private void _addRooms()
{
for(int i = 0; i < numRoomTries; i++)
{
int width = UnityEngine.Random.Range(minRoomX, maxRoomX)*2+1;
int height = UnityEngine.Random.Range(minRoomY, maxRoomY)*2+1;
int x = UnityEngine.Random.Range(0, dimensionX) * 2 + 1;
int y = UnityEngine.Random.Range(0, dimensionY) * 2 + 1;
var room = new Rect(x, y, width, height);
bool overlaps = false;
foreach(var other in _rooms)
{
if (room.Overlaps(other))
{
overlaps = true;
break;
}
}
if (overlaps) continue;
if (x + width > dimensionX*2+1 || y + height > dimensionY*2+1) continue;
_rooms.Add(room);
_startRegion();
for(int m=x; m < x + width; m++)
{
for(int n = y; n < y + height; n++)
{
_carve(new Vector2(m, n));
}
}
}
}
void generateStage()
{
for(int i = 0; i < dimensionX * 2 + 1; i++)
{
for(int j = 0; j< dimensionY * 2 + 1; j++)
{
//GameObject m_cube=GameObject.CreatePrimitive(PrimitiveType.Cube);
//m_cube.GetComponent<Renderer>().material.color = Color.white;
//m_cube.transform.position = new Vector3(i, j, 0);
//cubes[i, j] = m_cube;
cubes[i, j] = Instantiate(cube);
cubes[i, j].GetComponent<Renderer>().material.color = Color.white;
cubes[i, j].transform.position = new Vector3(i, 0, j);
}
}
Camera.main.transform.position = new Vector3(dimensionX, 10, dimensionY);
Camera.main.orthographicSize = dimensionX > dimensionY ? dimensionX + 5 : dimensionY + 5;
}
Tiles getTile(Vector2 pos)
{
return TileType[(int)pos.x, (int)pos.y];
}
private void _carve(Vector2 pos)
{
TileType[(int)pos.x, (int)pos.y] = Tiles.floor;
cubes[(int)pos.x, (int)pos.y].GetComponent<Renderer>().material.color = floorColor;
_regions[(int)pos.x, (int)pos.y] = _currentRegion;
}
}