Translate

Thursday, October 16, 2014

How to use Blender as a Box2D Platformer Level Editor [Tutorial]



Hey guys so you want a level editor but don't want to build one yourself right? Who does? You basically need to create a entire tangential project, with full GUI editor and undo tree etc. Unless that's a goal of yours (selling etc?) you probably want an easier solution.

Well folks, look no further than your good friend Blender 3D. The same tool you use for modeling crazy cartoon knights swinging swords and whatnot, can be your level editor for your 2D game.



A Plan

So how do we do this? What do we need?


  • We need a output format that's easy to parse.
  • We need to decide on attributes that we can assign meaning to (for box2D and our game)
  • The texturing and vertex placement etc, comes for free since we're using a 3D modeling format :)




The OBJ file format.


Why OBJ? This is super easy to parse. You could use another format, if you're already using a parser (I recommend ASSIMP :) ), but for this tutorial, I'm going to use OBJ so you can see all the gory details of parsing it. Don't run yet, it's easier than it sounds :)

Ok so we know OBJ can specify vertex types, color, texture etc, but how does this relate to Box2D and how will we attribute context so we know what object type to create?


Aha, that's pretty simple too. We can just use parts of the Blender material name to assign meaning. For example, for solid static objects we could have the substring "solid" to designate this is a Box2D object which will be collidable with other physics objects.

For example

material name = "solid_wood"




Notice material name "solid_wood"




For maybe a dynamic rock we could use:

material name = "ball_dynamic"

Or even easier, if the code knows about this object type, we could just name the type of game object outright:

material name = "rabid_squirrel"

Then we just do a string compare on the material name of each object read to see if we should make a "rabid_squirrel" at that position.

 




rabid_squirrel is the material name, but it's also the name of the GameObject (RabidSquirrel) :) easy peezy


Note: For Hoarder Monkey, I just assume anything without a special keyword "solid" etc, or an outright object type we know, about is just a decoration object that has no collision associated with it (pass through).

You could also do nutty things in blender, such as assign meaning to different objects that should be joined with a joint. E.g.

One object in Blender could be:

material name = "wood_object1_jointA"


The other part of the joint could be:
material name = "wood_object2_jointA"

Then you could assign a pivot point by another object(s) and just use the center of this object as the anchor point(s), etc. But I won't get into that in this tutorial. It's a bit complex, for just getting started.

We can also assign other meaning to the z value of the objects. For example, far away objects could use a special shader for parallax etc (see other post)

Anyway, with these little tricks we can repurpose Blender as a pretty solid Box2D editor.



Lets get started.


First let's build a OBJ parser. For brevity I'm not going to explain OBJ format or the parser, but I am open to any questions so I can clarify anything.

Just to note the current known limitations are:
  • Joined objects will crash the parser 
  • No colored vertices, just textures 
  • Don't export normals (it might do weird things?) 
  • Important: Make sure the winding order of your objects is correct (the normal shown in blender should point out of the screen).  This is subtle, so if you're unsure, turn them on in the viewer:
    • First push 1 on your Number pad so your view has the blender Z axis up and blender X axis to the right.


Select Item; Push N to get item properties menu; Go into edit mode (Tab); Select Face normals.

I had to shift my Ortho view by holding middle mouse button so I could see the normal is point out of the screen.





  • All objects must be triangulated before exporting to OBJ (attach the "triangulate" modifier to every object. No need to apply it though, if you set the "apply modifiers" option on export). 
    • If you don't, things will not render correctly and or, Box2D will crash due to sending vertices in the wrong order (see issue above) . Let's just avoid all these issues and triangulate :)


  • When exporting to obj: make sure that your 
    • Forward axis is set to "-Z Forward"  (for opengl )
    • Up Axis to "Y Up"
    • I also chose relative path mode so textures paths are referenced correctly):



So great, now how do we model this in blender? For dynamic objects I simply use the "Import as planes" menu option.




If this option is not available in your menu (it isn't by default usually) enable it by going to:

File->User Preferences; then search for "plane" and check the box on "Import-Export: Import Images as Planes", addon.



When you import your plane, Be sure to rotate the object by 90 degrees on the x axis. 

For repeating texture type objects, ground, etc, you can simply model your 2D object however you want, taking care that your normals are pointing out of the screen. Then you can UV map the object to your liking.




Parsing the ".obj"

Feel free to skip over this section!

Remember, this is just to be a clear example. Please feel free to use assimp for a free out of the box, excellent parser. But if you are having a hard time porting to your platform, the below obj parser, while limited, should do just fine for most situations.

Here's our class definitions for storing the obj data. ObjFile is your main data source, that will contain all your ObjModels and a list of vertices and texture coordinates that they will index from (that's the structure of Obj).

    Ok, ok I'll give you a little example on the file output, but this is a bit out of scope of this tutorial:

    # Blender v2.70 (sub 0) OBJ File: 'test_blender_level.blend
    # www.blender.org
    o palm_tree_Plane.046
    v 5.825770 2.393540 -25.426136
    v 36.587597 2.393540 -25.426136
    v 5.825770 33.155369 -25.426109
    v 36.587597 33.155369 -25.426109
    vt 0.999900 0.000100
    vt 0.999900 0.999900
    vt 0.000100 0.999900
    vt 0.000100 0.000100
    usemtl palm_tree
    s off
    f 18/9 20/10 19/11
    f 17/12 18/9 19/11
    In this case the line starting with:

  • 'o' is the object name; 
  • 'v' a vertex, 
  • 'usemtl' is the material name of the object (important!), 
  • 's', ignore it it's about shading (if you're doing just textured sprites, you won't care) 
  • 'f', the "faces" of the object. (e.g "f 18/9 20/10 19/11" The vertex at index 18, is paired with texture coord at index 9, etc). 

  • Anyway, that's not terribly pertinent to this tutorial. Please look up wavefront format to get a better grasp on the file if you're interested.
    [ ObjLoader.h ]
 #ifndef OBJLOADER_H  
 #define OBJLOADER_H   
   
 #include <vector>  
 #include <string>  
 #include <map>  
   
 #include "Vector2D.h"  
 #include "Vector3D.h"  
   
 //***************************************************************************  
 /**  
  */  
 class ObjMaterial  
 {  
   
 public:  
   
  std::string name;  
  std::string kd_image_path;  
  std::string ka_image_path;  
  std::string ke_image_path;  
   
  std::string toString() const;  
  void loadTextures();  
 };  
   
 /// <material_name, ObjMaterial>  
 typedef std::map<std::string, ObjMaterial> ObjMaterialMap;  
   
 //***************************************************************************  
 /**  
  * Set of faces associated with a material  
  */  
 class Faces  
 {  
   
 public:  
  std::string material_name;  
   
  /// All face indices of model  
  std::vector<int> vertex_index;  
  /// All texture indices of model (if they exist)  
  std::vector<int> texture_index;  
   
 };  
   
 class ObjFile;  
 //***************************************************************************  
 /**  
  */  
 class ObjModel  
 {  
 public:  
  /// Name of model  
  std::string name;  
   
  /// All faces as a list of vertex/texture indexes  
  std::vector<Faces> faces;  
   
  ObjFile* obj_file;  
   
  ObjModel(ObjFile* file):  
   obj_file(file)  
  {}  
   
 };  
   
 //***************************************************************************  
 class ObjFile  
 {  
 public:  
  std::vector<ObjModel> obj_models;  
   
  ObjMaterialMap material_map;  
   
  /// All 3D vertices of model  
  std::vector<Vector3Df> vertex;  
  /// All texture coords of model (if they exit)  
  std::vector<Vector2Df> texture_coord;  
   
  ObjFile():  
   obj_models(),  
   material_map(),  
   vertex(),  
   texture_coord()  
  {}  
   
  static void createVerts (const ObjModel& obj,  
    std::vector<Vector2Df>& tex_coords,  
    std::vector<Vector3Df>& verts,  
    std::vector<uint16_t>& indexes,  
    const Faces& faces);  
   
 };  
   
 //***************************************************************************  
 /**  
  * All OBJ model loading functions  
  */  
 namespace ObjLoader  
 {  
   
 void load_obj_file(const std::string file,  
   ObjFile& obj_file);  
 }  
   
 #endif /* OBJLOADER_H */  
