This time we look at
creating interesting boundaries and useful connectivity information between all
our city zones.
We want a city with interesting connectivity between zones |
Connections
It is as much the
interconnections as the spaces in an environment that make a place
interesting. Now the zones are laid out
we need to look at how they are connected.
As we have a number of limitations in how we implement this I'm going to
use a fairly basic connection model similar to one I have used before which was
based on having a single connection pathway between adjacent areas, like each
side of a room having at most one door.
The algorithm had the following rules (expressed in terms of rooms and
doors):
- For your starting 'room', specify a door on each side (as desired).
- Decide where to split based on door positions (i.e. don't split down a doorway).
- When you split an area into two, always add a door into the new wall.
- Recurse (go-to step 2) for both sides.
This produces a map
where all the four outer doors are guaranteed to be connected, and you can
reach every generated room.
Previous connectivity test, all rooms remain connected, various in-fill rooms. |
When is a door not a door?
For our city, the
door/wall terminology is going to be used to mean something slightly different:
- A door corresponds to a region of free travel between areas, be it pedestrian, or vehicular. This can mean either both sharing the same flat height level at the boundary, or that they both guarantee to match some underlying landscape shape at the boundary.
- A wall corresponds to a barrier between areas, this can take several forms and may or may not have some sort of minimum barrier height. This could be a wall, fence, steep drop, ditch, or just allow the back of a building to sit along that edge.
Barrier 'wall' (red) and open 'door' (green) segments along the side of an area. |
This has an
important effect on the splitting constraints; in that I want to allow
splitting the open part in two by a boundary.
This will introduce more interesting topologies and should create
greater connectivity than the 'single path to exit' algorithm above.
Splitting the area in two but with splitting of an opening allowed. |
This does come at a
cost though as the calculations needed to maintain connectivity are in-fact
more complicated. I decided to try and
track access back to the four starting sides more thoroughly this time so that
when splitting, we can work out if we have the option not to add a door, again increasing the connection graph
variety.
Also, remember, we
are splitting the city up into zones at the moment, these are fairly
high-level, and quite large. The blocked
boundaries aren't going to be massive long walls, but opportunities for the
next stages of subdivision to use them in different ways to partition the city
up into lots of interesting spaces.
Similarly, a long gap may not imply a continuous connection, only that
there be some form of connection along the length of each area. Different areas may decide to achieve this in
different ways, be-it steps, a ramp, a wall with an archway, and so-on.
No Structures
A quick aside about
structure support in Apparance. At the
moment there are only a handful of fundamental data types you can pass
around. It is feasible to support
arbitrary collections of these wrapped up into custom data types (structures)
that can be passed around together more easily using single connection points
and wires. This would make propagating
things like the access parameters and connectivity information much, much
easier and also allow us to modify and add to this information without the huge
amount of rewiring it would otherwise need.
Unfortunately, as it stands, I can stretch what I have and make a few
design compromises enough that proper structure support isn't enough of a
priority. Something I have mused over a
lot, definitely elegantly implementable, and definitely a future feature. For now though we will pack out information
into existing types.
To door or not to door?
Access
The presence of an
opening in an side is represented by two horizontal values, the position of the
left edge and the position of the right edge.
The absence of an opening is represented by
either it's left edge being past the right end of the side, or the right
edge bring past the left end of the side.
Opening specified by position of left and right ends. Fully blocked side specified by either both at the right end of the wall or both at the left end of the wall. |
To pass this
information around we'll pack it into a Vector3, one of the fundamental types
that is used to represent a 3D vector.
It has three floating point values X, Y, and Z of which we use X and Y
to be the left and right edges of the opening.
The Z value isn't used yet, but will be needed when we want to encode
some height information into the opening.
Packing of edge connectivity into a Vector3 |
Each area (or room)
consists of four sides, each potentially accessible via openings, and so we
need to track and pass around four of these Vector3 access values. As a convention, these are referred to as
West, East, South, and North (or their initial), and in that order (lower X
axis, upper X axis, lower Y axis, upper Y axis).
West, East, South, and North sides of an area. |
Connectivity
We also need to
track connectivity available via each side of an area as we sub-divide. Since we only need to track whether we can
reach each of the four outer sides when we only need four bool values, or four
bits of data. This is much simpler
information and rather than passing round a value for each side we can pack the
information needed into a single integer as a bit-field.
How we pack information about which side of our area can provide access to which side of the city into a 32 bit integer. A bit value of '1' means yes, we can get to it, and '0' means no we can't. |
We will need add a
few new bit-wise operators to support the packing and unpacking logic. These are bit shift, AND/OR, and maybe a bit
test operator too. We can then build
packing/unpacking procedures from these.
Connectivity packing and unpacking procedures |
NOTE: I've been playing with clearer labelling of some of the simpler operators. They can now have either large text, or characters from the Symbol font. This combined with blank IO names and title suppression produces some nice compact visual representations. You'll see some more below.
Implementation
We are now ready to
implement this new functionality on top of the work we did last time splitting our city into zones. To
start with I factored out the splitting process into own procedure, this helps
manage the complexity of the already fairly complicated procedure that decides whether
we should split and which way.
The newly factored out Split procedure in place |
Do the splits
New split point,
side calculation, and access logic are all in their own procedure now. To make the splitting logic simpler we
abstract away the orientation of the split by 'normalising' the incoming area
information and then 'de-normalising' it back afterwards. This allows us to implement the split logic
once and be able to use it for both horizontal and vertical splits. The four sides are termed Left, Right,
Bottom, and Top instead of the W.E.S.N. directions we use normally, to help
distinguish normalised from de-normalised.
The split operation. Mostly consisting of the logic to help us treat either orientation of split the same way. |
Where to split
The split itself is
broken into two parts, where to split, and then how to split.
First we need to
work out where across the area are we going to split. Since we are allowing splits across openings
this is technically only limited by the need to ensure we don't create any area
too small (min zone size design parameter).
Neat & tidy, tidy & neat
At this point it
would be nice to also limit the minimum feature size we create during a
split. Since an arbitrary split could be
any distance from an arbitrary opening edge then we can create very tiny
openings and short walls. It would be
good to reduce this effect and we can achieve it in two ways. One is to snap to some overall grid spacing,
and second to snap to the edges of the openings in the two walls we are
intersecting. The Split Point procedure handles all this for us.
Deciding on where to split the area and generating access information for either side of the split. |
When splitting an
area into two halves, the connectivity is going to be affected by whether there
is an opening in the new wall. To decide
on this we need to work out where each new area is already connected to and use
this as the potential new connectivity
across the new divide. By checking to
see if each side of the divide includes the opening from the two sides being
intersected and combining their connectivity with the connectivity of the
opposite wall we get a connectivity value from each side of the split.
How to split
Deciding on the
dimensions of an opening in the new wall is fairly straightforward. We do however need to check whether we need to force
an opening though to maintain connectivity between the city outer walls.
Side splitting and connectivity update. |
Results
This update was
implemented in two stages, first the access generation (wall/door) for each
area, then the connectivity calculation and sub-division side opening decision
making. Here is an example where you can
clearly see areas that end up disconnected from each other due to no
connectivity management.
Disconnected regions (highlighted) present before connectivity checks were implemented |
Once the
connectivity system was in place it all started working properly. Here is a selection of city zone layouts with
connectivity information diagnostics display enabled.
Examples of a fully connected city |
Next
Next we will start
breaking down each of our zones into city blocks. The process is similar to before, but each
district type may have different requirements as the layout of each should
differ to keep things interesting.
We will also starting thinking about where significant buildings are going to be placed since we will soon require a skyline silhouette to appear as we approach the lower detail
end of our city. Development is a process
of adding detail, top-down, exactly analogous to approaching the city gradually
from a distance. Think about what you
expect to see, and when.