Bullet Hell Dungeon is my take on the bullet hell genre. The game takes heavy inspiration from Enter the Gungeon.
Art by DhiMaximus
Bullet Hell Dungeon is an individual class project built in my personal engine. We were given 6 weeks to come up with an idea and bring it to life. I wanted to challenge myself to make a heavily data driven game to allow for easy iteration on game design. Data driving would allow me to come back later and easily add more to it. I also wanted to see if I could match Enter the Gungeon’s smooth integration of the player character and weapons.
Data Data Everywhere
A big challenge I faced was handling lining up the weapons and the player. I wasn’t able to get sprites without arms, so I had to try and make do with what I had. My solution was lots and lots of XML for both the player (below) and weapon (right). The player needed animations for all 8 directions standing and moving. This meant the weapons also needed that many positions. The weapon sprites also had to have information on where on the sprite to shoot from (muzzlePosition) and where to pivot around (triggerPosition) The positions work the same way as UVs going from 0 to 1
XML Usage
Character directional animations
Character weapon placement
Sound effects
Weapon effects
Bullet effects
Tile Art and image glyph color
Below is a snippet of rendering of the Actor. GetCurrentAnimationName uses the players orientation and velocity to get one of the above animations. You’ll see I have to flip the weapon UVs when the orientation goes past 90 degrees and the weapon has to change from being in front of the player to behind to match with the sprite.
void Actor::Render() const { const SpriteAnimSet& currentSpriteAnimSet = *m_actorDef->m_actorAnimations; std::string animationName = GetCurrentAnimationName(); //Do logic on direction to find out what AnimDef to use const SpriteAnimDefinition& currentSpriteAnimDef = *currentSpriteAnimSet.GetSpriteAnimDefinition(animationName); float spriteTime = m_timeSinceCreation; if( m_isDodging ) { spriteTime = (float)m_dodgeTimer.GetElapsedSeconds(); } const SpriteDefinition& currentSpriteDef = currentSpriteAnimDef.GetSpriteDefAtTime( spriteTime ); AABB2 actorUVs; currentSpriteDef.GetUVs( actorUVs.mins, actorUVs.maxs ); AABB2 actorBounds = m_actorDef->m_drawBounds; actorBounds.Translate(m_position); Rgba8 actorTint = m_actorDef->m_tint; const Texture& actorTexture = g_actorSpriteSheet->GetTexture(); if( !m_isWeaponInFront && !m_isDodging && !m_isDead ) { RenderWeapon(); } if( m_didJustTookDamage ) { actorTint.a =(unsigned char)( 128.f + 128.f * CosDegrees( 360.f * (float)m_playerInvulnerabilityTimer.GetElapsedSeconds() ) ); } g_theRenderer->BindTexture( &actorTexture ); g_theRenderer->SetBlendMode( eBlendMode::ALPHA ); g_theRenderer->DrawRotatedAABB2Filled(actorBounds,actorTint,actorUVs.mins,actorUVs.maxs,0.f); if( m_isWeaponInFront && !m_isDodging && !m_isDead ) { RenderWeapon(); } }
Tile Mapping using an Image
I was presented with the problem of building levels quickly in a user friendly way in my engine. Using XML with characters representing tiles could also have worked but wouldn’t as easily show what each tile actually is. In the below image you may already be able to tell what the colors means. The red shows if it has a wall on the right or left side. The green shows if it there is a top or bottom wall. Blue is left to signify its its a corner tile. The original image is 30 pixels by 30 pixels.
Networking Refactor
After all of the above was completed, I decided to use this project to try creating a networked game. The hardest part was refactoring the code into a client server architecture. This was followed up by asking what data will I be sending to and from the client and server? I don’t believe my implementation was perfect, but I was able to get four player games working using port forwarding and direct IP connections.
Post Mortem
What went right
Worked well iterating with an artist who had never made art for games.
Let features go when they didn’t make the game better
Commenting and cleaning code as you go will save you headaches in the future.
QA is necessary and important before development ends.
What was learned
Last 20% of a game is the hardest part. Getting everything polished is very important to getting the feel correct.
Balancing clean and fast code is important during deadlines. Important systems require clean code, but systems that could be wholly replaced in the future can be written fast.