Preface
This week features no direct in-game demo, instead this is the theory behind procedural generation of pathways for each houses driveway and front door paths. I totally understand if the theory is something you skip — but there are some pretty pictures along the way that you can scroll through and check out 🙂
Either way thank you for stopping in — and with this theory will come (I hope) a wicked awesome post next week. Thank you!
Procedural Generation of House Paths
I want to start implementing procedural generation of the environment. This will allow me to expand the objects used in a scalable way as well as increase the variability of the environment with random generation. So to start I built a python program to generate paths from two points, the garage door and the front door, to the road.
For a little added flair the script creates a GIF to show step by step how the paths are constructed, this also helps with debug.
# -*- coding: utf-8 -*-
"""
Created on Mon Dec 20 06:34:29 2021
@author: Travis Adsitt
"""
from PIL import Image, ImageDraw, ImageColor
import random, math
GLOBAL_EDGE_COUNT = 260
CUBE_EDGE_SIZE = 20
PATH_BUFFER = 5
ROAD_Y = 200
CUBES_PERLINE = GLOBAL_EDGE_COUNT / CUBE_EDGE_SIZE
def GetNewBufferedLocation(not_x=None, not_y=None):
"""
Parameters
----------
not_x : int, optional
Avoid this x value. The default is None.
not_y : int, optional
Avoid this y value. The default is None.
Returns
-------
x : int
A new random x-point within the buffered area.
y : int
A new random y-point within the buffered area.
"""
buffered_house_area = GLOBAL_EDGE_COUNT - CUBE_EDGE_SIZE
x = random.randint(CUBE_EDGE_SIZE, buffered_house_area)
if not_x:
while x == not_x:
x = random.randint(CUBE_EDGE_SIZE, buffered_house_area)
y = random.randint(CUBE_EDGE_SIZE, ROAD_Y)
if not_y:
while y == not_y:
y = random.randint(CUBE_EDGE_SIZE, buffered_house_area)
return (x,y)
def GetRoadBoundaryLine():
"""
Helper function to get the two points describing the roads boundary
Returns
-------
List[Tuple(int x, int y), Tuple(int x, int y)]
Left and right most point representing the road boundary line.
"""
P1 = (0,ROAD_Y)
P2 = (GLOBAL_EDGE_COUNT, ROAD_Y)
return [P1,P2]
def GetGaragePathPoints(prev_point):
"""
Helper function to yield points until the driveway meets the road
Parameters
----------
prev_point : Tuple(int x, int y)
The most recent garage path point.
Yields
------
Tuple(int x, int y)
The next point for the driveway.
"""
curr_point = prev_point
while curr_point[1] <= ROAD_Y:
curr_point = (curr_point[0], curr_point[1] + 1)
yield curr_point
def GetDistance(P1, P2):
"""
Helper function to get the distance between two points in non-diagonal way
Parameters
----------
P1 : Tuple(int x, int y)
Point one.
P2 : Tuple(int x, int y)
Point two.
Returns
-------
int
Distance between the two input points on a non-dagonal basis.
"""
x1 = P1[0]
y1 = P1[1]
x2 = P2[0]
y2 = P2[1]
xd = x1 - x2 if x1 > x2 else x2 - x1
yd = y1 - y2 if y1 > y2 else y2 - y1
return xd + yd# math.sqrt(xd ** 2 + yd ** 2)
def GetFrontDoorPathTarget(fd_point, garage_points):
"""
Helper function to get the target point for the front door path
Parameters
----------
fd_point : Tuple(int x, int y)
The point of the front doors location.
garage_points : List[Tuple(int x, int y)]
All of the points describing the driveway.
Returns
-------
Tuple(int x, int y)
The point to target for front pathway.
"""
# Get the closest road point and its distance
road_point = (fd_point[0], ROAD_Y)
road_point_distance = GetDistance(fd_point, road_point)
# Get the closest drive way point and set a start distance
garage_point = garage_points[0]
garage_point_distance = GetDistance(fd_point, garage_point)
# Iterate all driveway points to ensure there isn't one closer
for point in garage_points:
curr_point_dist = GetDistance(fd_point, point)
if curr_point_dist < garage_point_distance:
garage_point = point
garage_point_distance = curr_point_dist
if curr_point_dist > garage_point_distance:
break
# Return whichever point (driveway or road) is closer to be used as a target
return garage_point if garage_point_distance < road_point_distance else road_point
def GetFrontDoorPathPoints(prev_point, garage_points):
"""
Helper function to properly draw a path from the front door to the
drive way or the road whichever is closer
Parameters
----------
prev_point : Tuple(int x, int y)
Front door location.
garage_points : List[Tuple(int x, int y)]
List of points describing the driveways path.
Yields
------
curr_point : Tuple(int x, int y)
The next point in the front path.
"""
# Get our target, either the nearest driveway point or the road
target = GetFrontDoorPathTarget(prev_point, garage_points)
# Local variable for current point tracking
curr_point = prev_point
# Iterate the y direction before the x, no diagonals are allowed
while curr_point[1] != target[1]:
yield curr_point
curr_point = (curr_point[0], curr_point[1] + 1)
# Iterate the x direction to complete the path
while curr_point[0] != target[0]:
yield curr_point
delta = 1 if curr_point[0] < target[0] else -1
curr_point = (curr_point[0] + delta, curr_point[1])
def GetPaths(garage_door_point, front_door_point):
"""
Helper function to get the garage door and front door paths to the road.
Parameters
----------
garage_door_point : TYPE
DESCRIPTION.
front_door_point : TYPE
DESCRIPTION.
Yields
------
Tuple(int x, int y) || None
Next Driveway point.
Tuple(int x, int y) || None
Next Front Pathway point.
"""
curr_g_door_point = garage_door_point
curr_f_door_point = front_door_point
garage_points = []
front_points = []
# Initial path buffer of PATH_BUFFER length to ensure distance from doors
for i in range(PATH_BUFFER):
garage_points.append(curr_g_door_point)
front_points.append(curr_f_door_point)
curr_g_door_point = (curr_g_door_point[0], curr_g_door_point[1] + 1)
curr_f_door_point = (curr_f_door_point[0], curr_f_door_point[1] + 1)
yield (curr_g_door_point, curr_f_door_point)
# Finish writing points for the garage door to go to the road
for g in GetGaragePathPoints(curr_g_door_point):
garage_points.append(g)
yield (g, None)
# Finish writing points for the front door to go to the road or garage path
for f in GetFrontDoorPathPoints(curr_f_door_point, garage_points[5:]):
yield (None, f)
if __name__ == "__main__":
# Get random front door and garage locations
front_door = GetNewBufferedLocation()
garage_door = GetNewBufferedLocation(not_x=front_door[0])
# A place to store all our images
images = []
# A place to keep track of all points to be drawn on each frame
r_points = []
g_points = []
for point in GetPaths(garage_door,front_door):
# Create a new image for current frame
out = Image.new("RGBA", (GLOBAL_EDGE_COUNT,GLOBAL_EDGE_COUNT))
d = ImageDraw.Draw(out)
# Draw Road Boundary
d.line(GetRoadBoundaryLine(), fill="blue")
# Append garage and front path points to respective lists
if point[0]:
r_points.append(point[0])
if point[1]:
g_points.append(point[1])
d.point(r_points,fill="red")
d.point(g_points,fill="green")
images.append(out)
# Save our GIF
images[0].save(
'house_paths.gif',
save_all=True,
append_images=images[1:],
optimize=True,
duration=100,
loop=0
)
Some examples of the output (Green == Front Door Path :: Red == Driveway):
So it is clear this is generating unrealistic front door and garage locations — but luckily at this stage I don’t really care about realism, I just want to know the algorithm is working. This demonstrates that it is, now we need to expand this to represent points a bit differently. In a 2D space we can have objects that cover multiple pixels, and so the first solution I propose is to represent each game object by its upper left point and its lower right point. From these two points we should be able to infer a whole lot of information about the object itself, and it should give us everything we need to properly position it in the world.
2-Point Object Representation
Assumptions
To start lets get some assumptions out there.
- All objects we care to place are rectangular
- All 3D rectangular objects can be measured by two corners
- one at the lowest z position in any corner
- the other at the highest z position in the opposite diagonal corner from the first
High Level
At a high level these two points can tell us everything we need to know about the path or driveway block we are trying to place. Specifically its rotation in the world, how it aligns or doesn’t align with other objects in the world, also should the block be to large in any one direction we should be able to work out the amount it needs to be scaled by to fit the spot. For the Unity translation this will mean attaching two empties to the parent object we can search for when generating the world. If anyone has a better way of doing this please please comment, I have been looking for an alternative including bounding boxes of the Renderer but those weren’t as reliable as the empty placement is.
So with that lets get to coding a 2D representation of what we want using Python, and hopefully after this we can move the logic to Unity and begin adding more houses without having to manually place paths!
Code
# -*- coding: utf-8 -*-
"""
Created on Tue Dec 21 09:14:20 2021
@author: Travis Adsitt
"""
from enum import Enum
from PIL import Image, ImageDraw, ImageColor
import random
WORLD_EDGE_SIZE = 250
CUBE_EDGE_SIZE = 10
ROAD_Y = 200
class CompassDirection(Enum):
North="North"
East="East"
South="South"
West="West"
class Point:
def __init__(self, x, y):
"""
Object initializer for a 2D Point
Parameters
----------
x : int
The x value of the point.
y : int
The y value of the point.
Returns
-------
None.
"""
self.x = x
self.y = y
@classmethod
def is_point(cls, P1):
"""
Used to check if an object is of type point
Parameters
----------
P1 : Object
Object to test if it is of type Point.
Returns
-------
Boolean
"""
return isinstance(P1, Point)
@classmethod
def distance(cls, P1, P2, keep_orientation=False):
"""
Get the distance between two points
Parameters
----------
P1 : Point
First point.
P2 : Point
Second point.
Returns
-------
xd : int
Distance between the points in the x direction.
yd : int
Distance between the points in the y direction.
"""
assert Point.is_point(P1), "P1 must be a point for distance"
assert Point.is_point(P2), "P2 must be a point for distance"
x1 = P1.x
y1 = P1.y
x2 = P2.x
y2 = P2.y
xd = x1 - x2 if x1 > x2 and not keep_orientation else x2 - x1
yd = y1 - y2 if y1 > y2 and not keep_orientation else y2 - y1
return (xd,yd)
@classmethod
def midpoint(cls, P1, P2):
"""
Get the midpoint between two points
Parameters
----------
P1 : Point
First point.
P2 : Point
Second point.
Returns
-------
Point
The midpoint located at the center of the two points.
"""
dist_x, dist_y = cls.distance(P1, P2)
x_offset = P1.x if P1.x < P2.x else P2.x
y_offset = P1.y if P1.y < P2.y else P2.y
return Point((dist_x / 2) + x_offset, (dist_y / 2) + y_offset)
def as_list(self):
"""
Helper function to put this point in a list
Returns
-------
list
[x, y] values.
"""
return [self.x, self.y]
def __add__(self, other):
"""
Dunder method to add two points together
Parameters
----------
other : Point
The point to add to this one.
Returns
-------
ret_point : Point
The point that results from the addition.
"""
assert Point.is_point(other), "Cannot add a Point to a non-Point"
ret_point = Point(self.x + other.x, self.y + other.y)
return ret_point
def __str__(self):
return f"({self.x},{self.y})"
ORIGIN = Point(0,0)
class Object2D:
def __init__(self, P1, P2, fill_color="gray", outline_color="red"):
"""
Object initializer for a 2D game object
Parameters
----------
P1 : Point
Corner one of the 2D object.
P2 : Point
Corner two of the 2D object.
Returns
-------
None.
"""
assert Point.is_point(P1), "P1 must be a point for 2D Object"
assert Point.is_point(P2), "P2 must be a point for 2D Object"
self.points = {"p1": P1, "p2": P2}
self.fill = fill_color
self.outline = outline_color
def move_xy(self, move_to, reference_point=None):
"""
Function to move this object using a reference point
Parameters
----------
move_to : Point
Where to move this object.
reference_point : Point, optional
The point to move in reference to. The default is None.
Returns
-------
None.
"""
ref_point = reference_point or self.points["p1"]
assert Point.is_point(move_to), "move_to must be of type Point"
assert Point.is_point(ref_point), "reference_point must be of type Point"
# Get our delta for the reference point to the move point
ref_move_delta_x, ref_move_delta_y = Point.distance(ref_point, move_to, keep_orientation=True)
self.points["p1"] += Point(ref_move_delta_x, ref_move_delta_y)
self.points["p2"] += Point(ref_move_delta_x, ref_move_delta_y)
def move_y(self, move_to, self_point="p1"):
"""
Move to a point only on the x direction
Parameters
----------
move_to : Point
Where to move to.
self_point : str, optional
The internal point to use as reference. The default is "p1".
Returns
-------
None.
"""
assert Point.is_point(move_to), "move_to must be of type Point"
assert self_point in self.points, "self_point must be either 'p1' or 'p2'"
ref_point = Point(self.points["p1"].x, move_to.y)
self.move_xy(move_to, ref_point)
def scale_x(self, percentage_scale):
"""
Helper function to scale this object by a percentage
Parameters
----------
percentage_scale : float
The percentage to scale the object by.
Returns
-------
None.
"""
# This is the amount each point should be moved
point_offset = (self.get_width() * percentage_scale) / 2
P1 = self.points["p1"]
P2 = self.points["p2"]
P1.x = P1.x + point_offset if P1.x < P2.x else P1.x - point_offset
P2.x = P2.x + point_offset if P2.x < P1.x else P2.x - point_offset
self.points["p1"] = P1
self.points["p2"] = P2
def scale_y(self, percentage_scale):
"""
Helper function to scale this object by a percentage
Parameters
----------
percentage_scale : float
The percentage to scale the object by.
Returns
-------
None.
"""
# This is the amount each point should be moved
point_offset = (self.get_height() * percentage_scale) / 2
P1 = self.points["p1"]
P2 = self.points["p2"]
P1.y = P1.y + point_offset if P1.y < P2.y else P1.y - point_offset
P2.y = P2.y + point_offset if P2.y < P1.y else P2.y - point_offset
self.points["p1"].y = P1.y
self.points["p2"].y = P2.y
def move_x(self, move_to, self_point="p1"):
"""
Move to a point only on the y direction
Parameters
----------
move_to : Point
Where to move to.
self_point : str, optional
The internal point to use as reference. The default is "p1".
Returns
-------
None.
"""
assert Point.is_point(move_to), "move_to must be of type Point"
assert self_point in self.points, "self_point must be either 'p1' or 'p2'"
ref_point = Point(move_to.x, self.points["p1"].y)
self.move_xy(move_to, ref_point)
def get_width(self):
"""
Helper function to get this objects width
Returns
-------
float
The width of this object.
"""
p1X = self.points["p1"].x
p2X = self.points["p2"].x
p1X = p1X * -1 if p1X < 0 else p1X
p2X = p2X * -1 if p2X < 0 else p2X
return p1X - p2X if p1X > p2X else p2X - p1X
def get_height(self):
"""
Helper function to get this objects height
Returns
-------
float
The height of this object.
"""
p1Y = self.points["p1"].y
p2Y = self.points["p2"].y
p1Y = p1Y * -1 if p1Y < 0 else p1Y
p2Y = p2Y * -1 if p2Y < 0 else p2Y
return p1Y - p2Y if p1Y > p2Y else p2Y - p1Y
def get_midpoint(self):
"""
Helper function to get the midpoint of this Object2D
Returns
-------
Point
The midpoint of the object.
"""
p1 = self.points["p1"]
p2 = self.points["p2"]
return Point.midpoint(p1, p2)
def get_compass_direction_edge_midpoint(self, direction=CompassDirection.South):
"""
Helper function to get the world coordinate for one of our edges
in the direction of the input compass direction.
Parameters
----------
direction : CompassDirection, optional
Which edges midpoint to get world location for.
The default is CompassDirection.South.
Returns
-------
Point
The world location for the midpoint of an edge.
"""
p1X = self.points["p1"].x
p1Y = self.points["p1"].y
p2X = self.points["p2"].x
p2Y = self.points["p2"].y
midpoint = Point.midpoint(self.points["p1"], self.points["p2"])
westX = p1X if p1X < p2X else p2X
eastX = p1X if p1X > p2X else p2X
northY = p1Y if p1Y < p2Y else p2Y
southY = p1Y if p1Y > p2Y else p2Y
if(direction == CompassDirection.North):
return Point(midpoint.x, northY)
elif(direction == CompassDirection.East):
return Point(eastX, midpoint.y)
elif(direction == CompassDirection.South):
return Point(midpoint.x, southY)
elif(direction == CompassDirection.West):
return Point(westX, midpoint.y)
def draw_self(self, draw_object):
rect_points = []
rect_points.extend(self.points["p1"].as_list())
rect_points.extend(self.points["p2"].as_list())
try:
draw_object.rectangle(rect_points, fill=self.fill, outline=self.outline)
except:
print("Couldn't draw myself :(")
class SidewalkSquare(Object2D):
def __init__(self, spawn_point=ORIGIN, side_length=CUBE_EDGE_SIZE):
P1 = spawn_point
P2 = Point(P1.x + side_length, P1.y + side_length)
super().__init__(P1, P2)
class DrivewayRectangle(Object2D):
def __init__(
self,
spawn_point=ORIGIN,
long_edge_len=3*CUBE_EDGE_SIZE,
short_edge_len=2*CUBE_EDGE_SIZE
):
P1 = spawn_point
P2 = Point(P1.x + short_edge_len, P1.y + long_edge_len)
super().__init__(P1, P2)
def GetRoadBoundaryLine():
"""
Helper function to get the two points describing the roads boundary
Returns
-------
List[Tuple(int x, int y), Tuple(int x, int y)]
Left and right most point representing the road boundary line.
"""
P1 = (0,ROAD_Y)
P2 = (WORLD_EDGE_SIZE, ROAD_Y)
return [P1,P2]
def GetNewPaddedLocation(not_x=None, not_y=None, boundary=0):
"""
Parameters
----------
not_x : int, optional
Avoid this x value. The default is None.
not_y : int, optional
Avoid this y value. The default is None.
boundary : int, optional
Avoid +- this on either side of not_x or not_y
Returns
-------
x : int
A new random x-point within the buffered area.
y : int
A new random y-point within the buffered area.
"""
buffered_house_area = WORLD_EDGE_SIZE - CUBE_EDGE_SIZE
x = random.randint(CUBE_EDGE_SIZE, buffered_house_area)
if not_x:
while x < not_x + boundary and x > not_x - boundary:
x = random.randint(CUBE_EDGE_SIZE, buffered_house_area)
y = random.randint(CUBE_EDGE_SIZE, ROAD_Y)
if not_y:
while y < not_y + boundary and y > not_y - boundary:
y = random.randint(CUBE_EDGE_SIZE, buffered_house_area)
return Point(x,y)
def GetDoorandGarageLocations():
"""
Helper function to get two points for the Garage and Front Door locations
Returns
-------
dict
"Garage" -- the garage point :: "Door" -- the frontdoor point.
"""
# Start with the garage point
GaragePoint = GetNewPaddedLocation()
# Get the front door point, with the boundary of half a GarageRect and
DoorPoint = GetNewPaddedLocation(
not_x=GaragePoint.x,
boundary=DrivewayRectangle().get_width() / 2
)
return {"Garage":GaragePoint,"Door":DoorPoint}
def place_north_to_south(base_rect, move_rect):
"""
Helper function to place one objects north to the other objects south
Parameters
----------
base_rect : Object2D
This object stays in its original position, its southern midpoint will
be where we place the other objects northern midpoint.
move_rect : Object2D
This object is moved, we move it in reference to its northern midpoint
and place it at the base_rect southern midpoint.
Returns
-------
None.
"""
# Get the two points we care about
root = base_rect.get_compass_direction_edge_midpoint(CompassDirection.South)
place = move_rect.get_compass_direction_edge_midpoint(CompassDirection.North)
# Move the object as expected
move_rect.move_xy(root, place)
def GetDrivewayPoints(garage_point, road_boundary_line):
"""
Helper function to yield each driveway block
Parameters
----------
garage_point : Point
Where the driveway is to start
road_boundary_line : List[Point]
The points that represent the road boundary
Yields
------
Object2D
The blocks to be drawn for the driveway
"""
road_y = road_boundary_line[0][1]
driveway_blocks = []
def scale_and_move(garage_point, road_y):
"""
Helper function to scale a driveway block to fit the space between
the last block and the road if it is over reaching.
Parameters
----------
garage_point : Point
The start of the driveway.
road_y : int
The y position of the road.
Returns
-------
None.
"""
move_block = driveway_blocks[-1]
# We need to set the root block and place it at the last blocks
# southern point before proceeding
if len(driveway_blocks) > 1:
print("Moving North to South 1")
root_block = driveway_blocks[-2]
place_north_to_south(root_block, move_block)
# Check if we are over the road
over_boundary = move_block.get_compass_direction_edge_midpoint(CompassDirection.South).y > road_y
# Shrink to fit if we are over the road
if over_boundary:
print("Over boundary")
drive_y = move_block.get_compass_direction_edge_midpoint(CompassDirection.South).y
shrink_perc = (drive_y - road_y) / move_block.get_height()
move_block.scale_y(shrink_perc)
# If we are over the boundary and have more than one block move north to south
if len(driveway_blocks) > 1 and over_boundary:
print("Moving North to South 2")
place_north_to_south(root_block, move_block)
# If we are over boundary on the first block move to the garage point
elif over_boundary:
driveway_blocks[-1].move_xy(
garage_point,
driveway_blocks[-1].get_compass_direction_edge_midpoint(
CompassDirection.North
)
)
return over_boundary
# Create and move our first block to the start position
driveway_blocks.append(DrivewayRectangle())
driveway_blocks[-1].move_xy(
garage_point,
driveway_blocks[-1].get_compass_direction_edge_midpoint(
CompassDirection.North
)
)
# Now continue creating and placing blocks
while not scale_and_move(garage_point, road_y):
yield(driveway_blocks[-1])
driveway_blocks.append(DrivewayRectangle())
# Finally get the last block in the list that wouldn't yield in the
# While loop
yield(driveway_blocks[-1])
def GetFrontDoorPathwayBlocks(front_door_point, target_point):
"""
Helper function to generate new sidewalk squares for a pathway that leads
either to the driveway or the road depending on distance.
Parameters
----------
front_door_point : Point
Where the center of the front door is.
target_point : Point
The location we want the pathway to go to.
Yields
------
Object2D
The next sidewalk square to be rendered.
"""
sidewalk_blocks = []
def place_on_south_edge():
"""
Helper function to spawn and place a new block on the southern edge of
the previous block.
Returns
-------
None.
"""
sidewalk_blocks.append(SidewalkSquare())
root_block = sidewalk_blocks[-2]
move_block = sidewalk_blocks[-1]
root_midpoint = root_block.get_midpoint()
target_midpoint = Point(root_midpoint.x, root_midpoint.y + SidewalkSquare().get_height())
move_block.move_xy(target_midpoint, move_block.get_midpoint())
def place_on_lateral_edge():
"""
Helper function to spawn and place a new sidewalk square on either the
east or west edges of the last block based on target location.
Returns
-------
None.
"""
sidewalk_blocks.append(SidewalkSquare())
root_block = sidewalk_blocks[-2]
move_block = sidewalk_blocks[-1]
root_midpoint = root_block.get_midpoint()
to_right = root_block.get_midpoint().x < target_point.x
offset = SidewalkSquare().get_width() if to_right else -SidewalkSquare().get_width()
target_midpoint = Point(root_midpoint.x + offset, root_midpoint.y)
move_block.move_xy(target_midpoint, move_block.get_midpoint())
def in_edge(block, target):
"""
Helper function to check if our target is lined up with either the
vertical or horizontal edges of our current block.
Parameters
----------
block : Object2D
The object we are checking is lined up with the target.
target : Point
What we want the object to be lined up with.
Returns
-------
vertical_in_edge : bool
If we are lined up in the vertical direction.
horizontal_in_edge : bool
If we are lined up in the horizontal direction.
"""
pm_width = block.get_width() / 2
pm_height = block.get_height() / 2
midpoint = block.get_midpoint()
top = midpoint.y + pm_height
bottom = midpoint.y - pm_height
left = midpoint.x - pm_width
right = midpoint.x + pm_width
to_right = block.get_midpoint().x < target_point.x
# Check for the in edge
vertical_in_edge = target.y > bottom and target.y < top or target.y < top
horizontal_in_edge = target.x < right and target.x > left
# Check if we accidentally passed the edge in the horizontal direction
if to_right:
horizontal_in_edge = horizontal_in_edge or target.x < left
else:
horizontal_in_edge = horizontal_in_edge or target.x > right
return (vertical_in_edge, horizontal_in_edge)
def scale_final_block(horizontal):
"""
Helper function to scale the final block in our front door pathway
Parameters
----------
horizontal : bool
Tells the algorithm if this block is intended for a horizontal
scaling or a vertical one.
Returns
-------
None.
"""
root_block = sidewalk_blocks[-2] if len(sidewalk_blocks) > 1 else -1
move_block = sidewalk_blocks[-1]
# Shrink vertical aka 'not horisontal'
if not horizontal:
block_y = move_block.get_compass_direction_edge_midpoint(CompassDirection.South).y
shrink_perc = (block_y - target_point.y) / move_block.get_height()
move_block.scale_y(shrink_perc)
if len(sidewalk_blocks) > 1:
move_block.move_xy(
root_block.get_compass_direction_edge_midpoint(CompassDirection.South),
move_block.get_compass_direction_edge_midpoint(CompassDirection.North)
)
# Scale the horizontal
else:
# get the direction we will need to measure and move in
if root_block.get_midpoint().x < move_block.get_midpoint().x:
direction = CompassDirection.East
else:
direction = CompassDirection.West
# Get the x values to measure from
root_x = root_block.get_compass_direction_edge_midpoint(direction).x
target_x = target_point.x
# Measure the gap we need to fill
gap_width = root_x - target_x if root_x > target_x else target_x - root_x
# Get the srink percentage
shrink_perc = (move_block.get_width() - gap_width) / move_block.get_width()
# Scale the block
move_block.scale_x(shrink_perc)
# Move the block into place dependent on direction
if direction == CompassDirection.East:
move_block.move_xy(
root_block.get_compass_direction_edge_midpoint(direction),
move_block.get_compass_direction_edge_midpoint(CompassDirection.West)
)
else:
move_block.move_xy(
root_block.get_compass_direction_edge_midpoint(direction),
move_block.get_compass_direction_edge_midpoint(CompassDirection.East)
)
# Add the first block
sidewalk_blocks.append(SidewalkSquare())
sidewalk_blocks[-1].move_xy(
front_door_point,
sidewalk_blocks[-1].get_compass_direction_edge_midpoint(
CompassDirection.North
)
)
horizontal = False
# Add in the vertical direction first
while not in_edge(sidewalk_blocks[-1], target_point)[0]:
yield sidewalk_blocks[-1]
place_on_south_edge()
# Add in the horizontal direction second
while not in_edge(sidewalk_blocks[-1], target_point)[1]:
horizontal = True
yield sidewalk_blocks[-1]
place_on_lateral_edge()
scale_final_block(horizontal)
yield sidewalk_blocks[-1]
if __name__ == "__main__":
# Generate the road boundary
roadBoundaryLine = GetRoadBoundaryLine()
# Generate some random points for the garage and door locations
GandDPoints = GetDoorandGarageLocations()
# specific variables for easier use
garage_point = GandDPoints["Garage"]
frontdoor_point = GandDPoints["Door"]
# Which direction is the driveway from the front door
frontdoor_left_of_driveway = frontdoor_point.x < garage_point.x
dw_center_offset = DrivewayRectangle().get_width() / 2
# Get a x value for where to target the driveway
if frontdoor_left_of_driveway:
driveway_point_x = garage_point.x - dw_center_offset
else:
driveway_point_x = garage_point.x + dw_center_offset
# This is to ensure the front walk comes out a certain amount before bending
# to lead to the driveway
block_lead = frontdoor_point.y + (3 * SidewalkSquare().get_height())
# Get a y value for where to target the driveway
if garage_point.y > frontdoor_point.y:
driveway_point_y = garage_point.y + SidewalkSquare().get_height() / 2
else:
driveway_point_y = block_lead
# Create the points
fpt_driveway_point = Point(driveway_point_x, driveway_point_y)
fpt_road_point = Point(frontdoor_point.x, roadBoundaryLine[0][1])
# Measure the distance to both
dist_fp_dw = sum(Point.distance(frontdoor_point, fpt_driveway_point))
dist_fp_road = sum(Point.distance(frontdoor_point, fpt_road_point))
# Choose which to target
if dist_fp_dw < dist_fp_road:
print("Targetting Driveway")
fpt = fpt_driveway_point
else:
print("Targetting Road")
fpt = fpt_road_point
images = []
dws = []
sss = []
def draw_items(d):
"""
Helper function to draw items in each frame
Parameters
----------
d : draw object
What to use to draw
Returns
-------
None.
"""
d.line(roadBoundaryLine)
d.point(garage_point.as_list())
d.point(frontdoor_point.as_list())
for dw in dws:
dw.draw_self(d)
for ss in sss:
ss.draw_self(d)
# Get all the driveway blocks and draw them
for dw in GetDrivewayPoints(garage_point, roadBoundaryLine):
out = Image.new("RGBA", (WORLD_EDGE_SIZE,WORLD_EDGE_SIZE))
d = ImageDraw.Draw(out)
dws.append(dw)
draw_items(d)
images.append(out)
# Get all the front pathway blocks and draw them
for fp in GetFrontDoorPathwayBlocks(frontdoor_point, fpt):
out = Image.new("RGBA", (WORLD_EDGE_SIZE,WORLD_EDGE_SIZE))
d = ImageDraw.Draw(out)
sss.append(fp)
draw_items(d)
images.append(out)
# Save our GIF
images[0].save(
'house_paths.gif',
save_all=True,
append_images=images[1:],
optimize=True,
duration=100,
loop=0
)
Some examples of the output (small squares are the front pathway)
The theoretical code got away from me in time, I didn’t intend for this to take as long as it did, which is why there isn’t any in-game demo. However it did give me interesting problems that I might not have figured out so quickly in Unity.
First we need to be aware of which direction to scale the blocks at the end, for the driveway this is easy, it is only going to be in the vertical direction and determined by the roads y position. For the front door pathway it is a bit more tricky because you have to know which direction the path is headed in order to scale and adjust its location properly.
Second, determination of what to target for the front door path is difficult in its own ways, such as measuring the distance to an arbitrary point on the drive way and which side of the path the driveway is on.
Conclusion
With the logic worked out(mostly there are still some hidden bugs I will have to work out), I am ready to begin porting this to Unity this next week which will bring with it many more houses. I look forward to the next post and the NEW YEAR happy New Year everyone!