DIM Game Project
DIM is a non-euclidean space exploration game, with focus on story, light puzzles, and pizza.
Me and 2 Artists developed this game for our College thesis. I was the main programmer in this project, all C# code was done by me. I also did the GameLighting, this game uses baked Global Illumination.
It's a story game about exploring non-euclidean spaces. This game is not out yet and will not be for a while.
Mayor Game Systems
Non-Euclidean Portal System.
It is similar to what is implemented in This Excelent Video With some differences:
- Recursive portals are not supported for performance reasons. Also because there is no player character model.
- Multiple portals are supported, rendering portals through portals is supported.
- Physics objects are partially supported.
- Portals are not rendered when Out of View
- Cameras and RenderTextures are Pooled.
- Custom Room Occlusion Integration.
- The portal is not a No-Culling cube, but rather a cube rendering only backfaces. The shader also only renders the half of the portal facing away from where the camera is
Custom Room Occlusion
TLDR: Unity's Occlusion is Terrible. It also does not support the Portal System. I made my own, Heavily simplified Occlusion System.
With this system, I can place axis aligned boxes to Divide Rooms (Red). I can place more boxes, called Connectors (Yellow) to connect rooms. I press a button in either of these components, and each room is assigned Connectors in the Editor, automatically. Connectors also have some data assigned.
Every Frame:
-
All Rooms set their Child GameObjects.active to the value of
Enabled
- There are also Physics objects that are dynamically assigned to each room, based on their position.
- All Rooms are set to Enabled = false.
- The camera set's the current Room it is in as Enabled = true.
-
The camera also checks the Room's connectors, and checks if they are in
view.
- If they are, the room that connector connects to is also marked as Enabled = true.
- It does the same for that room recusrively. However, it also checks if that Room's Connectors are visible Through the previous connector
- Portals are automatically disabled when their room isn't loaded (The Portal won't Render)
- It also checks Recursively through that Portal, using it's Camera.
- Again, checking if that Room's Connectors are visible Through the Portal the camera is rendering for
This implementation has Zero Memory Allocations.
// Runs in Update Loop Once.
public void DoChecks()
{
// Check Current (current_rooms are rooms the player collider overlaps with)
for (int i = 0; i < current_rooms.Count; i++)
{
if (current_rooms[i].WorldBounds.Contains(transform.position))
{
Current = current_rooms[i];
break;
}
}
// Update Visibility
// Set Base Room Visibility
for (int i = 0; i < allrooms.Count; i++)
{
// Set's GOs enabled from previous frame calculations.
allrooms[i].UpdateVisibility();
if (allrooms[i] == Current)
{
allrooms[i].Visible = true;
}
else
{
allrooms[i].Visible = false;
}
}
if (!Current)
return;
// Recursive Traversal
CheckConnections(Current, cam, maxIterations, portalDepth);
}
private static void CheckConnections(
RenderRoom room, Camera camera,
int recursiveDepth = 2, int portalDepth = 1,
(Transform t, Bounds b) prev = default)
{
// Makes a Bounds relative to a Transform
static Bounds LocalToWorld((Transform t, Bounds b) obj) =>
new Bounds(obj.b.center + obj.t.position, obj.b.size);
for (int i = 0; i < room.Connectors.Count; i++)
{
if (room.Connectors[i].Closed)
continue;
if (PortalMath.VisibleFromCamera(room.Connectors[i].Bounds, camera) && (
prev.t == null || // Only check the following if prev.t isn't null
PortalMath.NewBoundsOverlapOnScreen(
LocalToWorld(prev),
LocalToWorld(
(room.Connectors[i].transform,
room.Connectors[i].LocalBounds)
), camera)
))
{
var other = room.Connectors[i].GetOtherRoom(room);
other.Visible = true;
if (recursiveDepth > 0)
{
CheckConnections(other, camera, recursiveDepth - 1, portalDepth,
(room.Connectors[i].transform, room.Connectors[i].LocalBounds)
);
}
}
}
if (portalDepth < 1)
return;
// Iterate through Room's Portals
for (int i = 0; i < room.Portals.Count; i++)
{
var portalBounds = room.Portals[i].GetWorldBounds();
if (PortalMath.VisibleFromCamera(portalBounds, camera) && (
prev.t == null ||
PortalMath.NewBoundsOverlapOnScreen(
LocalToWorld(prev),
LocalToWorld(
(room.Portals[i].transform,
room.Portals[i].GetLocalBounds())),
camera)
))
{
var otherPortalCam = room.Portals[i].GetCamera();
var otherRoom = room.Portals[i].GetOther();
var otherBounds = room.Portals[i].GetOtherLocalBounds();
if (!otherRoom)
continue;
otherRoom.Visible = true;
if (otherPortalCam && otherBounds.size.magnitude > .01f)
{
CheckConnections(otherRoom, otherPortalCam, recursiveDepth, portalDepth - 1,
(room.Portals[i].GetOtherTransform(), otherBounds)
);
}
}
}
}
Editor-Lua Scripting.
For many of the scripted events, Lua is used to provide greater functionality. Scrips can be placed in MonoBehaviours like this:
[SerializeField]
GameScript m_OnActivate;
[SerializeField]
ScriptObjectInterface api;
public void Awake()
{
GM.Script.Run(m_OnActivate, api);
}
GameScript has a CustomPropertyDrawer, that creates a
dropdown from where Lua can be written in the Editor. This may seem
Un-Optimal, but the scripts written are never more than 10 lines of code. Also
the script source is saved with the Scene as a string.
ScriptObjectInterface contains a List of objs. This can be any
UnityEngine.Object
. It then becomes a Global in the Lua script,
as a UserData object. The
Global is named after a Name by the user.
GM
is my GlobalManager's Manager. Script
is my
LuaRuntime Manager.
Scripts can also access these Managers with the Same Syntax.
Universal Save System
The Game Supports saving and Loading from anywhere (with some exceptions). This works using an Interface which implements save functionality in a MonoBehaviour.
public interface ISaveNode
{
Guid Guid { get; }
bool HasGuid { get; }
void CreateNewGuid();
void SaveData(SaveBuffer buffer);
void LoadData(SaveBuffer buffer);
}
Each MonoBehaviour must then have a
Guid
as a way to Identify itself. They then use a SaveBuffer
as a way
to Write/Read their own block of Serialized Data.
This is an Implementation to save a GameObject's enabled
state
public class SaveGameObjectActive : MonoBehaviour, ISaveNode
{
[SerializeField, SaveID]
string usid;
public Guid Guid
{
get
{
if (string.IsNullOrEmpty(usid))
Debug.LogError($"EMPTY USID AT {gameObject.name}", this);
return Guid.Parse(usid);
}
}
public void SaveData(SaveBuffer buffer)
{
buffer.WriteBool(gameObject.activeSelf);
}
public void LoadData(SaveBuffer buffer)
{
gameObject.SetActive(buffer.ReadBool());
}
public bool HasGuid => Guid.TryParse(usid, out var _);
public void CreateNewGuid()
{
usid = Guid.NewGuid().ToString();
}
}
SaveID
is a CustomPropertyDrawer Atribute. It makes the string a
Hex representation of a Guid (Random 128bit Integer). And has a button for
creating a Guid using Guid.NewGuid()
There is also a USID Manager Window. It find GUID Conflicts and Unnasigned Guids in a Loaded Scene. This prevents erros later from Guid Conflicts or missing guids
Comentarios
Publicar un comentario