This article is a part of my full LaTeX guide here article
Here some captions of the game:
How did i create the game ?
First, the architecture.
We have a bash loop script (loop.sh) that waits for inputs.
When it receives input, it updates a state.tex variables.
This file is in fact an \input{...} of the main LaTeX file main.tex.
We won't focus on the bash file, but here is its code:
#!/bin/bash
# --- INPUT SETUP ---
exec 3</dev/tty
stty -icanon -echo
trap "stty sane" EXIT
# --- STATE ---
step=0
scaleval=0.5
shoot=0
demon=0
gameover=0
lst="0,1,2"
score=0
# --- SCALE PER STEP ---
update_scale() {
case $step in
0) scaleval=0.5 ;;
1) scaleval=0.9 ;;
2) scaleval=1.4 ;;
esac
}
# --- DEMON SPAWN ---
spawn_demon() {
# 30% chance
if (( RANDOM % 100 < 70 )); then
demon=1
echo "DEMON SPAWN"
else
demon=0
fi
}
# --- RESET ---
reset_game() {
step=0
shoot=0
demon=0
gameover=0
turndir=0
lst="0,1,2"
score=0
update_scale
}
# --- WRITE STATE ---
write_state() {
cat > state.tex <<EOF
\def\step{$step}
\def\lst{$lst}
\def\scaleval{$scaleval}
\def\shoot{$shoot}
\def\demon{$demon}
\def\gameover{$gameover}
\def\score{$score}
\def\turndir{$turndir}
EOF
}
# --- RENDER ---
render() {
pdflatex -interaction=nonstopmode main.tex > /dev/null < /dev/null
}
# --- INIT ---
reset_game
update_scale
write_state
render
while true; do
update=0
key=""
read -u 3 -rsn1 -t 0.1 key
# reset shoot each frame
shoot=0
turndir=0
case "$key" in
k) # forward
if [ "$gameover" -eq 0 ]; then
if [ "$step" -lt 2 ]; then
step=$((step + 1))
echo "FORWARD -> step=$step"
if [ "$step" -eq 2 ]; then
spawn_demon
fi
fi
fi
update=1
;;
j) # backward
if [ "$gameover" -eq 0 ]; then
if [ "$step" -gt 0 ]; then
# leaving step 2 -> check demon
if [ "$step" -eq 2 ] && [ "$demon" -eq 1 ]; then
echo "DEMON GOT YOU"
gameover=1
fi
step=$((step - 1))
echo "BACK -> step=$step"
fi
fi
update=1
;;
l) # right turn
if [ "$step" -eq 2 ] && [ "$gameover" -eq 0 ]; then
if [ "$demon" -eq 1 ]; then
echo "TURNED WITH DEMON -> DEAD"
gameover=1
else
echo "TURN RIGHT"
turndir=2
write_state
render
turndir=0
sleep 0.3
step=0
fi
fi
update=1
;;
h) # left turn
if [ "$step" -eq 2 ] && [ "$gameover" -eq 0 ]; then
if [ "$demon" -eq 1 ]; then
echo "TURNED WITH DEMON -> DEAD"
gameover=1
else
echo "TURN LEFT"
turndir=1
write_state
render
turndir=0
sleep 0.5
step=0
fi
fi
update=1
;;
s) # shoot
if [ "$step" -eq 2 ] && [ "$demon" -eq 1 ]; then
echo "DEMON KILLED"
demon=0
shoot=1
score=$((score+1))
fi
update=1
;;
r)
echo "RESET"
reset_game
update=1
;;
q)
echo "QUIT"
exit 0
;;
esac
if [[ "$step" -eq 0 ]]; then
lst="0,1,2"
fi
if [[ "$step" -eq 1 ]]; then
lst="0,1"
fi
if [[ "$step" -eq 2 ]]; then
lst="0"
fi
if [[ "$update" -eq 1 ]]; then
update_scale
write_state
render
sleep 0.1
fi
done
Now the LaTeX file.
\documentclass{article}
\usepackage{tikz}
\usepackage{pgfplots}
\usepackage{xcolor}
\usepackage{graphicx}
\pgfplotsset{compat=1.18}
\pagecolor{red!20!black}
\begin{document}
\input{state.tex}
\begin{center}
\begin{tikzpicture}[scale=\scaleval]
\pgfmathsetmacro{\W}{15}
\pgfmathsetmacro{\H}{8}
%\fill[red!20!black] (-0.0 * \W, -0.9 * \H) rectangle (\W + 0.0 * \W, \H + 0.9 * \H);
\pgfmathsetmacro{\N}{9 - 2*\step}
\pgfmathsetmacro{\cell}{\W / \N}
\pgfmathsetmacro{\mid}{(\N - 1) / 2}
\pgfmathsetmacro{\xA}{\mid * \cell}
\pgfmathsetmacro{\xB}{(\mid + 1) * \cell}
% make it square
\pgfmathsetmacro{\size}{\xB - \xA}
\pgfmathsetmacro{\yA}{(\H - \size)/2}
\pgfmathsetmacro{\yB}{\yA + \size}
\draw[thick, fill=red!15!black] (\xA,\yA) rectangle (\xB,\yB);
% drawing wall
\pgfmathsetmacro{\sizeRef}{\size}
\pgfmathsetmacro{\xALast}{\xA}
\pgfmathsetmacro{\xBLast}{\xB}
\pgfmathsetmacro{\yALast}{\yA}
\pgfmathsetmacro{\yBLast}{\yB}
\foreach \i in \lst {
\pgfmathsetmacro{\offset}{\size * (2^\i)}
\pgfmathsetmacro{\yext}{\offset / 2}
\ifcase\i
\def\wallcolor{black}
\else
\pgfmathsetmacro{\shade}{int(2 + 15*\i)}
\edef\wallcolor{red!\shade!black}
\fi
% LEFT
\pgfmathsetmacro{\xAi}{\xALast - \offset}
\fill[fill=\wallcolor, draw=black]
(\xAi,\yALast-\yext)
-- (\xAi,\yBLast+\yext)
-- (\xALast,\yBLast)
-- (\xALast,\yALast)
-- cycle;
% RIGHT
\pgfmathsetmacro{\xBi}{\xBLast + \offset}
\fill[fill=\wallcolor, draw=black]
(\xBi,\yALast-\yext)
-- (\xBi,\yBLast+\yext)
-- (\xBLast,\yBLast)
-- (\xBLast,\yALast)
-- cycle;
% GROUND
\fill[fill=red!20!black, draw=black]
(\xALast,\yALast)
-- (\xBLast,\yALast)
-- (\xBi,\yALast - \yext)
-- (\xAi,\yALast - \yext)
-- cycle;
% CEILING
\fill[fill=red!20!black, draw=black]
(\xALast,\yBLast)
-- (\xBLast,\yBLast)
-- (\xBi,\yBLast + \yext)
-- (\xAi,\yBLast + \yext)
-- cycle;
\pgfmathparse{\xALast - \offset}
\xdef\xALast{\pgfmathresult}
\pgfmathparse{\xBLast + \offset}
\xdef\xBLast{\pgfmathresult}
\pgfmathparse{\yALast - \yext}
\xdef\yALast{\pgfmathresult}
\pgfmathparse{\yBLast + \yext}
\xdef\yBLast{\pgfmathresult}
}
\pgfmathsetmacro{\weaponY}{\yALast / \scaleval}
\pgfmathsetmacro{\demonY}{\weaponY + 0.1 * \H}
\ifnum\demon=1
\node at ({\W/2}, {\demonY}) {
\includegraphics[width=6 cm]{demon.png}
};
\fi
\ifnum\shoot=0
\node at ({\W/2}, {\weaponY}) {
\includegraphics[width=6 cm]{weapon.png}
};
\else
\node at ({\W/2}, {\weaponY}) {
\includegraphics[width=6 cm]{weapon_shoot.png}
};
\fi
\ifnum\gameover=1
\node[white, scale=3] at ({\W/2}, {\H/2}) {GAME OVER};
\node[red!70!black, scale=3] at ({\W/2}, {\H/2 + 1}) {SCORE: \score};
\fi
\ifnum\turndir=1
\fill[white]
({\W/2 - 2}, {\H/2})
-- ({\W/2}, {\H/2 + 1})
-- ({\W/2}, {\H/2 - 1})
-- cycle;
\fi
\ifnum\turndir=2
\fill[white]
({\W/2 + 2}, {\H/2})
-- ({\W/2}, {\H/2 + 1})
-- ({\W/2}, {\H/2 - 1})
-- cycle;
\fi
\end{tikzpicture}
\end{center}
\end{document}
There is really no new thing when it comes to the synthax apart from:
\pagecolor{red!20!black}
Allowing to change the ENTIRE background color of the document --> redish here.
All is based on vanishing line to create depth illusion.
So the basic idea is to draw a centered square, and 4 vanishing lines that starts at each square corner and that ... vanishes to the exterior.
-
top-left corner -> vanishing line that goes to top-left
-
top-right corner -> vanishing line that goes to top-right
-
bottom-left corner -> vanishing line that goes to bottom-left
-
bottom-right corner -> vanishing line that goes to bottom-right
First, we declare a Width (W) and a Height (H) variables, assign them respectively max Width and respectable Height value.
\pgfmathsetmacro{\W}{15}
\pgfmathsetmacro{\H}{8}
After that we calculates N.
\pgfmathsetmacro{\N}{9 - 2*\step}
What is N ?
Its value whole purpose is to compute the coordinates of the centered square.
I explain.
If you are in front of a wall, it will apear bigger (width and height proportionally) than when you are farer from it.
Because we will use N as:
\pgfmathsetmacro{\cell}{\W / \N}
Which gives us width of units (called \cell) we can work with.
The more there are units, the less wide they become (\W is a constant = 15).
Also, note that the more steps we have done, the closer we get to the wall / square.
Then, the lower is N the less units are and they are wider.
I spoke about units, so now, the trick is to compute the middle that tells, how many units i need to get to the middle unit.
\pgfmathsetmacro{\mid}{(\N - 1) / 2}
Here is the thing, i do not mean middle, i said the unit that is in the middle, like a median.
This is key, because the start coordinate of the middle unit when i'm close to the wall (so step is high), is the coordinate of one of the firsts units when i'm far from the wall (step made lower).
After realizing that, it's simple, i just compute the x coordinate of the startig unit and ending x of the width --> x start and end of the square.
\pgfmathsetmacro{\xA}{\mid * \cell}
\pgfmathsetmacro{\xB}{(\mid + 1) * \cell}
You see, x end of the square is just the x start of the next width unit.
(\mid + 1) * \cell
So, the size of the edges of the square is just the difference.
\pgfmathsetmacro{\size}{\xB - \xA}
Now we compute y start and y end.
\pgfmathsetmacro{\yA}{(\H - \size)/2}
\pgfmathsetmacro{\yB}{\yA + \size}
So (\xA,\yA) is actually bottom-left of the square.
After that, we draw the square.
\draw[thick, fill=red!15!black] (\xA,\yA) rectangle (\xB,\yB);
Wall - Vanishing lines
Now most interesting part.
Look at the state.tex file:
\def\step{1}
\def\lst{0,1}
\def\scaleval{0.9}
\def\shoot{0}
\def\demon{1}
\def\gameover{1}
\def\score{2}
\def\turndir{0}
What is important is the evolution of \step, \lst and \scaleval
For \step == 0.
\scaleval= 0.5\lst= 0,1,2 --> 3 walls
For \step == 1.
\scaleval= 0.9\lst= 0,1 --> 2 walls
For \step == 2.
\scaleval= 1.4\lst= 0,1,2 --> 1 wall
\lst is just a list of numbers, what is important is its length, so it defines the iteration number in the next loop.
\scaleval is also important, because wo so not want the user to zoom in the PDF viewer (like zathura) we scale appropriately the scenes.
The \scaleval are "trust me bros" variables, but works.
The only "trust me bro" part in this article :)
Now the drawing walls part.
\pgfmathsetmacro{\xALast}{\xA}
\pgfmathsetmacro{\xBLast}{\xB}
\pgfmathsetmacro{\yALast}{\yA}
\pgfmathsetmacro{\yBLast}{\yB}
\foreach \i in \lst {
\pgfmathsetmacro{\offset}{\size * (2^\i)}
\pgfmathsetmacro{\yext}{\offset / 2}
\ifcase\i
\def\wallcolor{black}
\else
\pgfmathsetmacro{\shade}{int(2 + 15*\i)}
\edef\wallcolor{red!\shade!black}
\fi
% LEFT
\pgfmathsetmacro{\xAi}{\xALast - \offset}
\fill[fill=\wallcolor, draw=black]
(\xAi,\yALast-\yext)
-- (\xAi,\yBLast+\yext)
-- (\xALast,\yBLast)
-- (\xALast,\yALast)
-- cycle;
% RIGHT
\pgfmathsetmacro{\xBi}{\xBLast + \offset}
\fill[fill=\wallcolor, draw=black]
(\xBi,\yALast-\yext)
-- (\xBi,\yBLast+\yext)
-- (\xBLast,\yBLast)
-- (\xBLast,\yALast)
-- cycle;
% GROUND
\fill[fill=red!20!black, draw=black]
(\xALast,\yALast)
-- (\xBLast,\yALast)
-- (\xBi,\yALast - \yext)
-- (\xAi,\yALast - \yext)
-- cycle;
% CEILING
\fill[fill=red!20!black, draw=black]
(\xALast,\yBLast)
-- (\xBLast,\yBLast)
-- (\xBi,\yBLast + \yext)
-- (\xAi,\yBLast + \yext)
-- cycle;
\pgfmathparse{\xALast - \offset}
\xdef\xALast{\pgfmathresult}
\pgfmathparse{\xBLast + \offset}
\xdef\xBLast{\pgfmathresult}
\pgfmathparse{\yALast - \yext}
\xdef\yALast{\pgfmathresult}
\pgfmathparse{\yBLast + \yext}
\xdef\yBLast{\pgfmathresult}
}
In fact, it computes x offset and y offset that is the half of the first.
x offset evolves as 2^step, and that is just the offset from the last wall ! --> Strong vanishing lines
\pgfmathsetmacro{\offset}{\size * (2^\i)}
\pgfmathsetmacro{\yext}{\offset / 2}
"Why are they computed ?"
To compuet wall coordinates and drawing them.
% LEFT
\pgfmathsetmacro{\xAi}{\xALast - \offset}
\fill[fill=\wallcolor, draw=black]
(\xAi,\yALast-\yext)
-- (\xAi,\yBLast+\yext)
-- (\xALast,\yBLast)
-- (\xALast,\yALast)
-- cycle;
After that, the variables are of course updated thanks to \pgfmathparse that alows to compute formulas, and \xdef for the definition.
\pgfmathparse{\xALast - \offset}
\xdef\xALast{\pgfmathresult}
\pgfmathparse{\xBLast + \offset}
\xdef\xBLast{\pgfmathresult}
\pgfmathparse{\yALast - \yext}
\xdef\yALast{\pgfmathresult}
\pgfmathparse{\yBLast + \yext}
\xdef\yBLast{\pgfmathresult}
We also compute a color gradient of the right and left walls by their depth - step made.
\ifcase\i
\def\wallcolor{black}
\else
\pgfmathsetmacro{\shade}{int(2 + 15*\i)}
\edef\wallcolor{red!\shade!black}
\fi
At this point, i discovered that if i do the same for the top and bottom, it really gets awful visually, so i keep the same color for whole bottom and top and just apply gradient for walls.
For other stuffs.
We got some basics boolean logic.
Add the deamon png.
\ifnum\demon=1
\node at ({\W/2}, {\demonY}) {
\includegraphics[width=6 cm]{demon.png}
};
\fi
When firering the weapon, we replace the png for certain amounts of frames by the a variant png.
\ifnum\shoot=0
\node at ({\W/2}, {\weaponY}) {
\includegraphics[width=6 cm]{weapon.png}
};
\else
\node at ({\W/2}, {\weaponY}) {
\includegraphics[width=6 cm]{weapon_shoot.png}
};
\fi
Same for making appear a left or right arrow when choosing a direction.
\ifnum\turndir=1
\fill[white]
({\W/2 - 2}, {\H/2})
-- ({\W/2}, {\H/2 + 1})
-- ({\W/2}, {\H/2 - 1})
-- cycle;
\fi
\ifnum\turndir=2
\fill[white]
({\W/2 + 2}, {\H/2})
-- ({\W/2}, {\H/2 + 1})
-- ({\W/2}, {\H/2 - 1})
-- cycle;
\fi
And gameover screen, with score.
\ifnum\gameover=1
\node[white, scale=3] at ({\W/2}, {\H/2}) {GAME OVER};
\node[red!70!black, scale=3] at ({\W/2}, {\H/2 + 1}) {SCORE: \score};
\fi
Hope you found this article useful !
!! Ciao Ciao !!