Stratoskirmish - In-development vertical shooting game

A place for people with an interest in developing new shmups.
Post Reply
User avatar
Verticen
Posts: 87
Joined: Fri Oct 11, 2019 4:57 pm

Stratoskirmish - In-development vertical shooting game

Post by Verticen »

Hello shmupsforum, I would like to introduce my in-development vertical shooting game title, Stratoskirmish! The gameplay takes recognizable inspiration from Toaplan games; notably Twin Hawk/Daisenpu as well as a healthy amount of YGW-Raizing shooting game DNA. Enemy bullets move rather quickly, and may have devious trajectories. This game is intended to feel dangerous with a manageable amount of onscreen enemy fire.

The player controls an aeroplane armed with front-facing autoguns which are upgradable with powerups. The player can maneuver their aircraft with the arrow keys, WASD, 8-way joystick, d-pad, or an analog stick. Players can also use a special weapon (in place of a traditional bomb); The player can deploy and command a small fleet of flying “Aerotorpedos”. Deployed Aerotorpedos can shoot and be maneuvered, but are also vulnerable to enemy fire as the player. These work similar to ‘wingmen’ or ‘options’ in other games. Additionally, in certain states Aerotorpedoes can be launched forward as an offensive projectile attack. Players may find strategic ways to use their Aerotorpedos to deal with specific challenges throughout the game.

For additional score, skilled players can collect gold bars that are dropped from some killed enemies. These grow in both size and point value the longer they remain onscreen. There are also potentially substantial point bonuses awarded at the end of each stage. This isn’t the most complex scoring system, but my intent is that it will be easy to understand, make the game more challenging in advanced play, and will deliberately force scoring players away from point-blanking at the top of the screen.

This is a 2d game made in Godot Engine (and GodotSteam). I’m a newcomer to godot, but I selected the engine over something like STG Builder or shmup creator because I desired the engine’s capability to utilize normal maps with 2d sprites for enhanced lighting effects I seek to accomplish. Additionally, Godot has proven itself to be a relatively easy, lightweight, Linux-supported, capable, 2d game engine, though not without its fair share of limitations and jank. If there are any experienced godot devs that lurk about here let me know, I might like to rack your brain for advice. An early build of what evolved into this game was shown in Shmup Slam V (2022) as ‘Project Stratowar’, though the project has since deviated from much of the visuals, themes, and sound presented in the Slam trailer.
Image Image Image
The game’s graphics are made from raster pre-renders of detailed 3d models that I custom created in Blender. Within Godot I apply additional dynamic lighting effects to all “hard surface” sprites using baked surface normal maps to create pseudo-2d dynamic surface lighting. Using this technique, the game will feature a variety of lighting effects that I hope will make for a unique and appealing presentation.
A more comprehensive breakdown of my graphics workflow is planned for some time after the release of the game. The short of it is Blender (Cycles render, normal map bake) → Libresprite (auto-cropping and spritesheet generation) → GIMP (color grading adjustments, pixel cleanup, file optimization) → Godot editor.
A fair portion of art is not final, many visuals will be subject to change.

Currently in development for SteamOS, Linux, and Windows 64. Idk system reqs thb. This game has been developed targeting the Valve Steamdeck hardware, but will likely run on more limited hardware. I could probably do a Mac port too if there’s sufficient interest in one, but it’s not currently planned.

Other promises:
  • Controller rumble!
  • Sound!
  • TATE mode!
  • Turtles!
Coming in 202X! You can visit the Steam storepage and wishlist or try the beta demo there if you fancy. There are still some missing features at the moment, but anyone who wishes to take time to playtest and give feedback is appreciated.
Last edited by Verticen on Mon Aug 14, 2023 3:34 am, edited 1 time in total.
Ark
Posts: 50
Joined: Sat Jan 16, 2021 1:28 pm

Re: Stratoskirmish - In-development vertical shooting game

Post by Ark »

The game kept me on my toes with the enemy bullet speed.
The mini planes helped a lot. I was wondering will you have bombs in the game?
I am feeling the nostalgia, looking forward at how this progresses. I almost want the player
ship to move a little faster. I like the destruction environment elements, would be cool if the enemy boss
tank left trails on the ground when moving. :)
User avatar
Sumez
Posts: 8044
Joined: Fri Feb 18, 2011 10:11 am
Location: Denmarku
Contact:

Re: Stratoskirmish - In-development vertical shooting game

Post by Sumez »

Ooh, you had me at its traditional look and style!
Will this be available outside of Steam?
User avatar
Verticen
Posts: 87
Joined: Fri Oct 11, 2019 4:57 pm