[ ObjLoader.cpp ]
#include <fstream>  
 #include <vector>  
 #include <string>  
 #include <sstream>  
 #include <string.h>  
   
 #ifdef ANDROID_NDK  
 #include <android/asset_manager.h>  
 #include <android/asset_manager_jni.h>  
 #include "AndroidJNIHelper.h"  
 #endif  
   
 #include "Vector2D.h"  
 #include "Vector3D.h"  
 #include "Logging.h"  
 #include "Game.h"  
   
 #include "FileIO.h"  
 #include "ObjLoader.h"  
   
 using namespace std;  
   
 const string MATERIAL_HEADER = "usemtl";  
 const string MATERIAL_FILE_HEADER = "mtllib";  
 const string NEW_MATERIAL_HEADER = "newmtl";  
 const string MAP_KD = "map_Kd";  
 const string MAP_KA = "map_Ka";  
 const string MAP_KE = "map_Ke";  
 const string MAP_D = "map_d";  
   
 //***********************************************************************  
   
 inline int StrToInt(const string &str)  
 {  
  int i;  
  if (sscanf(str.c_str(), "%i", &i) == 1)  
    return i;  
  else return 0;  
 }  
   
 //***********************************************************************  
 inline std::vector<string> split_string(const string& str,  
   const string& split_str)   
 {  
   
  //printf("split_string: %s\n",str.c_str());  
  vector<string> stlv_string;  
  string part_string("");  
  string::size_type i;  
   
  i=0;  
  while(i < str.size())   
  {  
   // If this char in string is a split char  
   if(split_str.find(str[i]) != string::npos)   
   {  
    // Save the built string so far  
    stlv_string.push_back(part_string);  
    // Reset to capture further string pieces  
    part_string="";  
   
    //  Ignore multiple delimiters in a row.  
    while(split_str.find(str[i]) != string::npos)   
    {  
     ++i;  
    }  
   }  
   else   
   {  
    part_string += str[i];  
    ++i;  
   }  
  }  
  if (!part_string.empty())  
    stlv_string.push_back(part_string);  
   
  //printf("stlv_string: %s\n",part_string.c_str());  
  //exit(1);  
  return stlv_string;  
 }  
   
 //***********************************************************************  
 /**  
  * Clean up path of ObjMaterial  
  */  
 void cleanPath(ObjMaterial& material)  
 {  
   
  while(replace(material.ka_image_path, "../", "./"));  
  while(replace(material.kd_image_path, "../", "./"));  
  while(replace(material.ke_image_path, "../", "./"));  
   
  replace(material.ka_image_path, "assets/", "/");  
  replace(material.kd_image_path, "assets/", "/");  
  replace(material.ke_image_path, "assets/", "/");  
   
  LOGD("Cleaned paths Material: %s\n", material.toString().c_str());  
 }  
   
 //***********************************************************************  
 void load_mtl_file( const std::string file,  
   ObjFile& obj_file)  
 {  
   
  ObjMaterialMap& material_map = obj_file.material_map;  
   
  //---------------------------------------------------  
  //  Open the file   
  //---------------------------------------------------  
  FILEIO_OPEN(infile,file);  
   
  printf("About to load material file: %s\n", file.c_str());  
   
  bool parsing_material = false;  
  ObjMaterial material;  
   
  //  Iterate through each line in the file  
  string current_line;  
   
  while(file_io.getLineAsset(infile, current_line) > 0)  
  {  
   
   
   stringstream line_stream(current_line);  
   LOGD("MTL line: %s\n", current_line.c_str());  
   
   string header;  
   line_stream >> header ;  
   LOGD("Found header: %s\n", header.c_str());  
   
   if(current_line.find(NEW_MATERIAL_HEADER) != string::npos)  
   {  
    string name;  
    line_stream >> name;  
    LOGD("Found new material name: %s\n", name.c_str());  
    if(parsing_material)  
    {  
   
     cleanPath(material);  
     material_map[material.name] = material;  
     LOGD("Saving material: %s\n", material.name.c_str());  
    }  
   
    material = ObjMaterial();  
    material.name = name;  
    parsing_material = true;  
   }  
   
   else if( current_line.find(MAP_KD) != string::npos)  
   {  
    string path;  
    line_stream >> path;  
    material.kd_image_path = path;  
    LOGD("Saving kd_map: %s\n", path.c_str());  
   }  
   
   else if( current_line.find(MAP_KA) != string::npos)  
   {  
    string path;  
    line_stream >> path;  
    material.ka_image_path = path;  
    LOGD("Saving ka_map: %s\n", path.c_str());  
   }  
   
   else if( current_line.find(MAP_KE) != string::npos)  
   {  
    string path;  
    line_stream >> path;  
    material.ke_image_path = path;  
    LOGD("Saving ke_map: %s\n", path.c_str());  
   }  
   
  }  
   
  if(parsing_material)  
  {  
   
   cleanPath(material);  
   material_map[material.name] = material;  
   LOGD("Saving material: %s\n", material.name.c_str());  
  }  
   
 }  
   
 //***********************************************************************  
 /**  
  * Extract path of a file minus the file name  
  *  Will work with backslash and forward slash as path delimiters  
  */  
 std::string getDirectoryOfFile(std::string file)  
 {  
  unsigned found = file.find_last_of("/\\");  
  return file.substr(0, found);  
 }  
   
 //***********************************************************************  
 /**  
  * Load a obj file   
  *  
  * @param file file name to load  
  */  
   
 void ObjLoader::load_obj_file(const std::string file,  
   ObjFile& obj_file)  
 {  
   
  std::vector<ObjModel>& obj_models = obj_file.obj_models;  
   
  printf("Entered ObjLoader::load(char*)...\n");  
  printf("File:%s\n",file.c_str());  
   
  printf("About to load file...\n");  
   
  // Count the number of faces placed in structure  
  int face_count = 0;  
   
  // The current model we are parsing for  
  bool parsing_faces = false;  
  Faces faces;  
  bool parsing_model = false;  
  ObjModel model(&obj_file);  
   
  // Save dir name of this file  
  string dirname = getDirectoryOfFile(file);  
   
  // The materials file for this OBJ if it exists  
  string material_file = "";  
   
  //---------------------------------------------------  
  //  Open the OBJ file   
  //---------------------------------------------------  
  FILEIO_OPEN(infile,file);  
   
  //  Iterate through each line in the file  
  string current_line;  
   
  while(file_io.getLineAsset(infile, current_line) > 0)  
  {  
   
   stringstream line_stream(current_line);  
   LOGD("OBJ line: %s\n", current_line.c_str());  
   
   switch (current_line[0])  
   {      
   
    //---------------------------------------------------  
    // Object name  
    //---------------------------------------------------  
    case 'o':  
     {  
      string o;  
      string object_name;  
      line_stream >> o >> object_name;  
      LOGD("Found new object name: %s\n", object_name.c_str());  
   
      // Save old model if we were parsing one  
      if(parsing_model)  
      {  
   
       if(faces.vertex_index.size())  
       {  
        model.faces.push_back(faces);  
        LOGD("Saved old faces: %s\n", faces.material_name.c_str());  
        faces = Faces();  
        parsing_faces = false;  
       }  
       obj_models.push_back(model);  
       LOGD("Saving model: %s\n", model.name.c_str());  
      }  
   
      model = ObjModel(&obj_file);  
      model.name = object_name;  
      parsing_model = true;  
     }  
     break;  
   
     //---------------------------------------------------  
     // Take care of vertex types  
     //---------------------------------------------------  
    case 'v':  
     {  
   
      // check the next character on the line  
      switch(current_line[1])  
      {  
   
       // Parse normals  
       case 'n':  
        {  
         // Vertex cordinates  
         Vector3Df v;  
         if(sscanf(current_line.c_str(), "vn %f %f %f",   
            &v.x,&v.y,&v.z) != 3)  
         {  
          LOGE("Error reading line: %s", current_line.c_str());  
          continue;  
         }  
         LOGE("Normals ignored!: %s\n", current_line.c_str());  
        }  
        break;  
   
        // Parse texture coords  
       case 't':  
        {  
         Vector2Df v;  
         if(sscanf(current_line.c_str(), "vt %f %f",   
            &v.x,&v.y) != 2)   
         {  
          LOGE("Error reading line: %s", current_line.c_str());  
          continue;  
         }  
         obj_file.texture_coord.push_back(v);  
        }  
        break;  
   
        // Parse a normal vertex  
       default:  
        {  
         // Vertex cordinates  
         Vector3Df v;  
         if(sscanf(current_line.c_str(), "v %f %f %f",   
            &v.x,&v.y,&v.z) != 3)  
         {  
          LOGE("Error reading line: %s\n", current_line.c_str());  
          continue;  
         }  
         LOGD("Saving default vertex: %s\n", v.toString().c_str());  
         obj_file.vertex.push_back(v);  
        }  
        break;  
   
      }  
     }  
     break;  
   
     //---------------------------------------------------  
     //  Parse faces from file  
     //---------------------------------------------------  
    case 'f':  
     {  
   
      Vector3Di vert_index;  
      Vector3Di text_index;  
   
      if(sscanf(current_line.c_str(), "f %d %d %d\n",   
         &vert_index.x,  
         &vert_index.y,  
         &vert_index.z)  
        == 3)  
      {  
       LOGD("Parsed face line: %s\n", current_line.c_str());  
       faces.vertex_index.push_back(vert_index.x-1);  
       faces.vertex_index.push_back(vert_index.y-1);  
       faces.vertex_index.push_back(vert_index.z-1);  
       LOGD("Vertex indexes saved (subtract 1): %s\n",   
         vert_index.toString().c_str());  
      }  
   
      else if(sscanf(current_line.c_str(), "f %d/%d %d/%d %d/%d\n",   
         &vert_index.x,  
         &text_index.x,  
         &vert_index.y,  
         &text_index.y,  
         &vert_index.z,  
         &text_index.z  
         )  
        == 6)  
      {  
       LOGD("Parsed face line: %s\n", current_line.c_str());  
       faces.vertex_index.push_back(vert_index.x-1);  
       faces.vertex_index.push_back(vert_index.y-1);  
       faces.vertex_index.push_back(vert_index.z-1);  
       faces.texture_index.push_back(text_index.x-1);  
       faces.texture_index.push_back(text_index.y-1);  
       faces.texture_index.push_back(text_index.z-1);  
       LOGD("Vertex indexes saved: %s\n", vert_index.toString().c_str());  
       LOGD("Texture indexes saved: %s\n", text_index.toString().c_str());  
      }  
   
      else  
      {  
   
       LOGE("Error reading face line: %s\n", current_line.c_str());  
       LOGD("Could be that there are normals in this file?\n");  
       continue;  
      }  
   
      ++face_count;  
   
     }  
     break;  
   
     // Skip comments  
    case '#':  
     break;  
   
     //---------------------------------------------------  
     // ObjMaterials lib  
     //---------------------------------------------------  
    case 'm':  
     {  
   
      if(current_line.find(MATERIAL_FILE_HEADER) == string::npos)  
      {  
       LOGE("Could not find material header:%s in line: %s\n",  
         MATERIAL_FILE_HEADER.c_str(), current_line.c_str());  
       continue;  
      }  
   
      string header;  
      line_stream >> header >> material_file;  
      LOGD("Found material file: %s\n", material_file.c_str());  
     }  
     break;  
   
     //---------------------------------------------------  
     // ObjMaterial type found  
     //---------------------------------------------------  
    case 'u':  
     {  
   
      if(current_line.find(MATERIAL_HEADER) == string::npos)  
      {  
       LOGE("Could not find material header:%s in line: %s\n",  
         MATERIAL_HEADER.c_str(), current_line.c_str());  
       continue;  
      }  
   
      string header;  
      string material_type;  
      line_stream >> header >> material_type;  
      LOGD("Found material type: %s\n", material_type.c_str());  
      if(parsing_faces)  
      {  
       model.faces.push_back(faces);  
       LOGD("Saved old faces: %s\n", faces.material_name.c_str());  
      }  
      faces = Faces();  
      faces.material_name = material_type;  
      parsing_faces = true;  
     }  
     break;  
   
     // Skip unknowns  
    default:   
     LOGD("Skipped unknown line: %s\n", current_line.c_str());  
     break;  
   }  
  }  
   
  // Save old model if we were parsing one  
  if(parsing_model)  
  {  
   
   if(faces.vertex_index.size())  
   {  
    model.faces.push_back(faces);  
    LOGD("Saved old faces: %s\n", faces.material_name.c_str());  
    faces = Faces();  
    parsing_faces = false;  
   }  
   obj_models.push_back(model);  
   LOGD("Saving model: %s\n", model.name.c_str());  
  }  
   
  // Parse materials file  
  if(material_file.size() > 0)  
  {  
   load_mtl_file(dirname + "/" + material_file,  
     obj_file);  
   
  }  
   
 }  
   
 //***********************************************************************  
 void ObjMaterial::loadTextures()  
 {  
   
  Game& game = Game::getInstance();  
   
  if(kd_image_path.size() )  
  {  
   game.addTextureToLevel(kd_image_path);  
  }  
   
  if(ka_image_path.size())  
  {  
   game.addTextureToLevel(ka_image_path);  
  }  
   
  if(ke_image_path.size())  
  {  
   game.addTextureToLevel(ka_image_path);  
  }  
   
 }  
   
 //*****************************************************************************  
 /**  
  */  
 void ObjFile::createVerts(const ObjModel& obj,  
   std::vector<Vector2Df>& tex_coords,  
   std::vector<Vector3Df>& verts,  
   std::vector<uint16_t>& indexes,  
   const Faces& faces)  
 {  
   
  int stored_index = 0;  
  for(size_t vi = 0; vi < faces.vertex_index.size(); vi++)  
  {  
   
   int index = faces.vertex_index[vi];  
   const Vector3Df& vert = obj.obj_file->vertex[index];  
   bool found = false;  
   
   for(size_t i = 0; i < verts.size() && !found; i++)  
   {  
   
    if(verts[i] == vert)  
    {  
     found = true;  
     // Found vert in list so store it's index  
     indexes.push_back(i);  
    }  
   }  
   
   if(!found)  
   {  
    verts.push_back(vert);  
   
    // Add texture coordinates in the same order as the verts in the   
    // face list e.g.:  
    //  f 4/40 7/20  
    // vet[4] will be same index as tex[40] when stored   
    if(faces.texture_index.size() == faces.vertex_index.size())  
    {  
     const Vector2Df& tex =   
      obj.obj_file->texture_coord[ faces.texture_index[vi]];  
     tex_coords.push_back(tex);  
    }  
   
    indexes.push_back(stored_index);  
    stored_index++;  
   }  
   
  }  
   
 }  
   
 //***********************************************************************  
 std::string ObjMaterial::toString() const  
 {  
  string to_ret;  
  to_ret =   
   "Name: "+ name + "\n"  
   "kd path: " + kd_image_path +"\n"  
   "ka path: " + ka_image_path + "\n"  
   "ke path: " + ke_image_path + "\n";  
  return to_ret;  
 }  
   


