Cable Clock Progress Report #1

I would like to preface this by acknowledging the delay of this entry. I’ve been developing this project for the past 4 weeks, but have not had the chance to enter any updates. With that out of the way, let’s catch up to speed.

Concept

I cannot take credit for the initial Idea behind the clock, it was proposed to me by my brother and colleague, Travis Adsitt. He prompted me initially with a task, to write a function that would represent the length of two strings, attached to either side of a weight, that would move the weight in a circle. In this model, the weight would be the indicator of time, as it would move in a circular motion on a clock face, and wherever it lay is the current time. To help visualize this, I have attached a rough render of this idea.

Now with this concept, it clearly needs a lot to bring it to life. I began by creating functions for the length of the string using the following steps:

  1. Locate the point of the rotor
  2. Locate the point it meets the circumference of the circle
  3. Find the distance between the points

This was relatively simple to do, and I was able to create a working script in Python. This script was relatively simple, it used parsArgs to take user input of the position of the rotors, as well as the diameter and position of the clock face. I then used a “For Loop” in this script, and had it output a render of each value that was put in, or create an image for each hour and minute on the clock. I was able to compile these images into an animation, which is below.

Supplies

Now with this render completed, and knowing I had gotten the math stapled, the next step was to begin working with some hardware. Most of the materials are readily available, and I will list below for reference.

Single Motor Testing

With these positioned using a cardboard frame, the first prototype was created. Of course, it still needed to be developed and functioning. To start with this, I downloaded the Arduino IDE and plugged into my ESP32, and got straight to working. I began with using the myStepper library, just at an attempt to get a single stepper motor moving, as it is the most basic, yet important piece of the project. Below is the initial code snippet, using the myStepper library.

void setup() {
  //Set Stepper Speed
  myStepper.setSpeed(5);
  //Begin Serial Monitoring
  Serial.begin(115200);
}
void loop() {
// Run stepper motor, with steps set
myStepper.step(stepsPerRevolution); //move clockwise 1 rotation
  delay(1000);
myStepper.step(-stepsPerRevolution); //move counter-clockwise 1 rotation
  delay(1000);
  while(true){};
}

Above the visible segment attached, I defined stepsPerRevolution as 2048, the number of steps for a full revolution, which is specific to the 28BYJ-48 Motor. Upon uploading this segment, the single motor connected should do 1 full rotation clockwise, followed by 1 full rotation counter-clockwise. It then ends with a while(true) loop, so that the program can continue to run without looping endlessly.

Two Motor Application

Now, the next step in this process is to get two stepper motors, with individual control. Since this is not supported in the myStepper library, I switched to the accelStepper library, as it is designed for multiple stepper motors.

void setup()
{

// Set Maximum Speed, Acceleration

    stepper1.setMaxSpeed(300.0);
    stepper1.setAcceleration(200.0);

// Repeat for stepper2
    
    stepper2.setMaxSpeed(300.0);
    stepper2.setAcceleration(200.0); 
}
void loop()
{
  stepper1.moveTo(2048);  // set target forward 2048 steps
  stepper2.moveTo(2048);  // set target forward 2048 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }

  stepper1.moveTo(0);  // set target back to 0 steps
  stepper2.moveTo(0);  // set target back to 0 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
}

With this segment, I was able to get both motors to do a full rotation clockwise, or move forward 2048 steps, before moving counterclockwise a full rotation, back to the original position.

Controlling String Lengths

With the ability to move both motors, now all I needed to do was control the rotations and move the weight to specific positions. To begin with this, I positioned the weight at the bottom of the circle I wanted to use as my clock face. From now on, this is position zero. From position zero, I moved the left motor Counter-Clockwise, and the right motor clockwise, moving the weight upward. I repeated this in 500 step increments, until I reached the top of the clock face. I took careful note of how many steps it took to go from top to bottom of the face, and how much the string length changed. My documentation of these changes is below.

Testing a Sample Set

