2023-10-02 - A bit of planning
Here we ago again!
After a (quite long) forced break, I'm starting today the second phase of development.
I'm going to label this new phase "Advanced Prototyping" and it's basically going to be about cleaning up and expanding the first "16 days PoC".
Basically, I plan to refactor the prototype to bring it from "game jam" to "serious project" quality, at least in terms of code architecture, and along the way I will also remove some restrictions I accepted in the PoC because of the time constraints.
My first goal is going to be allowing arbitrary level design.
Currently, I have a procedurally generated hexagon grid, itself of hexagonal shape. I can easily change the size, so that I can have a bigger or smaller grid, but that's it.
Instead, I want to have arbitrary configurations of platforms (but keeping the same "empty spaces between platforms" restriction).
Let's call a configuration of hexagonal platforms a Zone.
First, we're going to implement a system to display a Zone placing the platforms according to a ZoneDesc (a "pure data" description of a platform layout). Then, we're going to introduce a special kind of platform that can link together two zones.
Opening the portal on a normal platform allows you to step onto an adjacent platform. Doing the same on a "special" platform, instead, will let you move to a different zone.
This should be able to scale arbitrarily.
When stepping on a "special" platform (or even when at a certain distance from it), we're going to load in background the target zone.
When the player is going to open the portal, such zone will be there, available to step into. And that can work both ways, in case one wants to go back to the "previous" zone.
So we just have to make sure that each zone has reasonable size, so that any two interlinked zones can be properly loaded and rendered together, and here you go, we have an unbounded game world with, if we do things right, no visible loading.
So, let's think about potential elements involved in all this, without trying to be 100% accurate:
ZoneDesc
a logical description of a zone, indicating where the platforms of the Zone are going to be placed
Zone
a loaded zone, where the player can move and interact
ZoneManager
a system that handles the loading/unloading of zones, so that the player can seamlessly move between them
That sounds reasonable. One last important detail could be that a ZoneDesc could transparently be procedurally generated or statically defined through a level editor.
Actually, it makes total sense to me that the current "hexagonally shaped" procedural grid becomes part of this system, so that we can still have that, but it's just one of the available options.
That's enough planning for the way I like to work. Time to get some coding done.
I shuffled a bit of code around, moving already existing methods splitting already existing functionalities to a more appropriate place.
Then, I added a new namespace `BinaryCharm.ParticularReality.LevelManagement` and sketched a few classes/structs into it:
Basically:
PlatformDesc
andZoneDesc
, as emphasized by theDesc
suffix, will be passive, "data" classes, suitable to be easily serialized and deserialized.
Zone
is a class representing a loadedZoneDesc
, in the sense that it will allow us to access the actual scene elements that are going to be instantiated and positioned as indicated by aZoneDesc
.ZoneManagerBhv
is a manager class that will take care of loading/unloading zones as needed, being able to turn aZoneDesc
into an actual, operatingZone
.IZoneGenerator
is a simple interface that will be implemented by classes providing aZone
, be it procedurally generated with an algorithm, statically defined through code, based on file containing its description, etcProcHexagonallyShapedZoneGenerator
is going to be the first class implementingIZoneGenerator
, and it will basically wrap the logic that, currently, does the platforms setup on startup.
At the end of this refactoring step, we're going to have no visible change to the current behaviour, but right after that, we're going to change the layout with other implementations of IZoneGenerator
.
Of course, I might have overlooked things, but we're going to find out along the way.
And that's it for today.
2023-10-03 - Taking hexagonal grids seriously
My first implementation of the hexagonal grid was done in a very hacky way.
Basically, I just checked the calculation of the offset to be applied from one "row" of hexagons to the next, so that they would be placed into a honeycomb pattern.
Then, I modified the loop placing the platforms so that it would skip some platforms, leaving only those I wanted. I saved the row/col coordinates of such "alive" platforms, and added a bit of code to query the grid to check if there was a nearby platform along a certain direction.
Now, instead, I'm going to do things properly.
This means, for starters, doing a bit of research.
There was an amazing resource in my gamedev bookmarks, and it is the page about Hexagonal Grids by Red Blob Games.
It's basically the bible of hexagonal grids, complete with implementation guides.
I could use one of the provided libraries, but I won't.
Why? It's going to probably take less time to do my own implementation that to check all the linked C#/Unity implementation and decide which one to pick.
Additionally, I generally prefer to limit external dependencies if possible.
And, at least for now, we only need a small subset of the features provided by these libraries.
Moreover, if I end up changing engine, I would then also need change library if switching to a C++ or Rust based engine, and consequently changing the code which calls into the libraries. Doing a minimal implementation of my own, using a simple and clean style, could make it for an easier port.
After skimming through the aforementioned page, I started taking some decisions and writing a bit of code.
Of course, we'll change later if needed, but for today, I decided to use for the project cube coordinates and hexagons with flat-top orientation.
Please check the Red Blob Games page for details, but essentially, this means:
We define as `
size
` of a hexagon the radius of the outer circle, touching the cornersthe `
width
` of the hexagon is `2 * size
`the `
height
` of the hexagon is `sqrt(3) * size
`
Tiling hexagons in honeycomb pattern, between adjacent hexagons
the `
horiz
` distance is `0.75 * width
`the `
vert
` distance is `height
`
We assign three `
q/r/s
` integer coordinates to each hexagon in the grid.
These "cubic coordinates":keep for every hexagon the invariant `
q + r + s = 0
`change according to the following scheme, considering that every direction on the hex grid lies between two of the these directions:
For example
if we are at coordinates `
(q=0, r=0, s=0)
` , and we want the coordinates of our north neighbour, we go between `++s
` and `--r
`, so we increment `s
` and decrement `r
` and find ourselves at `(q=0, r=-1, s=1)`if we are at `
(q=0, r=-1, s=1)
` and we step south east, going between `++q
` and `--s
`, we get to `(q=1, r=-1, s=0)
`
Notice that taking one step in the grid always increments one value and decrements another, so the invariant is kept.
It sounds a bit strange coming from the row/column reasoning, but it's not complicated.
I encourage to try the interactive visualizations on the Red Blob Games page to better understand what's going on.
I'm going to do a super-simple implementation, using a `Vector3Int` as backing store of the `q/r/s
` coordinates.
Why not simply do a struct with three `int
` fields?
As explained in the linked page, cube coordinates are 3d cartesian coordinates, so we can easily add/subtract and multiply/divide by a scalar reusing the `Vector3Int
` operations (that hopefully feature an optimized implementation) and not implement them ourselves.
The same applies to the `GetHashCode()
` method we're going to need to use a `Hex
` struct as key of a `Dictionary
`.
using System.Runtime.CompilerServices;
using UnityEngine;
namespace BinaryCharm.ParticularReality.HexGrids {
public struct Hex {
public readonly Vector3Int m_vCubeCoords;
public Hex(Vector3Int vCubeCoords) {
m_vCubeCoords = vCubeCoords;
}
public Hex(int q, int r, int s)
: this(new Vector3Int(q, r, s)) {
}
public Hex(int q, int r)
: this (new Vector3Int(q, r, -q -r)) {
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int q() { return m_vCubeCoords.x; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int r() { return m_vCubeCoords.y; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int s() { return m_vCubeCoords.z; }
// order matters (see rHEX_DIRECTIONS)
public enum EDir {
SE,
NE,
N,
NW,
SW,
S
};
private static readonly Vector3Int[] rHEX_DIRECTIONS =
new Vector3Int[6] {
new Vector3Int( 1, 0, -1), //SE
new Vector3Int( 1, -1, 0), //NE
new Vector3Int( 0, -1, 1), //N
new Vector3Int(-1, 0, 1), //NW
new Vector3Int(-1, 1, 0), //SW
new Vector3Int( 0, 1, -1) //S
};
public Hex getNeighbour(EDir eDir) {
return new Hex(m_vCubeCoords + rHEX_DIRECTIONS[(int)eDir]);
}
}
}
Note that we could easily change the backing store to a `Vector2Int
`, storing only `q
` and `r
`, and calculating `s
` as `-q -r
`.
At some point we might very well do that (packing more stuff in less memory is always good), but right now I don't think it would not be a good idea: being able to inspect the `s
` value through the debugger might prove much more valuable.
2023-10-04 - Hex layout and orientation
We need to complete the implementation of the hex grid following the indications of the Red Blob Games page.
Basically, what we need is a way to calculate where to put the actual hexagonal platform game objects in the scene.
To be able to do this, for starters, we port the `Orientation
` and `Layout
` structs to our implementation. L
using UnityEngine;
namespace BinaryCharm.ParticularReality.HexGrids {
public struct Orientation {
public readonly float m_fF0, m_fF1, m_fF2, m_fF3;
public readonly float m_fB0, m_fB1, m_fB2, m_fB3;
public readonly float m_fStartAngle; // in multiples of 60°
private Orientation(
float fF0, float fF1, float fF2, float fF3,
float fB0, float fB1, float fB2, float fB3,
float fStartAngle) {
m_fF0 = fF0;
m_fF1 = fF1;
m_fF2 = fF2;
m_fF3 = fF3;
m_fB0 = fB0;
m_fB2 = fB2;
m_fB1 = fB1;
m_fB3 = fB3;
m_fStartAngle = fStartAngle;
}
public static Orientation FLAT_TOP = new Orientation(
3f / 2f, 0f, Mathf.Sqrt(3f) / 2f, Mathf.Sqrt(3f),
2f / 3f, 0f, -1f / 3f, Mathf.Sqrt(3f) / 3f,
0f
);
public static Orientation POINTY_TOP = new Orientation(
Mathf.Sqrt(3f), Mathf.Sqrt(3f) / 2f, 0f, 3f / 2f,
Mathf.Sqrt(3f) / 3f, -1f / 3f, 0f, 2f / 3f,
0.5f
);
}
}
Note that I used a `private
` constructor for `Orientation
`: this way, I restrict its use to the definition of the two static instances for the "flat top" and "pointy top" cases (publicly available).
using UnityEngine;
namespace BinaryCharm.ParticularReality.HexGrids {
public struct Layout {
public readonly Orientation m_orientation;
public readonly Vector2 m_vSize;
public readonly Vector2 m_vOrigin;
public Layout(Orientation orientation, Vector2 size, Vector2 origin) {
m_orientation = orientation;
m_vSize = size;
m_vOrigin = origin;
}
public Vector2 getHexPos(Hex h) {
float x = (
m_orientation.m_fF0 * h.q() + m_orientation.m_fF1 * h.r()
) * m_vSize.x;
float y = (
m_orientation.m_fF2 * h.q() + m_orientation.m_fF3 * h.r()
) * m_vSize.y;
return new Vector2(x + m_vOrigin.x, y + m_vOrigin.y);
}
}
}
I ported almost blindly the code, just changing the `double
` values to `float
` precision, which should be more than enough in our case. I also used the Unity `Vector2
` instead of the original `Point
` struct, of course.
We already have it at our disposal, and anyway using a `Point
` type for a size would feel a bit dirty to me. It's still two float values, but in my opinion a `Vector2
` is a more "neutral" type, while a `Point
` brings with itself a specific semantic, which would make sense for `m_vOrigin
` but not for `m_vSize
`.
For now, we put `Hex
`, `Orientation
` and `Layout
` into a `BinaryCharm.ParticularReality.HexGrids
` namespace.
There's nothing specific to the game itself, so it would make sense to move this code out of the `ParticularReality
` namespace (like, `BinaryCharm.HexGrids
`) and make a unity package from it.
But honestly, it's very little code, and maybe I won't ever need it again, so I won't bother doing any of this right know.
The important thing are knowing that I could do it when/if needed, and confining the code into a namespace (doesn't matter too much which namespace). I make heavy use of namespaces, which are critical to
keep names short an sensible, but avoiding name clashes
make the dependencies between subsystems obvious (thanks to the `
using
` directives)
2023-10-05 - Hex maps
The last thing we need to port from the Red Blob Games guide are the algorithms to create various shapes of hexagon maps.
using System.Collections.Generic;
using UnityEngine;
namespace BinaryCharm.ParticularReality.HexGrids {
public static class Map {
public static HashSet<Hex> getParallelogram(
int iQ1, int iQ2,
int iR1, int iR2) {
HashSet<Hex> rMap = new HashSet<Hex>();
for (int q = iQ1; q <= iQ2; q++) {
for (int r = iR1; r <= iR2; r++) {
rMap.Add(new Hex(q, r, -q - r));
}
}
return rMap;
}
public static HashSet<Hex> getTriangle(int iSize) {
HashSet<Hex> rMap = new HashSet<Hex>();
for (int q = 0; q <= iSize; q++) {
for (int r = 0; r <= iSize - q; r++) {
rMap.Add(new Hex(q, r, -q - r));
}
}
return rMap;
}
public static HashSet<Hex> getTriangle_flipped(int iSize) {
HashSet<Hex> rMap = new HashSet<Hex>();
for (int q = 0; q <= iSize; q++) {
for (int r = iSize - q; r <= iSize; r++) {
rMap.Add(new Hex(q, r, -q - r));
}
}
return rMap;
}
public static HashSet<Hex> getHexagon(int iSize) {
HashSet<Hex> rMap = new HashSet<Hex>();
for (int q = -iSize; q <= iSize; q++) {
int r1 = Mathf.Max(-iSize, -q - iSize);
int r2 = Mathf.Min(iSize, -q + iSize);
for (int r = r1; r <= r2; r++) {
rMap.Add(new Hex(q, r, -q - r));
}
}
return rMap;
}
public static HashSet<Hex> getRectangle_pointyTop(
int iLeft,
int iRight,
int iTop,
int iBottom) {
HashSet<Hex> rMap = new HashSet<Hex>();
for (int r = iTop; r <= iBottom; r++) {
int r_offset = Mathf.FloorToInt(r / 2f);
for (int q = iLeft - r_offset; q <= iRight - r_offset; q++) {
rMap.Add(new Hex(q, r, -q - r));
}
}
return rMap;
}
public static HashSet<Hex> getRectangle_flatTop(
int iLeft,
int iRight,
int iTop,
int iBottom) {
HashSet<Hex> rMap = new HashSet<Hex>();
for (int q = iLeft; q <= iRight; q++) {
int q_offset = Mathf.FloorToInt(q / 2f);
for (int r = iTop - q_offset; r <= iBottom - q_offset; r++) {
rMap.Add(new Hex(q, r, -q - r));
}
}
return rMap;
}
}
}
I did a very basic port of the original code snippets, wrapped in static methods.
Note that, except for the parameter names, I somewhat ignored my coding conventions: as I was just porting some snippets, using the same variable names chosen in the original code feels fine.
In terms of API design, I'm not crazy about `getTriangle_flipped
` and of the need to have two `getRectangle
` methods, each suitable for one of the two flat/pointy top orientation.
But I can definitely live with that, at least until we figure out what are the actual needs of the game.
When there's no architecture that is obviously right, I prefer to just do something simple and keep going with the project: there's always time to refactor later, when we have a better understanding of the problem/requirements. The alternative is spending lot of time trying to "predict the future", which often ends up being a waste of time.
In this case, for example, we might pass in an `Orientation
` to a single `getRectangle
` method and do the appropriate calculation on the basis of the parameter. However, if we end up always using the same `Orientation
` in the project, we could get rid of the other method and avoid the parameter. But if we do that, we lose the generality of the `HexGrids
` namespace, making it less reusable.
So, there's no obvious perfect answer, and spending too much thinking about it doesn't feel right, especially because we might end up not using this kind of procedural generation at all.
And now, it's time to actually use the work we did to improve the scene initialization
2023-10-06 - Better scene setup
Today I'm going to start changing the prototype so that the spawning and setup of the hexagonal platforms in the Unity scene is well isolated from the data that defines its layout.
As we discussed in the initial planning, the idea is having a `ZoneManager
` that loads a `ZoneDesc
`, turning it into a loaded `Zone
` which actually handles nodes and behaviours in the active Unity scene.
At this point, we're not dealing with generic hexagonal grids, but with the `LevelManagement
` of the game, that will make use of such grids (and of other things).
So, we are now going to work in the `BinaryCharm.ParticularReality.LevelManagement
` namespace.
We sketched `PlatformDesc
` and `ZoneDesc
` during the planning, but now we're ready to actually implement them.
We are going to wrap a `Hex
` struct into a `PlatformDesc
`.
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public enum EPlatformType {
normal,
zoneConnector
};
public struct PlatformDesc {
public readonly Hex m_hexCoords;
public readonly EPlatformType m_ePlatformType;
public PlatformDesc(Hex hexCoords, EPlatformType ePlatformType) {
m_hexCoords = hexCoords;
m_ePlatformType = ePlatformType;
}
}
}
We're still defining a logical description that is going to be part of a `ZoneDesc
`, but we are not only considering the platform position in the grid.
For now, we are just going to set that `m_ePlatformType
` field, but all kinds of information related to the platform in the context of the level description could there. Does it contain a trigger? Is it the spawn point of a power up or of an enemy? Does it use a particular texture?
Of course it's not the only approach possible: one could define items separately and specify at which coordinate they are placed. But this should be the easier approach, especially considering the idea of doing a level editor (or using an already existing one, which is something I'm going to try).
We can now complete a first implementation of `ZoneDesc
` and of `ProcHexagonallyShapedZoneGenerator
`, in very few lines of code.
using System.Collections.Generic;
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public struct ZoneDesc {
private readonly PlatformDesc[] m_rPlatforms;
public ZoneDesc(PlatformDesc[] rPlatforms) {
m_rPlatforms = rPlatforms;
}
public IEnumerable<PlatformDesc> getPlaformDecs() {
return m_rPlatforms;
}
//TEMP - make a bunch of "normal" platform descriptions from a Hex set
public static ZoneDesc getZoneDescFromHexSet(HashSet<Hex> rSet) {
List<PlatformDesc> rPlatformsDescs = new List<PlatformDesc>();
foreach (Hex h in rSet) {
PlatformDesc pd = new PlatformDesc(h, EPlatformType.normal);
rPlatformsDescs.Add(pd);
}
return new ZoneDesc(rPlatformsDescs.ToArray());
}
}
}
using System.Collections.Generic;
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public class ProcHexagonallyShapedZoneGenerator : IZoneGenerator {
private int m_iSize;
public ProcHexagonallyShapedZoneGenerator(int iSize) {
m_iSize = iSize;
}
public ZoneDesc getZone() {
HashSet<Hex> rHexagonHexSet = Map.getHexagon(m_iSize);
return ZoneDesc.getZoneDescFromHexSet(rHexagonHexSet);
}
}
}
Basically, we're using the `Map.getHexagon
` method to get the hexagonally shaped grid, and for each hexagonal cell we are creating a basic platform description (that is, a `PlatformDesc
` with type set to `EPlatformType.normal
`).
So we did a first step and went from just the description of a hex grid, to a description of a zone composed by platforms (arranged following the grid).
The next step is going from a `ZoneDesc
` to an actually loaded `Zone
`.
That's a job for `ZoneManagerBhv
`, but first we need to define some details.
First, we need to specify somewhere the size of the actual platforms and their orientation.
Let's wrap these setting in a simple structure
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public struct ZonePlatformsSettings {
public readonly float m_fRadius;
public readonly float m_fThickness;
public readonly Orientation m_orientation;
public ZonePlatformsSettings(
float fRadius,
float fThickness,
Orientation orientation) {
m_fRadius = fRadius;
m_fThickness = fThickness;
m_orientation = orientation;
}
}
}
Then, we need a method to actually create the platforms game objects.
Here, things start to get a bit messy, because we need to live with the Unity APIs.
Things like the fact that, when one creates a new `GameObject
` instance in the code, a new node pops-up into the current scene.
We're going to implement an `Instantiate
` static method for our `Zone
` class, trying to fit the Unity style.
using System.Collections.Generic;
using UnityEngine;
using BinaryCharm.ParticularReality.Behaviours;
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public class Zone {
private Dictionary<Hex, PlatformBhv> m_rPlatformsMap =
new Dictionary<Hex, PlatformBhv>();
private Zone(Dictionary<Hex, PlatformBhv> rPlatformsMap) {
m_rPlatformsMap = rPlatformsMap;
}
public static Zone Instantiate(
ZoneDesc zoneDesc,
ZonePlatformsSettings zps,
Vector3 vOffset) {
// the three materials used to assign colors to the platforms
Material[] rMats = ResourcesManagerBhv.i().m_rPlatformMats;
// create a "template" platform with appropriate orientation
GameObject rPlatformTemplate = HexagonPlatformGenerator.Generate(
"tmp",
zps.m_fRadius,
zps.m_fThickness,
rMats[0]);
rPlatformTemplate.transform.localRotation = Quaternion.AngleAxis(
zps.m_orientation.m_fStartAngle * Mathf.Deg2Rad,
Vector3.up);
// platforms will be children of this node
GameObject rZoneParent = new GameObject("zoneParent");
rZoneParent.transform.position = vOffset;
// here we'll store references to the platforms behaviours
Dictionary<Hex, PlatformBhv> rZonePlatformsMap =
new Dictionary<Hex, PlatformBhv>();
// we want the platforms separated by gaps the same size of
// the platforms themselves, so we use double the radius
Layout rLayout = new Layout(
zps.m_orientation,
2f * new Vector2(zps.m_fRadius, zps.m_fRadius),
Vector2.zero
);
foreach (PlatformDesc pd in zoneDesc.getPlaformDecs()) {
// calculate the platform position
Vector2 vPos2D = rLayout.getHexPos(pd.m_hexCoords);
Vector3 vLocalPos = new Vector3(vPos2D.x, 0f, vPos2D.y);
// instantiate and label a platform
GameObject rNewPlatformGO = GameObject.Instantiate(
rPlatformTemplate,
vOffset + vLocalPos,
rPlatformTemplate.transform.rotation,
rZoneParent.transform
);
rNewPlatformGO.name = pd.m_hexCoords.getLabel();
// adjust so that adjacent platforms are of different colors
PlatformBhv rNewPlatformBhv =
rNewPlatformGO.AddComponent<PlatformBhv>();
int iMatId = pd.m_hexCoords.getColorId();
rNewPlatformBhv.setMaterial(rMats[iMatId]);
// store reference to the platform behaviour
rZonePlatformsMap.Add(pd.m_hexCoords, rNewPlatformBhv);
}
// we're done with the "template", let's get rid of it
GameObject.Destroy(rPlatformTemplate);
// that's it!
Zone rNewZone = new Zone(rZonePlatformsMap);
return rNewZone;
}
}
}
I commented the code much more heavily that I would normally do, so that comments also work as explanation for this article.
It's pretty basic stuff, we create a template hexagonal platform of the required radius/thickness, rotate it so that it can work with pointy-top or flat-top orientation, and then we use a loop to copy it many times. For each, copy, we do a basic setup: we adjust node name, position and color.
When we're done, we destroy the template.
Note that I added to the `Hex` struct these two simple methods:
public string getLabel() {
return "hex_" + q() + "_" + r() + "_" + s();
}
public int getColorId() {
return ((q() - r()) % 3 + 3) % 3;
}
We use these to label each node with the cube coordinates (useful while checking the scene in the editor) and to select which of the three colors is to be assigned to each platform to make sure that no two adjacent platforms are of the same color.
Of course, this "three colors" thing is just a temporary hack to easily tell apart the platforms when teleporting, as we previously discussed. We might move or eliminate these methods at some point, but for now it makes sense to put them in `Hex
`.
Finally, we need to actually call `Instantiate
` in the initialization of `ZoneManagerBhv
`.
using UnityEngine;
using BinaryCharm.Behaviours;
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public class ZoneManagerBhv : ManagerBhv<ZoneManagerBhv> {
private Zone m_rCurrentZone;
private Zone m_rAdjacentZone; // TODO
private const float fHEXP_RADIUS = 1.2f;
private const float fHEXP_THICKNESS = 0.1f;
private readonly Orientation HEXP_ORIENTATION = Orientation.FLAT;
private void Awake() {
registerInstance(this);
}
private void Start() {
IZoneGenerator rStartupZoneGenerator =
new ProcHexagonalShapedZoneGenerator(3);
ZoneDesc startupZone = rStartupZoneGenerator.getZone();
ZonePlatformsSettings zps = new ZonePlatformsSettings(
fHEXP_RADIUS,
fHEXP_THICKNESS,
HEXP_ORIENTATION
);
m_currentZone = Zone.Instantiate(startupZone, zps, Vector3.zero);
}
}
}
No surprises here: we generate an hexagonal zone with the `ProcHexagonalShapedZoneGenerator
`, using the same size of the "old" grid.
Then, we define the characteristics of the actual platforms that will populate the scene: we want platforms with radius 1.2 meters, 10 cm thick, with flat top orientation.
Finally we instantiate the platforms. Does it work?
Yep.
We still have "the other" platforms, of course, but in the image you can see the new set of platforms highlighted.
We can hide the old set of platform and check the scene view, from top. It's nice that we have the central platform, with cube coordinates 0, 0, 0, at the origin of the game world.
Controlling a point of reference of a Zone
(which I tried to anticipate adding the vOffset
parameter to Instantiate
) will be important in being able to load multiple zones together.
There's one last thing I feel like doing to double check what we did: try a quick edit to instantiate platform along another shape.
We add another implementation of `IZoneGenerator
`, creating triangular zones.
using System.Collections.Generic;
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public class ProcTriangularShapedZoneGenerator : IZoneGenerator {
public enum EOrientation {
primary,
flipped
};
private int m_iSize;
private EOrientation m_eOrientation;
public ProcTriangularShapedZoneGenerator(
int iSize,
EOrientation orientation = EOrientation.primary) {
m_iSize = iSize;
m_eOrientation = orientation;
}
public ZoneDesc getZone(string sId) {
HashSet<Hex> rSet = m_eOrientation == EOrientation.primary ?
Map.getTriangle(m_iSize) : Map.getTriangle_flipped(m_iSize);
return ZoneDesc.getZoneDescFromHexSet(sId, rSet);
}
}
}
Then we add these temporary test calls in the `Start
` method of `ZoneManagerBhv
`:
Zone.Instantiate(
new ProcTriangularShapedZoneGenerator(
4, ProcTriangularShapedZoneGenerator.EOrientation.primary
).getZone(),
zps,
new Vector3(10f, 0f, 0f)
);
Zone.Instantiate(
new ProcTriangularShapedZoneGenerator(
4, ProcTriangularShapedZoneGenerator.EOrientation.flipped
).getZone(),
zps,
new Vector3(-10f, 0f, 0f)
);
And voilà, here's the two test zones (after disabling the other stuff).
That's the end of our first week of part-time development.
I've not done much, but it feels pretty clean. I can anticipate quite well the next steps, and hopefully they will fit nicely the structure we have prepared.
This weekend I'll be busy, but on Monday I'll be back for week #2.
We need to bind the gameplay to the "new" platforms and get rid of the "old" ones.
Then, we're going to focus on the passage from one zone to another.