Re: Stratoskirmish - In-development vertical shooting game

Post by Verticen »

Ark wrote:The game kept me on my toes with the enemy bullet speed.
The mini planes helped a lot. I was wondering will you have bombs in the game?
I am feeling the nostalgia, looking forward at how this progresses. I almost want the player
ship to move a little faster.
Thanks for playing! I was a bit concerned I made bullets too fast; I’ll certainly buff the player’s speed a bit, and might slow down enemy bullets slightly as well if it still feels a tad unforgiving. I’m kinda figuring on toning down game difficulty overall, but I suppose I succeeded in making it challenging. Enemy bullet speeds are a bit lower at difficultly levels below ‘Arcade.’ This can be toggled in ‘Settings’

I’d like the flying ‘Areo-Torpedo’ mini-planes to be able to be launched forward like rockets and explode on impact + screen clear (or I-frames?), however this mechanic is not yet (fully) implemented.
Ark wrote:I like the destruction environment elements, would be cool if the enemy boss
tank left trails on the ground when moving. :)
I want to do this graphical effect too, but pulling it off might be a bit difficult since I plan for the tracked ground bosses to sway back and forth horizontally during their vertical movements. I’ll see what I can do, but it might end up kinda jank. :P
Sumez wrote:Will this be available outside of Steam?
Steam is the only one set in stone currently, but a release on another PC distributor might happen. I think I’d like to pitch to gog.com release after the game has a bit more (beta) development polish, but no guarantee if that would work out. The game is not planned for any home console platform.
User avatar
Nahar
Posts: 141
Joined: Tue Nov 22, 2022 10:23 am

Re: Stratoskirmish - In-development vertical shooting game

Post by Nahar »

It looks cool. Keep up the good work!
User avatar
Lander
Posts: 874
Joined: Tue Oct 18, 2022 11:15 pm
Location: Area 1 Mostly

Re: Stratoskirmish - In-development vertical shooting game

Post by Lander »

Bit late to the party, but I absolutely love the "arcade flyer with animated screenshots" idea. That's a rad way to catch the eye; convincingly old school with a modern touch :)

Nice to see sprite lighting too - that always seemed an intuitive way to make use of modern rendering tech for 2D, but is so often passed up in favour of high-res deformation rigs.

If you're still looking for an experienced Godot dev to quiz, ask away - I've been wrestling with 3.x for about three years now. Recently made the switch to Bevy for my own stuff, but I still use Godot at work and am always happy to share knowledge.
User avatar
Verticen
Posts: 87
Joined: Fri Oct 11, 2019 4:57 pm

Re: Stratoskirmish - In-development vertical shooting game

Post by Verticen »

Nahar wrote:It looks cool. Keep up the good work!
Thank you. Work may creep along slow and quietly, but I’m still working. In a small update, the stage 1 boss should be a bit easier now and boss fights in general should be a bit more interesting.
Lander wrote:Bit late to the party, but I absolutely love the "arcade flyer with animated screenshots" idea. That's a rad way to catch the eye; convincingly old school with a modern touch :)
Thanks, the arcade flyer took a good deal of planning, but I am very pleased with how the result turned out. I intend my stg to be somewhat of a graphics spectacle for the little game that it is, and I wanted a memorable poster which communicated that.
Lander wrote:Nice to see sprite lighting too - that always seemed an intuitive way to make use of modern rendering tech for 2D, but is so often passed up in favour of high-res deformation rigs.
I am quite excited for the sprite surface lighting – I think that this effect will turn out quite nicely, and was one of the driving factors to begin this project. I could not cite many other 2D vertical shmups who use sprites and normal maps like this. The tech has been there to do this for awhile, but does not seem to be commonly used (anymore?). It can presumably be designed with low-overhead, as I first became familiar with this rendering technique from a video analyzing New Super Mario Bros DS which employed this technique on DS Hardware. I experimented with this tech first in Blender Game Engine, and later in Godot I found I can achieve a variety of unique-looking lighting effects with this technique. Muzzle flashes and explosions ‘illuminating’ ‘surfaces’ is one such unique effect I intend to effectively showcase.
Lander wrote:If you're still looking for an experienced Godot dev to quiz, ask away - I've been wrestling with 3.x for about three years now. Recently made the switch to Bevy for my own stuff, but I still use Godot at work and am always happy to share knowledge.
I’ll keep that in mind. It’s good to know that you’ve used 3.x; I’m version froze on Godot 3.4.2 right now. I may consider moving up to 3.5 if there’s a good reason too. This project is far enough along in production that I do not anticipate moving to Godot 4.+ as I imagine these new versions may break things in the project and also add additional bloat.