With this information, I knew how to adjust the string length in centimeters, by changing the number of steps on either motor. I had observed and then verified that rotating one motor a full rotation, or 2048 steps, resulted in the correlating string length changing by 10 centimeters, dependent on the direction of rotation. To move forward from here, I then did the calculations given the motor positions as well as the position and specifications of the clock face, to find a sample set of points around the clock. I took each hour and found the string lengths needed, and then converted those lengths and converted them into the steps for each motor. Once I got these values verified I made a table of the values and where each motor needed to be for each hour.

I then put these values into my Arduino IDE, and was able to format it in a manner that the motors would move to the positions specified, at the same time, with the same rate of change. The code for this is very similar to the test values, just extended for each of the hours. The code is attached below.

void setup()
{

// Set Maximum Speed, Accelleration

    stepper1.setMaxSpeed(300.0);
    stepper1.setAcceleration(200.0);

// Repeat for stepper2
    
    stepper2.setMaxSpeed(300.0);
    stepper2.setAcceleration(200.0); 
}
void loop()
{
  stepper1.moveTo(-3200);  // set target forward 500 steps
  stepper2.moveTo(-3200);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }

  stepper1.moveTo(-4190);  // set target back to 500 steps
  stepper2.moveTo(-1966);  // set target back to 0 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(-4506);  // set target forward 500 steps
  stepper2.moveTo(-817);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(-3842);  // set target forward 500 steps
  stepper2.moveTo(80);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(-2670);  // set target forward 500 steps
  stepper2.moveTo(391);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(-1415);  // set target forward 500 steps
  stepper2.moveTo(252);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(0);  // set target forward 500 steps
  stepper2.moveTo(0);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(252);  // set target forward 500 steps
  stepper2.moveTo(-1415);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(391);  // set target forward 500 steps
  stepper2.moveTo(-2671);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(80);  // set target forward 500 steps
  stepper2.moveTo(-3842);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(-817);  // set target forward 500 steps
  stepper2.moveTo(-4506);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
  stepper1.moveTo(-1966);  // set target forward 500 steps
  stepper2.moveTo(-4190);  // set target forward 500 steps
  while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) 
  {
    stepper1.run();
    stepper2.run(); 
  }
}

You will notice this segment is extremely inefficient and lengthy, but that’s alright, as it is just a test set, and will not be used in the final iteration of the project. After writing these values in, I was able to upload the script, and watch the first working version of the clock. The video is attached below.

Conclusion and Final Thoughts

Now, as you may notice, these number sets only apply to the specific clock diameter, motor position, and pulley diameter that relate to this specific model. My goal now, is to allow this script to be fully adjustable, so the user can simply input the measurements of the specific clock being used, and it will perform the necessary functions to find the string lengths needed. This process is still under-way but can be expected soon. I will cover the creation of these functions as well as go into detail on the physical clock prototype in my next entry. Stay posted.

Arduino RP2040 Nano Connect SPI Bus

This post is to document the setup for a simple SPI Bus test using a RP2040 Nano Connect as a controller and the Arduino Uno as a peripheral device. The board connections look as follows:

I couldn’t find a good example of how to use the RPi4 as a peripheral device, and didn’t want to spend the time of coding it myself. So the Arduino Uno’s ATMega328p chip seemed like a reasonable fit for this — plenty of examples and simple enough for me to wrap my head around it.

With all that said I will be using the Arduino IDE to program both devices and the SPI library to handle byte transfers.

Useful Links

Peripheral Device Code

I like to start with the full picture and drill down into it, so below is the full code for the peripheral device.