Ok sorry for the dump there. Like I said, you could skip that if you're not interested in parsers. The key thing to take away here is not the low level details, but the overall idea on how to tie together Blender 3D file exports with creating a level in your game.



How to give this Obj data Context

Now let's take the data that we parsed and create our game objects. First we take in the big hunk of data (ObjFile) and create every game object from the ObjModel objects it contains.


//*****************************************************************************  
 /**  
  *  Load level based on OBJ parsed info  
  */  
 void Game::loadLevel( ObjFile& obj_file)  
 {  
  Vector3Df location;  
  Vector3Df size;  
  for(ObjModel obj : obj_file.obj_models)  
  {  
   if(obj.faces.size() == 0)  
   {  
    LOGE("Obj to load has no faces!: %s\n",  
      obj.name.c_str());  
    continue;  
   }  
   loadModel(obj);  
  }  
 }  


Here's an example of using a material property to set a GameObject property. In other words using Blender as your Game level editor! Extend as you'd like.

This just uses the string comparison to see if the object is a METAL_BOX and creates it based on that specific material name.

//*****************************************************************************  
 /**  
  *  Load model based on OBJ parsed info (from #ObjFile)  
  */  
 void Game::loadModel( ObjModel& obj_model)  
 {  
  string material = obj_model.faces[0].material_name.c_str();  
  LOGD("Loading model: %s, material: %s\n", obj_model.name.c_str(),  
    material.c_str());  
  if(obj_model.faces[0].material_name == METAL_BOX_MATERIAL)  
  {  
   LOGD("Create a METAL BOX from OBJ: %s\n",  
     obj_model.name.c_str());  
   MetalBoxSprite* b = new MetalBoxSprite();  
   b->createFromObj(obj_model);  
  }  
....

Here's another example of using a material property to set a GameObject property. Extend as you'd like.

This one is a little different than the last, it will take the partial match of the material name to decide if the object should be semi transparent.




//*****************************************************************************  
 /**  
  *  Create a sprite from an OBJ model  
  */  
 void Sprite::createFromObj(const ObjModel& obj)  
 {  
   
  FrameCoord tex_coords;  
  Vector3Df position = createVertsFromObjModel(obj, tex_coords);  
   
  string material = obj.faces[0].material_name.c_str();  
  LOGD("Loading Sprite model: %s, material: %s\n", obj.name.c_str(),  
    material.c_str());  
   
  // Example of using material name for object property 
  if(material.find(SEMI_TRANSPARENT_PARTIAL_NAME) != string::npos ||   
    obj.name.find(SEMI_TRANSPARENT_PARTIAL_NAME) != string::npos)  
  {  
   
   LOGD("Found semi transparent sprite for material: %s\n",  
     material.c_str());  
   setSemiTransparent(true);  
  }  
  else  
  {  
   setSemiTransparent(false);  
  }  
   
  // Override this in subclass to load a texture other than the OBJ one  
  loadTextureBasedOnObjModel(obj, tex_coords);  
   
  // Must be overridden in a subclass to do anything!  
  createAnimationFrames(tex_coords);  
  createBox2DBody(Vector2Df::convertTo2D(position));  
 }   

....

//*****************************************************************************  
 /**  
  *  
  *  @param obj to parse data from  
  *  @param tex_coords texture coords from ObjFile for this ObjModel   
  *   stored here after parsing @p obj.  
  *  
  *  @return position to use for creating a Box2D obj  
  */  
   
 Vector2Df Sprite::createVertsFromObjModel(const ObjModel& obj,  
   std::vector<Vector2Df>& tex_coords)  
 {  
   
  assert(obj.faces.size());  
   
  std::vector<Vector3Df> temp_verts;  
  ObjFile::createVerts(obj, tex_coords, temp_verts, index_list, obj.faces[0]);  
  Vector2Df min = Vector2Df::convertTo2D(setVertexesAndCalcCenter(temp_verts));  
  calculateCenter();  
  return min;  
 }   

...
 //*****************************************************************************  
 /**  
  * Create the box2D body for this Sprite  
  */  
 b2Body* Sprite::createBox2DBody(const Vector2Df& position,  
   float density, float friction, b2BodyType type)  
 {  
   
  box2d_body = Box2DContext::createBox2DBodyPolygon(  
    position, density, friction,  
    vertex, index_list, this, type);  
  return box2d_body;  
 }    


Physics Magic



Here's where the physics magic happens!  We will not create the box2D object from the ObjModel parsed. I just use a Box2D, b2PolygonShape object so we can create any editor object (so long as they are counter-clockwise winding vertices)

//*****************************************************************************  
 /**  
  * Create the box2D body for this Sprite  
  */  
 b2Body* Box2DContext::createBox2DBodyPolygon(const Vector2Df& position,  
   float density, float friction,  
   const std::vector<Vector3Df>& vertex,   
   const std::vector<uint16_t>& index_list,  
   GameObject* obj,  
   b2BodyType type)  
 {  
   
  if(vertex.size() == 0)  
  {  
   LOGE("vertex.size() == 0, not creating a b2Body!\n");  
   return NULL;  
  }  
  if(obj == NULL)  
  {  
   LOGE("User data was NULL in createBox2DBodyPolygon!\n");  
   return NULL;  
  }  
   
  if(obj->getGameObjectType() == INVALID_GAME_OBJECT)  
  {  
   LOGE("INVALID_GAME_OBJECT was passed to "  
     "createBox2DBodyPolygon() "  
     "position: %s, density: %f, friction: %f"  
     "\n",  
     position.toString().c_str(), density, friction);  
  }  
   
  // Get current Box2D context  
  Box2DContext& box2d_context = Box2DContext::getInstance();  
   
  b2BodyDef bd;  
  bd.type = type;  
  bd.userData = obj;  
   
  bd.position.x = position.x;  
  bd.position.y = position.y;  
  b2Body *box = box2d_context.getb2World()->CreateBody(&bd);  
   
  vector<Vector2Df> list_2d;  
  convertTo2D(list_2d, vertex);  
   
  LOGD("list_2d.size(): %zu\n", list_2d.size());  
   
  // Number of fixtures created  
  int i = 0;  
   
  // Code to render when not using a vertex index list  
  //  (for generated sprites not from blender etc)  
  if(index_list.size() == 0)  
  {  
   
   b2FixtureDef fixture_def;  
   vector<Vector2Df> face;  
   for(auto v: list_2d)  
   {  
    face.push_back(v);  
    if(face.size() == 3)  
    {  
   
     LOGD("Creating fixture: %d\n", i);  
     b2PolygonShape shape;  
     shape.Set((b2Vec2*)&face[0], (int32)face.size());  
     fixture_def.shape = &shape;  
     fixture_def.density = 1.0f;  
     fixture_def.friction = 0.3f;  
     box->CreateFixture(&fixture_def);  
     face.clear();  
     i++;  
    }  
   }  
  }  
  else  
  {  
   
   LOGD("Creating Box2D fixtures from vertex index!\n");  
   b2FixtureDef fixture_def;  
   vector<Vector2Df> face;  
   
   for(auto index: index_list)  
   {  
   
    face.push_back(list_2d[index]);  
    if(face.size() == 3)  
    {  
     LOGD("Creating fixture: %d\n", i);  
     b2PolygonShape shape;  
     shape.Set((b2Vec2*)&face[0], (int32)face.size());  
     fixture_def.shape = &shape;  
     fixture_def.density = 1.0f;  
     fixture_def.friction = 0.3f;  
     box->CreateFixture(&fixture_def);  
     face.clear();  
     i++;  
    }  
   }  
  }  
   
  if(i < 2)  
  {  
   LOGE("Only created %d fixtures!\n", i);  
  }  
   
  // Save body reference  
  return box;  
   
 }  


Ultimately, you decide how you want to hack the material name string to do your evil bidding.










Hacking Blender for your game level editor is so.... evil 












Saturday, August 23, 2014

How to render Parallax Background and Foreground Layers Completely in the Shader [Tutorial]

EDIT Oct 16 2014: Changed precision to lowp for fragment shader. Consider using this as a default for mobile, unless you really need something better.  I saw a 2x speed increase on Kindle by doing so.


Hi all!

Here's a quick little diddy for anyone curious on a quick way to get a layered effect in your 2D game (note: we are still using 3D vertices):

I did a little more progress on Hoarder Monkey  and I am happy to have parallax backgrounds working all in the shader and in 1 draw call per object type (static VBO, dynamic sprites, Semi-transparent VBO, semi-transparent sprites).  :)

