Janbo's Towerdenfense
Hello everyone,
this is my entry for the current comunity competition.
I not only want to showcase the Project but explain a bit about what i did, maybe some sources to learn the algorithm's and methods i used, so some of you might learn one or two things from it.
So let me begin with what i was aiming for because i knew from the start that i wanted to make a TD that was more like the open world equivalent to TD games, which are part of the classical TD's I think.
So when I think of TD's, i think of Mods/Maps for Warcraft 3 especially those where you can create a labyrinth for the enemys to walk through as i liked them the most as a kid and add more variety because everyone is doing it a bit different.
So i Just started by creating a project with some standard files like the main.agc, menu.agc, game.agc, input.agc and core.agc.
The reason why i started with those is because i also knew it'll become to big of a project to not seperate the code properly.
So In main there is nothing except of the stuff the AppGameKit wizzard creates for you and a call to the Init function for the menu which is inside the menu.agc ofc.
Then Creating a menu i use a finite state machine which translates pretty much to select case and looks like this for my case:
#constant STATE_EXIT -1
#constant STATE_MAIN_MENU 0
#constant STATE_GAME_MENU 1
#constant STATE_GAME 3
#constant STATE_GAME_OVER 4
#constant STATE_OPTIONS 5
#constant STATE_HIGHSCORE 6
function Menu_Init()
do
Select MenuState
case STATE_EXIT
exit
endcase
case STATE_MAIN_MENU
Game_Exit()
MenuState=Menu_Main()
endcase
case STATE_GAME_MENU
MenuState=Menu_Game()
endcase
case STATE_GAME
MenuState=Game_Main()
endcase
case STATE_GAME_OVER
MenuState=Game_Over()
Game_Exit()
endcase
case STATE_OPTIONS
MenuState=Menu_Options()
endcase
case STATE_HIGHSCORE
MenuState=Menu_Highscore()
endcase
endselect
Sync()
loop
endfunction
function Menu_Main()
do
if GetRawKeyReleased(KEY_ESCAPE)
MenuState=STATE_EXIT
exit
endif
sync()
loop
endfunction MenuState
// add more menu's here
Here I hope you see how it is used and helps organizing the different menu branches, you also see that i use prefixs like "Menu_" and "Game_" which i use for all functions and variables that are meant to be used outide, kinda like the access level modifier "public" in other object orientated languages but also just to not run into a naming limit when i need multiple init functions for example as we don't have namespaces in AppGameKit Tier 1.
The core.agc which is a collection of functions i use often in my projects which I'm more than happy to share and most of them come from the comunity anyways:
// File: core.agc
type Core_Vec3Data
X# as float
Y# as float
Z# as float
endtype
type Core_Vec2Data
X# as float
Y# as float
endtype
type Core_Int3Data
X as integer
Y as integer
Z as integer
endtype
type Core_Int2Data
X as integer
Y as integer
endtype
function Core_StringInsertAtDelemiter(String$,Insert$,Delemiter$)
Left$=GetStringToken(String$,Delemiter$,1)
Right$=GetStringToken(String$,Delemiter$,2)
NewString$=Left$+Insert$+Right$
endfunction NewString$
function Core_CurveValue(current# as float, destination# as float, speed# as float)
local diff# as float
if speed# < 1.0 then speed# = 1.0
diff# = destination# - current#
current# = current# + ( diff# / speed# )
endfunction current#
function Core_CurveValueOnFrame(current# as float, destination# as float, speed# as float, frametime# as float)
local diff# as float
diff# = destination# - current#
current# = current# + ( diff# / speed# ) * frametime#
endfunction current#
function Core_CurveAngle(current# as float, destination# as float, speed# as float)
local diff# as float
if speed# < 1.0 then speed# = 1.0
destination# = Core_WrapAngle( destination# )
current# = Core_WrapAngle( current# )
diff# = destination# - current#
if diff# <- 180.0 then diff# = ( destination# + 360.0 ) - current#
if diff# > 180.0 then diff# = destination# - ( current# + 360.0 )
current# = current# + ( diff# / speed# )
current# = Core_WrapAngle( current# )
endfunction current#
function Core_CurveAngleOnFrame(current# as float, destination# as float, speed# as float, frametime# as float)
local diff# as float
destination# = Core_WrapAngle( destination# )
current# = Core_WrapAngle( current# )
diff# = destination# - current#
if diff# <- 180.0 then diff# = ( destination# + 360.0 ) - current#
if diff# > 180.0 then diff# = destination# - ( current# + 360.0 )
current# = current# + ( diff# / speed# ) * frametime#
current# = Core_WrapAngle( current# )
endfunction current#
function Core_WrapAngle( angle# as float) // xaby: that breakout 10000 seems like a bad style
local iChunkOut as integer
local breakout as integer
iChunkOut = angle#
iChunkOut = iChunkOut - mod( iChunkOut, 360 )
angle# = angle# - iChunkOut // xaby: is that complete function not possible with some math?
breakout = 10000
while angle# < 0.0 or angle# >= 360.0
if angle# < 0.0 then angle# = angle# + 360.0
if angle# >= 360.0 then angle# = angle# - 360.0
dec breakout
if breakout = 0 then exit
endwhile
if breakout = 0 then angle# = 0.0
endfunction angle#
function Core_ManhattanDistance2D(StartX,StartY,EndX,EndY)
DistX=abs(EndX-StartX)
DistY=abs(EndY-StartY)
Dist=DistX+DistY
endfunction Dist
function Core_Distance2D(StartX#,StartY#,EndX#,EndY#)
DistX#=EndX#-StartX#
DistY#=EndY#-StartY#
Dist#=sqrt(DistX#*DistX#+DistY#*DistY#)
endfunction Dist#
function Core_Distance3D(StartX#,StartY#,StartZ#,EndX#,EndY#,EndZ#)
DistX#=EndX#-StartX#
DistY#=EndY#-StartY#
DistZ#=EndZ#-StartZ#
Dist#=sqrt(DistX#*DistX#+DistY#*DistY#+DistZ#*DistZ#)
endfunction Dist#
function Core_Lerp(Start#,End#,Time#)
endfunction Start#+Time#*(End#-Start#)
function Core_InverseLerp(Start#,End#,Value#)
endfunction (Value#-Start#)/(End#-Start#)
function Core_Remap(InMin#,InMax#,OutMin#,OutMax#,Value#)
Time#=Core_InverseLerp(InMin#,InMax#,Value#)
Result#=Core_Lerp(OutMin#,OutMax#,Time#)
endfunction Result#
function Core_Clamp(Value#,Min#,Max#)
if Value#>Max# then Value#=Max#
if Value#<Min# then Value#=Min#
endfunction Value#
function Core_Sign(Value#)
Result = ((Value#>0)*2)-1
endfunction Result
function Core_FillTextEndWithSpaces(String$,MaxWidth#,Size#,Spacing#)
StringTextID=CreateText(String$)
SetTextSize(StringTextID,Size#)
SetTextSpacing(StringTextID,Spacing#)
StringWidth#=GetTextTotalWidth(StringTextID)
SpaceTextID=CreateText(" ")
SetTextSize(SpaceTextID,Size#)
SetTextSpacing(SpaceTextID,Spacing#)
SpaceWidth#=GetTextTotalWidth(SpaceTextID)
RemainingWidth#=MaxWidth#-StringWidth#
SpaceCount=round(RemainingWidth#/SpaceWidth#)
String$=String$+Spaces(SpaceCount)
DeleteText(StringTextID)
DeleteText(SpaceTextID)
endfunction String$
function Core_FillEndWithSpaces(String$,MaxLength)
Length=len(String$)
SpaceLength=MaxLength-Length
String$=String$+Spaces(SpaceLength)
endfunction String$
function Core_RequestString(String$)
EditBoxID=CreateEditBox()
SetEditBoxPosition(EditBoxID,25,50)
SetEditBoxSize(EditBoxID,50,15)
FixEditBoxToScreen(EditBoxID,1)
SetEditBoxDepth(EditBoxID,1)
SetEditBoxFocus(EditBoxID,1)
SetEditBoxText(EditBoxID,String$)
while GetEditBoxHasFocus(EditBoxID)
sync()
endwhile
String$=GetEditBoxText(EditBoxID)
DeleteEditBox(EditBoxID)
endfunction String$
The Game.agc is selfexplanatory i hope.
Now I dive directly into the level creation/generation cause my idea was to let the levels be generated differently for every single game session.
As the grapth to store the level i went for the easiest which is just a grid and pretty much translates to a 2D array where i can reference the different sprites/tiles, that way it will also be easy to work with later.
If you want to create a game with many sprites like i do/did i advise you to use atlas textures which you can load a single image using
LoadSubImage()
To generate the map i knew i needed some sort of noise algorithm and we have a great comunity which already created a few noise librarys in the past and altered and trimed the code to my likings.
I also needed a way to dynamically select the single tiles frome the tile sheet/atlas texture which you can do with an auto tiling function maybe you can call it algorithm if you feel fancy but in the end you just do some bitmasking.
Here is
the best website for tiling methods i could find on the Interwebz:
cr31 Home
So loaded some media from
Kenney's Free Assets used the noise library to generate a grid of empty and solid cells and placed the tiles using the 2-corner Wang Tile autotiling method.
This is how the autotiling looked like with a totally randomized level:
I used the perlin noise algorithm to tell my grid where to create solids/grass and where to create the path.
Then I added my flow field pathfinding library to work on the exact same 2D Array i use for the Tiles just with some additional attributes.
When I made my Library from like almost decades ago i used this website to learn from:
Red Blob Games
The flow field pathfinding method is espacally usefull for this usecase as it doesnt need to calculate a path for every enemy but only once for every change in the graph and the enemys just follow the destination cell's stored in each cell of the grapth/grid/2DArray.
My library is also capable to calculate the path and distance to not only one destination but multiple destinations so it would allow for more complex levels after this competition
With the help of the pathfinding i could look for the enemy spawn positions, which i did by just iterating from all directions to the center of the level and just take the first cell that was reachable by the Pathfinder, but you can imagine all sorts of automated spawn placement methods with all those informations i have at this time.
And this is what it looked like after I made sure there is space around the destinations, added the enemy spawns, some foliage and added something i'd call vignette to the noise algorithm so it makes less and less path cells the further i reach the limit of the level until i just create a completely solid border around it:
After that the progression curve was pretty steep as i had the backbone ironed out already and could dive into enemy's and towers.
The Enemys, again all assets from Kenney, only needed to follow each cells destination cell with a smooth turn rate realized with the Core_CurveAngle() function from the core.agc
Now for the towers to aim and shoot at the enemys i had to get more creative as it is easy to tell that hundreds of towers and enemys checking for each other will be quite CPU demanding especally for the slow itteration speeds of AppGameKit Tier1.
So I had to come up with something that reduces the itterations from each tower to each enemy in range.
I tried manny things including Quadtrees but i setteled with partitioning the small cells into larger cells like minecraft has many blocks inside a chunck.
I made the Chunks larger than the maximum range of the towers so in the worst case the tower with the largest range only had to check for enemys inside 4 chunks.
Which i think is a good solution considereing i can make over 300 towers aim at over 1k Enemys at over 60 FPS ...on my PC and in Tier 1 atleast:
Then i added some graphical user interrfaces, infos and a digging feature as i had all the bacebone code for it in place and makes for a more interesting gameplay i think:
By now i have a Highscore list which is hosted on my Raspberry Pi i setup for this purpose only.
I installed Raspbian, apache, mariaDB, PHP and Adminer.
The updates to the Highscore are done using a set and get php script which looks like this:
<?php
$action = $_POST["action"];
if($action=="insert")
{
$pdo = new PDO($dsn, $myUser, $myPassword);
$stmt = $pdo->prepare("INSERT INTO myTablename (name, ...) VALUES (:name, ...)");
$stmt->bindParam(':name', $name, PDO::myDATATYPE, myLength);
$name = $_POST["name"];
$stmt->execute();
echo $pdo->lastInsertId();
}
else($action=="other actions...")
{
do other actions here...
}
?>
The highscore list is only updated if you beat your own local record and can be watched in the game menu or via
website
All images where taken at the day and time after i reached a milestone.
i still need to add some tutorual messages and options in the option menu.
Every tower and enemy attribute maybe even the level generation is still subject to change and Im more than happy if you make suggestions for all and everything.
Happy Testing !
I hope to see you on the highscore list
Janbo's Towerdefense on itch.io