To reiterate from my previous godot related post (asteroids). I'm no expert with the Godot Engine; I'm just documenting my journey as I learn here. My experience is mostly trial-and-error with lots of research along the way. Through this "log", I hope to share what I've learned so far and provide a starting point for others who may be in a similar situation.
If you want to try a HTML version of this project before reading all of this, you can find it at /games/ahballs. It is not exported with mobile browsers in mind and works best on a PC.
To see all code and download the project, take a look in my Azure repo at https://dev.azure.com/drygast/public/_git/GodotAhBalls, but I'll copy a few things here.
The game still need a bit of TLC, but I think I've learned what I want to learn from this rather quick project and leave it as is.
Loading Screen
Yup - I started this whole project since I wanted a proper loading screen that ran async and displayed the progress while loading occured.
I started hacking something together, but then I came across this video: How to Make a Loading Screen in Godot 4! by @Queble and used the concept described there. I only changed a few smaller things like fading to black aninmation instead of the move position animation so I don't think I should dump the entire code here - just watch the video instead!.
If you want to find the files in the code, these are the ones that are used to make the whole thing work:
- src/utils/scene_loader.gd
- scenes/utils/loading_screen.tscn
- src/utils/loading_screen.gd
The first time I use the class to do anything is in the main.gd
file by actually change scene SceneLoader.Load(Globals.MENU_SCENE_PATH)
to the menu.
Another thing that is needed to make this thing work is to add the files to the AutoLoader config in project settings:
And with that done I could continue with the actual game.
Suika
Suika is a refreshing and addictively fun game where players drop fruits into a bowl and combines them to evolve into a larger fruit. If any fruit overflows the bowl - the game is over. The goal is to get as high score as possible, but a first sub-goal is to get all the way to the final fruit - the watermelon (Suika in Japanese).
I watched some streamers struggle with the game and I though - how hard could it be? I played a couple of clones and realised that it was actually quite hard. Luck also had a lot to do with it, but anyway - it was fun (and a bit annoying at times) to play. Loved it.
So I wanted to make my own clone of the whole thing, but did not want to use fruits. Balls was a better idea. They are (mostly) round and exists in many different sizes.
With my "impressive" inkscape skills, here is the base vector-graphics that I made:
I know - it's not great, but it will do for this small test-project.
When creating the different spart-balls above, I started off beleiving that when 2 balls combined - the resulting merged ball would be double the area, but that is not the case. Turns out that in the original game, the resulting fruit have a smaller area than the 2 combined fruits. I tried to find the magic ratio by measuring and calculating, but finally gave up and simply measured each fruit from the screen and created my sport-balls with the same difference in size as the original game.
Ah Balls!
Another way of saying, ah shit, or oh f**k, or godf**kindammit
Not sure why I chose that name, but whatever... Anyway - I now had a fancy scene loader and some basic graphics so it was time to start coding for reals. I created the game scene and made sure I could load it from the menu. Then I kind of just dumped a lot of things in there. I knew this was gaing to be a small game so I did not really care about dividing scenes and code into smaller and more managable parts.
The bowl is made out of a sprite in the background and then 3 StaticBody2D
with rectangular CollisionShape2D
attached to them.
A cloud that is used to indicate where the ball-spawner is was also created as a simple sprite that moced according to the mouse pointer position (but only within the bowl constraints).
And then I needed a node to spawn the balls in the scene, and that is the Balls (Node2D
) node.
Next I created some basic classes for each individual type of sportsball and then the ball spawner code:
func _process(_delta):
if (Globals.gameState == Globals.GameState.RUNNING):
var xPos = get_viewport().get_mouse_position().x
if xPos < 434:
xPos = 434
if xPos > 846:
xPos = 846
cloud.position.x = xPos
if(Input.is_action_just_pressed("Left Mouse Button")):
Logger.LogDebug("Game:_process() - Left Mouse Button")
_addNewBall(xPos, 97)
func _addNewBall(xPos, yPos):
Logger.LogDebug("Game:_addNewBall()")
var newBall = BALL_SQUASH_SCENE.instantiate()
newBall.position.x = xPos
newBall.position.y = yPos
balls.add_child(newBall)
OK - so what happens here? We basically use the position of the cloud to create the new ball (squash type for now) and position it at the same position. We also need some functionality for the ball itself in order to make the ball be affected by gravity.
Here is the code of the squash ball at this point in time:
extends RigidBody2D
class_name BallSquash
@onready var sprite_2d = $Sprite2D
const BallDiameter:int = 32
const BallIndex:int = 0
func getClassName(): return "BallSquash"
var screen_size
func _init() -> void:
Logger.LogDebug("BallSquash:_init()")
max_contacts_reported = 16
contact_monitor = true
func _ready():
Logger.LogDebug("BallSquash:_ready()")
screen_size = get_tree().get_root().size
func _process(delta):
if (Globals.gameState == Globals.GameState.GAME_OVER):
return
func _on_body_entered(body):
# Logger.LogDebug("BallSquash:_on_body_entered(%s)" %body.get_class())
if (markedForDeletion):
return
match body.get_class():
"StaticBody2D":
return
"RigidBody2D":
if (getClassName() == body.getClassName()):
if (BallIndex<10): # BallIndex 10 = Max
pass # _mergeWith(body)
I also created some collision layers (BowlEdges, KillZone and Balls) and after that I'm pretty sure I could start dropping the balls into the bowl. A lot of things happens by default when using nodes such as StaticBody2D
and RigidBody2D
. There is also a lot of settings (such as gravity) to tweak if necessary.
The rest is pretty much creating signals, making sure that collisions between balls of the same type results in merge actions and such. I also created some basic UI stuff with game over screen and scores.
Instead of trying to cover every small thing I had to do, I will stop rambling now. The full (not optimized even the slightest) code is available for download if it might help someone, but beware - it's not my finest coding ever. The whole project is basically in the "it works" phase, and that is where I think I will keep it. If I ever were to actually publish something like this I would spend a lot of time to optimize and iron out the bugs.
Final thoughts
This is fun! No, really - I might just spend even more time with this in order to actually produce something that could eventuelly be seen as an actual game. First I have a few more experiments and things to figure out though, but my mind is starting to think that I might be able to produce something eventually.
The game is playable and the very basic physics and collision nodes in Godot were pretty easy to get started with. I'm sure I'm missing a ton of more advanced stuff and that with experience - things will get more complicated, but for now I'm really happy with how things worked ouut.
I hope you enjoy the game! (I still don't have a highscore over 3000)