The most general question I have with making an stg with Godot is how to handle (ground) enemies. Currently I have the enemies as children of a stage which is a Node2D that is scrolled down (Y). When enemies, like basic tanks, reach a certain ‘Y’ value they switch states from a ‘dummy’ state to an ‘active’ state where they can move around, shoot, take damage, and everything you’d expect.

Code: Select all

enum ENEMY_STATE {
	DORMANT, #sits and checks y-value to see if should become active
	ACTIVE, #moves and shoots, etc
	DYING, #cease movement, darkens.
}

export(ENEMY_STATE) var current_enemy_state = ENEMY_STATE.DORMANT

# - - - - - - 

func _physics_process(delta):
	match current_enemy_state:
		ENEMY_STATE.DORMANT:
			if global_position.y > Activation_Y: #every frame check if the enemy has been scrolled to within a Y threshold (“above screen”)
				activate()
				current_enemy_state = ENEMY_STATE.ACTIVE
		ENEMY_STATE.ACTIVE:
			try_to_despawn() #function that basically does the same idea as ‘Dormant’; every frame checks if is below Y threshold (“below screen”) and ‘queue_free’s if so.
This works well enough, allows me to ‘drag-and-drop’ tanks right onto the stage where I want them to be, and most importantly, is decently consistent. However, I think that I am essentially creating a secondary ‘ready()’ function that is ‘called’ when the screen reaches the desired position; Ideally I would not have 3-dozen+ tanks doing nothing but sitting around checking if they are at the correct Y-level every frame – I would only start running a tank script whenever that tank reached the Y threshold. Perhaps I could employ a ‘yield’ in somehow to help with this?

I have several other miscellaneous issues. I think that I will soon make a post over on Godotforum for additional help with intricate issues.
User avatar
Lander
Posts: 874
Joined: Tue Oct 18, 2022 11:15 pm
Location: Area 1 Mostly

Re: Stratoskirmish - In-development vertical shooting game

Post by Lander »

Verticen wrote:I am quite excited for the sprite surface lighting – I think that this effect will turn out quite nicely, and was one of the driving factors to begin this project. I could not cite many other 2D vertical shmups who use sprites and normal maps like this. The tech has been there to do this for awhile, but does not seem to be commonly used (anymore?). It can presumably be designed with low-overhead, as I first became familiar with this rendering technique from a video analyzing New Super Mario Bros DS which employed this technique on DS Hardware. I experimented with this tech first in Blender Game Engine, and later in Godot I found I can achieve a variety of unique-looking lighting effects with this technique. Muzzle flashes and explosions ‘illuminating’ ‘surfaces’ is one such unique effect I intend to effectively showcase.
The most memorable use of it I can think of is Brigador, which uses it with pre-rendered isometric sprites and ends up looking really convincing. I don't recall seeing it in a shmup, though I'm mostly on the emulation side of things there.
I recall seeing trailers for a couple of high-budget indie adventure games that used it for everything and looked incredible, but they ended up dropping off the radar at some point so I'm not sure if they ever made it to release.
Verticen wrote:I’ll keep that in mind. It’s good to know that you’ve used 3.x; I’m version froze on Godot 3.4.2 right now. I may consider moving up to 3.5 if there’s a good reason too. This project is far enough along in production that I do not anticipate moving to Godot 4.+ as I imagine these new versions may break things in the project and also add additional bloat.
Probably wise; major version increments might as well be a whole new engine in many ways. And unforunately Godot's development trajectory doesn't seem great; 2D is pretty serviceable, but there are some fundamental things like 3D physics that just aren't there yet, and the focus seems to be more on ecosystem stuff and features than it does making sure what's there is as robust as possible.
Verticen wrote:The most general question I have with making an stg with Godot is how to handle (ground) enemies. Currently I have the enemies as children of a stage which is a Node2D that is scrolled down (Y). When enemies, like basic tanks, reach a certain ‘Y’ value they switch states from a ‘dummy’ state to an ‘active’ state where they can move around, shoot, take damage, and everything you’d expect.

Code: Select all

enum ENEMY_STATE {
	DORMANT, #sits and checks y-value to see if should become active
	ACTIVE, #moves and shoots, etc
	DYING, #cease movement, darkens.
}

export(ENEMY_STATE) var current_enemy_state = ENEMY_STATE.DORMANT

# - - - - - - 