/*
    SPI Bus Test
    Copyright (C) 2021  Travis Adsitt

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include<SPI.h>

#define SPI_MODE 3
#define MAX_BUFFER_SIZE 50

volatile boolean received;
volatile byte peripheral_received,peripheral_send;

char print_buffer [50]; // Used for print formatting

byte *command_buffer;
byte *command_buffer_end;
byte *command_buffer_start;
int command_buffer_count;

byte *send_buffer;
byte *send_buffer_end;
byte *send_buffer_start;
int send_buffer_count;

int counter; // Counter to control
byte counter_low;
byte counter_high;

void setup()

{
  Serial.begin(115200);
  
  pinMode(MISO,OUTPUT);                   

  SPCR |= _BV(SPE);   
  SPCR |= _BV(SPIE);
  // SPCR |= _BV(DORD);
  SPCR |= (SPI_MODE << 2);
  
  command_buffer_count = 0;
  
  command_buffer = calloc(0, MAX_BUFFER_SIZE);
  command_buffer_start = command_buffer;
  command_buffer_end = command_buffer;

  
  send_buffer_count = 0;
  
  send_buffer = calloc(0, MAX_BUFFER_SIZE);
  send_buffer_start = send_buffer;
  send_buffer_end = send_buffer;

  counter = 0;
  
  SPI.attachInterrupt();            
}

void add_command_to_buffer(byte command){
  if(command_buffer_count == MAX_BUFFER_SIZE || command == 0x00) return;
  
  *command_buffer_end = command;

  if(command_buffer_end == (command_buffer + MAX_BUFFER_SIZE)){
    command_buffer_end = command_buffer;
  }else{
    command_buffer_end++;
  }
  
  command_buffer_count++;
}

byte get_command(){
  if(!command_buffer_count) return 0x00;
  
  byte command = *command_buffer_start;
  if(command_buffer_start == (command_buffer + MAX_BUFFER_SIZE)){
    command_buffer_start = command_buffer;
  }else{
    command_buffer_start++;
  }
  
  command_buffer_count--;

  return command;
}

byte get_send_buffer_byte(){
  if(!send_buffer_count) return 0x00;

  byte ret_send = *send_buffer_start;
  
  if(send_buffer_start == (send_buffer + MAX_BUFFER_SIZE)){
    send_buffer_start = send_buffer; 
  }else{
    send_buffer_start++;
  }

  send_buffer_count--;

  return ret_send;
}

void add_byte_to_send_buffer(byte data_to_send){
  if(send_buffer_count == MAX_BUFFER_SIZE){
    return;
  }

  *send_buffer_end = data_to_send; 
  
  if(send_buffer_end == (send_buffer + MAX_BUFFER_SIZE)){
    send_buffer_end = send_buffer; 
  }else{
    send_buffer_end++;
  }

  send_buffer_count++;
}

ISR (SPI_STC_vect)                         
{
  byte command = SPDR;
  
  SPDR = get_send_buffer_byte();
  
  add_command_to_buffer(command);
}


void loop()
{ 
  if (command_buffer_count > 0) //Check for more commands
  {
    // Grab the next command to process
    peripheral_received = get_command();
  
    // Print command recieved
    sprintf(print_buffer,"Processing Command: %x Commands Left: %d",peripheral_received, command_buffer_count);
    Serial.println(print_buffer);
    switch(peripheral_received){
      case 0x01: // Add to the counter
        Serial.println("Adding to counter");
        counter++;
        break;
      case 0x02: // Get the counter
        add_byte_to_send_buffer(counter & 0xff);
        add_byte_to_send_buffer((counter >> 8) & 0xff);
        break;
      case 0x03: // Reset the counter
        Serial.println("Resetting the Counter");
        counter = 0;
        break;
      case 0x04: // Are you still there?
        Serial.println("Sending heartbeat");
        add_byte_to_send_buffer(0xff); // Yes :)
        break;
    }
    received = false;
  }
}

Ok to start lets look at the main loop…

void loop()
{ 
  if (command_buffer_count > 0) //Check for more commands
  {
    // Grab the next command to process
    peripheral_received = get_command();
  
    // Print command recieved
    sprintf(print_buffer,"Processing Command: %x Commands Left: %d",peripheral_received, command_buffer_count);
    Serial.println(print_buffer);
    switch(peripheral_received){
      case 0x01: // Add to the counter
        Serial.println("Adding to counter");
        counter++;
        break;
      case 0x02: // Get the counter
        Serial.println("Sending the Counter");
        add_byte_to_send_buffer(counter & 0xff);
        add_byte_to_send_buffer((counter >> 8) & 0xff);
        break;
      case 0x03: // Reset the counter
        Serial.println("Resetting the Counter");
        counter = 0;
        break;
      case 0x04: // Are you still there?
        Serial.println("Sending heartbeat");
        add_byte_to_send_buffer(0xff); // Yes :)
        break;
    }
    received = false;
  }
}

Our SPI bus is configured manually using the SPI Control Register(SPCR), I will leave it up to you to review the ATMega328P datasheet for more information on the bit values I set in there.

In the loop first we check to see if any new commands have come across the bus by checking the global variable ‘command_buffer_count’. If there are commands waiting then grab the next one in the queue with a call to ‘get_command()’. With the command stored in our ‘peripheral_received’ variable we can enter the switch case statement that identifies the commands we care about, namely:

  • 0x01
    • Add to our internal counter
  • 0x02
    • Send the current state of the counter
  • 0x03
    • Reset our counter
  • 0x04
    • Send heartbeat, this is used to verify there is indeed a device online

You might notice in each of the case blocks there are calls similar to our get_command() call to helper routines that manage a queue for us. Lets dig into this concept a bit.

A Queue for Command Management

Before we look at how the queue is setup and managed lets look at the Interrupt Service Routine(ISR) that is in our program:

ISR (SPI_STC_vect)                         
{
  byte command = SPDR;
  
  SPDR = get_send_buffer_byte();
  
  add_command_to_buffer(command);
}

This little block of code is run anytime there is a byte of data fully received and available in the SPI Data Register(SPDR), it is an interrupt because anything executing on the Microcontroller(MCU) at the time is immediately interrupted to handle the data in the buffer. You want your interrupt routines to be extremely simple as to not miss any data that might be being shifted in while you are executing the handler code.

So the first thing we do is grab the current byte from the register, and replace it with the next byte we want to send (if there isn’t a byte to send we replace it with 0x00). Then we add it to our command buffer queue to be processed in our main loop, we don’t process it here because that would be too compute expensive for the rate at which commands might be coming across the bus.

Finally we come to the concept of this queue… The command and send_data buffers are setup as such:

  command_buffer_count = 0;
  
  command_buffer = calloc(0, MAX_BUFFER_SIZE);
  command_buffer_start = command_buffer;
  command_buffer_end = command_buffer;

  
  send_buffer_count = 0;
  
  send_buffer = calloc(0, MAX_BUFFER_SIZE);
  send_buffer_start = send_buffer;
  send_buffer_end = send_buffer;

Basically we have an arbitrary ‘MAX_BUFFER_SIZE’ that we use with calloc(clear allocation, meaning we set all the memory to 0 in this case after we allocate it — this is more expensive but at least you know the start state of your memory) to get a set amount of memory for our buffer.

Then two pointers and a count variable to manage the queue are established, having all three of these can be seen as a bit of overkill considering you should be able to infer everything from a counter and list start pointer, or even start and end pointers to infer count, but I want to keep it simple and just track a bit more.

Using those buffers we have the command add and get routines below:

void add_command_to_buffer(byte command){
  if(command_buffer_count == MAX_BUFFER_SIZE || command == 0x00) return;
  
  *command_buffer_end = command;

  if(command_buffer_end == (command_buffer + MAX_BUFFER_SIZE)){
    command_buffer_end = command_buffer;
  }else{
    command_buffer_end++;
  }
  
  command_buffer_count++;
}

byte get_command(){
  if(!command_buffer_count) return 0x00;
  
  byte command = *command_buffer_start;
  if(command_buffer_start == (command_buffer + MAX_BUFFER_SIZE)){
    command_buffer_start = command_buffer;
  }else{
    command_buffer_start++;
  }
  
  command_buffer_count--;

  return command;
}

When adding a command we check if the buffer is full or the command is 0x00 and just return if either is the case. If not though, we set the end pointers data equal to the command handed in, check if we are at the end of the buffer and if so then wrap it to the buffers location if not we just add one to the buffer end pointer. Finally add one to the buffers count.

On the get command side we do much the same except we pull from the start of the buffer and set the start to +1 from its current location and subtract from the buffer_count.

For the send_data buffer you will see exactly the same layout:

byte get_send_buffer_byte(){
  if(!send_buffer_count) return 0x00;

  byte ret_send = *send_buffer_start;
  
  if(send_buffer_start == (send_buffer + MAX_BUFFER_SIZE)){
    send_buffer_start = send_buffer; 
  }else{
    send_buffer_start++;
  }

  send_buffer_count--;

  return ret_send;
}

void add_byte_to_send_buffer(byte data_to_send){
  if(send_buffer_count == MAX_BUFFER_SIZE){
    return;
  }

  *send_buffer_end = data_to_send; 
  
  if(send_buffer_end == (send_buffer + MAX_BUFFER_SIZE)){
    send_buffer_end = send_buffer; 
  }else{
    send_buffer_end++;
  }

  send_buffer_count++;
}

In fact, it is so similar it might make sense to generalize the functions and let the caller specify the buffer using a struct or something… For now that is all for the peripheral device.

Controller Device Code

If you are still with me the controller code is much simpler and should be a breeze to review. Again we will start with the whole picture:

/*
    SPI Bus Test
    Copyright (C) 2021  Travis Adsitt

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include<SPI.h>

// Define our Chip Select pin
#define CS 10

// SPI settings
SPISettings settings(9600, LSBFIRST, SPI_MODE3);

char print_buffer [50]; // Used for print formatting

void setup()
{
  // Begin a serial session for debug printing
  Serial.begin(9600);
  // Begin our SPI bus library functionality
  SPI.begin();

  // Setup all pin podes
  pinMode(CS, OUTPUT);
  pinMode(MOSI, OUTPUT);
  pinMode(MISO, INPUT);
  pinMode(SCK, OUTPUT);
  pinMode(9, OUTPUT);

  // Set our Chip Select high, this turns it 'off'
  digitalWrite(CS, HIGH);
}

void loop(void)
{
  // A place for us to store the counter when we request it
  int counter = 0;

  // Drop Chip Select and begin a transaction
  digitalWrite(CS, LOW);
  SPI.beginTransaction(settings);

  // We are simply going to count up and send as a command
  int i = 1;
  while(i < 5){
    // If we are sending a two expect the return to 
    // be the current counters value.
    if(i == 2){
      counter = SPI.transfer(i);
    }else{
      SPI.transfer(i);
    }
    i++;
  }
  // Send 0x00 to clear SPDR preventing bad data on next transaction
  SPI.transfer(0x00);
  // Bring chip select back up and end the transaction
  digitalWrite(CS, HIGH);
  SPI.endTransaction();

  // Print our counter and delay for easier reading on serial
  // terminal.
  sprintf(print_buffer,"Counter Lower Value: %x",counter);
  Serial.println(print_buffer);
  delay(500);
}

The setup section is just specifying the pin modes for the SPI library and ourselves. We control the Chip Select(CS) line but the rest is handled by the SPI library. Despite the library handling things I highly recommend reading up on what that library is doing, it is really impressive and very cool.

Anyway, in the main loop you will see we are simply counting from 0-4 and sending the current index as a command to the peripheral. We capture the return value when sending 2 as that should return the value of the counter. And because we are in Least Significant Bit First(LSBFIRST) mode we expect the first bit to be a one as we have only added 1 to the peripherals counter. If we cared to look, when we send the 4 we would expect also to see the 0xFF of the heartbeat, that can be seen in the O-Scope capture below:

O-Scope capture of CIPO(MISO) line over Clock line

In this picture you can actually see the decoded values below the wave form, this includes the heartbeat at the end.

Output

Peripheral Serial Output

Processing Command: 1 Commands Left: 0
Adding to counter
Processing Command: 2 Commands Left: 0
Sending the Counter
Processing Command: 3 Commands Left: 1
Resetting the Counter
Processing Command: 4 Commands Left: 0
Sending heartbeat

Here we see the command that is being processed along with the queue remaining. Because it is such a short burst we see it only start to stack commands when we are processing command ‘0x03’.

Controller Serial Output

Counter Lower Value: 1

As expected we receive a value of one from the peripheral.

Conclusion

This dive into SPI communication was very informative for me, I hope this article is helpful in guiding someone else by showing a working example. Please comment if you find any inaccuracies that should be corrected or would like to discuss the content. Thank you for reading!

Paper Boy Progress Report #5

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.

  1. All objects we care to place are rectangular
  2. 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!

Arduino RP2040 Connect Nano Setup

This is to document the setup instructions for flashing an Arduino RP2040 Connect Nano from a RPi4 using the command line tool ‘picotool’ and the pico-sdk. Not only to publish but for my own reference 😀

As a side note, I am using a RPi4 as a host because I like the Linux interface and would prefer it for situations of CI and things that may come in future projects.

Helpful Links

Getting the tools

These are to be executed on the RPi4 over SSH:

  1. Install picotool
    • See here for instructions
    • Make sure that running ‘picotool’ doesn’t give you a ‘cannot be found’ sort of error.
  2. Get the pico-sdk
    • See here for instructions

Setting up the project

With the tools downloaded and installed we should be ready to get started creating our project directories.

mkdir blink
cd blink
touch blink.c
touch CMakeLists.txt
# Insert the data below into your CMakeLists.txt files with vim here
cp ~/pico/pico-examples/pico_sdk_import.cmake ./ 
mkdir build
cd build
cmake -D"PICO_BOARD=arduino_nano_rp2040_connect" ..

In CMakeLists.txt add:

cmake_minimum_required(VERSION 3.12)

include(pico_sdk_import.cmake)

project(blink)

pico_sdk_init()

add_executable(blink
    blink.c
)

pico_add_extra_outputs(blink)

target_link_libraries(blink pico_stdlib)

And in your blink.c file:

#include "pico/stdlib.h"
#include "pico/binary_info.h"

const uint LED_PIN = PICO_DEFAULT_LED_PIN;

int main() {

    bi_decl(bi_program_description("First Blink"));
    bi_decl(bi_1pin_with_name(LED_PIN, "On-board LED"));

    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (1) {
        gpio_put(LED_PIN, 0);
        sleep_ms(250);
        gpio_put(LED_PIN, 1);
        sleep_ms(1000);
    }
}

Now with both those files populated execute the following from your build directory:

make

After the program has been compiled you should see in your build folder a blink.uf2 file, this is what we will use for flashing the board.

Flashing the Board

Your board should be setup as follows:

Notice the wire on the RP2040 Nano Connect, when you press the reset button on the board this will put the board in a state that allows flashing.

Once you have pressed the reset and entered the flashing state the wire must be disconnected before flashing. Once disconnected run the following from your build folder:

sudo picotool load ./blink.uf2

Once this flashes you should be able to hit the reset button once again to reboot and run the code we just flashed to the board. If all went well the integrated LED on the board should now be flashing!

Paper Boy Progress Report #4

This week was busy preparing at work for the holiday break, but I still managed to get some things done on the game. The following are the changes for this week and I am very excited for the future of this game, it is getting to the point now where I almost feel comfortable sharing an APK here on the site for people to download and try. Thank you for checking in!

Change Log

  • Added Hand Break
    • Pull trigger to increase decay on slowdown
  • Added Sound, and Music
    • This will expand as we go it is a minimal implementation at this point
  • Added a room for the lobby
  • Added persistent game data for round saving across app restarts
  • Fixed mesh placement and spawning system
  • Fixed paper throw, you can now hit red targets

Paper Boy Progress Report #3

This week I would say we had a bit of a regression (Meshes), and a couple big moves forward. Thank you for coming to this progress report, please leave a comment with feedback/encouragement if you have it, and enjoy the update!

Change Log

  • Added a main menu
  • Added game modes
  • Added game results display in main menu
  • Added dampening function to bike movement speed to prevent motion sickness when bike stops
  • Changed house meshes
    • Reduced poly count on houses by importing a new unity asset set
    • Lower quality but definitely made a difference with frame rate
    • This also allowed us to dress up the scene a lot more, with hopes to procedurally generate the house surroundings in the future
  • Changed to a multi-scene work flow
    • In addition we reorganized code to allow for better future expansion (this still needs work)
  • Changed targets to a cube area as apposed to an orb

Demo

Paper Boy Progress Report #2

This weeks update was awesome! Huge milestone hit, WE GOT THE HEADSET CONNECTED TO THE BIKE! I didn’t have as much time this week to work on the app but getting the headset to connect to the bike was a huge win! Also this week I added an intro to the demo, along with overlaid narration to explain the changes.

Change Log

  • In-Game speed adjusted by real world bike cadence
  • Mirrored Houses to ‘fill-out’ the neighborhood
    • Increases immersion
  • Added a sound for the paper hitting the ground (not audible in the demo)

Lessons/Funny Story

  • This week I learned how to package our game into and APK and side load it onto our headset, this allowed me to use the Bluetooth device in the headset to connect to the bike.
  • The sound used for the paper hitting the ground is actually extracted from a very random YouTube video here.

Assets Used

Demo

Paper Boy Progress Report #1

This week marks a big change to the project started last weekend, see the previous post for more details on where we left off. Short recap, last week I got a bicycle to move forward across a blank plane. Along with this I created a goal for the user to throw an object into an orb, which triggered the goal to be moved.

Additions This Week

  • Added house meshes
  • Added road meshes
  • Added spawning system for roads and houses
  • Goals now
    • Fade in and out to better indicate as a goal
    • Go transparent when struck
    • Have different colors for different point counts
      • Red – 3 points
      • Yellow – 2 points
      • Green – 1 point

Lessons Learned

Building on this mechanic I learned a couple lessons about Unity’s execution flow and how things are spawned into the world.

  1. If there is an error in the console, don’t assume the things you see on screen are unrelated
    • I had errors in the console, but assumed the execution was only halted for part of game setup. Because of these assumptions I spent a lot of time debugging my logic when the problem was the script had halted at the location of the error preventing the logic I was debugging from ever being executed.
  2. Meshes and Renderers are tricky, don’t assume their coordinates or their bounding box coordinates are what you expect.
    • I wanted to spawn houses based on the left and right sides of the house on the X axis, I assumed I could get these positions by cycling through each meshes renderer and getting the left most x boundary. This turned out to be way difficult because coordinates are with respect to some origin, I still don’t fully understand why I couldn’t get this to work
    • I ended up using empty’s as children to the parent empty to mark left and right positions of the object.

Demo

There are clearly still some mesh and collision box issues but I am very happy with the progression here.

Next weekend I would like to add some sound to the game and do a better job of hiding the empty void surrounding the player with fog or something. Along, with this I would like to add a feature to have all of the goals for a house to be disabled when one of them is disabled from a hit.

Thank you for reading! See you next week!

First Multi-Weekend Game

This weekend I started a bigger project than what can be encapsulated in a single weekend, so I fully expect to be posting about this for at least the next 2 weekends of work (if holidays allow, if not I will continue work at some point).

The mechanics I am going to share from this weekend are just the start, with a few more this will be a bit more of a complete game.

Mechanic #1 “Inventory”

For the game I have in mind you must have an ‘inventory’, it only needs to contain a single type of item but must contain many of them. For this I used an integer to represent the number remaining in the inventory and a GameObject object in the class to hold a prefab for instantiation each time the inventory object is ‘grabbed’.

So in short:

  1. Player hovers over inventory cube
  2. Player tries to grab
  3. This instantiates an instance of the prefab used
  4. Player is forced to grab the instantiated prefab

In the demo you might ask, where is the inventory cube? The answer to this is that it is actually a flat large cube behind the players head to give the impression they are grabbing it from their back.

Mechanic #2 “Goal Throw Point”

In this game you are going to be pulling objects out of an inventory and throwing them at goal points, think of basket ball for this mechanic. I needed a way to signify a point the player was supposed to throw an object, for this I used a sphere that was see through. For bonus points I added the ability for this sphere to be randomly spawned after being hit with the prefab from mechanic #1 in the area represented by a cube.

Mechanic #3 “Scoreboard”

In previous small projects for the scoreboard I would simply add a public variable for the UI.Text object in a script attached to whatever object I wanted to control the score. In this one I needed something new, something that allowed me to have multiple ‘reporters’ for score. For this I decided to use events, thus each of the goals have ‘scored’ events and the inventory has a ‘grabbed’ event. This allows us to show the user how many of the prefabs they have left and how many shots they have scored.

In the demo below you will see this on the handle bars of the bike the user is placed behind.

Demo

Some explanation is needed here… In the beginning I am trying to show the score board and object count on the handlebars of the bicycle. Once I grab the first cube from behind my head the bike starts moving forward and I have to throw the cubes at the orbs. Once struck the orbs move to new locations for a new challenge.

Anyway, that is where I end the weekend, and start the work week. Thank you for reading 🙂

First Unity Game

This weekend I knew I needed to produce something of my own, and like the Echelon Bicycle Monitor I wanted it to be a standalone ‘thing’ — meaning that once the weekend was over I wanted to be ‘done’ with it. I quote done because nothing is ever done, well I guess until you say it is, so forget the quotes this is done :D.

As the title declares, this is a Unity Game, its not anything crazy, but it did force me to use a large set of tools that I haven’t touched in a while. Those tools are Blender, and the Unity Game Engine. This project serves as a warm up to get reacquainted with the tools in preparation for more ambitious projects.

Blender

I started with a tutorial on modelling a low poly car in blender, I just needed a prop to control in Unity, so why not a car? I followed this tutorial, and ended up with:

Low Poly Car Model

As with anything I am bound to explore more than necessary and I rather enjoyed modeling things in Blender, I desire to get more proficient with materials and shaders though. Shaders were outside of the scope of the goal for the weekend so I moved on with the bare minimum, though I did model more than the car (couldn’t help myself).

Construction Scene Models

Unity Engine

With the low poly car created it was time to start figuring out the control mechanisms in Unity using the Oculus Quest 2 headset. For this I followed this tutorial for the physics of the car controller. For the input though I had to refer to the Unity API for XR Controllers, which isn’t nearly as easy as the old ‘Input’ object I was used to for mouse and key board, but was doable. Ill only explain the input here as more than that would just be reiterating what is in the linked tutorials.

Unity Controller Code

First you have to get the controller, to do this I used the following code:

private void GetRightHand()
    {
        var rightHandDevices = new List<UnityEngine.XR.InputDevice>();
        UnityEngine.XR.InputDevices.GetDevicesAtXRNode(UnityEngine.XR.XRNode.RightHand, rightHandDevices);
        if (rightHandDevices.Count < 1) return;
        device = rightHandDevices[0];
    }

I call this once on ‘Start’ and in a check in ‘FixedUpdate’, the check is to ensure that if we don’t get the controller on game start we get it when it does finally connect.

Once we have the controller we need to get the trigger value, this code handles that:

// Get Accelerator Input
        device.TryGetFeatureValue(UnityEngine.XR.CommonUsages.trigger, out VerticalInput);

Left right control will be done with the joystick on the right hand with this code:

// Get Left Right Input
        device.TryGetFeatureValue(UnityEngine.XR.CommonUsages.primary2DAxis, out joyStickValue);
        HorizontalInput = joyStickValue.x;

Finally, we need to get one button to handle resetting the cars position

device.TryGetFeatureValue(UnityEngine.XR.CommonUsages.primaryButton, out bool reset);
        resetCarPosition = reset;

Unity Scene Setup

To get started with Unity XR setup I followed this tutorial. Once setup it is just moving assets around and getting a game scene setup which for me looks like this:

Low Poly Weekend Car Game Scene View

In the above photo you can see this black box with a car in it and a white track. On the plane infront of the user you can see the goal of the game and when you complete the challenge you are notified with ‘You Won’ on that same screen.

Conclusion

I am pretty sure this meets the criterion for a ‘game’ though it isn’t the most polished thing. I really enjoyed this process and though the number of options available when building games can feel overwhelming going into this process with a clear goal and limited expectations proved to make this a satisfying experience. The files for you to play this locally on your unity instance are below. Thank you for reading 🙂