Now that we have a character smoothly walking around our area with fellow players the next thing we need to tackle is world collisions. Since this is an authoritative server we can’t trust the characters clients position so we have to mimic the clients world server side. To simplify things I made the decision when the character collides with an object it just stops all movement. This was done to simplify the re-creation of the physics on the server and eliminated the need to emulate the Unity slide off physics. Later on I may adjust the moving to create something similar for for know the simple approach is working well. To be able to re-create client side movements server side I need to be able to export all the map tile colliders as well as any objects that have attached 2D colliders. To do this I need to create some Unity buttons and scripts to export the needed data to a format that I can use on the server side of things.
World Map Colliders
To create area’s of the map where the Hero is unable to walk through I create multiple layers for the world map tile which include 2 layers for collision tiles.
Now we create/add a tile palette and make some tile spites that will generate a collider mesh. This is done by adding a physics shape to the tile spite and assign a “Tilemap Collider 2D/Composite Collider 2D” to the GameObject that is acting as our collision layer for the map. Now this works great for getting client side collisions but how to we replicate this on the server? Will the answer is quite easy, I created a Unity UI button called “Export Colliders” and wrote a little script to export all the physics data of the tiles. Once the button and script is created all I need to do to export the data is click the “Grid” Game Object (Parent Object of all my World Tile Layers) and then click “Export Colliders” in the Inspector window.
Export Tile Colliders Script
[CustomEditor(typeof(Grid))]
public class ExportTileEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (GUILayout.Button("Export Tiles"))
{
var tilemapExport = new StringBuilder("");
tilemapExport.AppendLine("{");
tilemapExport.AppendLine("tiles: [");
//Cast Grid
var grid = (Grid)target;
//Get All Tilemaps In Grid
var tilemaps = grid.GetComponentsInChildren<Tilemap>();
foreach (var tilemap in tilemaps)
{
//Check To See If Tilemap Has a Collider
if (tilemap.GetComponentInParent<TilemapCollider2D>() == null)
continue;
//Loop Through All Tiles of Tilemap with Coillider
for (var x = tilemap.cellBounds.xMin; x < tilemap.cellBounds.xMax; x++)
{
for (var y = tilemap.cellBounds.yMin; y < tilemap.cellBounds.yMax; y++)
{
for (var z = tilemap.cellBounds.zMin; z < tilemap.cellBounds.zMax; z++)
{
//Get Local Vector
var localPlace = new Vector3Int(x, y, z);
//Check To See If It Has A Tile
if (!tilemap.HasTile(localPlace))
continue;
//Get Tile
var tile = tilemap.GetTile(localPlace);
//Get Tile Sprite
var tileSprite = tilemap.GetSprite(localPlace);
//Get Physic Object of Sprite
var tilePhysicsShapeVertices = new List<Vector2>();
tileSprite.GetPhysicsShape(0, tilePhysicsShapeVertices);
//Get World Location Of Tile
var tileToWorld = tilemap.CellToWorld(localPlace);
//Get Tile Rotation
var tileRotation = tilemap.GetTransformMatrix(localPlace).rotation.eulerAngles;
//Debug.Log("Local: x:" + localPlace.x + ", y:" + localPlace.y + ", z:" + localPlace.z);
//Export Tile with 0.25 y offset.
tilemapExport.AppendLine(JsonUtility.ToJson(new ExportTile(tile.name, tileToWorld.x + tilemap.tileAnchor.x / 2, tileToWorld.y + tilemap.tileAnchor.y / 2, tileToWorld.z, tileRotation, tilePhysicsShapeVertices.ToArray())) + ",");
}
}
}
}
tilemapExport.Remove(tilemapExport.Length - 3, 3).AppendLine("");
tilemapExport.AppendLine("]");
tilemapExport.AppendLine("}");
//Save JSON Data
File.WriteAllText(Application.dataPath + "/Exports/Tilemap.json", tilemapExport.ToString());
Debug.Log("Tilemap Exported!");
}
}
}
Now that we have a JSON export of all the map tile colliders I wrote a little import script on the server. This script uses all the exported vertices, creates a PolygonCollider (the SAT collision routine uses this), and store it in the worlds QuadTree.
Tile Collider Server Import
public void LoadTileColliders(string tilemapJson)
{
//Parse Collider Json Export
dynamic parsedJson = JObject.Parse(tilemapJson);
//Create Collider Objects
foreach (var collider in parsedJson.tiles)
{
//Get Name/Position
var name = (string)collider.Name;
var position = new Position(float.Parse(FormatFloat((string) collider.X)), float.Parse(FormatFloat((string)collider.Y)), 0);
var rotation = new Position(float.Parse(FormatFloat((string)collider.Rotation.x)), float.Parse(FormatFloat((string)collider.Rotation.y)), float.Parse(FormatFloat((string)collider.Rotation.z)));
//Get Vertices
var vertices = new List<Vector>();
foreach (var point in collider.Vertices)
{
var x = float.Parse(FormatFloat((string)point.x));
var y = float.Parse(FormatFloat((string)point.y));
vertices.Add(new Vector(x, y));
}
_mapQuadTree.Insert(new PolygonCollider(position.X, position.Y, vertices));
}
}
World Objects
For world objects it works very similar, I put all my objects into a Parent “Decorations” GameObject and attach the needed collider to them (Polygon, Box, Circle). Then I created a Unity Button and attached a script that export all the Colliders to a JSON file. On the server I import this JSON file and create the matching collider (Polygon, Box, Circle) and store it in the world QuadTree.
Export World Object Colliders
if (GUILayout.Button("Export Colliders"))
{
//Cast GameObject
var gameObject = (GameObject)target;
var colliderExport = new StringBuilder("");
colliderExport.AppendLine("{");
colliderExport.AppendLine("colliders: [");
//Check For Circle Colliders
foreach (var circleCollider2D in gameObject.GetComponentsInChildren<CircleCollider2D>())
{
colliderExport.AppendLine(JsonUtility.ToJson(new ExportCircleCollider(circleCollider2D.name, 1, circleCollider2D)) + ",");
}
//Check For Box Colliders
foreach (var boxCollider2D in gameObject.GetComponentsInChildren<BoxCollider2D>())
{
colliderExport.AppendLine(JsonUtility.ToJson(new ExportBoxCollider(boxCollider2D.name, 2, boxCollider2D)) + ",");
}
//Check For Polygon Colliders
foreach (var polygonCollider2D in gameObject.GetComponentsInChildren<PolygonCollider2D>())
{
colliderExport.AppendLine(JsonUtility.ToJson(new ExportPolygonCollider(polygonCollider2D.name, 3, polygonCollider2D)) + ",");
}
//Add To String Builder
colliderExport.Remove(colliderExport.Length - 3, 3).AppendLine("");
colliderExport.AppendLine("]");
colliderExport.AppendLine("}");
//Save JSON Data
File.WriteAllText(Application.dataPath + "/Exports/GameObjectColliders.json", colliderExport.ToString());
Debug.Log("Colliders Exported!");
}
Import World Collider Objects
public void LoadColliders(string colliderJson)
{
//Parse Collider Json Export
dynamic parsedJson = JObject.Parse(colliderJson);
//Create Collider Objects
foreach (var collider in parsedJson.colliders)
{
//Get Name/Position
var name = (string)collider.Name;
var position = new Position(float.Parse(FormatFloat((string)collider.X)), float.Parse(FormatFloat((string)collider.Y)), 0);
switch ((ColliderType)collider.Type)
{
case ColliderType.Circle:
var radius = float.Parse(FormatFloat((string)collider.Radius));
_mapQuadTree.Insert(new CircleCollider(position.X, position.Y, radius));
break;
case ColliderType.Box:
var size = new SizeF(float.Parse(FormatFloat((string)collider.Length)), float.Parse(FormatFloat((string)collider.Height)));
_mapQuadTree.Insert(PolygonCollider.Rectangle(position.X, position.Y, size.Width, size.Height));
break;
case ColliderType.Polygon:
var vertices = new List<Vector>();
foreach (var point in collider.Points)
{
var x = float.Parse(FormatFloat((string)point.x));
var y = float.Parse(FormatFloat((string)point.y));
vertices.Add(new Vector(x, y));
}
_mapQuadTree.Insert(new PolygonCollider(position.X, position.Y, vertices));
break;
default:
break;
}
}
}
Now the server has an exact replica of the client’s world map that we can now test for collisions to validate players movements. You just have to remember every time you adjust your map in Unity that you have to re-export the colliders for the server. I hope this helps anyone trying to make an authoritative server without using Unity as a backend. Next post I’ll go about how I’m going to handle adding NPCs to my game along with some simple patrolling code.