The background scrolling is all handled in the shader, and works surprisingly well.  I also do some color tweaking to make things a little darker in the foreground and blue-ish in the background:


Notice the closest graphics are darker


Notice the background objects are a more blue/white hue.



In case your curious the shader code is pretty simple (as it should be for a shader):



Here's the Vertex shader for parallax.  Notice I just arbitrarily chose < -50 to be the furthest away, scrolling layer (right before the background).  This code will render based on camera movement half the distance that the action layer, will move (action layer == z distance where player is):

new_offset = u_camera_offset.xy / 2.0;

The closest palm trees and stuff  will move twice as fast as the action layer, abritrarily chosen to be distance Z > 50.0 (Keep in mind, in OpenGL -Z is further away from the camera, while +Z is towards the screen!):

new_offset = u_camera_offset.xy * 2.0;

uniform mat4 u_projection_matrix;
uniform float u_cutoff_alpha;
uniform vec2 u_camera_offset;
uniform float u_scale;

attribute vec4 a_Position;
attribute vec2 a_texture_coords;

varying vec2 v_texture_coords;
varying float v_cutoff_alpha;
varying float v_zdepth;

void main()
{

  vec2 new_offset = u_camera_offset;
  v_texture_coords = a_texture_coords;

  if(a_Position.z < -50.0)
  {
    new_offset = u_camera_offset.xy / 2.0;

  }
  else if(a_Position.z > 50.0)
  {
    new_offset = u_camera_offset.xy * 2.0;

  }

  vec4 pos =  u_scale* (vec4(new_offset.x, new_offset.y, 0.0, 0.0) + 
      vec4(a_Position.x, a_Position.y, a_Position.z/2.0, a_Position.a));

  gl_Position = u_projection_matrix * pos;
  v_cutoff_alpha = u_cutoff_alpha;
  v_zdepth = a_Position.z;

}


