Interactivity - A Parking Panic Concept
I have been seeing these ads come up for this game called "Parking Panic", and several of its clones. While ads are often just a nuisance, sometimes they can be interesting. I had never heard of the Parking Panic game before. The concept is simple: there's a parking garage with several cars that can move either horizontally or vertically, and the main car is the only one that can come out.
Your goal is to move your red car to the exit. Move the other cars and trucks out of the way so that you can reach it. To move any car, click and drag it forward or backward using your mouse.
The idea was simple enough I felt it was worth giving this a try on my own. At least the basic structure of the application.
Tool Inventory
The primary framework of choice for something like this was Vue JS. It made it super simple to get started with minimal configuration.
For the interaction, I had to do some research first and see what would be available to set up. I wanted a simple tool that could be implemented fairly easily within a Vue app. I decided to go with interact.js.
The Basics
So for the game to work and be similar to what I had seen in the ads and played online (for research purposes), I had to first come up with some clear parameters.
Parking Lot
The parking lot was the easiest part to implement. I designed it to be a simple square box with some custom borders.
The code for the parking lot is pretty straightforward. I needed a wrapper div #parking-lot
which would be set up in this way:
#parking-lot {
width: 875px;
height: 875px;
background-color: #010101;
margin: 5px;
z-index: 10;
}
Nothing too crazy here, just setting up a div with 875px
height and width. However, setting the border was a little tricker than I had anticipated. I wanted something a bit more custom than a solid
border that would go all the way around, or a dotted
border. I wanted something that would look closer to the game and ultimately more like a parking lot.
#parking-lot {
/* previous code */
--b: 5px; /* thickness of the border */
--c: yellow; /* color of the border */
--w: 38%; /* width of border */
border: var(--b) solid transparent;
--_g: #0000 90deg, var(--c) 0;
--_p: var(--w) var(--w) border-box no-repeat;
background:
conic-gradient(from 90deg at top var(--b) left var(--b), var(--_g)) 0 0 / var(--_p),
conic-gradient(from 180deg at top var(--b) right var(--b), var(--_g)) 100% 0 / var(--_p),
conic-gradient(from 0deg at bottom var(--b) left var(--b), var(--_g)) 0 100% / var(--_p),
conic-gradient(from -90deg at bottom var(--b) right var(--b), var(--_g)) 100% 100% / var(--_p);
}
There is not a lot of logic to deal with the overall feel and structure of the game. That will be addressed in the next section.
Cars
Once the parking lot was complete I had to start adding cars. The car was built on a simple div
element.
<div
:id="id"
class="draggable-car"
:data-orientation="orientation"
:data-relative-position-x="props.relativePosition.x"
:data-relative-position-y="props.relativePosition.y"
>
<div v-if="debug">
<p>{{ id }}</p>
<p>orientation: {{ orientation }}</p>
<p>value: {{ currentValue }}</p>
<p>x: {{ props.relativePosition.x }} / y: {{ props.relativePosition.y }}</p>
</div>
</div>
The building blocks of the cars involved almost everything in terms of configuration passed in as a prop:
const props = defineProps({
id: {
type: String,
required: true
},
relativePosition: {
type: Object,
default: () => ({ x: 0, y: 0 })
},
carPosition: {
type: Number,
default: 0
},
vertical: {
type: Boolean
},
large: {
type: Boolean,
default: false
},
winningCar: {
type: Boolean,
default: false
}
})
id
: For the cars to be distinguished I generated a random unique ID for each car, the only purpose was for the parking lot to know which car was which.relativePosition
: The relative position value would determine the startingx
andy
points of each car within the parking lot.carPosition
: The car position is a percentage of 0-100 of how far along the car would be. For horizontal cars that is measured from left to right, and for the vertical cars that is measured from the bottom to the top.vertical
: A bool value to indicate whether the car needs to go vertically, by default all cars go horizontally.large
: Another bool value, this would distinguish between a normal size car and an oversized car (possibly a truck).winningCar
: A bool value to determine if the car would be allowed to win the game.
With the values out of the way, I could start fleshing out a little more of the underlying logic for the car:
onMounted(() => {
setSize(props.large, props.vertical)
setRelativePosition(props.relativePosition.x, props.relativePosition.y)
setCarPosition(props.carPosition, props.vertical)
})
When the car is mounted in the browser we need to set up some values to get the HTML in the DOM.
setSize()
This method will do two things, it will set up the container for the car. The container is the line in which the car will be allowed to move back and forth. So the two parameters passed here are important, for a large
container I wanted to make it wider, and also know which direction the car would be facing.
setRelativePosition()
Once the container and the car had been set up it would be a matter of actually placing it within the parking lot parent div. This is a one-liner.
const setRelativePosition = (x: number, y: number): void => {
getElement().style.transform = `translate(${x}px, ${y}px)`
}
setCarPosition()
Finally, once the container and the car were set up and placed in the parking lot, I needed to place it within the container's percentage.
Here I needed to use a bit more math and logic along with interact.js
to tie everything together.
Looking at the logic to set the position for a horizontal car within our parking lot looks like this:
const setCarPosition = (value: number, isVertical: boolean) => {
// ... code
if (isVertical) {
// ... code
} else {
const carWidth = parseFloat(getElement().style.getPropertyValue('--car-width'))
const carWidthPercentage = carWidth / interact.getElementRect(getElement()).width
const valueWithCarWidth = value - carWidthPercentage
getElement().style.paddingLeft = valueWithCarWidth * 100 + '%'
}
// ... more code
}
Here I first need to get the width of the car, which is set as a CSS variable. Then I divide the car's width by the width of the entire element. Finally, I can subtract the desired starting percentage (value
) from the car's width percentage, and apply the difference in padding to the left.
Interact.js Library
With all of the above components in place, I was ready to move to the interactive part of the code. The [library's documentation](library's documentation) was great and made it fairly straightforward to understand what was going on and how to get started.
interact('#' + id).draggable({
origin: 'self',
inertia: true,
listeners: {
move(event) {
const isVertical = orientation.value === 'vertical'
const sliderWidth = interact.getElementRect(event.target).width
const sliderHeight = interact.getElementRect(event.target).height
const sliderDimensions = isVertical ? sliderHeight : sliderWidth
const pagePosition = isVertical ? event.pageY : event.pageX
const value = pagePosition / sliderDimensions
setCarPosition(value, isVertical)
}
},
modifiers: [
interact.modifiers.restrict({
restriction: 'self',
endOnly: false
})
]
})
Then when using the interact library I could take all of the values and using their move
event, call the setCarPosition
method, which is just the one-liner to transform
the x
and y
axis position. That way the car would be allowed to move within itself.
The Result
The final result ended up being fairly polished for a proof-of-concept of making this Parking Panic game using only web technologies. This also gave me a chance to explore interactivity a bit deeper than I had before. With some unique use-cases that, while not super useful in what I have done for work, still interesting to know it exists and I can leverage it for future use.