HJEngine2 is an ongoing 2D game engine colloberation project among a couple of friends. I am not very into video games, but I like the challenge of doing something different and applying what I learned in Mathematics. We have competed in two Lundum Dare game jams with our own engine written Python. HJEngine2 is written in C++ using OpenGL and is meant to provide more flexibility and performance compared to the first iteration.
Code samples can be found at the bottom of this page.
Team:
- Alex Cope - Lead Designer, Documentation, Scripting, and Art
- Michael Blackwood - Programming (first iteration) and Art
- Dustin Gibson - Programming (first and second iteration)
Git Link
Latest Build: Map Editor
Latest Build: HJEngine2
Map Editor (C#):
- Import, add tile, and remove tile from tilesets
- Add objects and properties
- Add and modify tile collision
- Place, fill, delete, and copy tiles and objects
- Add and remove layers
- Mini-map, grid, and zoom display
- Expand or shrink map area
- Draw images using map file produced by the editor
- State machine to handle game, objects, and menu lifecycles
- Visibility algorithm to hide view
- Few effects
- Supports fragment and vertex shaders
- Object-Oriented - HJEngine2 is heavily centered on objected oriented design. Due to the demand of creating and removing objects on the fly, the factory pattern is the most common design pattern used. Inheritance is used throughout the code. For an example: individual effects are represented as individual classes and are inhearited by the abstract Effect class with common properties and methods.
- OpenGL and SDL - The rendering API used is OpenGL. However, SDL is used to provide input and timer methods. Box2D is used for physics and collision detection.
- Compactness - A lot of game programs have an issue of having multiple files spread out. This becomes a pain to manage. Almost everything is in one map file produced by the map editor. Everything in the file is managed by the map editor.
- OpenGL is very challenging to learn since it’s much more technical than most APIs I have written with. Writing shaders requires working in a different paradigm. Drawing a simple image requires converting the image into a texture, binding it, set up buffers that determines the properties of textures, running it through shaders, and then drawing it.
- As for the map editor, the .NET framework isn’t a library that comes to mind when dealing with image manipulation. With .NET Bitmap object and picturebox widgets, it can be difficult to display exactly what you want the user to see. In hindsight, I would have used Qt since it integrates with OpenGL and SDL very nicely.
Code Design
Challenges and Lessons Learned:
Unpack Data (C++)
void TileMap::LoadData()
{
unsigned char* main_buffer = GetByteArray();
name = TraverseToString(main_buffer,20);
//Tile Width
tile_width = TraverseToInt(main_buffer,2);
//Tile Height
tile_height = TraverseToInt(main_buffer,2);
//Rows
row_num = TraverseToInt(main_buffer,2);
//Columns
col_num = TraverseToInt(main_buffer,2);
//Tile Sheet Columns
tile_set_x = TraverseToInt(main_buffer,2);
//Tile Sheet Rows
tile_set_y = TraverseToInt(main_buffer,2);
//Tile Set MaxCol
max_col = TraverseToInt(main_buffer,2);
tile_set = new TileSet(tile_width,tile_height,tile_set_x,tile_set_y);
//Number of Layers
int layer_count = TraverseToInt(main_buffer,2);
//Tile Set Images
for(int i=0; i < tile_set_x; i++)
for(int j=0; j < tile_set_y; j++)
{
if( j == tile_set_y - 1 && max_col < i)
break;
int img_size = TraverseToInt(main_buffer,4);
TraverseToBitmap(main_buffer,img_size);
//tile_set->AddTile(bitmap_val,CPoint(i,j));
}
//Full Bitmap Size
int full_bitmap_size = TraverseToInt(main_buffer,4);
//Full Bitmap
this->full_sheet = TraverseToBitmap(main_buffer,full_bitmap_size);
//Map Set
for(int i = 0; i < layer_count; i++)
{
std::map* map_set = new std::map();
std::vector*>* temp_vect_x = new std::vector*>();
for(int j = 0; j < col_num; j++) {
std::vector* temp_vect_y = new std::vector();
for(int k=0; k < row_num; k++) {
int tmp_x = TraverseToInt(main_buffer,2);
int tmp_y = TraverseToInt(main_buffer,2);
int tmp_val_x = (short)TraverseToInt(main_buffer,2);
int tmp_val_y = (short)TraverseToInt(main_buffer,2);
unsigned char* tmp_col = TraverseToBytes(main_buffer,2);
CPoint key_pnt = CPoint(tmp_x,tmp_y);
CPoint val_pnt = CPoint(tmp_val_x,tmp_val_y);
TileValue tile_val = TileValue(tile_world,val_pnt,tmp_col);
temp_vect_y->insert(temp_vect_y->end(),tile_val);
}
temp_vect_x->insert(temp_vect_x->end(),temp_vect_y);
}
layer_set->insert(layer_set->begin(),temp_vect_x);
}
//Number of object templates
int num_obj_template = TraverseToInt(main_buffer, 2);
for(int i=0; i < num_obj_template; i++)
{
std::map* prop_map = new std::map();
ImageStates image_states = ImageStates();
//Object Name
std::string obj_name = TraverseToString(main_buffer, 20);
//Number of Properties
int prop_num = TraverseToInt(main_buffer, 4);
for(int j=0; j < prop_num; j++)
{
//Property name
std::string prop_name = TraverseToString(main_buffer, 20);
//Property data type
std::string prop_data_type = TraverseToString(main_buffer, 20);
Property temp_prop = Property(prop_name, prop_data_type, 0, NULL);
prop_map->insert(std::make_pair(prop_name, temp_prop));
}
//Number of States
int state_num = TraverseToInt(main_buffer, 2);
for(int j=0; j < state_num; j++)
{
//State name
std::string state_name = TraverseToString(main_buffer, 20);
//Bitmap size
int state_size = TraverseToInt(main_buffer, 4);
//Bitmap itself
TextureGroup* state_image = TraverseToBitmap(main_buffer, state_size);
image_states.AddState(state_name,state_image);
}
object_factory->AddTemplate(obj_name,prop_map,image_states);
}
//Number of Object Instance
int obj_instance_num = TraverseToInt(main_buffer,2);
for(int j=0; j < obj_instance_num; j++)
{
std::map* cur_prop_map = new std::map();
//Name of Object
std::string obj_inst_name = TraverseToString(main_buffer,20);
//Visible
int visible = TraverseToInt(main_buffer,2);
//Int x
int x_inst = TraverseToInt(main_buffer, 2);
//Int y
int y_inst = TraverseToInt(main_buffer, 2);
//Number of Properties
int prop_num = TraverseToInt(main_buffer, 2);
for(int j=0; j < prop_num; j++)
{
//Property name
std::string prop_name = TraverseToString(main_buffer,20);
//Propety Data Type
std::string prop_data_type = TraverseToString(main_buffer, 20);
//User asset?
int use_asset = TraverseToInt(main_buffer, 2);
//Asset name
std::string asset_name = TraverseToString(main_buffer, 20);
//Property size
int prop_size = TraverseToInt(main_buffer, 4);
//Propety value
unsigned char* prop_value = TraverseToBytes(main_buffer, prop_size);
Property* prop_temp = new Property(prop_name, prop_data_type, use_asset, asset_name, prop_size, prop_value);
cur_prop_map->insert(std::make_pair(prop_name,prop_temp));
}
CPoint inst_point = CPoint(x_inst, y_inst);
//ObjectInstance* obj_instance = new ObjectInstance(obj_inst_name, cur_prop_map, inst_point, visible);
object_factory->AddObject(obj_inst_name, cur_prop_map, inst_point, visible);
//obj_instance_list->insert(obj_instance_list->begin(), obj_instance);
}
//Number of Assets
int num_assets = (short)TraverseToInt(main_buffer, 2);
for(int i = 0; i < num_assets; i++)
{
//Asset name
std::string asset_name = TraverseToString(main_buffer, 20);
//Asset data type
std::string asset_data_type = TraverseToString(main_buffer, 20);
//Asset size
int asset_size = TraverseToInt(main_buffer, 4);
//Asset buffer
unsigned char* asset_buffer = TraverseToBytes(main_buffer, asset_size);
object_factory->AddAssetMap(asset_name,asset_data_type,asset_size,asset_buffer);
}
}
Render Visual Scene (C++)
FPoint TileMap::FindIntersection(FPoint blockA, FPoint blockB, FPoint rayA, FPoint rayB)
{
float xa0 = blockA.x;
float ya0 = blockA.y;
float ma = (blockB.x - blockA.x);
float na = (blockB.y - blockA.y);
float xb0 = rayA.x;
float yb0 = rayA.y;
float mb = (rayB.x - rayA.x);
float nb = (rayB.y - rayA.y);
if( ma == 0 )
{
float t = ( xa0 - xb0 ) / mb;
float xb = xb0 + mb*t;
float yb = yb0 + nb*t;
float s = (yb - ya0) / na;
if( t >= 0.0 && t <= 1.0 && s >= 0.0 && s <= 1.0 )
return FPoint( xb, yb );
}
else if( na == 0 )
{
float t = ( ya0 - yb0 ) / nb;
float xb = xb0 + mb*t;
float yb = yb0 + nb*t;
float s = (xb - xa0) / ma;
if( t >= 0.0 && t <= 1.0 && s >= 0.0 && s <= 1.0 )
return FPoint( xb, yb );
}
return FPoint(-1,-1);
}
float TileMap::GetDistance(FPoint a, FPoint b)
{
return abs( a.x - b.x ) + abs( a.y - b.y );
//return sqrt( ((a.x - b.x)*(a.x - b.x)) + ((a.y - b.y)*(a.y - b.y)) );
}
bool TileMap::IsIntersection(FPoint a, FPoint b, FPoint s, FPoint t)
{
if( a.x >= s.x && a.x <= t.x && a.y >= s.y && a.y <= t.y)
if( b.x >= s.x && b.x <= t.x && b.y >= s.y && b.y <= t.y)
return true;
return false;
}
void TileMap::RenderVisualScene(Player* ply, int width, int height )
{
CPoint ply_pnt = Globals().ToPixels(ply->body->GetPosition().x,ply->body->GetPosition().y,ply->width,ply->height);
for(int i = 0; i < 150; i++)
{
FPoint rayA = FPoint(0.0,0.0);
FPoint rayB = FPoint(0.0,0.0);
float angle_x = t_cos[i];
float angle_y = t_sin[i];
float ar = (float)width/height;
float unorm_tx = (float)(width/2) + angle_x*width;
float unorm_ty = (float)(height/2) + angle_y*height;
float c_x = camera_point.x * width;
float c_y = camera_point.y * height;
float r_x = (ply_pnt.x+16.0) + c_x;
float r_y = height - (ply_pnt.y+16.0) - c_y;
float min_distance = 999999999;
FPoint intPoint = FPoint( unorm_tx, unorm_ty );
for(int i = 0; i < hard_tile_set->size(); i++)
{
HardTile* curTile = hard_tile_set->at(i);
float bx = curTile->pnt.x + c_x;
float by = curTile->pnt.y + c_y;
rayA = FPoint( r_x , r_y + 32.0 );
rayB = FPoint( unorm_tx, unorm_ty);
FPoint northA = FPoint( bx, by);
FPoint northB = FPoint( bx + tile_width, by);
FPoint southA = FPoint( bx, by + tile_height);
FPoint southB = FPoint( bx + tile_width, by + tile_height);
FPoint westA = FPoint( bx, by);
FPoint westB = FPoint( bx, by + tile_height);
FPoint eastA = FPoint( bx + tile_width, by );
FPoint eastB = FPoint( bx + tile_width, by + tile_height);
FPoint northInt = FindIntersection( northA, northB, rayA, rayB );
FPoint southInt = FindIntersection( southA, southB, rayA, rayB );
FPoint eastInt = FindIntersection( eastA, eastB, rayA, rayB );
FPoint westInt = FindIntersection( westA, westB, rayA, rayB );
int tile_x = curTile->pnt.x / 32.0;
int tile_y = curTile->pnt.y / 32.0;
if( tile_x > 0 && tile_y > 0 && tile_x < width/tile_width && tile_y < height/tile_height )
for(int k=0; k < layer_set->size(); k++)
layer_set->at(k)->at(tile_x)->at(tile_y).view = true;
if( !(northInt == FPoint(-1,-1)))
{
float dist = GetDistance( rayA, northInt );
if( dist <= min_distance ) {
min_distance = dist;
intPoint = northInt;
}
}
if( !(eastInt == FPoint(-1,-1)))
{
float dist = GetDistance( rayA, eastInt );
if( dist <= min_distance ) {
min_distance = dist;
intPoint = eastInt;
}
}
if( !(southInt == FPoint(-1,-1)))
{
float dist = GetDistance( rayA, southInt );
if( dist <= min_distance ) {
min_distance = dist;
intPoint = southInt;
}
}
if( !(westInt == FPoint(-1,-1)))
{
float dist = GetDistance( rayA, westInt );
if( dist <= min_distance ) {
min_distance = dist;
intPoint = westInt;
}
}
}
float norm_int_x = intPoint.x/width;
float norm_int_y = ((height-intPoint.y)/height);
float norm_rx = r_x / width;
float norm_ry = (r_y / height);
float pX = rayA.x - c_x;
float pY = rayA.y - c_y;
float iX = intPoint.x - c_x;
float iY = intPoint.y - c_y;
float t_step = 0.01;
for(float t = 0.0; t <= 1.0; t += t_step)
{
float f_x = (pX + (iX - pX)*t) ;
float f_y = (pY + (iY - pY)*t) ;
int tile_x = f_x / tile_width;
int tile_y = f_y / tile_height;
CPoint render_point = CPoint(f_x,f_y);
if( tile_x > 0 && tile_y > 0 && tile_x < width/tile_width && tile_y < height/tile_height )
for(int k=0; k < layer_set->size(); k++)
layer_set->at(k)->at(tile_x)->at(tile_y).view = true;
}
}
}
Map Editor Placement Sample (C#)
private void mapBox_MouseDown(object sender, MouseEventArgs e)
{
if (loaded)
{
mx = e.X;
my = e.Y;
clicked = true;
int tw = tileMap.tileWidth;
int th = tileMap.tileHeight;
int markX = (mx / tw);
int markY = (my / th);
if (placementButton.Checked )
tileMap.setTile(curLayer,new Point(markX, markY), tileSetSel);
if (selectionButton.Checked || fillButton.Checked)
{
if (clicked)
{
dragX1 = mx;
dragY1 = my;
}
}
if (placeObjectButton.Checked)
{
String objName = selObjTemplate.name;
Dictionary objPropDict = selObjTemplate.propertyDict;
objPropDict = sanitizePropDict(objPropDict);
tileMap.objInstList.Add(new ObjectInstance(objName, objPropDict, new Point(mx, my),1));
}
if (selectionButton.Checked && objectViewButton.Checked)
{
foreach( ObjectInstance objInst in tileMap.objInstList )
{
Bitmap defImg = tileMap.objTemplates[objInst.objName].imgStates.getImg("default");
int instW = defImg.Width;
int instH = defImg.Height;
int instX = objInst.pnt.X;
int instY = objInst.pnt.Y;
if( mx >= instX && mx <= instX +instW )
if (my >= instY && my <= instY + instH)
{
ObjectInstanceForm objInstForm = new ObjectInstanceForm(objInst.objName,objInst.propDict,tileMap.assetDict,objInst.visible);
if (objInstForm.ShowDialog() == DialogResult.OK)
{
clicked = false;
objInst.propDict = objInstForm.propDict;
objInst.visible = objInstForm.visible;
mapBox.Refresh();
break;
}
}
}
}
if (deletionButton.Checked)
{
if (objectViewButton.Checked)
{
foreach (ObjectInstance objInst in tileMap.objInstList)
{
Bitmap defImg = tileMap.objTemplates[objInst.objName].imgStates.getImg("default");
int instW = defImg.Width;
int instH = defImg.Height;
int instX = objInst.pnt.X;
int instY = objInst.pnt.Y;
if (mx >= instX && mx <= instX + instW)
if (my >= instY && my <= instY + instH)
{
tileMap.objInstList.Remove(objInst);
break;
}
}
}
if (selList.Count > 0)
{
foreach (Point pnt in selList.Values.ToList())
{
tileMap.setTile(curLayer,pnt, new TileValue(new Point(-1, -1), 0));
clearSelList();
}
}
else
{
tileMap.setTile(curLayer,new Point(markX, markY), new TileValue(new Point(-1, -1), 0));
}
}
if (moveButton.Checked)
{
TileMap tmpTileMap = new TileMap(tileMap);
int aMx = mx / tw;
int aMy = my / th;
Dictionary tmpSelList = new Dictionary();
Point tmpInitSelPnt = new Point(aMx, aMy);
int tmpSelW = selW;
int tmpSelH = selH;
for (int i = aMx; i < aMx + selW; i++)
for (int j = aMy; j < aMy + selH; j++)
{
int normX = i - aMx + initSelPnt.X;
int normY = j - aMy + initSelPnt.Y;
if (selList.ContainsKey(new Point(normX, normY)))
{
TileValue tValue = tmpTileMap.getValue(curLayer, selList[new Point(normX, normY)]);
tileMap.setTile(curLayer,new Point(i, j), tValue );
tmpSelList.Add(new Point(i, j), new Point(i, j));
}
//g.DrawRectangle(System.Drawing.Pens.Red, i * tw, j * th, tw, th);
}
foreach (Point pnt in selList.Values.ToList())
{
tileMap.setTile(curLayer, pnt, new TileValue(new Point(-1, -1), 0));
}
clearSelList();
selW = tmpSelW;
selH = tmpSelH;
initSelPnt = tmpInitSelPnt;
selList = tmpSelList;
}
mapBox.Refresh();
}
}