2023-10-16 - Design ideas for the game world
And here we go with week #3!
As I previously anticipated, I want to implement a mechanism to allow teleporting between zones.
We defined that a zone is composed by a set of arbitrarily placed platforms that are loaded and placed in the game world. The player can move between adjacent platforms of a zone.
Now, we want to add a special kind of platform that can bring you to the current zone to another one.
The game world will then be composed by many zones, with these special platforms that act as bridge between two of them. Actually, let's call them "bridge" platforms from now on.
How can we decompose this task?
we need to prepare the "next" zone for the player, when appropriate
we need to render the "next" zone so that it only appears in the portal of the bridge platform
when the player crosses the portal, the "next" zone will become the "current" one.
So, the first question to ask ourselves is: what data do we need to represent the bridge platforms?
The simplest setup I can imagine involves each zone having a single entry platform. For every bridge platform, we would only need to define an identifier that tells which is the target zone for that platform. As the entry platform for the target zone would be a characteristic of the zone itself, that would be it.
The second question would be: after going from zone A to zone B (using an exit bridge platform of zone A to reach the entry platform of zone B), should you be able to go back to zone A crossing back the same portal (appearing on the exit you just used, and not the entry)?
Keeping the stored data minimal and deducing/calculating other stuff is generally a good thing, but in this case I don't like this kind of solution, it feels limiting.
I don't want to be forced to make gameplay decisions right now: can the player go back? Maybe. Should a zone have multiple entry points and not a single one? Possibly. Should walking back into the portal bring you in the same place where you came from? Probably, but maybe in a special level something else could happen.
So, let's do something similarly simple, but more flexible.
Let's say that:
we assign to a bridge platform three attributes:
id
destZoneId (optional)
destPlatformId (optional)
every zone has at least one bridge platform
With this setup, we have the flexibility to define multiple entry platforms in a single zone, and also to specify bridge platforms as unidirectional or bidirectional, or even to create strange portals that bring you from zone A to B when crossed in one direction, but from B to C (and not back to A) when crossed back.
If we won't make use of these features, the simple case will be handled anyway, with very little extra effort in the data definition (which could be a burden put on the level editor, anyway).
I rarely use string identifiers, but this looks like a good place for them, as they will be useful for labelling platforms and zones in the level editor and to lookup zone definitions by readable names - something that definitely proves useful while debugging.
Now that we have an idea about what data we need, let's imagine a simple test layout, so that we have a specific target to reach while implementing.
We're going to define three zones:
`
corridorZone
`, rectangular`
leftTriZone
`, triangular pointing left`
rightTriZone
`, triangular pointing right
The three zones will be linked by bridge platforms as shown in the picture. The red circles are the platforms, the blue arrows indicate how they are connected.
This should be interesting enough:
`
corridorZone
` features the startup platform (A) and a bridge platform to `leftTriZone
` (B)`
leftTriZone
` has three bridge platforms in the three triangle corners. The leftmost (C) is the one that connects back to `corridorZone
`, while the other two, on the right (D and E), will bring the player on two different platforms of `rightTriZone
` (F and G, respectively).`
rightTriZone
` has the two bridge platforms that link back to `leftTriZone
` (F going back to D, G going back to E), and just for fun we're going to setup a third bridge platform in the rightmost corner (H), and link it to the startup platform (A). This will be a "one way" portal (you won't be able to go back from A to H).
Now, let's specify the data for the bridge platforms of this configuration:
`corridorZone`
A
`id = start`
B
`id = exit`
`destZoneId = leftTriZone`
`destPlatformId = left_mid`
`leftTriZone`
C
`id = left_mid`
`destZoneId = corridorZone`
`destPlatformId = exit`
D
`id = right_top`
`destZoneId = rightTriZone`
`destPlatformId` = `left_top`
E
`id = right_bottom`
`destZoneId = rightTriZone`
`destPlatformId` = `left_bottom`
`rightTriZone`
F
`id = left_top`
`destZoneId = leftTriZone`
`destPlatformId = right_top`
G
`id = left_bottom`
`destZoneId = leftTriZone`
`destPlatformId = right_bottom`
H
`id = right_mid
`destZoneId = corridorZone`
`destPlatformId = start`
And that's it - I'm out of time for today, with zero lines of code written, but I see a clear path forward.
Tomorrow I'll start implementing and we'll see along the way if I overlooked something during this preliminary design phase.
2023-10-17 - Building the test world
Today, more code and less blabbering!
Let's start by building in game the test setup we designed yesterday.
We already have the `ProcTriangularShapedZoneGenerator
` for the two triangular zone, let's quickly add a similar `ProcRectangularZoneGenerator
` for the `corridorZone
`.
using System.Collections.Generic;
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public class ProcRectangularZoneGenerator : IZoneGenerator {
public enum EHexOrientation {
pointy_top,
flat_top
};
private int m_iWidth;
private int m_iHeight;
private EHexOrientation m_eHexOrientation;
public ProcRectangularZoneGenerator(
int iWidth,
int iHeight,
EHexOrientation eHexOrientation = EHexOrientation.flat_top) {
m_iWidth = iWidth;
m_iHeight = iHeight;
m_eHexOrientation = eHexOrientation;
}
public ZoneDesc getZone() {
HashSet<Hex> rSet =
m_eHexOrientation == EHexOrientation.flat_top ?
Map.getRectangle_flatTop(0, m_iWidth, 0, m_iHeight) :
Map.getRectangle_pointyTop(0, m_iWidth, 0, m_iHeight);
return ZoneDesc.getZoneDescFromHexSet(rSet);
}
}
}
Then, we can run a small, temporary setup method and see if zones come out as we want them:
private void setupTestWorld(in ZonePlatformsSettings zps) {
ZoneDesc rCorridorZD = new ProcRectangularZoneGenerator(4, 0)
.getZone();
ZoneDesc rLeftTriZD = new ProcTriangularShapedZoneGenerator(4)
.getZone();
ZoneDesc rRightTriZD = new ProcTriangularShapedZoneGenerator(
4, ProcTriangularShapedZoneGenerator.EOrientation.flipped
).getZone();
Zone.Instantiate(rCorridorZD, zps, Vector3.zero);
Zone.Instantiate(rLeftTriZD, zps, Vector3.zero);
Zone.Instantiate(rRightTriZD, zps, Vector3.zero);
}
Running the game and moving (using the scene view) each `zoneParent` node at vertical offsets of 25 units, so they are not overlapping , we get this:
I highlighted the platforms that we need to set as bridges.
And of course, when I took my 50/50 chance about which of the `ProcTriangularShapedZoneGenerator
` orientation was the "pointing right" and which the "pointing left", I got it wrong.
Let's see if we can make a revised version using the `Zone.Instantiate
` offset parameter to place the zones how we imagined them:
private void setupTestWorld(in ZonePlatformsSettings zps) {
ZoneDesc rCorridorZD = new ProcRectangularZoneGenerator(4, 0)
.getZone();
ZoneDesc rLeftTriZD = new ProcTriangularShapedZoneGenerator(
4, ProcTriangularShapedZoneGenerator.EOrientation.flipped
).getZone();
ZoneDesc rRightTriZD = new ProcTriangularShapedZoneGenerator(4)
.getZone();
Vector3 vStart = Vector3.zero;
Vector3 vHorizOffset = new Vector3(25f, 0f, 0f);
Zone.Instantiate(rCorridorZD, zps, vStart);
Zone.Instantiate(rLeftTriZD, zps, vStart + vHorizOffset);
Zone.Instantiate(rRightTriZD, zps, vStart + 2 * vHorizOffset);
}
Ok, better but there's an obvious problem, that I highlighted by adding three red spheres at the coordinates of the three zoneParent nodes, which are are correctly placed at the offsets I wanted:
0, 0
25, 0
50, 0
What I'd really want is having the three platforms that in this context I consider as reference points (highlighted in the image) at the origin point for each zone (which is where the red spheres are).
So, let's add a parameter to `Zone.Instantiate
` that identifies which platform must be at the origin point, and we're going to add an offset in the platform placement logic to take that into account.
public static Zone Instantiate(
ZoneDesc zoneDesc,
ZonePlatformsSettings zps,
Vector3 vOffset,
Hex originCoords) {
// [...]
GameObject rZoneParent = new GameObject("zp_" + zoneDesc.m_sId);
// [...]
// let's calculate the "origin platform offset"
Vector2 v2origPlatformOffset = rLayout.getHexPos(originCoords);
Vector3 v3origPlatformOffset = new Vector3(
v2origPlatformOffset.x, 0f, v2origPlatformOffset.y
);
foreach (PlatformDesc pd in zoneDesc.getPlaformDecs()) {
Vector2 vPos2D = rLayout.getHexPos(pd.m_hexCoords);
Vector3 vLocalPos = new Vector3(vPos2D.x, 0f, vPos2D.y);
GameObject rNewPlatformGO = GameObject.Instantiate(
rPlatformTemplate,
vOffset - v3origPlatformOffset + vLocalPos, // new offset here
rPlatformTemplate.transform.rotation,
rZoneParent.transform
);
[...]
}
[...]
}
I also added add an `id` field to `ZoneDesc` so that we can also store the zone string identifier. This is a trivial change involving multiple files, so it's not worth posting the code. You can see its usage in the line creating and labelling the `rZoneParent` game object, making it more easy to spot in the scene hierarchy when multiple zones are loaded.
Finally, we update our setup code and check that everything works as expected.
private void setupTestWorld(in ZonePlatformsSettings zps) {
ZoneDesc rCorridorZD = new ProcRectangularZoneGenerator(4, 0)
.getZone("corridorZone");
ZoneDesc rLeftTriZD = new ProcTriangularShapedZoneGenerator(
4, ProcTriangularShapedZoneGenerator.EOrientation.flipped
).getZone("leftTriZone");
ZoneDesc rRightTriZD = new ProcTriangularShapedZoneGenerator(4)
.getZone("rightTriZone");
Vector3 vStart = Vector3.zero;
Vector3 vHorizOffset = new Vector3(25f, 0f, 0f);
Zone.Instantiate(rCorridorZD, zps,
vStart, new Hex(0, 0, 0));
Zone.Instantiate(rLeftTriZD, zps,
vStart + vHorizOffset, new Hex(0, 4, -4));
Zone.Instantiate(rRightTriZD, zps,
vStart + 2 * vHorizOffset, new Hex(0, 2, -2));
}
Tomorrow, we'll setup the bridges and hopefully load/unload zones as needed.
2023-10-18 - Storing bridge platforms data
Now that we have the three zones for our "test world" and that we can easily load them at a specific point in the scene, it's time to add to our zone definitions the "bridge platforms" data.
At least for now, I want to keep using readonly structs to hold the level data, so I won't change the platforms settings by code. I'm going to save the "plain" zones to JSON files end edit them to add the missing information.
Let's add two utility methods to `PersistenceUtils
`:
private const string sZONEDESCS_PATH = "_zoneDescs/";
public static void storeZoneDesc(ZoneDesc zd) {
string sFilePath = sZONEDESCS_PATH + zd.m_sId + ".json";
store(sFilePath, zd);
}
public static ZoneDesc fetchZoneDesc(string sZoneId) {
string sFilePath = sZONEDESCS_PATH + sZoneId + ".json";
return fetch<ZoneDesc>(sFilePath);
}
This way, we can save and load each zone to a JSON file in a standard location, and using the string id as lookup information. At some point we're going to change the persistence backend, but the API should remain (more or less) stable.
private void setupTestWorld(in ZonePlatformsSettings zps) {
ZoneDesc rCorridorZD = new ProcRectangularZoneGenerator(4, 0)
.getZone("corridorZone");
ZoneDesc rLeftTriZD = new ProcTriangularShapedZoneGenerator(
4, ProcTriangularShapedZoneGenerator.eOrientation.flipped
).getZone("leftTriZone");
ZoneDesc rRightTriZD = new ProcTriangularShapedZoneGenerator(4)
.getZone("rightTriZone");
PersistenceUtils.storeZoneDesc(rCorridorZD);
PersistenceUtils.storeZoneDesc(rLeftTriZD);
PersistenceUtils.storeZoneDesc(rRightTriZD);
[...]
}
After saving the three zones to JSON files, I can edit them and set the `id
`, `destZoneId
` and `destPlatformId
` values for the platforms that must act as bridges.
I can use the scene view as a guide, clicking on a platforms and getting its hex coordinates from the label of the selected node in the scene hierarchy.
Here's the edited `corridorZone.json
`, with the data for platform A and B (extracted from the list at the end of day 2023-10-16) filled in.
{
"sId": "corridorZone",
"rPlatforms": [
{
"hexCoords": {
"vCubeCoords": {
"x": 0,
"y": 0,
"z": 0
}
},
"id": "start"
},
{
"hexCoords": {
"vCubeCoords": {
"x": 1,
"y": 0,
"z": -1
}
}
},
{
"hexCoords": {
"vCubeCoords": {
"x": 2,
"y": -1,
"z": -1
}
}
},
{
"hexCoords": {
"vCubeCoords": {
"x": 3,
"y": -1,
"z": -2
}
}
},
{
"hexCoords": {
"vCubeCoords": {
"x": 4,
"y": -2,
"z": -2
}
},
"id": "exit",
"destZoneId": "leftTriZone",
"destPlatformId": "left_mid"
}
]
}
I similarly edited `leftTriZone.json
` and `rightTriZone.json
`, setting up three bridges platform for both, as previously planned.
Now, I can stop calling `setupTestWorld()
` and modify the initialization in `initStartupZone()
`:
//ZoneDesc startupZone = PersistenceUtils.fetch<ZoneDesc>("prZoneHexMod.json");
ZoneDesc startupZone = PersistenceUtils.fetchZoneDesc("corridorZone");
Of course, with this setup only `corridorZone
` will be instantiated.
But soon, we're going to instantiate any valid target zone when needed.
Before proceeding, especially considering that I'm hand editing the JSON files with no specific level editor, it makes sense to implement a validation method that checks that every zone loaded from JSON features valid bridge platform definitions.
To make our life easier, we're going to load all the JSON files at start-up and, validate them at once.
Done that, we'll know that we have a valid set of zone descriptions in memory, to be accessed according to our needs during gameplay
Of course, this is just a "development mode" check that won't be needed in a release build.
The validation logic could still be useful in case I decide to distribute a level editor and want to allow players the loading of custom levels (which honestly, is something I'd really like to do).
So, what do we need to check?
zone identifiers must be unique
it's not required (even if it comes natural) that the basename of a JSON files matches the `
m_sId
` of a `ZoneDesc
`, so we need to check that there are no multiple JSON files containing zone descriptions with the same `m_sId
`
in each zone, platform identifiers must be unique
it's fine to have platforms with the same identifier in different zones, but in the same zone no two platforms must specify the same `
m_sId
`
a valid bridge platform definition must
specify both `
sDestZoneId
` and `sDestPlatformId
` - if only one of the two values is specified, the definition makes no sensepoint to a platform in a different zone: `
sDestZoneId
` can't be the id of the zone hosting the bridge platform itselfpoint to an existing and valid `
sDestZoneId
` and `sDestPlatformId
`: there must be a valid `ZoneDesc
` with id `sDestZoneId
`, and it must contain the platform with id `sDestPlatformid
`
Let's see the implementation of this logic, done in the simplest way I can think of:
public static Dictionary<string, ZoneDesc> getZoneDescs() {
Dictionary<string, ZoneDesc> ret =
new Dictionary<string, ZoneDesc>();
foreach (string s in Directory.EnumerateFiles(sZONEDESCS_PATH)) {
string sFileName = new FileInfo(s).Name;
string sZoneId = Path.GetFileNameWithoutExtension(sFileName);
ZoneDesc zd = fetchZoneDesc(sZoneId);
if (ret.ContainsKey(zd.m_sId)) {
Debug.LogWarning("skipping loading of ZoneDesc in file "
+ s
+ " - duplicate identifier: "
+ zd.m_sId
);
} else {
ret.Add(zd.m_sId, zd);
}
}
// first pass: fetch all the platform ids from each zone
Dictionary<string, HashSet<string>> rZoneIdToPlatformIds =
new Dictionary<string, HashSet<string>>();
foreach(var kv in ret) {
string sZoneId = kv.Key;
HashSet<string> rPlatformsWithId = new HashSet<string>();
rZoneIdToPlatformIds.Add(sZoneId, rPlatformsWithId);
foreach(PlatformDesc pd in kv.Value.getPlaformDecs()) {
if (pd.m_sId != null) {
if (rPlatformsWithId.Contains(pd.m_sId)) {
Debug.LogError("ZoneDesc " + sZoneId
+ " contains multiple platforms with the"
+ " same identifier: "
+ pd.m_sId
);
} else {
rPlatformsWithId.Add(pd.m_sId);
}
}
}
}
// second pass: check that the bridge definitions are valid
foreach (var kv in ret) {
foreach (PlatformDesc pd in kv.Value.getPlaformDecs()) {
bool bHasDestZoneId = pd.m_sDestZoneId != null;
bool bHasDestPlatformId = pd.m_sDestPlatformId != null;
// both null, it's not a bridge platform
if (!bHasDestZoneId && !bHasDestPlatformId) continue;
// if one is null and not the other, broken def
bool bIsInvalid = bHasDestZoneId != bHasDestPlatformId;
if (bIsInvalid) {
Debug.LogError("ZoneDesc " + kv.Key
+ " contains invalid bridge platform definition - "
+ " destZoneId: " + pd.m_sDestZoneId
+ " destPlatformId: " + pd.m_sDestPlatformId
);
continue;
}
// here we know we have both values,
// now we gotta check they point to a valid target
// source and target zones must be different
if (pd.m_sDestZoneId == kv.Key) {
Debug.LogError("ZoneDesc " + kv.Key
+ " contains invalid bridge platform definition - "
+ " platform " + pd.ToString()
+ " points to the zone itself!"
);
continue;
}
// the target zone must be defined
HashSet<string> rTargetZonePlatforms;
if (!rZoneIdToPlatformIds.TryGetValue(
pd.m_sDestZoneId, out rTargetZonePlatforms)) {
Debug.LogError("ZoneDesc " + kv.Key
+ " contains invalid bridge platform definition - "
+ " platform " + pd.ToString()
+ " points to unknown zone " + pd.m_sDestZoneId
);
continue;
}
// the target zone must contain the specified platform
if (!rTargetZonePlatforms.Contains(pd.m_sDestPlatformId)) {
Debug.LogError("ZoneDesc " + kv.Key
+ " contains invalid bridge platform definition - "
+ " platform " + pd.ToString()
+ " points to unknown platform "
+ pd.m_sDestPlatformId
+ " in zone " + pd.m_sDestZoneId
);
continue;
}
// all good if we got here!
}
}
return ret;
}
I was a good boy and added some comments and decent error messages for you readers, but if you strip those out, it's very few lines of code that exploit the convenience of `Dictionary
` and `HashSet
`.
After this new addition, I can change `initStartupZone()
` in `ZoneManagerBhv
` again:
//[...]
private Dictionary<string, ZoneDesc> m_rZoneDescs;
//[...]
private void initStartupZone() {
m_rZoneDescs = PersistenceUtils.getZoneDescs();
ZoneDesc startupZone = m_rZoneDescs["corridorZone"];
//[...]
}
After the "once and for all" loading of JSON data done by `PersistenceUtils.getZoneDescs()
` , which also validates the set of zones, I can just fetch each `ZoneDesc
` by id from `m_rZoneDescs
`.
2023-10-19 - Platform and Zone refactoring
I want to have a clear separation between the minimal level data that gets saved and loaded, and the runtime data, which can be augmented for our convenience.
We already started doing this separation with `ZoneDesc
` and `Zone
`.
To follow along this way, we can do something similar with `PlatformDesc
` and a new `Platform
` data type.
Currently, in `Zone
`, we stored in a `Dictionary
` (using the coordinates as keys) references to the `MonoBehaviour
` scripts attached to the platform game objects that we instantiated. See the end of Week 1 for details.
private Dictionary<Hex, PlatformBhv> m_rPlatformsMap =
new Dictionary<Hex, PlatformBhv>();
It's tempting to stuff the `PlatformDesc
` data into `PlatformBhv
`, but I prefer to add another data type to keep the logic and visualization well separated. `PlatformBhv
` should only deal with the components attached to its game object (currently, we use it to change the `Material
` of the platform `MeshRenderer
`, so that we can have platforms of three different shades of grey).
So, let's add this level of indirection, creating a `Platform
` class moving there part of the `Zone.Instantiate
` we used so far.
As the zone and platform identifier specifying a bridge destination will always go together, let's put them into a `ZoneBridgeDest
` struct. One might argue that it would be good to add this type directly to the `ZoneDesc
` structure that gets saved to JSON, but keeping the persistent data structure simple also has its advantages. Anyway, we might revisit this at some point, but not today.
using UnityEngine;
using BinaryCharm.ParticularReality.Behaviours;
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public readonly struct ZoneBridgeDest {
public readonly string m_sDestZoneId;
public readonly string m_sDestPlatformId;
public ZoneBridgeDest(string sDestZoneId, string sDestPlatformId) {
m_sDestZoneId = sDestZoneId;
m_sDestPlatformId = sDestPlatformId;
}
}
public class Platform {
public readonly string m_sId;
public readonly ZoneBridgeDest? m_zoneBridgeDest;
public readonly PlatformBhv m_rBhv;
private Platform(
string sId,
ZoneBridgeDest? zoneBridgeDest,
PlatformBhv rBhv) {
m_sId = sId;
m_zoneBridgeDest = zoneBridgeDest;
m_rBhv = rBhv;
}
public static Platform Instantiate(
PlatformDesc platformDesc,
Transform rZoneParentTr,
GameObject rPlatformTemplateGO,
Layout rLayout,
Vector3 vOffset) {
// calculate the platform position
Vector2 vPos2D = rLayout.getHexPos(platformDesc.m_hexCoords);
Vector3 vLocalPos = new Vector3(vPos2D.x, 0f, vPos2D.y);
// instantiate and label a platform
GameObject rNewPlatformGO = GameObject.Instantiate(
rPlatformTemplateGO,
vOffset + vLocalPos,
rPlatformTemplateGO.transform.rotation,
rZoneParentTr
);
rNewPlatformGO.name = platformDesc.m_hexCoords.getLabel();
// adjust so that adjacent platforms are of different colors
PlatformBhv rNewPlatformBhv =
rNewPlatformGO.AddComponent<PlatformBhv>();
int iMatId = platformDesc.m_hexCoords.getColorId();
Material[] rMats = ResourcesManagerBhv.i().m_rPlatformMats;
rNewPlatformBhv.setMaterial(rMats[iMatId]);
bool bIsBridgePlatform =
platformDesc.m_sDestPlatformId != null
&& platformDesc.m_sDestZoneId != null;
ZoneBridgeDest? zbd = bIsBridgePlatform ?
new ZoneBridgeDest(
platformDesc.m_sDestZoneId,
platformDesc.m_sDestPlatformId
) :
null;
string sId = string.IsNullOrEmpty(platformDesc.m_sId) ?
null :
platformDesc.m_sId;
return new Platform(sId, zbd, rNewPlatformBhv);
}
}
}
Of course, I also had to edit `Zone` accordingly, so that it deals with `Platform` and not `PlatformBhv` directly, and that it makes use of `Platform.Instantiate` in `Zone.Instantiate`.
Here's some highlights of the changes.
//[...]
namespace BinaryCharm.ParticularReality.LevelManagement {
public class Zone {
private Dictionary<Hex, Platform> m_rPlatformsMap =
new Dictionary<Hex, Platform>();
//[...]
public static Zone Instantiate(
ZoneDesc zoneDesc,
ZonePlatformsSettings zps,
Vector3 vOffset,
Hex originCoords) {
//[...]
// here we'll store references to the platforms
Dictionary<Hex, Platform> rZonePlatformsMap =
new Dictionary<Hex, Platform>();
//[...]
// let's place the platforms
foreach (PlatformDesc pd in zoneDesc.getPlaformDecs()) {
Platform rPlatform = Platform.Instantiate(
pd,
rZoneParent.transform,
rPlatformTemplate,
rLayout,
vOffset - v3origPlatformOffset
);
rZonePlatformsMap.Add(pd.m_hexCoords, rPlatform);
}
//[...]
return rNewZone;
}
}
}
Tomorrow, with a little luck, we should get to load a target `Zone
` when needed.
2023-10-20 - Target zone loading
Recap: we have all the `ZoneDesc
` entries of our test world loaded at start-up, accessible from a `Dictionary
`, and we load `corridorZone
` as first zone for the player. We can access the "bridge destination" information of a `Zone
` in a sensible way.
To have working portals between bridge platforms, we will need to revisit `PlayerManagerBhv
` and `PortalManagerBhv
`, that I sketched in the first 16 days of prototyping.
For now, tough, I'm going to focus on the zones loading and unloading.
To decide if it needs to load/unload zones, `ZoneManagerBhv
` just needs to be aware of one thing: which platform the player is currently standing on.
So, for testing purpose, let's provide an alternative "platform selector" that we can easily move through the keyboard. When everything works, we're going to bind the zone loading/unloading to the actual player/portal subsystems.
Let's create a `MockPlayerManagerBhv
` that lets us move a red sphere on the platforms.
We're going to use the keypad, using the top six keys `7-8-9-4-5-6
`, each assigned to our six directions of the flat top hexagons setup.
using System;
using UnityEngine;
using BinaryCharm.Behaviours;
using BinaryCharm.ParticularReality.Behaviours;
using BinaryCharm.ParticularReality.HexGrids;
using BinaryCharm.ParticularReality.LevelManagement;
namespace BinaryCharm.ParticularReality.Mock {
public class MockPlayerManagerBhv : ManagerBhv<MockPlayerManagerBhv> {
private Transform m_rDebugSphereTr;
private Hex m_playerCoords;
private void Awake() {
registerInstance(this);
}
private void Start() {
GameObject rTestSphereGO =
GameObject.CreatePrimitive(PrimitiveType.Sphere);
m_rDebugSphereTr = rTestSphereGO.transform;
m_rDebugSphereTr.localScale = Vector3.one * 0.5f;
rTestSphereGO.GetComponentInChildren<Renderer>().material =
ResourcesManagerBhv.i().test_m_rDebugRedMat;
m_playerCoords =
ZoneManagerBhv.i().getCurrZone().getStartCoords();
}
private void Update() {
Action<Hex.EDir> movePlayerPlaceholder = (Hex.EDir dir) => {
Hex? hex = ZoneManagerBhv.i().getCurrZone()
.getAdjacentPlatformCoords(m_playerCoords, dir);
if (hex.HasValue) {
m_playerCoords = hex.Value;
}
};
if (Input.GetKeyDown(KeyCode.Keypad7)) {
movePlayerPlaceholder(Hex.EDir.NW);
} else if (Input.GetKeyDown(KeyCode.Keypad8)) {
movePlayerPlaceholder(Hex.EDir.N);
} else if (Input.GetKeyDown(KeyCode.Keypad9)) {
movePlayerPlaceholder(Hex.EDir.NE);
} else if (Input.GetKeyDown(KeyCode.Keypad4)) {
movePlayerPlaceholder(Hex.EDir.SW);
} else if (Input.GetKeyDown(KeyCode.Keypad5)) {
movePlayerPlaceholder(Hex.EDir.S);
} else if (Input.GetKeyDown(KeyCode.Keypad6)) {
movePlayerPlaceholder(Hex.EDir.SE);
}
m_rDebugSphereTr.position = ZoneManagerBhv.i()
.getCurrZone().getPlatformPosByCoords(m_playerCoords);
}
public Hex getCoords() {
return m_playerCoords;
}
}
}
Now, let's try to instantiate a target zone when the player steps on a bridge platform.
We must
check if the player is on a bridge platform
in that case, if the target zone has not been already instantiated, instantiate it
to instantiate it, we must calculate where to put it to keeps things tidy
This is test code that we're going to revisit soon, of course. I'm adding more comments I normally would for you readers.
private void updateZones() {
Hex currPlatformCoords = MockPlayerManagerBhv.i().getCoords();
Platform rCurrPlatform =
getCurrZone().getPlatformByCoords(currPlatformCoords);
ZoneBridgeDest? zbd = rCurrPlatform.m_zoneBridgeDest;
if (m_rAdjacentZone == null && zbd.HasValue) {
// for now, let's hardcode on which direction the zones are
// "bound" by the bridge platform (north east)
Hex bridgeDestVirtualCoords =
currPlatformCoords.getNeighbour(Hex.EDir.NE);
// calculate the "origin point" of the destination Zone
Vector3 currPlatformOffset =
getCurrZone().getOffsetByCoords(bridgeDestVirtualCoords);
// fetch destination zone data
ZoneDesc zd = m_rZoneDescs[zbd.Value.m_sDestZoneId];
// which platform of the destination Zone must go at the
// origin point (0 relative to the zone parent node)?
Hex destPlatformCoords =
zd.getPlatformCoordsById(zbd.Value.m_sDestPlatformId);
// instantiate adjacent zone at the calculated offset
m_rAdjacentZone = Zone.Instantiate(
zd,
s_ZONE_PLATFORMS_SETTINGS,
currPlatformOffset,
destPlatformCoords
);
}
}
Does it work? Yep!
Well, good enough for a first test and for this week.
But let's write a TODO list to make it easier to come back to the project on Monday:
allow our "mock" player to move between zones
stop hardcoding the start platform coordinates to 0, 0, 0
unload the previous zone when appropriate
render zones to different layers (needed for portal visualization)
reuse platforms instead of instantiating/destroying them (use a pool)
improve architecture (even better logic/visualization separation)