func _physics_process(delta):
	match current_enemy_state:
		ENEMY_STATE.DORMANT:
			if global_position.y > Activation_Y: #every frame check if the enemy has been scrolled to within a Y threshold (“above screen”)
				activate()
				current_enemy_state = ENEMY_STATE.ACTIVE
		ENEMY_STATE.ACTIVE:
			try_to_despawn() #function that basically does the same idea as ‘Dormant’; every frame checks if is below Y threshold (“below screen”) and ‘queue_free’s if so.
This works well enough, allows me to ‘drag-and-drop’ tanks right onto the stage where I want them to be, and most importantly, is decently consistent. However, I think that I am essentially creating a secondary ‘ready()’ function that is ‘called’ when the screen reaches the desired position; Ideally I would not have 3-dozen+ tanks doing nothing but sitting around checking if they are at the correct Y-level every frame – I would only start running a tank script whenever that tank reached the Y threshold. Perhaps I could employ a ‘yield’ in somehow to help with this?
Yeah, that's definitely doable. yield is kind of adjacent, in that you can use it to stall out a function invocation until some condition is met (i.e. a signal is emitted from some object), but I don't think it would be that useful in this case since the engine might stack up further _process invocations that in turn stall themselves until the condition is met, then fire all at once - the joys of async concurrency :)
Though I'm not sure of that behaviour off the top of my head; best case it just doesn't re-call until the yield finishes, but point is that you'd probably be better off using set_process(false) and / or the physics equivalent to disable inactive tanks completely. There may be a UI checkbox in the inspector you can use to set it statically, but failing that it can be called in _ready().

In terms of improving the algorithm / structure, here's a step-by-step breakdown of how I'd solve for it:
Spoiler
So what you have currently is a polling-based approach, or "are we there yet?" from the perspective of each tank, which in terms of performance scaling is O(num_inactive_tanks) - i.e. you pay some fixed performance cost for each one.
In theory that's fine so long as num_inactive_tanks doesn't get high enough to noticeably affect performance, but in practical terms it's suboptimal since the performance ceiling will differ across hardware, and GDScript has its own overhead on account of being a scripting language.

Many to One / One to Many
Ideally, you want a solution that is O(1) - i.e. fixed cost regardless of its inputs. That's a bit of a blue sky ideal for any case where the number of inputs (i.e. num_inactive_tanks) is itself variable, but we can definitely get closer to it.
A good first step in solving any compsci problem is trying to invert it; if we currently have every tank asking "are we there yet?", is there a way to flip that so some singular thing tells each tank "we are there now" at the appropriate time instead?

Since the stage Node2D owns the data that decides when a tank should activate (i.e. its Y coordinate), and is the singular parent of all tanks, yes - it's a natural candidate to implement such an approach.
The naive solve from that angle would be to have the Node2D loop over all of its inactive tank children, check their Y, and invoke activation methods on each one.
This would perhaps save some nominal amount of CPU time on function call overhead, but otherwise works out the same as the O(num_inactive_tanks) many-to-one approach.

Caching
So how to improve on it? The O(n) performance scaling's root cause is the need to run a for-loop over all inactive tanks every frame (be that manually inside the Node2D, or implicitly inside a _process function called from some engine-side loop), so the solution is to somehow do less work each frame.
Thus, we should step back and examine why the for loop is needed. In this case, it's because number of inactive tanks, and Y position of each tank relative to the screen, is dynamic data that might change across invocations, and so needs to be read from its owner every time.
However, the actual problem isn't actually that dynamic - once a stage is loaded, both the number of inactive tanks and their local Y positions are known data that doesn't need to be recalculated every frame; the only truly dynamic aspect is the Y position of the scrolling Node2D stage, since we also know that num_inactive_tanks starts at a known value and reduces by 1 each time a tank is activated.

Thus, we can optimize around those properties by precalculating all of the non-static data at startup, and reading from that instead of the tanks themselves.
Since we need to know the local Y position of each tank in order to calculate its screen-relative Y, we can run a for loop in _ready() that stores a reference (FuncRef or WeakRef) to each tank alongside its Y in a key-value dictionary, and read from that in our _process loop instead of checking the tanks directly, removing entries as they get activated.
This is another incremental improvement over the previous solve - probably a little faster on account of a local dictionary having better cache coherency than querying foreign data, but stil fundamentally O(num_inactive_tanks) on account of iterating the whole thing each frame.

But now the data is stored locally, we have control over its shape. This is the key to lowering that O() number, and the big reason for using the one-to-many approach to begin with.