Ok, now that scrolling business has been taken care of, for that oh so nice cartoon layered depth illusion.  Now what about coloring.  How about we color the background layers a little bit blue-er and white than the closer layers, and maybe the foreground (closest branches and such) can be a little more black?

// The Blue/white far scrolling layer
orig_color.b *= 1.90; 
gl_FragColor = mix(vec4(1.0,1.0,1.0,orig_color.a), orig_color, .9);

// The closest layer (more black color)
gl_FragColor = mix(vec4(0.0,0.0,0.0,orig_color.a), orig_color, .5);

// The normal action layer (where player is etc, no color changes):
gl_FragColor = orig_color;


#ifdef GL_ES
precision lowp float;
#endif

uniform sampler2D u_texture;
varying vec2 v_texture_coords;
varying float v_cutoff_alpha;
varying float v_zdepth;

void main()
{

  vec4 orig_color =  texture2D(u_texture, v_texture_coords);

  if(orig_color.a < v_cutoff_alpha)
  {
    discard;
  }
  
  if(v_zdepth < -50.0)
  {

    orig_color.b *= 1.90;
    gl_FragColor = mix(vec4(1.0,1.0,1.0,orig_color.a), orig_color, .9);

  }
  else if(v_zdepth > 50.0)
  {

    gl_FragColor = mix(vec4(0.0,0.0,0.0,orig_color.a), orig_color, .5);

  }
  else
  {
    gl_FragColor = orig_color;

  }
  
}


