As promised, I've been working on a few visual things! There's not that much to show yet, but I have a model or two to show, plus a lot more to talk about in terms of where I want to take the game.
Before this, though, here's some progress in the coding department (that's where I'm best at):
Behind-the-scenes, I've been steadily working at overhauling and refactoring some of my code. I wrote the majority of my game's scripts 6 to 12 months ago, and some of it hasn't aged that well. Complicated code structures and wonky node paths such as get_node("/root/Main/LevelParent/Level/Camera/AimRayCastContainer/AimRayCast3D")
aren't things I want to maintain, so I'm trying my best to implement more best-practices – or at least better-practices.
One challenge I faced was referencing a node that has no clear direct relationship to the one I'm calling it from. Picture this node chain:
Say, for example, that I'm writing a script in Automatic
– that's the node that handles projectile spawning and their direction in automatic weapons, among other things – and I need to reference AimRayCast3D
to determine in which direction the projectile needs to be fired. Reasonable use case, seen as the RayCast3D
is attached to the Camera3D
to point exactly where the player is facing – except there's no real path I can follow to get to that node. Their closest relative is Level
, which is the root node of the level, and no less than eight nodes away from Automatic
! It's possible to retrieve the node in this way, starting from Automatic
, but it's not pretty...
var _ray_cast = get_node("../../../../../../../../Camera/AimRayCastContainer/AimRayCast3D")
Imagine if any of these nodes has their name changed – or worse, if the order of nodes is changed. It could spell disaster. This method grabs the node relative to Automatic
, which means it relies on this specific count of nodes as well as the names of the nodes, starting at Camera
. We could switch this around:
var _ray_cast = get_node("/root/Main/LevelParent/Level/Camera/AimRayCastContainer/AimRayCast3D")
...but now we're reliant on the order starting from the root node (from the top), plus we now have to be wary of even more node names, so this is no good either. For quick testing, sure, but not to be implemented in a production release – hopefully. How do we fix this? I thought of using a singleton script that holds references to the most important nodes (level parent, player, camera) to retrieve nodes relative to them:
var _ray_cast = GlobalReferences.camera.get_node("AimRayCastContainer/AimRayCast3D")
This is a lot more streamlined, but... it doesn't work. You need to initialise the camera
variable at some point after the level has loaded, but before the variable is used. If you use the variable in any _ready()
call, then you're hosed, because you must initialise the variables in the level's _ready()
and you cannot be certain that the level has its _ready()
function called in time for the other node to retrieve a proper reference in camera
instead of null
.
Instead, we can use the function get_first_node_in_group()
on the SceneTree
to retrieve any node! We just have to make sure that the node has a group name that is unique to it within the SceneTree
– in the case of a level camera, that's a given. Having multiple nodes with this group name is also possible by using get_nodes_in_group()
and then filtering the returned array by, for example, checking its class name.
var _ray_cast = get_tree().get_first_node_in_group("level_camera").get_node("AimRayCastContainer/AimRayCast3D")
Almost there! We can pretty this up by adding two more things:
"level_camera"
into a constant. I did this by creating a global class called Groups
and adding:const LEVEL_CAMERA: String = "level_camera"
get_node("AimRayCastContainer/AimRayCast3D")
call and replace it with a variable call. Inside the camera's script, we can add a variable referencing AimRayCast3D
quite easily because it's a child of the camera. Add to the camera script: @onready var aim_ray_cast: RayCast3D = $AimRayCastContainer/AimRayCast3D
camera.aim_ray_cast
to retrieve it. Remember to add @onready
because the node cannot be retrieved before _ready()
is called.In the end, we have the following statement:
var _ray_cast = get_tree().get_first_node_in_group(Groups.LEVEL_CAMERA).aim_ray_cast
We can use this anywhere in our game to retrieve the node, as long as it exists within the scene tree!
A while back, ButtMuncher7 on YouTube uploaded a video on projectiles in video games. They use the Godot engine as well, so anything they implement, I can implement as well. The issue they faced was that projectiles often fly through walls a bit before colliding, since they may move past an object's surface between frames. I finally got around to implementing their solution, which worked quite well: you add a raycast to the projectile pointing from its current location to its last location, see if collides with anything, and if it does, you move the projectile to that position. It's quite a neat solution!
I changed the sky to be brighter and more friendly in the Unity test level. This doesn't serve the game in its final form in any way, it's just a measure I took to give myself the impression that a lot has changed, since the game has visually changed.
Interestingly though, this did give me ideas. This sky shader has a setting for cloud fuzziness, which, when turned down, gives the clouds a more toon-like aesthetic (pictured here). This is great, because this is actually an aesthetic direction I had recently decided to pursue with Project N5.
I discovered this insanely cool planet generator by Deep-Fold on itch.io. It's a pixel planet generator, strictly speaking, but since its output is just determined by generated noise (I think), the resolution can be increased much further to create sharp toon-like planets. Here's one I generated that I quite liked:
This shader made me think that I really do want planets to be present in my game similar to how Ratchet & Clank does it – on the starmap as well as when flying towards the planets. Maybe you could even see a planet if you look into the sky from another planet's surface?
I'm unsure how I feel about using someone else's generator in my own game, though, so I followed a tutorial I found in the Godot documentation by coincidence – I wanted to look up something for an entirely different game. The result:
...it's a work-in-progress. A relatively decent start though! I think there's potential.
The debate of "should I use someone else's work in my own?" is one I find myself debating frequently, actually. It's always been a problem with me, and I think it started with using other people's samples and synth patches in my music, which I did do, but tried to avoid rather frequently. This sentiment still holds true to this day, whether it's in music, 3D modelling, or... literally any of the rubbish generated by AI these days. I think a healthy dose of attempting to create things on your own is good though, because it allows you to both expand your own skillset and adapt the work to your own vision much more easily than if you had to modify an existing work you didn't create.
No harm in studying other people's work though – in fact, I'd say that's super beneficial! Don't fall into the trap of "good artists copy, great artists steal"; go for "great artists are inspired by other people's works and incorporate this into their own works" instead. Doesn't roll off the tongue quite as nicely though.
Continuing the streak of visual elements, I worked on the N5 Blaster... again. This time I didn't tweak its visuals much, however; I modelled it from scratch to fix its terrible meshes.
Back when I modelled the N5 Blaster around a year ago, I didn't have much experience in using Blender. Thus, it wasn't modelled very well. The model consisted of 8 parts for the body, 3 or 4 parts for the grip, and the icosphere spinning in the middle of the gun. My goal today was to recreate the N5 Blaster with a more streamlined mesh, and I must say, I achieved my goal quite well: the gun now consists of one mesh for the body, one for the grip, and another for the icosphere.
With this change, I also adjusted the material slightly. The gun has a more matte look and the grip is more rounded. I should say that the blue glass is only a temporary material I assigned within Blender; it's transparent in-engine to display the icosphere hiding within.
Also of note is the reduced tri count I achieved by optimising the model: I managed to push the tris down from 2,520 to... 2,510. Of course, reducing tri count wasn't the primary objective, I just thought this minuscule reduction was somewhat amusing.
Oh and, with this change, I think I decided on the upgraded version of the N5 Blaster: instead of changing into a bigger weapon, I think it'd be neat if the character would use two of the blasters when upgraded. I like the look of the two blasters side-by-side in the picture above.
After I introduced a new ammo crate in the last devlog, I completely overhauled its look. It's now quite different, more simplistic in its mesh, and it's so round, I stopped calling it crate and started naming it canister.
It's simple! But I kinda like it that way. Plus, neat feature: the canister displays the icon of the weapon for which it holds ammo! So far, only the N5 Bomb Launcher's icon is implemented, but others are easy to put in once I create icons for more weapons.
Introducing: the N5 Bomb Launcher.
Okay, it's not exactly a looker at this stage, but it's functionally largely implemented. This is an, as the name implies, bomb-launching device with ground targetting. This sets it apart from other weapons, and is the reason for why I was so hesitant to imlpement it: it meant I had to change my existing code to accommodate for this.
It also meant that I had to figure out how the projectile will fly. The problem: I wanted a projectile that follows a predictable curve and has ground targetting so that you can see where the bomb will land. I achieved the former through using a Path3D
node and modelling a curve into it. The latter still proves a challenge, however; how do I retrieve a collision where the projectile is projected to land? I have not implemented anything of the sort yet. An idea solution would be a curved raycast, but that doesn't exist. Maybe a ShapeCast3D
and retrieving the collision closest to the player? We'll see.
I've been playing around with the explosion as well:
I like the idea of the explosion looking 'technical', if that makes sense. A quick explosion with a large wireframe icosphere. The colour definitely needs changing; I'm even considering changing it to blue, even if that doesn't match the N5 colours. Maybe for the V2 upgrade of the weapon.
The white stripes are strongly inspired by the art to Jaron's track ONCE. Looking at the art again, I actually quite like the colourshift effect, I might try to incorporate that as well. Then again, maybe the two effects (stripes and icosphere) don't match quite so well. I'll figure something out.
In an effort to gain more inspiration for my project's visual direction, I've turned to a place I never thought I would voluntarily step foot in: Pinterest. Turns out, Pinterest is not only good for fashion and home furnishing inspo (though I also like it for that), but it has a slew of game art as well. I only created my account 10 hours ago, and I've already collected quite a lot of cool art here!
Using Pinterest for this purpose allows me to visually see lots of different options I could go for with my character, which is a huge improvement over just... trying to come up with something exclusively on my own.
Seeing all of this art gives me ideas for how I could design a robot character, for instance. It allows me to decide on a shading style – there's a pin of a toon-shaded character in there that I quite like. It also makes me contemplate the direction of the character's identity. With designing a robot character, I find that falling into the stereotype of 'cool and strong male robot' is too easy. I've done it myself. Why does it have to be a male-looking robot, anyway? What made me reevaluate my ideas was this pin. It's not of robots, but I like the aesthetic of those outfits and hope to integrate some of that into my character somehow. The idea from the start was to create relatively non-binary robots anyway; after all, they're robots living 600 years in the future, why should they confine to humanity's societal standards?
It's been almost 14 months of working on Project N5 – sometimes actively, with occasional breaks in-between. Can you believe that this game started from this? (video from 2023-09-16)
Logically speaking, of course, that makes complete sense. I had to start from somewhere. But it's the fact that I managed to get so far already, working on my own, that I find crazy. I've already managed to implement many basics of this game, slowly but surely realising my vision. I think this really has potential. I should keep up the work.