Acceleration Structure
Since we know that a given stage's tanks will activate in a fixed order, and the Y positions describing that order are already being stored in a list-like structure, we can sort that list by Y position.
Having a sorted list means it's no longer necessary to iterate every entry every frame, because only the first N entries (where N is the number of tanks that enter the screen on a given frame) will need to be activated.
Thus, we can use a while loop instead of a for-each, breaking at the first entry whose Y is offscreen, and know we've updated everything we need to.

In so doing, performance scaling goes from O(num_inactive_tanks) to O(num_inactive_tanks_entered_screen_this_frame) - much better, and closer to the golden 'minimal solve'.
That's probably good enough for your use case, since it ticks the 'not O(n)' box without interfering with your existing level editing workflow.

Improvements
This approach assumes all tanks activate in a uniform way - i.e. when they reach some imaginary line either on or off the visible screen.
You could extend it to read more data from each tank alongside the Y position and store it inside a CachedTank struct within the dictionary,
allowing for more arbitrary behaviour (i.e. activating at different points on the screen) that can still be looked up and acted on in a performant way.

More...
It's possible to go deeper, since a linear STG fundamentally boils down to a pre-keyed animation timeline if you really think about it (i.e. tank activations as events that occur at a preset timestamp w.r.t. game time), which could be made extremely performant with techniques like object pooling.
But optimizing, around such things introduces more assumptions that need to be upheld, which in turn necessitates building custom tooling in order to specialize around the problem - i.e. a timeline-based level editor, or a script that analyzes an editor-assembled godot scene and munges it into timeline data. I suppose the latter is essentially a more complex version of the caching above, just with time instead of stage Y and full custom pattern data instead of only activations :)
User avatar
Verticen
Posts: 87
Joined: Fri Oct 11, 2019 4:57 pm

Re: Stratoskirmish - In-development vertical shooting game

Post by Verticen »

Lander wrote:In terms of improving the algorithm / structure, here's a step-by-step breakdown of how I'd solve for it:
Spoiler
So what you have currently is a polling-based approach, or "are we there yet?" from the perspective of each tank, which in terms of performance scaling is O(num_inactive_tanks) - i.e. you pay some fixed performance cost for each one.
In theory that's fine so long as num_inactive_tanks doesn't get high enough to noticeably affect performance, but in practical terms it's suboptimal since the performance ceiling will differ across hardware, and GDScript has its own overhead on account of being a scripting language.

Many to One / One to Many
Ideally, you want a solution that is O(1) - i.e. fixed cost regardless of its inputs. That's a bit of a blue sky ideal for any case where the number of inputs (i.e. num_inactive_tanks) is itself variable, but we can definitely get closer to it.
A good first step in solving any compsci problem is trying to invert it; if we currently have every tank asking "are we there yet?", is there a way to flip that so some singular thing tells each tank "we are there now" at the appropriate time instead?

Since the stage Node2D owns the data that decides when a tank should activate (i.e. its Y coordinate), and is the singular parent of all tanks, yes - it's a natural candidate to implement such an approach.
The naive solve from that angle would be to have the Node2D loop over all of its inactive tank children, check their Y, and invoke activation methods on each one.
This would perhaps save some nominal amount of CPU time on function call overhead, but otherwise works out the same as the O(num_inactive_tanks) many-to-one approach.

Caching
So how to improve on it? The O(n) performance scaling's root cause is the need to run a for-loop over all inactive tanks every frame (be that manually inside the Node2D, or implicitly inside a _process function called from some engine-side loop), so the solution is to somehow do less work each frame.
Thus, we should step back and examine why the for loop is needed. In this case, it's because number of inactive tanks, and Y position of each tank relative to the screen, is dynamic data that might change across invocations, and so needs to be read from its owner every time.
However, the actual problem isn't actually that dynamic - once a stage is loaded, both the number of inactive tanks and their local Y positions are known data that doesn't need to be recalculated every frame; the only truly dynamic aspect is the Y position of the scrolling Node2D stage, since we also know that num_inactive_tanks starts at a known value and reduces by 1 each time a tank is activated.

Thus, we can optimize around those properties by precalculating all of the non-static data at startup, and reading from that instead of the tanks themselves.
Since we need to know the local Y position of each tank in order to calculate its screen-relative Y, we can run a for loop in _ready() that stores a reference (FuncRef or WeakRef) to each tank alongside its Y in a key-value dictionary, and read from that in our _process loop instead of checking the tanks directly, removing entries as they get activated.
This is another incremental improvement over the previous solve - probably a little faster on account of a local dictionary having better cache coherency than querying foreign data, but stil fundamentally O(num_inactive_tanks) on account of iterating the whole thing each frame.

But now the data is stored locally, we have control over its shape. This is the key to lowering that O() number, and the big reason for using the one-to-many approach to begin with.