So there you have it.  In a nutshell this is how you can do parallax scrolling backgrounds all in the shader.  Of course you will still want to order your draw polygons to draw first:  closest to further away, and for semi-transparents draw first: further away to closest.

Draw ALL your semi-transparents last. Even after your background!  If you don't you'll get fun artifacts and it won't blend correctly.  Also, don't forget to use a TextureAtlas to reduce all your draws to just a few, and VBOs for any non-moving game objects (background decorations, non-moving platforms etc).

Happy GameDev-ing!

Sunday, August 17, 2014

How To Set Up Your Game Event Queue (Threaded) [Tutorial]


Hi all,

Finally back with a simple, but hopefully helpful, tutorial for you all to get an working event queue in a threaded environment.  In this case I will be using Android C++ and JNI as my playground to demonstrate.

What is an event queue


An event queue is a list of events that has occurred since your last game loop iteration.  Possible Events are:

  • Button presses on game controllers
  • Screen touch events (press up/down, multi touch, etc).
  • Keyboard events

So what is the point of this anyhow?  


Many times events will occur in the middle of your game loop, asynchronously.  In Android, they occur on a different thread by default, usually called the "UI thread".

If you process them as they come in on your "UI Thread" you could end up with some nasty concurrency bugs and crashes due to processing the event (in your game etc) right while the game loop thread is updating the same data.

This leads to some epic head scratching and lovely stack traces and pulling out some addr2line kung fu! (or ndk-stack in Android > NDK r6)

