Table of Contents
Technology
-
Visual Studio (2015)
-
C++
-
GLSL (OpenGL)
-
-
Photoshop
Genre Single player, Bullet Hell
Platform PC
Development Time 2 Months
Resources Used
Game Summary
This is a bullet hell style game in which the player fights infinite waves of enemies until you inevitably die. Each ship the player destroys increases their score by a certain amount. The player also has the option of choosing a ship variant.
This is what the player will see first coming into the game.
This is a snippet of gameplay upon going into the game.
This is a screen shot of the death screen for the game.
This is what the player will see first coming into the game.
Reading Data from XML
Summarized Bullet Points:
-
General code used is very similar to DrekLike's Entity Factories, for more information on that, please go here:
-
In this game's usage of XML, can define Player Ships, Enemy Ships, Bullet Formations, and Enemy Ship Formations
-
If I redid this project, I would:
-
Make it to where I could define Bullet types in XML
-
Add a behavior system for the Bullets; similar to how the NPC behaviors are handled in DrekLike.
-
Made collision be effected by the ship's scale instead of defined in the XML.
-
Add in actual lasers
-
Add in power ups.
-
Be able to define sprite images from XML
-
Be able to change ship color/tint from XML
-
A lot of this is similar to how I handled the Entity Factories over in DrekLike. Thus this section will mainly just walk over the particular choices made with this set of XML data, and what I might do differently if I did this project again. This is mostly as the other things I created for during this project ate up way more time than the actual game itself.
Presently, the XML can be used to define Player Ships, Enemy Ships, Bullet Formations, and Enemy Ship Formations. This is well and good, except for a few minor details. One, due to how the bullets are presently set up, how they can perform and act is very rigid. Thus if I wanted to have a bullet that moves straight forward, and in a circular pattern, or a flame thrower, its not easy to use, nor potentially even possible to set up; having a bullet behavior system would be helpful for enabling this. Two presently for each bullet formation, the user has to redefine each bullet and it's parameters, and can not use a template. It would be nice to predefine the bullets in another XML file, and call them by name in the Bullet Formations. Three the ship collision should honestly be effected by the ship's scale. Also, there are no power ups, super attacks nor even actual lasers in this game. I would want to add those to the game, which would require more XML data. Finally, the sprite images and associated names are presently hard coded in; it would be nice to be able to load that information from XML.
This is the xml file for the bullet configurations. As you can see, each bullet configuration defines a bullet, and each bullet is it's own definition.
This is the xml for the Player Ship definitions. Ships can be added or removed from this and the ship selection should be able to handle it up to 10
This xml file is where I define the Enemy Ship formations, which are used when spawning in ships at random.
This is the xml file for the bullet configurations. As you can see, each bullet configuration defines a bullet, and each bullet is it's own definition.
Sprite Renderer
Summarized Bullet Points:
-
Intention of the Sprite Renderer: to organize all of the entities in the game into layers, and handle all of the rendering. Makes renderering easier.
-
Pros:
-
Does as intended: It does make renderering easier.
-
Very easy to hook in and use.
-
Strongest part about my implementation appears to be how SpriteResources are handled as well as SpriteAnimationSequences.
-
-
Cons (or what I learned):
-
A LOT of areas in writing this that things can go wrong without a lot of forethought and planning.
-
My implementation has bugs and holes in it that make using parts of it difficult. Thus there are things I would change and better plan for if I tried to make this again.
-
-
-
What I would change, if I implemented this again:
-
Separate out some pieces of classes into new classes: classes for this system should be smaller and less bloated.
-
Remove scaling/resizing to handle scaling parts of code: those were the areas that really caused problems, and likely were written incorrectly. At the least, the sprite images/resources probably should not have their own sprite scale.
-
Need to remove the reliance on new/delete ptr types for this system; as is my current implementation will never hit 60fps if its a fairly big game.
-
Particle system needs to actually be able to render.
-
Would of been nice to have each layer have a frame buffer set up, and be able to apply that as it renders each layer.
-
About the Sprite Renderer:
The SpriteRenderer uses SpriteResource, SpriteDatabase, SpriteAnimationSequence, Sprite, and AnimatedSprite classes. The SpriteResource is a holder for the images and the texture coordinates for the individual sprite images. SpriteAnimationSequences contain a list of pointers to SpriteResources and times before switching to the next one (labeled frames). The SpriteDatabase holds onto all of the SpriteAnimationSequences and SpriteResources, so as to be able to pass it out to the Sprites/AnimatedSprites. The Sprite class hangs onto a pointer to it's current SpriteResource, and whenever it renders, uses that SpriteResource's information for it. Each SpriteResource has a default Material on it, and the individual Sprites have their own materials as well that they can use (which if their Material is null, then they'll use the SpriteResource's material).
The AnimatedSprite inherits from the Sprite class, and handles reading off the SpriteAnimationSequence data, and tries to set the current SpriteResource to that whenever updated. Sprite's, when constructed, will register themselves with the SpriteRenderer on their current layer, and when they are destroyed unregister themselves. The SpriteRenderer contains information on each layer it has, plus all of the Sprites that registered to that layer. It renders from the lowest index first, then slowly rises up to the top layer.
Profiler
Summarized Bullet Points:
-
Profiler intended to track how long it takes to do particular chunks of code, having a defined start and end point.
-
What I learned:
-
Currently only tracking the current and last frame's worth of data, need to hold onto more.
-
Need to make this thing thread safe.
-
The profiler times sections that I define with a start and end point. The closest style of structure to my version of the Profiler's node tree is a B-tree. All of the nodes can have a lot of children, each child keeps track of it's neighbors, children and parent. Whenever I push a node, the current node that I have gets assigned the new node as a child, and whenever I pop the node, that node gets it's end time and the current referenced node is set to the previous node's parent. I learned though that there are some niceties my version still yet needs. for starters, it only keeps track of the current frame and the last frame's worth of data, thus I need to make it more flexible about how much data I can have. Also, my profiler is not thread safe, so if I tried to have a profile call on the main thread and another thread, they would get out of sync and break it.
ProfilerNode Header File
Profiler Header File
Profiler CPP File
Memory Logging/Tracking
Summarized Bullet Points:
-
Memory Tracking can be surprisingly simple to set up. Just need a Callstack class, as well as a data structure to contain them in, and then to overload the New and Delete calls
-
Can also have the new's and deletes track how much data would be lost if the memory is not deleted. Just have to allocate additional space when new something, and set the memory at the start of the pointer to the additional data we want.
-
What I learned:
-
Again, need to make the callstacks thread safe at some point.
-
static versions of std::vector, std::map and other data containers don't delete their memory until program end, which can cause memory callstack to think there is still undeleted memory.
-
If must use these, then make the static versions a pointer, and new/delete on own terms.
-
-
Setting up memory tracking is actually pretty easy in C++, as we can overload how the new and delete calls are handled. In order to track specifically where the new that failed to get deleted is, first step is to create a class to contain the callstacks (aka, what is the last N number of steps done by the program before calling new). After having that, we would want to shove this newly made callstack handle plugged into a data container of our choice, I chose a double linked list format. And from there, whenever we new off something, we save off the callstack, and when we delete the data, we remove that callstack data. Whenever we close the program, we check how many callstacks are still there, and push that information out to the output window in our compiler. In that way, we can track where in the code we forgot to delete the data.
Another thing that can be done from overloading the news and deletes, is to be able to save off how much data the we would lose if the data fails to be destroyed, as well as what type of data it is, by allocating additional space for that data. Only thing with that is to make sure to destroy the pointer at the right location if do that.
Allocations Header File
Callstack Header File
Object Pool
Summarized Bullet Points:
-
ObjectPool enables preallocating a lot of pointers for objects as the program initializes. It is faster because it has a bunch of pointers made in one general same location in virtual memory, instead of having a bunch of pointers pointing to random places in memory.
-
What I learned:
-
Some steps were taken to make this thread safe, but might still need to verify that it works in all cases.
-
The ObjectPool class is used inside of the Profiler in order to keep track of all of it's available data for nodes. The general idea is that pointers are on the heap, and accessing the heap is slow. Each time a single new is created in addition to this, the new pointer is just placed wherever on top of the heap is available. This means the more news and deletes are done as time goes on, the more racing around inside of the heap may have to be done, or inconsistent the frame rate can get as the program tries to find the data in the heap. The point of the ObjectPool is to ensure all of the data that might be needed is in one place on the heap, thus minimizing how much racing around the heap is necessary.
ObjectPool Header File