Acceleration Structure
Since we know that a given stage's tanks will activate in a fixed order, and the Y positions describing that order are already being stored in a list-like structure, we can sort that list by Y position.
Having a sorted list means it's no longer necessary to iterate every entry every frame, because only the first N entries (where N is the number of tanks that enter the screen on a given frame) will need to be activated.
Thus, we can use a while loop instead of a for-each, breaking at the first entry whose Y is offscreen, and know we've updated everything we need to.

In so doing, performance scaling goes from O(num_inactive_tanks) to O(num_inactive_tanks_entered_screen_this_frame) - much better, and closer to the golden 'minimal solve'.
That's probably good enough for your use case, since it ticks the 'not O(n)' box without interfering with your existing level editing workflow.

Improvements
This approach assumes all tanks activate in a uniform way - i.e. when they reach some imaginary line either on or off the visible screen.
You could extend it to read more data from each tank alongside the Y position and store it inside a CachedTank struct within the dictionary,
allowing for more arbitrary behaviour (i.e. activating at different points on the screen) that can still be looked up and acted on in a performant way.

More...
It's possible to go deeper, since a linear STG fundamentally boils down to a pre-keyed animation timeline if you really think about it (i.e. tank activations as events that occur at a preset timestamp w.r.t. game time), which could be made extremely performant with techniques like object pooling.
But optimizing, around such things introduces more assumptions that need to be upheld, which in turn necessitates building custom tooling in order to specialize around the problem - i.e. a timeline-based level editor, or a script that analyzes an editor-assembled godot scene and munges it into timeline data. I suppose the latter is essentially a more complex version of the caching above, just with time instead of stage Y and full custom pattern data instead of only activations :)
Thank you for your response. I’ve been quiet here for a bit, but I did review your reply and created a mock-implementation from your advice to the best of my understanding. I don’t think that it implements all of the acceleration techniques which you suggested, but it is potentially a functionally-identical performance improvement over my current “polling-based” implementation.
The mock scene setup looks like this:

Code: Select all

Scroll Test
├ Sprite (dummy node)
├ Example_Actor
├ Example_Actor2
├ Example_Actor3
└ Example_Actor4
In this test implementation, all actors (e.g. tanks) have an exported, user-set activation Y variable which controls how many vertical pixels in advance the actor should begin its behavior relative to the top of the screen. On ready(), actors check their global Y coordinate value against their activation Y value. Actors which check this value to be within the ‘onscreen’ Y coordinate range onready proceed as normal, while actors which check themselves to be ‘offscreen’ add their ‘self’ & ‘activation Y’ to an array in the stage scroll node and disable their physics_process().

Code: Select all

extends Sprite

export var activation_Y = 34 #offset for when actor should start so that they can begin a bit before they're scrolled onscreen
# ^ (will also use negative values in real game implementation)
signal scroll_array #probably rename to something more intuitive

func _ready():
	if self.global_position.y > self.activation_Y: #already onscreen
		set_physics_process(true) # < likely 'unnessisary' line but 2b safe
	else: #"offscreen"
		var Scroll_Node = find_parent("Scroll Test")
		connect("scroll_array",Scroll_Node,"actor_array_populate",[[activation_Y,self]]) #use signal to not crash game in case find_parent returns 'null'
		emit_signal("scroll_array")
		set_physics_process(false)

func _physics_process(delta):
	spin(delta) #placeholder for whatever action the actor should do.

func spin(delta): 
	rotate(deg2rad(delta * 95)) #placeholder for whatever action the actor should do
Whenever the stage scrolls, the array is iterated through, checking if each actor’s global position Y value has scrolled passed its activation Y value; setting its physics_process(true) & removing it from the array if so.

Code: Select all

extends Node2D

var actor_array : Array = [] #all offscreen actors will add themselves to this on their ready()

func actor_array_populate(ActorInfo = [null,0]): #called by entities themselves
	actor_array.append(ActorInfo)
	print("THE ACTOR ARRAY IS" + str(actor_array))

func check_activate_actors():
	for actor in actor_array:
		if actor[1].get_global_position().y > actor[0]: 
			actor[1].set_physics_process(true) #do activate
			actor_array.erase(actor) #no longer needed in array
			
func _physics_process(delta):
	position.y += delta * 30 #basic screen scroll
	check_activate_actors()
