vistulans
Vistulans - graph-based strategy game about west slavic tribes with myths, legends and fantasy stories
Posted on December 12, 2019
A few days ago I had written about starting my new project for studies. Since then, I have created some basics from the concept. In this short article, I'm going to show what I already did and some code behind mechanisms. ๐
My game named Vistulans from Vistula (Latin name of Wisลa - large and long river in Poland and Slovakia, and historical Slavic tribes near) is inspired by Slavic mythology and graph-based strategy games. The game I am writing in C# with Unity Engine.
Firstly I had created a new scene with a plane for the ground. I added seamless grass texture and setup camera over it facing towards plane at small degree.
Then I added cube as a placeholder for representing vertex until I model something better in Blender. Vertex had vertex controller script for management of properties.
It contains information about Id (unique integer of vertex, used for edges), Vertex Type (enum, to distinct village, shrine and apiary), X and Y position (for future mechanism of travelling), Level (info how fast vertex should produce goods), connections (connected vertices), army power (number of units at vertex), owner (which player - human or AI owns the vertex), and few more but less important.
public GameObject BadgeObject;
// ...
public int Id;
public VertexType Type;
public int X;
public int Y;
public int Level;
public List<GameObject> Connections;
public int ArmyPower;
public OwnerType Owner;
[SerializeField]
private bool _selected = false;
// ...
Having vertices and connections between them I prepared a simple graph. However, because this game will have had many levels until release and I prefer a programmable approach to design, I decided to load levels from config file. Thanks to that, I can write the level editor and give it to the gamers or at least use it on my own to prepare more levels for the players.
Each level is built of lists of vertices, edges and metadata. Background as an integer is index of game object or 3d mesh from future list. Background is going to be visible in the current plane place.
using System;
using System.Collections.Generic;
[Serializable]
public class LevelConfig
{
public List<Level> levels;
}
[Serializable]
public class Level
{
public string title;
public int background;
public List<VertexConfig> verticies;
public List<EdgeConfig> edges;
}
[Serializable]
public class EdgeConfig
{
public int a;
public int b;
}
[Serializable]
public class VertexConfig
{
public int id, type, x, y, level, owner, power;
}
Loading the configuration from JSON file by parsing to above class instance and instantiating vertices in the world. Position of the vertex is aligned with constants. Then for each vertex assign properties from the configuration.
// ...
void Start()
{
TextAsset levelConfigContent = Resources.Load<TextAsset>("Config/levels");
Debug.Log($"Loaded level configuration: {levelConfigContent}");
LevelConfig levelConfig = JsonUtility.FromJson<LevelConfig>(levelConfigContent.text);
foreach (VertexConfig vertexConfig in levelConfig.levels[0].verticies)
{
GameObject newVertex = GameObject.Instantiate(VertexObject, new Vector3(vertexConfig.x * 1f, 0.5f, -vertexConfig.y * 1f), Quaternion.identity);
newVertex.GetComponent<VertexController>().X = vertexConfig.x;
newVertex.GetComponent<VertexController>().Y = vertexConfig.y;
newVertex.GetComponent<VertexController>().Owner = (OwnerType)vertexConfig.owner;
newVertex.GetComponent<VertexController>().Type = (VertexType)vertexConfig.type;
newVertex.GetComponent<VertexController>().ArmyPower = vertexConfig.power;
newVertex.GetComponent<VertexController>().Level = 0;
newVertex.GetComponent<VertexController>().Id = vertexConfig.id;
newVertex.tag = "Vertex";
newVertex.name = $"vertex{vertexConfig.id}";
}
foreach (EdgeConfig connection in levelConfig.levels[0].edges)
{
GameObject vertexA = GameObject.Find($"vertex{connection.a}");
GameObject vertexB = GameObject.Find($"vertex{connection.b}");
vertexA.GetComponent<VertexController>().Connections.Add(vertexB);
vertexB.GetComponent<VertexController>().Connections.Add(vertexA);
}
}
// ...
Sample configuration file:
{
"levels": [
{
"title": "Test map",
"background": 0,
"verticies": [
{
"id": 0,
"type": 0,
"x": -4,
"y": -4,
"level": 0,
"owner": 1,
"power": 25
},
{
"id": 1,
"type": 0,
"x": 4,
"y": -2,
"level": 0,
"owner": 2,
"power": 15
},
{
"id": 2,
"type": 1,
"x": 3,
"y": 3,
"level": 0,
"owner": 0,
"power": 40
},
{
"id": 3,
"type": 0,
"x": -4,
"y": 4,
"level": 0,
"owner": 0,
"power": 15
}
],
"edges": [
{
"a": 0,
"b": 1
},
{
"a": 1,
"b": 2
},
{
"a": 2,
"b": 3
},
{
"a": 0,
"b": 3
}
]
}
]
}
Above configuration creates 4 vertices, each connected with two other, nearest in the corners.
When I had created vertices, then I was working on travelling between nodes. That was simply. Touching the vertex set and unset origin and target. Touching the target when having origin instantiate new army unit object in direction of the target. Army power currently is constant and set to 50% of vertex army power. Also, it requires 2 or more units to avoid leaving empty vertex to prevent a further problem when writing simple AI.
// ...
public void OnVertexTouch(int id)
{
if (_touchedVertexAId == -1)
{
_touchedVertexAId = id;
} else
{
_touchedVertexBId = id;
}
}
public void FixedUpdate()
{
if (_touchedVertexAId != -1 && _touchedVertexBId == -1)
{
GameObject selectedVertex = GameObject.Find($"vertex{_touchedVertexAId}");
selectedVertex.GetComponent<Renderer>().material.color = Color.white;
foreach (GameObject connectedVertex in selectedVertex.GetComponent<VertexController>().Connections)
{
connectedVertex.GetComponent<Renderer>().material.color = Color.yellow;
}
}
if (_touchedVertexAId != -1 && _touchedVertexBId != -1)
{
GameObject selectedVertex = GameObject.Find($"vertex{_touchedVertexAId}");
foreach (GameObject possibleVertex in selectedVertex.GetComponent<VertexController>().Connections)
{
if (possibleVertex.GetComponent<VertexController>().Id == _touchedVertexBId)
{
if (selectedVertex.GetComponent<VertexController>().ArmyPower > 1)
{
int armyPowerToSend = selectedVertex.GetComponent<VertexController>().ArmyPower / 2;
selectedVertex.GetComponent<VertexController>().ArmyPower -= armyPowerToSend;
SendArmy(_touchedVertexAId, _touchedVertexBId, armyPowerToSend);
Debug.Log($"Sent unit from {_touchedVertexAId} to {_touchedVertexBId}");
}
}
}
foreach (GameObject vertex in GameObject.FindGameObjectsWithTag("Vertex"))
{
vertex.GetComponent<Renderer>().material.color = Color.clear;
}
_touchedVertexAId = -1;
_touchedVertexBId = -1;
}
}
public void SendArmy(int origin, int target, int amount)
{
GameObject vertexA = GameObject.Find($"vertex{origin}");
GameObject vertexB = GameObject.Find($"vertex{target}");
if (vertexA.GetComponent<VertexController>().ArmyPower >= amount)
{
Vector3 spawnPosition = vertexA.gameObject.transform.position;
spawnPosition.y = 0.25f;
GameObject newArmy = GameObject.Instantiate(ArmyObject, spawnPosition, Quaternion.identity);
newArmy.GetComponent<ArmyController>().Owner = vertexA.GetComponent<VertexController>().Owner;
newArmy.GetComponent<ArmyController>().ArmyPower = amount;
newArmy.GetComponent<ArmyController>().Origin = origin;
newArmy.GetComponent<ArmyController>().Target = target;
}
else
{
// insufficient army power
}
}
// ...
The army should interact with the enemy and other vertices. On collision with the enemy, the army with more unit power wins and remain the diff value. The lost army is destroyed. When the army collides vertex it removes enemy army power, and switch owner when it has less or equal to zero.
Army object shouldn't interact with other army send by the same owner and interact exactly once with the enemy's army. To do that I locked exection of the process in a collided object.
using UnityEngine;
public class ArmyController : MonoBehaviour
{
public int Origin = -1;
public int Target = -1;
private GameObject _targetObject;
public int ArmyPower = 0;
public OwnerType Owner = OwnerType.Wild;
public float MovementSpeed = 1f;
public bool AlreadyTriggering = false;
void UpdateTarget(int newTarget)
{
Target = newTarget;
_targetObject = GameObject.Find($"vertex{Target}");
}
void FixedUpdate()
{
if (_targetObject == null)
{
UpdateTarget(Target);
}
else
{
Vector3 targetDirection = _targetObject.gameObject.transform.position - transform.position;
targetDirection.y = 0;
transform.rotation = Quaternion.LookRotation(targetDirection);
gameObject.transform.position += gameObject.transform.forward * MovementSpeed * Time.deltaTime;
}
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "Army")
{
other.gameObject.GetComponent<ArmyController>().AlreadyTriggering = false;
}
}
private void OnTriggerEnter(Collider other)
{
if (AlreadyTriggering == false)
{
if (other.gameObject.tag == "Vertex")
{
if (other.gameObject.GetComponent<VertexController>().Id != Origin)
{
if (other.gameObject.GetComponent<VertexController>().Owner == Owner)
{
other.gameObject.GetComponent<VertexController>().ArmyPower += ArmyPower;
}
else
{
other.gameObject.GetComponent<VertexController>().ArmyPower -= ArmyPower;
if (other.gameObject.GetComponent<VertexController>().ArmyPower <= 0)
{
other.gameObject.GetComponent<VertexController>().Owner = Owner;
other.gameObject.GetComponent<VertexController>().ArmyPower = Mathf.Abs(other.gameObject.GetComponent<VertexController>().ArmyPower);
}
}
GameObject.Destroy(gameObject);
}
}
else if (other.gameObject.tag == "Army")
{
if (other.gameObject.GetComponent<ArmyController>().AlreadyTriggering == false && other.gameObject.GetComponent<ArmyController>().Owner != Owner)
{
other.gameObject.GetComponent<ArmyController>().AlreadyTriggering = true;
if (other.gameObject.GetComponent<ArmyController>().ArmyPower < ArmyPower)
{
ArmyPower -= other.gameObject.GetComponent<ArmyController>().ArmyPower;
GameObject.Destroy(other.gameObject);
}
else if (other.gameObject.GetComponent<ArmyController>().ArmyPower > ArmyPower)
{
other.gameObject.GetComponent<ArmyController>().ArmyPower -= ArmyPower;
GameObject.Destroy(gameObject);
}
else
{
GameObject.Destroy(gameObject);
GameObject.Destroy(other.gameObject);
}
}
}
}
}
}
Wooohooo! This is the moment when the first feature was done! A player can now move units before vertices and fight with others (but still controlled by a human).
Next day I spend working on simple badges with information about vertices. I attached to it billboard script to face always camera and made badges on canvas. Each badge is made of few TextMeshPro objects and sprite behind them. Sprite has a different look based on the owner of the vertex.
using TMPro;
using UnityEngine;
public class BadgeController : MonoBehaviour
{
public int Level;
public int ArmyPower;
public VertexType Type;
public OwnerType Owner;
public GameObject LevelText;
public GameObject PowerText;
public GameObject TypeText;
public GameObject Background;
public Sprite WildBackground;
public Sprite PlayerBackground;
public Sprite EnemyOneBackground;
public Sprite EnemyTwoBackground;
public Sprite EnemyThreeBackground;
// ...
}
Last but not least, I have added a camera controller with zoom in/out and moving around. Because game should work both on android/ios and pc like in the new games from Civilization series it checks if the user is already touching. Otherwise, it checks a mouse state. Tbh. part of this script about touch input was inspired by answer on stack overflow: Jinjinov
// ...
if (Input.touchCount == 1 && _isZooming == false)
{
Touch touch0 = Input.GetTouch(0);
_isTouchMove = touch0.phase == TouchPhase.Moved;
_isTouchDown = touch0.phase == TouchPhase.Stationary;
if (_isTouchDown)
{
_startScreenPosition = touch0.position;
_cameraPosition = transform.position;
}
if (_isTouchMove == true)
{
_isDragging = true;
_currentScreenPosition = touch0.position;
_currentScreenPosition.z = _startScreenPosition.z = _cameraPosition.y;
Vector3 direction = Camera.main.ScreenToWorldPoint(_currentScreenPosition) - Camera.main.ScreenToWorldPoint(_startScreenPosition);
direction = direction * -1;
_targetPosition = _cameraPosition + direction;
_isMovingTo = true;
}
}
// ...
if (Input.GetMouseButtonDown(0))
{
_startScreenPosition = Input.mousePosition;
_cameraPosition = transform.position;
}
if (Input.GetMouseButton(0))
{
_currentScreenPosition = Input.mousePosition;
_currentScreenPosition.z = _startScreenPosition.z = _cameraPosition.y;
Vector3 direction = Camera.main.ScreenToWorldPoint(_currentScreenPosition) - Camera.main.ScreenToWorldPoint(_startScreenPosition);
direction = direction * -1;
_targetPosition = _cameraPosition + direction;
_isMovingTo = true;
}
// ...
if (_isMovingTo)
{
_targetPosition.y = transform.position.y;
transform.position = _targetPosition;
if (transform.position == _targetPosition)
{
_isMovingTo = false;
}
}
// ...
void Zoom(bool zoomIn = true)
{
if (zoomIn)
{
gameObject.transform.Translate(new Vector3(0, -1, 2) * Time.deltaTime * _zoomSpeed);
}
else
{
gameObject.transform.Translate(new Vector3(0, 1, -2) * Time.deltaTime * _zoomSpeed);
}
}
Finally, I added incrementing of resources based on vertex type and the owner:
// ...
public int[] Mana;
public int[] Honey;
// ...
// ...
void Start()
{
_badgeObject = GameObject.Instantiate(BadgeObject, gameObject.transform.position - new Vector3(0, 1f, 2f), Quaternion.identity);
InvokeRepeating("IncreaseUnits", 2.0f, 2.0f);
if (_mechanismObject == null)
{
_mechanismObject = GameObject.Find("Mechanism");
}
}
void IncreaseUnits()
{
if (Owner != OwnerType.Wild)
{
switch (Type)
{
case VertexType.Shrine:
_mechanismObject.GetComponent<GameplayController>().Mana[(int)Owner - 1] += Level + 1;
break;
case VertexType.Village:
ArmyPower += Level + 1;
break;
case VertexType.Apiary:
_mechanismObject.GetComponent<GameplayController>().Honey[(int)Owner - 1] += Level + 1;
break;
}
}
}
This week was totally awesome! I am excited to work further on the game. What do you think about game? Do you have any advices or maybe gameplay idea to implement? Write it in the comment! ๐
Posted on December 12, 2019
Sign up to receive the latest update from our blog.