08-22 23:27:40.730: INFO/DEBUG(65): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
08-22 23:27:40.730: INFO/DEBUG(65): Build fingerprint: 'htc_wwe/htc_bravo/bravo/bravo:2.2/FRF91/218634:user/release-keys'
08-22 23:27:40.730: INFO/DEBUG(65): pid: 2474, tid: 2485  >>> com.test <<<
08-22 23:27:40.730: INFO/DEBUG(65): signal 11 (SIGSEGV), fault addr 00000001
...
08-22 23:27:40.790: INFO/DEBUG(65):          #00  pc 00018656  /data/data/com.t
...

Let's spare you all that. You've got enough to deal with in finishing your game!

Android only solution


Ok, if you're only going to be using android this could be the easiest solution.

// In the class that handles your input
  
  //*****************************************************************************
  /**
   *  Handle all touch events for the game
   */

  @Override 
  public boolean onTouch(View v, MotionEvent event)
  {

    final View v1 = v;
    final MotionEvent event1 = event;
        gl_surface.queueEvent(new Runnable() { 
            public void run()
            {
              onTouchRenderThread(v1, event1);
            } });
    return true;
  }

// ....


Take note of queueEvent.  queueEvent will queue the event handler you've specified (in this case onTouchRenderThread()) to be called on the Render Thread.  If you're lazy like me, you probably do all your game loop within the render thread, which is fine for small simple games.  This works great and pipes the event to be consumed by the render thread and you don't get any concurrency issues and all is right with the world.

Portable solution




Ok, what about if we want to port to IOS, Windows phone etc and we don't want to rewrite the event handling code?  Maybe these platforms don't have the same convenience function(s)?  That's where rolling your own event queue could make sense for your project.

We could write the solution in pure C++ (well except we are using JNI for capturing the event here but ignore that :) ), that way the crux of the event code is in a portable format that can be logic tested easier on multiple platforms.

If using Java for handling in coming events in Android, you'll still have this portion of (not as portable) code.  Notice there's no need for queueEvent, as we are going to handle concurrency by just mutexing the read and writes to the event queue itself.

Put on your C++ hat!  Here's a simple event queue for us to use:

/**
 *  Input types for the input queue
 */
typedef enum {
  MOVE_SINGLE_FINGER_INPUT_EVENT,
  TOUCH_DOWN_SINGLE_FINGER_INPUT_EVENT,
  TILT_INPUT_EVENT,
  TOUCH_DOWN_MULTI_FINGER_INPUT_EVENT,
  TOUCH_UP_INPUT_EVENT,
  PINCH_ZOOM_INPUT_EVENT,
  SCROLL_INPUT_EVENT,
}InputEventType;

//*****************************************************************************
/** 
 * Describe an input event
 */
class InputEvent
{

public:
  InputEventType type;
  int finger_num;
  float x;
  float y;
  float z;
  double percentage_zoom;

  InputEvent(InputEventType type_, int finger_num_, float x_,
  float y_, float z_, double percentage_zoom_):
    type(type_),
    finger_num(finger_num_),
    x(x_),
    y(y_),
    z(z_),
    percentage_zoom(percentage_zoom_)
  {
  }

};

//*****************************************************************************
class InputHandler
{

  private:

   //...

    /// Mutext to make sure input isn't written to by two threads at the
    /// same time
    pthread_mutex_t mutex;

    /// List of events to consume on render thread when ready
    std::vector<InputEvent> queued_events;

//...
    void queueEvent(const InputEvent& event);
    void getQueuedEvents(std::vector<InputEvent>& events);

    void processTouchDownDetected(int finger_num, float x, float y);
    void processTouchDownSingleFingerScreenSpace( 
        int finger_num, int x, int y);
    void processTouchDownMultiFingerScreenSpace(
    int finger_num, int x, int y);

    void processTouchUpScreenSpace(int finger_num, int x, int y);
    void processMoveSingleFingerScreenSpace(int finger_num, int x, int y);

//...

    InputHandler():
      first_touch(),
      up_touch(),
      paused(false),
      fingers_down(0)
    {

      pthread_mutex_init(&mutex, NULL);
      memset(last_touch, 0, sizeof(last_touch[0])*INPUT_FINGERS_SUPPORTED);
    }

    InputHandler(const InputHandler& i);

  public:
    static InputHandler& getInstance()
    {
      static InputHandler input;
      return input;

    }
//...

}
I put in some basic events that you can use for touch, scroll and zoom etc.  If you'd like to extend it for controllers/keyboards you can add int input_button to the InputEvent class etc and another event to the enum KEYBOARD_INPUT_EVENT, etc.

Here's that same Java code from earlier without the queueEvent function, shortcut. We don't need it since our own C++ InputQueue code will handle everything.

// In the class that handles your input

  //*****************************************************************************
  /**
   *  Handle all touch events for the game
   */

  @Override 
  public boolean onTouch(View v, MotionEvent event)
  {
    onTouchRenderThread(v, event);
    return true;
  }

// ....

This is the Java Touch event handler (all variants of touch events). It just makes sense of the types of touch events.

 What this does in this section isn't so important (so don't get bogged down by it), it's just shown as an example of how the data flows to native event handling.  I just wanted to be a bit verbose:

  //*****************************************************************************
  /**
   *  Handle all touch events for the game (to be called in render thread)
   */

  public boolean onTouchRenderThread(final View v, final MotionEvent event)
  {

    // Send all point data
    int touches = event.getPointerCount();
    int action = event.getActionMasked();

    //Log.v(Logging.TAG, "OnTouch() called..\n");

    switch(action & MotionEvent.ACTION_MASK)
    {

      // First finger down
      case MotionEvent.ACTION_DOWN:
        {

          active_pointer_id = event.getPointerId(0);

          processTouchDownDetected(touches-1, event.getX(), event.getY());
          break;
        }

        // First finger up
      case MotionEvent.ACTION_UP:
        {

          active_pointer_id = event.getPointerId(0);

          processTouchUpDetected(touches-1, event.getX(), event.getY());
          two_fingers_were_down = false;
          break;
        }

        // Second finger down
      case MotionEvent.ACTION_POINTER_DOWN:
        {

          Log.v(Logging.TAG, touches + " ACTION_POINTER down...\n");
          processTouchDownDetected(touches-1, event.getX(touches-1),
              event.getY(touches-1));

          two_fingers_were_down = true;

          //          Log.v(Logging.TAG, "pointer[0] " + last_touch[0] +
          //              "pointer[1]" + last_touch[1]);
          //          processMoveSingleFinger(0, event.getX(0), event.getY(0));

          break;

        }

        // Second finger up
      case MotionEvent.ACTION_POINTER_UP:
        {

          Log.v(Logging.TAG, touches + " Finger up...\n");

          break;
        }

        // Moving finger down
      case MotionEvent.ACTION_MOVE:
        {

          //final int index = event.findPointerIndex(active_pointer_id);
          if(touches > 1)
          {

            processTouchDownDetected(0, event.getX(0),
                event.getY(0));
            processTouchDownDetected(1, event.getX(1),
                event.getY(1));

            double distance = last_touch[0].distance(last_touch[1]);

            processPinchZoom(distance);
          }
          else if(two_fingers_were_down == false)
          {
            Log.v(Logging.TAG, "Moved Finger down...\n");
            processMoveSingleFinger(0, event.getX(0), event.getY(0));
          }

          break;
        }

    }

    if(touches <= 1 || 
      (action & MotionEvent.ACTION_MASK) != MotionEvent.ACTION_MOVE)
    {

      last_distance = 0;
    }

    return true;


  }