I imagine that this code could be cleaned up in several various ways. In your suggestion you mentioned using a dictionary, which I’m less familiar with using but may be more suited for accomplishing this. Also, perhaps the code could be made more efficient by cleverly sorting the array/dictionary by its information so that the iterative search somehow ‘knows’ when it can quit searching instead of blindly checking every node’s position like you mentioned under ‘Acceleration Structure’.
Lander wrote:
Verticen wrote:I am quite excited for the sprite surface lighting – I think that this effect will turn out quite nicely, and was one of the driving factors to begin this project. I could not cite many other 2D vertical shmups who use sprites and normal maps like this. The tech has been there to do this for awhile, but does not seem to be commonly used (anymore?). It can presumably be designed with low-overhead, as I first became familiar with this rendering technique from a video analyzing New Super Mario Bros DS which employed this technique on DS Hardware. I experimented with this tech first in Blender Game Engine, and later in Godot I found I can achieve a variety of unique-looking lighting effects with this technique. Muzzle flashes and explosions ‘illuminating’ ‘surfaces’ is one such unique effect I intend to effectively showcase.
The most memorable use of it I can think of is Brigador, which uses it with pre-rendered isometric sprites and ends up looking really convincing. I don't recall seeing it in a shmup, though I'm mostly on the emulation side of things there.
Brigador’s lighting capabilities appear miles ahead of what I plan to exhibit in this title. I’ve not discovered any shmups which use this technique either, and I’ve crawled several popular PC storefronts, but that’s not to say one’s not out there somewhere. Regardless, it’s an uncommonly seen effect which I hope will make the visuals standout in an appealing positive way.

Since you don’t seem afraid to offer elaborated dev advice, I’ll go ahead a lay out a few more of my other top technical hurdles that stand between release. You or anyone else feel free to take a crack at any of these.
Spoiler
Background Tilemaps
One of my top priorities is finding the best approach to working with tilemaps and selecting the best tile size for my circumstance. I think composing most of my background graphics with ~1-2 layered static tilemaps will allow me to rapidly develop multiple long, detail-rich stages with a light resource footprint. Maps will use some visually animated tiles (made from animatedtexture resource), but the maps themselves will not be modified by the game or use tile collision, so I don’t need to worry about any scripting for the maps. At the moment I’m concerned about what size tiles I should choose.

I’ve been weighing advantages between using a size 16x tile and a size 32x tile for the tilemaps; I have made several assets with a 32x grid in mind, but I am also considering using a 16x grid for a graphics versatility advantage. In my mind a 16x grid would theoretically have the same tile arrangement capabilities as a 32x size map plus options for greater refined detail (at a slightly larger expense to system resources.) In practice using ‘larger’ 32x tileset assets & props in a 16x tilegrid is possible, but tools such as the bucket tool, line draw, and autotile cannot be used with >16x assets since they use the 16x grid size regardless of what the (32x) tile size is. A larger 32x grid seems more convenient for developing large-scale background visuals and allows me to use 32x autotile features, but I’m concerned that the tiles will feel visually blockier and refined details will be more challenging to achieve.

Alternatively, I’ve considered I may be better off using an external tilemap editor which could better handle multiple sized tiles and could export to godot. The ideal editor could paint autotile with both 16x autotile assets and 32x autotile assets, even if the final export is just a plain 16x single tile map. I’m open to suggestions for compatible programs more capable than godot’s internal editor.

The reason this specific seemingly menial issue is of top significance is because I need to sort this out before I begin final background art; My pre-rendered sprites art workflow is such that I really need to know the tilesize before I begin doing final background artwork.

Score Extends Code Cleanup
I’ve recently implemented placeholder code for earning score extends and am open to suggestions to improve/clean it up. Right now I am using lower score values than will be used in the final game for ease of testing. This is what the code looks like now:

Code: Select all

var Player_1_Score : int = 0 setget update_P1_score
var Player_2_Score : int = 0 setget update_P2_score
var Score_Extends = [2500, 5000, 10000, 20000, 30000, 40000] #test
signal Score_Updated #signal that can be connected to the GUI node to make it update the scores

func update_P1_score(Add_Score): #Setter function for Player_1_Score
	for i in Score_Extends.size(): #begin 'extend check'
		if Player_1_Score > Score_Extends[i]:
			continue #already scored higher than this extend, try next extend.
		else: #previous score was less than needed for next extend; did they earn it?
			if Player_1_Score < Score_Extends[i] and Player_1_Score + Add_Score >= Score_Extends[i]: #place '<' first to ensure no milking extends from '+0' score updates
				#Player_1_Stock += 1 #[stock feature not implemented yet]
				print("AWARD EXTEND P1") #working placeholder
			break #failed to earn extend; cease array
	Player_1_Score += Add_Score #set score value (after extend check)
	emit_signal("Score_Updated") #this signal connects to score HUD so that score 'text' numbers are updated whenever the score value is

