2D Game Engine (GF2D)

GF2D is a single threaded 2D game engine written in C, using Simple Direct MediaLayer (SDL2) for user input, audio, image, text management and rendering.

 

Features:

  • Simple Direct MediaLayer (SDL2) *

  • Menu System

  • Entity System

  • Lighting System

  • AABB Collision System

  • Collision Bucketing

  • Seamless Level Transitioning

  • Content Editor

  • Rendering System *

(*) Note: Features not responsible for.


About Game Framework 2D

This project was completed for my 2D Game Engine Development class at the New Jersey Institute of Technology during 2022. Features above marked with a (*) are to denote sections of the project I did not work on personally. I worked on the project for about 4 months during the semester.

The goal of the project was to build upon the Game Framework 2D (Commit 9fdaaa5168) project. This meant building out the following systems, entity, collision, lighting, menu, audio and a content editor. Code samples for each can be found on my GitHub, linked at the top of this page.

Future of Game Framework 2D

As of now I am considering the project complete. I am moving onto newer and better projects. Notably a 3D C++ Game Engine that I hope to have a demo for this year.

I consider GF2D to be some of my best work from my undergrad and may contribute and commit tweaks in the future. I hope to soon make a downloadable executable for the purpose of demoing the project.


Challenges

Developing a 2D Lighting System

Iteration 1

One of the more major challenges I ran into during this project was the 2D lighting system. I did quite a bit of research on the topic prior to jumping in but for the most part attempted to develop my own implementation of various techniques.

My first method to solve this challenge was ray casting out from the player’s position to the vertices of all scenery and entity bounding boxes in the game world. This method seemed like a great idea until it came to actually rendering the lights.

Rendering lights with this method was nearly impossible. Since the vertices are stored in no particular order, sorting through and placing them into the proper order for rendering did not make much sense.

While this method failed when it came to rendering the light data, it was great for determining line of sight from the player.

Iteration 2

My second method was to take the width and height of the window and raycast from the player to each pixel bordering the window.

This was obviously slow so we then divided up the height and width to reduce the number of raycasts. By looping over every border position, we can store the hit point of every raycast. We can then use these points to draw triangles after inserting the origin of the light every two points.

While this method was a cleaner implementation than Version 1, it has the drawback of reduced lighting accuracy since the lights are locked to a border position instead of an entity or scenery object. This causes slight jittering when the player moves slowly around corners.

Iteration 2 - Note the gaps in the lighting

Iteration 2 Debug View - Note the calculated errors (red)

Code Sample of Iteration 2

// Insert origin to complete triangle

for (int i = 0; i < light_data.vertex_count - 1; i++)
{
	SDL_Vertex tri[3]; // Create a triangle
 
	// Set first vertex to light source origin
	tri[0].position.x = origin.x; 
	tri[0].position.y = origin.y;

	// Set light color
	tri[0].color.r = 200;
	tri[0].color.g = 200;
	tri[0].color.b = 28;
	tri[0].color.a = 25;

	// Set 2nd and 3rd vertices to border pixel positions
	tri[1] = light_data.verts[i];
	tri[2] = light_data.verts[i + 1];

	// Render
	SDL_RenderGeometry( gf2d_graphics_get_renderer(), NULL, tri, 3, NULL, 0 );
}

The primary issue with this version of the lighting system is the gaps/unlit triangles that are missing. This was caused by how the vertex data is being stored as well as a small piece of code meant to fix the issues caused by how the vertex data was stored.

Since the vertex list is composed of every vertex going in a clockwise direction, when the list is not perfectly divisible by 3, some errors begin to occur linking vertices in the wrong order (Note the red lines in the image that do not follow the green/purple lines).

One method to solving this problem was to check the distance between two vertices, if the vertices are too far apart then skip rendering that triangle, or try linking one vertex to the next in the list and skip over the vertex that seems to be out of place. While this removed the large triangles that were not supposed to be generated, it has the side effect of removing needed triangles where neighboring vertices were meant to be far apart (i.e. when a raycast hits the corner of an entity and then scenery next)

Iteration 3

The final version of my lighting system is a rewrite of iteration 2. Iteration 3 of the lighting system now renders the top, left, right, and bottom sets of triangles separately. By doing this the system avoids mixing vertices between sides. Additionally light_draw now resets the vertex list after drawing in order to support multiple draw passes. After implementing these changes, the system performs up to my expectations with a negligible hit to performance.

Iteration 3 - Functioning Lights!

Iteration 3 Debug View

Code Samples of Iteration 3

void light_update()
{
  Entity *player = entity_manager_get_player();
  if (!player) return;
  
  float x_space = g_screen_width / 200;  //	Accuracy of lighting
  float y_space = g_screen_height / 100; //
  
  for (float x = 0; x <= g_screen_width; x+= x_space)
  {
  	light_calculate( player->position, vector2d( x, 0 ) );
  }
  light_draw( player->position );
  
  for (float y = 0; y <= g_screen_height; y+= y_space)
  {
  	light_calculate( player->position, vector2d( 0, y ) );
  }
  light_draw( player->position );

  for (float x = g_screen_width; x >= 0; x-= x_space)
  {
  	light_calculate( player->position, vector2d( x, g_screen_height ) );
  }
  light_draw( player->position );
  
  for (float y = g_screen_height; y >= 0; y -= y_space)
  {
  	light_calculate( player->position, vector2d( g_screen_width, y ) );
  }
  light_draw( player->position );
}
void light_calculate(Vector2D pos, Vector2D target)
{
	if (light_data.vertex_count >= 1024) return;
 
	HitObj hit; // Create a HitObj for collision detection

	hit = raycast_between(pos, target, 9999, NULL, NULL);
 
	if ( !hit.entity && !hit.static_entity ) return; // If we hit nothing, return
 
	if ( hit.position.x < 0 || hit.position.y < 0 ) // If out of bounds, set position and return
	{
		
		light_data.verts[light_data.vertex_count].position.x = target.x;
		light_data.verts[light_data.vertex_count].position.y = target.y;
  
		light_data.verts[light_data.vertex_count].color.r = 255; // Set color
		light_data.verts[light_data.vertex_count].color.g = 255;
		light_data.verts[light_data.vertex_count].color.b = 255;
		light_data.verts[light_data.vertex_count].color.a = 255;

		light_data.vertex_count++;
		return;
	}
	light_data.verts[light_data.vertex_count].position.x = hit.position.x;
	light_data.verts[light_data.vertex_count].position.y = hit.position.y;
 
	light_data.verts[light_data.vertex_count].color.r = 255; // Set color
	light_data.verts[light_data.vertex_count].color.g = 255;
	light_data.verts[light_data.vertex_count].color.b = 255;
	light_data.verts[light_data.vertex_count].color.a = 255;


	light_data.vertex_count++;
	return;

}

Iterations 4 and beyond

The next steps to further improve the system will be to create a texture from the lighting data and blend that with a gradient map to create light falloff. Using the gradient map to blend between a lit and unlit background will better create the effect of dynamic lights. Additionally, implementing caching and preventing lighting updates when not necessary will create a more optimal lighting setup.