// ....


  /// Input event functions
  private native void processMoveSingleFinger(int finger, float x, float y);
  private native void processTouchDown(int finger, float x, float y);
  private native void processTilt(float x, float y, float z);
  private native void processTouchDownMultiFinger(int finger, float x, float y);
  private native void processTouchUp(int finger, float x, float y);
  private native void processPinchZoomNative(double distance);
  private native void processScroll(float x, float y);


Ok back to C++.  The following is an example of a Touch input handler in C++.

The oddly named function Java_com_razzlegames_HoarderMonkey_MyRenderer_processTouchDown is where JNI sends this particular touch event.

The function  InputHandler::processTouchDownSingleFingerScreenSpace, is where the event, is finally queued for the render thread (or whatever your main game logic sits in) to consume.

Keep in mind, we are still 100% in the UI thread for these call backs.

  

//***************************************************************************
/**
 *  Call back for single finger touches (in screen space)
 */

void InputHandler::processTouchDownSingleFingerScreenSpace(
    int finger_num, int x, int y)
{

  fingers_down++;
  if(paused)
  {
    return;
  }


  first_touch = Vector2Di(x,y);
  Vector2Df world_coord =
    InputHandler::transformPointScreenToWorld(Vector2Di(x,y));

  setLastTouch(x, y);

  queueEvent(InputEvent(TOUCH_DOWN_SINGLE_FINGER_INPUT_EVENT, finger_num, 
        world_coord.x, world_coord.y, 0, 0));

}


// This is just so we can decide not include this for other build platforms!
#ifdef ANDROID_NDK   


//...

//*****************************************************************************
/**
 *  Call back for single finger touches
 */

JNIEXPORT void JNICALL Java_com_razzlegames_HoarderMonkey_MyRenderer_processTouchDown(
    JNIEnv* env, jobject obj, jint finger, jfloat x, jfloat y)
{

  LOGD("Process touch down!");
  InputHandler& input_handler = InputHandler::getInstance();
  input_handler.processTouchDownSingleFingerScreenSpace(finger, x, y);

}

//...

#endif // ANDROID_NDK

Ok here's where we are finally going to queue up the message for main game loop thread to consume:

  
//*********************************************************************
/**
 */

void InputHandler::queueEvent(const InputEvent& event)
{
  pthread_mutex_lock(&mutex);
  queued_events.push_back(event);
  pthread_mutex_unlock(&mutex);

}

This is where we get the events.  Call this from the main Game loop thread:

//*********************************************************************
/**
 */

void InputHandler::getQueuedEvents(std::vector<InputEvent>& events)
{

  pthread_mutex_lock(&mutex);
  events = queued_events;
  queued_events.clear();
  pthread_mutex_unlock(&mutex);

}

 [ I decided to use a pass by reference, std::vector<InputEvent>& events, since I just didn't want to waste recreating all the vector and all the events on the stack by passing it back with return. If you prefer that method, it probably won't be much of a concern for performance. On average you won't queue up that many user input events in 1/60th - 1/30th of a second :)  (from my tests I was able to get a backlog of events of only a max of 2 or 3).  ]

Let's put it all together...


This is where all your hard work pays off and you actually use the event queue from your main Game loop, thread.

It's completely thread safe now since we are using pthread mutex to block reading and writing to the queue.

//*****************************************************************************
/**
 * Advance the game simulation
 */

void Game::stepGame()
{

  Renderer&amp; renderer = Renderer::getInstance();
  renderer.drawFrame();

  processInputQueue();

//.....

}


//*****************************************************************************
/**
 * Process all Input events from the input thread
 */

void Game::processInputQueue()
{

  InputHandler&amp; input_handler = InputHandler::getInstance();
  vector<InputEvent> events; 
  input_handler.getQueuedEvents(events);
  for(InputEvent e: events)
  {
    // Do things with events!
  }
}


Yay, you have synchronized Input Events!




You should now have a working structure to build your InputEvent queue for Android and tie it to your C++ game logic.  You can use this same InputEvent queue code for IOS, Android, Linux, probably even Windows with a Cygwin compiler.  

Please feel free to ask any questions below and have fun coding and learning!






Sunday, July 20, 2014

"Hoarder Monkey" Coming soon!

You're a Monkey with a serious collecting problem!  Hoarder Monkey is a fresh take on action platformers that melds tilt action rolling movement, with Donkey Kong style action mechanics, with plenty of goofy humor and surprises.

Can you collect all your precious missing junk before time runs out?  Beat your friends, impress the ladies... be *that guy*! Forget your 12 step programs, play Hoarder Monkey and indulge in all your OCD fantasies!


















Insane enemies, Rabid Squirrels and Monkey with a trash bag strapped to his back!  It has everything!


This guys didn't satisfy his collecting urges by playing Hoarder Money! Don't be this GUY!


I mean  common... Rabid squirrels and such!



"It's a squireeel! It's dead!".  Well nearly.. anyhow.  

So yeah, that's it for now, still working on levels features and all that Hoarder Monkey specific Blog on the way!

Coming soon to: 
Linux, Android and IOS!