func update_P2_score(Add_Score): #Setter function for Player_2_Score; mirror of P1's, but needs to be separate b/c these are used as 'setget' funcs for separate score vars
	for i in Score_Extends.size(): #begin 'extend check'
		if Player_2_Score > Score_Extends[i]:
			continue #already scored higher than this extend, try next extend.
		else: #previous score was less than needed for next extend; did they earn it?
			if Player_2_Score < Score_Extends[i] and Player_1_Score + Add_Score >= Score_Extends[i]: #place '<' first to ensure no milking extends from '+0' score updates
				#Player_2_Stock += 1 #[stock feature not implemented yet]
				print("AWARD EXTEND P2") #working placeholder
			break #failed to earn extend; cease array
	Player_2_Score += Add_Score
	emit_signal("Score_Updated")
The reason I separate the functions for player 1 & 2 scores because their score variables use the functions as setter funcs.
I’d also like the ability to award extends ‘every #0000’ indefinitely, not that players will be earning that many extends in practice. Instead of filling out a long array with consecutive #0k ints, could the last element of the score extends array be some sort of function that returns the next #0000 score value?

Shrapnel Improvements
I’m already somewhat pleased with the appearance of the spinning, contorted metal chunks which fly out from destroyed targets. It’s a decent visual effect now, but I’d like to revisit how the shrapnel files are set up and programmed to fix some issues and make improvements.
  • My currently implemented method has a shrapnel base scene with an attached script and a blank child sprite node.

    Code: Select all

    Shrapnel_Node2D
    ├ Shrapnel_Sprite
    └ Despawn Timer
    
    Separate inherited child scene files are saved for each different strip of shrapnel frames. Targets preload all the shrapnel scene files they may need to use, and when they explode they instance the desired shrapnel scenes to the scenetree. I see little definitively ‘wrong’ with this fairly straightforward approach apart from requiring many ‘preloaded’ files and the code looking somewhat clunky.

    An alternative approach I’ve considered is similar to the previous but with each different shrapnel frame sequence included as a child sprite node of a single shrapnel scene.

    Code: Select all

    Shrapnel_Node2D
    ├ Shrapnel_type01_Sprite
    ├ Shrapnel_type02_Sprite
    ├ Shrapnel_type03_Sprite
    ├ Shrapnel_type04_Sprite
    └ Despawn Timer
    
    Before being added to scenetree, the shrapnel parent script would select which sprite node to keep and queue free the other child nodes. This method would simplify the shrapnel scene files and would allow the easy addition of new shrapnel animations, but seems like it could be overly bloated with all of the initial sprite nodes.

    Another route I’ve considered would be to have a single shrapnel node scene with all of the different shrapnel frame strips combined into a single image file. The script would cycle the sprite node through the set of frames corresponding to one of the shrapnel strips and would not show the other frames. This might have some advantage depending on how intelligently godot manages multiple sprites re-referencing the same image data in RAM/VRAM, but also seems like a stricter, more overly complex method I’m less likely to use.
Pre-mature optimization isn’t something I’d normally fret much about, but there may be circumstances where several dozen of these shrapnel pieces could appear onscreen at once, so I’d like to select a decently efficient solution.

Replay System (Not Planned)
Replays are a mere curiosity for the time being. They’d be an ideal feature to have, but I have concerns with how well they would work as well as more pressing overall game issues which demand priority. Online documentation about godot replay features is limited, though some resources exist. I walked though this tutorial for a fairly simple physics_process-based input replay system with good results. This solution seemed similar, and later comments to this post also argue that it should be immune to desyncs. I don’t easily trust replay systems to withstand arcade-play stress however; I am concerned that in practice this system and others like it may not retain the precision needed for recording or playback over an intense, extended 10+ minute period of gameplay without the slightest desync. Additionally, while I try to keep randomness to a minimum, Idk how consistently the rest of my game plays back to the exact frame. Are there are any examples of godot engine shmups or equivalent ‘intense precision action games’ which successfully implement a replay system? I’d be less hesitant if someone else had already confidently figured this out. I’ll happily listen to anyone’s advice on replays, but can’t promise I’ll implement them.
Few changes to the steam release have been made since the last post, though I made a few game balance adjustments and changes, notably having bosses begin shooting a new aimed spread attack once a bosspart has been destroyed. Ideally this should counter bosses getting ‘easier’ as more guns are destroyed, make any potential safe spots more risky to use, and make bossfights more engaging overall.
Post Reply