www.xbdev.net
xbdev - software development
Monday July 15, 2024
Home | Contact | Support | 3D File Formats The bits and bytes...

DirectX3D MD2 Class - ~Quake 2 File Format~
Author bkenwright@xbdev.net

Here it is, this is a lot of code.. and gives you a class which allows you to use the .md2 file format very easily.  It works like this:

-1- Include the header in your file:

 

#include "md2.cpp"

-2- You create an instance of your class:

CMD2X g_md2;

-3- Call the member function "ImportMd2(..)" once with your filename etc:

g_md2.ImportMD2("pac3D.md2", "pac3D.bmp"); // -3- Load our data into our CLoadMD2 class instance.

-4- Inside your gameloop, you just call the member function as follows:

// Which Animation, e.g. 0 for stand

//            |          1 for run

//            |          2 for jump etc. etc.

//           \|/

//            |

m->Animate(   0   );   // This simply sets up our vertex buffer data in our class instance.

 

// Clear the back buffer to a blue color

g_pD3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );

     

m->RenderFrame();    // Renders our model to the screen.

 

// After rendering the scene we display it.

g_pD3DDevice->Present( NULL, NULL, NULL, NULL );

 

So what do you get on the screen if you decide to download the code?  Well here are the screen-shots as usual:

Well what you see in the right is some screen shots of the various key-frame animations, e.g. crawl, walk, jump etc.  All the timing code is contained within the class.

 

The code may seem long and bloated but it contains a lot of added features which I think are essential for smooth animation... for instance if you chose a really slow time delay the animations would be perfectly smooth but very slow (no jumpy-ness).  Also if you suddenly change from walking to crawling it will change gradually - it won't suddenly jurk!

The class is not the best most optimum solution but its pretty tidy, and for example you could create 20-30 instances of the model I've been using on a PC and it would work fine... it may only need increased optimisation if you are using extremely high definition md2 characters where poly counts go into the hundreds of thousands.

For this tutorial I chose to make the files into 3 parts as usual, seperating the main parts into 3 files.

main.cpp dxdraw.cpp md2.cpp

You can download the full source code and executable to improve upon and examine it if you want.  (download source code).

Now lets have a look at the code which does all that amazing work shall we... well hold on tight, as its a lot of code to swallow in one go...

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

/*                                                                         */

/* File: md2.cpp                                                           */

/* Author: bkenwright@xbdev.net                                       */

/* Date: 10-11-2002                                                        */

/*                                                                         */

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

// This file holds our self contained .md2 (quake2) class for loading in and

// displaying our .md2 file in directX3D.

 

struct stMd2Header

{

      int magic;              // The magic number used to identify the file.

      int version;            // The file version number (must be 8).

      int skinWidth;          // The width in pixels of our image.

      int skinHeight;         // The height in pixels of our image.

      int frameSize;          // The size in bytes the frames are.

      int numSkins;           // The number of skins associated with the model.

      int numVertices;  // The number of vertices.

      int numTexCoords; // The number of texture coordinates.

      int numTriangles; // The number of faces (polygons).

      int numGlCommands;      // The number of gl commands.

      int numFrames;          // The number of animated frames.

      int offsetSkins;  // The offset in the file for the skin data.

      int offsetTexCoords;// The offset in the file for the texture data.

      int offsetTriangles;// The offset in the file for the face data.

      int offsetFrames; // The offset in the file for the frames data.

      int offsetGlCommands;// The offset in the file for the gl commands data.

      int offsetEnd;          // The end of the file offset.

};

 

// Some structures to hold or read in data in.

struct stMd2Skins

{

      char skinName[64];

};

 

struct stMd2TexCoords

{

      short u, v;

};

 

struct stMd2Triangles

{

      short vertexIndex[3];

      short texIndex[3];

};

 

struct stMd2Vertices

{

      float vertex[3];

      float normal[3];

};

struct stMd2Frames

{

      char name[16];

      stMd2Vertices* pFinalVerts;

};

 

// These two variables are declared in dxdraw.cpp thats why they have the

// extern keyword in front of them.

extern LPDIRECT3D8 g_pD3D;

extern LPDIRECT3DDEVICE8 g_pD3DDevice;

 

struct stKeyFrame

{

      int start;

      int end;

      char szName[16];

};

struct stKey

{

      int numKeys;

      stKeyFrame *pKey;

};

 

// Our DirectX3D structure definition.

struct my_vertex

{

      FLOAT x, y, z;  // D3DFVF_XYZ

      FLOAT tu, tv;   // D3DFVF_TEX1

};

 

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

/*                                                                         */

/* The CLoadMD2 class, yup its name speaks for itself, it loads the 3D     */

/* model data from the .md2 file and then we can access its public data    */

/* variables to use the data.                                              */

/*                                                                         */

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

 

class CMD2X

{

public:

      CMD2X(){ m_vertex_description = (D3DFVF_XYZ | D3DFVF_TEX1); };

      ~CMD2X(){};

public:

      bool ImportMD2(char* szFileName, char* szTexName);

      void Release();

     

      void RenderFrame();

 

      void Animate(UINT iAnimKey);

protected:

      void ReadMD2Data();

      void SetUpFrames();

      void SetUpDX();

     

      bool Timer(float* t);

      void SetDXVertices(UINT iAnimKey);

 

      FILE* m_fp;

protected:

      stMd2Header       m_Md2Header;

 

      stMd2Skins        *m_pSkins;

      stMd2Triangles    *m_pTriangles;

      stMd2TexCoords  *m_pTexCoords;

      stMd2Frames       *m_pFrames;

protected:

      stKey             m_Keys;

      int m_curFrame;

      int m_nextFrame;

      int m_curAnimKey;

protected:

      float m_lastTime;

      float m_elapsedTime;

protected:// dx variables

      UINT m_vertex_description;

      IDirect3DTexture8* m_pTexture;

      IDirect3DVertexBuffer8 * m_vb; // vertex buffer

};

 

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

/*                                                                         */

/* The action implimentations of our CLoadMD2 class.                       */

/*                                                                         */

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

bool CMD2X::ImportMD2(char* szFileName, char* szTexName)

{

      m_fp = fopen(szFileName, "rb");

 

      ReadMD2Data();

      SetUpFrames();

      SetUpDX();

 

      fclose(m_fp);

     

      // Load or textures into our DX.

      D3DXCreateTextureFromFile(g_pD3DDevice, szTexName, &m_pTexture);

 

      return true;

}

 

void CMD2X::ReadMD2Data()

{

      fread(&m_Md2Header, 1, sizeof(m_Md2Header), m_fp);   

 

      // Allocate memory for our data so we can read it in.

      m_pSkins          = new stMd2Skins  [ m_Md2Header.numSkins ];

      m_pTexCoords      = new stMd2TexCoords[ m_Md2Header.numTexCoords ];

      m_pTriangles      = new stMd2Triangles[ m_Md2Header.numTriangles ];

      m_pFrames         = new stMd2Frames [ m_Md2Header.numFrames ];

 

      // -1- Seek to the start of our skins name data and read it in.

      fseek(m_fp, m_Md2Header.offsetSkins, SEEK_SET);

      fread(m_pSkins, sizeof(stMd2Skins), m_Md2Header.numSkins, m_fp);

 

      // -2- Seek to the start of our Texture Coord data and read it in.

      fseek(m_fp, m_Md2Header.offsetTexCoords, SEEK_SET);

      fread(m_pTexCoords, sizeof(stMd2TexCoords), m_Md2Header.numTexCoords, m_fp);

 

      // -3- Seek to the start of the Triangle(e.g. Faces) data and read that in.

      fseek(m_fp, m_Md2Header.offsetTriangles, SEEK_SET);

      fread(m_pTriangles, sizeof(stMd2Triangles), m_Md2Header.numTriangles, m_fp);

 

      // -4- Finally lets read in "one" of the frames, the first one.!

     

      struct stAliasVerts

      {

            byte vertex[3]; // an index reference into the location of our vertexs

            byte lightNormalIndex; // in index into which tex coords to use.

      };

      struct stAliasFrame

      {

            float scale[3];

            float translate[3];

            char name[16];

            stAliasVerts aliasVerts[1];

      };

     

      unsigned char largebuffer[50000];

      stAliasFrame* pTempFrame = (stAliasFrame*) largebuffer;

 

      fseek(m_fp, m_Md2Header.offsetFrames, SEEK_SET);

 

      for(int iFrame=0; iFrame< m_Md2Header.numFrames; iFrame++)

      {

 

            fread(pTempFrame, 1, m_Md2Header.frameSize, m_fp); // We have read in all the frame data here (into a temporyary!!..eeEKK)..

 

            m_pFrames[iFrame].pFinalVerts = new stMd2Vertices[ m_Md2Header.numVertices ];

           

            strcpy( m_pFrames[iFrame].name,  pTempFrame->name );

            // CONVERSION!  A few things before we can use our read in values,

            // for some reason the Z and Y need to be swapped, as Z is facing up

            // and Y is facing into the screen.

            // Also our texture coordinates values are between 0 and 256, we just

            // divide them all by 256 which makes them between 0 and 1.

 

            // Swap Z<->Y

            for(int i=0; i< m_Md2Header.numVertices; i++)

            {

                  m_pFrames[iFrame].pFinalVerts[i].vertex[0] = pTempFrame->aliasVerts[i].vertex[0] * pTempFrame->scale[0]

                                                                                                            + pTempFrame->translate[0];      // x

                  m_pFrames[iFrame].pFinalVerts[i].vertex[2] = -1*(pTempFrame->aliasVerts[i].vertex[1] * pTempFrame->scale[1]

                                                                                                            + pTempFrame->translate[1]); // z

                  m_pFrames[iFrame].pFinalVerts[i].vertex[1] = pTempFrame->aliasVerts[i].vertex[2] * pTempFrame->scale[2]

                                                                                                            + pTempFrame->translate[2];      // y

            }

      }

      // Scale Textures.

      for (int j=0; j< m_Md2Header.numTexCoords; j++)

      {

            // WARNING.. you can't put a decimal number into a short...e.g.

            // you can't put 0.1 into a unsigned short int, it will be changed to 0.

            /*

            m_pTexCoords[j].u = m_pTexCoords[j].u ;// 256; //float(m_Md2Header.skinWidth);

            m_pTexCoords[j].v = m_pTexCoords[j].v ;// 256; //float(m_Md2Header.skinHeight);

            */

      }

     

}

 

void CMD2X::SetUpFrames()

{

      //stand01, stand02... walk01 etc.

      //First lets see how many key frames there are.. e.g.  walk, run, stand etc.

      m_Keys.numKeys = 0;

      char strName[16] = {0};

      char strLastName[16] = {0};

      char strVeryFirstName[16] = {0};

 

      for(int iFrame=0; iFrame< m_Md2Header.numFrames; iFrame++)

      {

            strcpy(strName, m_pFrames[iFrame].name);

            int stringLength = strlen( strName );

            for(int i=0; i< stringLength; i++)

            {

            // a:97   A:65   z:122  Z:90

                  if( (strName[i] < 'A') || (strName[i] > 'z' ) )

                  {

                        // Its an integer if we are here.

                        strName[i] = '\0';

                        if( strcmp( strLastName, strName ) )

                        {

                              strcpy(strLastName, strName);

                              m_Keys.numKeys++;

                        }

                        if( !strcmp( strVeryFirstName, "" ) )

                              strcpy( strVeryFirstName, strName );

                        break;

                  }

            }    

      }

      // Now that we are here, we know how many of each key frame there

      // are, e.g. there are 4 for stand, 6 for walk etc.

      //m_Keys.numKeys--;

      m_Keys.pKey = new stKeyFrame[ m_Keys.numKeys ];

 

      strcpy( strName, "");

      strcpy( strLastName, strVeryFirstName);

      //strcpy(strLastName, "");

      int curKey = 0;

 

      m_Keys.pKey[0].start = 0;

      strcpy( m_Keys.pKey[0].szName, strVeryFirstName );

 

      for(iFrame=0; iFrame< m_Md2Header.numFrames; iFrame++)

      {

            strcpy(strName, m_pFrames[iFrame].name);

            int stringLength = strlen( strName );

            for(int i=0; i< stringLength; i++)

            {

                  // a:97   A:65   z:122  Z:90

                  if( (strName[i] < 'A') || (strName[i] > 'z' ) )

                  {

                        // Its an integer if we are here.

                        strName[i] = '\0';

                        if( strcmp( strLastName, strName ) )

                        {

                              m_Keys.pKey[curKey].end = iFrame-1;

                              curKey++;

                              m_Keys.pKey[curKey].start = iFrame;

 

                              strcpy(strLastName, strName);

                              strcpy(m_Keys.pKey[curKey].szName, strName);

                        }

                        else

                        {

                              m_Keys.pKey[curKey].end = iFrame;

                        }

                        break;

                  }

            }    

      }

 

 

}

 

void CMD2X::SetUpDX()

{

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

      /* Data is transfered into the directX vertex buffers etc.                 */

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

      int numFaces = m_Md2Header.numTriangles;

 

      // Create a vertex buffer for a single model frame.. all frames are the same size.

      g_pD3DDevice->CreateVertexBuffer( numFaces * 3 * sizeof(my_vertex), 0, m_vertex_description, D3DPOOL_MANAGED, &m_vb );

 

 

}

 

bool CMD2X::Timer(float* t)

{

      float delay = 0.2f; // 0.2 of a second between changes. ~Speed of your models animation~

 

      float time = GetTickCount();

 

      m_elapsedTime = time - m_lastTime;

      m_elapsedTime = m_elapsedTime /1000.0f; // convert to seconds.

      m_elapsedTime = m_elapsedTime / delay;  // make into a percentage of 100 e.g. 0->1

 

      *t = m_elapsedTime;

 

      if( m_elapsedTime > 1.0f )

      {

            m_lastTime = time;

            m_elapsedTime = 0.0f;

 

            *t = m_elapsedTime;

            return true;

      }

      return false;

}

 

void CMD2X::SetDXVertices(UINT iAnimKey)

{

      // First Create the position of our new verts using

      // p(t) = p0 + t(p1 - p0);

 

      // For those who are sharp to the eye, well its arranged

      // this way, as if you change from a walk to a death, or run

      // etc, it will change gradually... not jump... creates a really

      // smooth effect.

 

      float t;

 

      if( Timer(&t) )

      {

            m_curFrame = m_nextFrame;

            m_nextFrame = (m_nextFrame + 1);

 

            if( m_curAnimKey != iAnimKey )

            {

                  m_nextFrame = m_Keys.pKey[ iAnimKey ].start; // (curFrame + 1);

                  m_curAnimKey = iAnimKey;

            }

      }

 

 

      if( m_curFrame == m_nextFrame )

            m_nextFrame++;

 

      if( m_nextFrame > m_Keys.pKey[ iAnimKey ].end)

            m_nextFrame = m_Keys.pKey[ iAnimKey ].start;

     

 

 

      // Copy our array which is in computer memory over to the directX memory.. using that pointer we

    // just created etc.

    unsigned char *temp_pointer_vb;

    m_vb->Lock(0,0, &temp_pointer_vb, 0);

      my_vertex* p_mem = (my_vertex*)temp_pointer_vb;

 

      int numFaces = m_Md2Header.numTriangles;

 

      stMd2Vertices*  pCurFrame =  m_pFrames[m_curFrame].pFinalVerts;

      stMd2Vertices*  pNexFrame =  m_pFrames[m_nextFrame].pFinalVerts;

 

      for(int p=0; p< numFaces; p++)

      {

            for(int c=0; c<3; c++) // 3 sides to each face (e.g. triangle).

            {

                  int index = m_pTriangles[p].vertexIndex[c];

                  float x1 = pCurFrame[index].vertex[0];//x

                  float y1 = pCurFrame[index].vertex[1];//y

                  float z1 = pCurFrame[index].vertex[2];//z

 

                  float x2 = pNexFrame[index].vertex[0];//x

                  float y2 = pNexFrame[index].vertex[1];//y

                  float z2 = pNexFrame[index].vertex[2];//z

 

                  p_mem[p*3+c].x = x1 + t * (x2 - x1);

                  p_mem[p*3+c].y = y1 + t * (y2 - y1);

                  p_mem[p*3+c].z = z1 + t * (z2 - z1);

                       

                  int texIndex = m_pTriangles[p].texIndex[c];

                  p_mem[p*3+c].tu = m_pTexCoords[ texIndex ].u / 256.0f;

                  p_mem[p*3+c].tv = m_pTexCoords[ texIndex ].v / 256.0f;

            }

      }

      m_vb->Unlock();

     

     

}

 

void CMD2X::Animate(UINT iAnimKey)

{

      // Check to make sure that the iAnimKey is within range.

      iAnimKey = iAnimKey % m_Keys.numKeys;

 

      SetDXVertices(iAnimKey);

}

 

void CMD2X::RenderFrame()

{

      unsigned int numFaces = m_Md2Header.numTriangles;    

 

    // Okay at this point... our graphics card has our vertices stored in it... we've just copied

    // them over :) Some stuff to setup or graphics card!

    // Turn off lighting becuase we are specifying that our vertices have colour

    g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

    g_pD3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);

      g_pD3DDevice->SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE);

 

     

 

      // Draw our triangle.

      g_pD3DDevice->SetStreamSource(0, m_vb, sizeof(my_vertex));

      g_pD3DDevice->SetVertexShader(m_vertex_description);

      g_pD3DDevice->SetTexture(0,m_pTexture);

 

      //                                  number of primitives

      //                                starting vertex      |

      //                                              |      |

      //                                             \|/    \|/

      //                                              |      |

      g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, numFaces);

 

}

 

void CMD2X::Release()

{

      m_vb->Release();

 

      // We can delete all our allocated memory as its now been transfered over

      // to the directX vertex buffer.

      for(int iFrame; iFrame< m_Md2Header.numFrames; iFrame++)

      {

            delete[] m_pFrames[iFrame].pFinalVerts;

      }

 

      delete[] m_pSkins;

      delete[] m_pTexCoords;

      delete[] m_pTriangles;

      delete[] m_pFrames;

 

      delete[] m_Keys.pKey;

}

Now for the other parts of the file... again these two files are just the same old code...e.g. windows code and directX init/render code.

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

/*                                                                         */

/* File: main.cpp                                                          */

/* Author: bkenwright@xbdev.net                                       */

/* Date: 10-11-2002                                                        */

/*                                                                         */

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

 

#include <windows.h>

#include <stdio.h>

 

#pragma comment(lib, "D3d8.lib") //directX 8

#pragma comment(lib, "D3dx8.lib")

#include <d3dx8.h>

 

 

WNDCLASS a; HWND hwnd; MSG c;

long _stdcall zzz (HWND, UINT, WPARAM, LPARAM);

 

#include "md2.cpp"

#include "dxdraw.cpp"

 

CMD2X g_md2; // Important piece of code here! ->Using our CLoadMD2 Class<--

 

void gameloop()

{

      Render(&g_md2);

}

 

 

int _stdcall WinMain(HINSTANCE i, HINSTANCE j, char *k, int l)

{

      a.lpszClassName="a1";

      a.hInstance = i;

      a.lpfnWndProc = zzz;

      a.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

      RegisterClass(&a);

      hwnd=CreateWindow("a1", "xbdev.net", WS_OVERLAPPEDWINDOW, 30,30,300,300,NULL,NULL,i,NULL);

 

      init(hwnd);                               // -1- Init DirectX3D.

      ShowWindow(hwnd,1);                       // -2- Show our window.

      g_md2.ImportMD2("pac3D.md2", "pac3D.bmp");   // -3- Load our data into our CLoadMD2 class instance.

      while(1)

      {

           

            if (PeekMessage(&c, NULL, 0, 0, PM_NOREMOVE))

            {

                  if(!GetMessage(&c, 0,0,0))

                        break;

                  DispatchMessage(&c);

            }

            else

                  gameloop();                   // -4- Display our data (e.g. calls Render in dxdraw.cpp).

      }

 

      return 1;

}

 

long _stdcall zzz (HWND w, UINT x, WPARAM y, LPARAM z)

{

      if (x == WM_DESTROY)

      {

            de_init();                          // -5- Destroy DirectX code.

            g_md2.Release();              // -6- Destroy our CLoadMD2 instance.

            PostQuitMessage(0);

      }

      return DefWindowProc(w,x,y,z);

}

And our dxdraw.cpp file:

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

/*                                                                         */

/* File: dxdraw.cpp                                                        */

/* Author: bkenwright@xbdev.net                                       */

/* Date: 10-11-2002                                                        */

/*                                                                         */

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

 

LPDIRECT3D8 g_pD3D = NULL;

LPDIRECT3DDEVICE8 g_pD3DDevice = NULL;

 

void init(HWND hWnd)

{

    //First of all, create the main D3D object. If it is created successfully we

    //should get a pointer to an IDirect3D8 interface.

    g_pD3D = Direct3DCreate8(D3D_SDK_VERSION);

    //Get the current display mode

    D3DDISPLAYMODE d3ddm;

    g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);

    //Create a structure to hold the settings for our device

    D3DPRESENT_PARAMETERS d3dpp;

    ZeroMemory(&d3dpp, sizeof(d3dpp));

    //Fill the structure.

    //We want our program to be windowed, and set the back buffer to a format

    //that matches our current display mode

    d3dpp.Windowed = TRUE;

    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;

    d3dpp.BackBufferFormat = d3ddm.Format;

    //For depth buffering (e.g.) the z-buffer

    d3dpp.BackBufferCount=1;

    d3dpp.AutoDepthStencilFormat = D3DFMT_D16;

    d3dpp.EnableAutoDepthStencil = TRUE;

    //Create a Direct3D device

    g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDevice);

    //Turn on z-buffering

    g_pD3DDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);

 

    //Turn on back face culling. This is becuase we want to hide the back of our polygons

    g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);

}

 

void de_init()

{

      g_pD3DDevice->Release();

    g_pD3DDevice = NULL;

    g_pD3D->Release();

    g_pD3D = NULL;

}

void set_camera()

{

    // [1] D3DTS_VIEW

    D3DXMATRIX v;

    g_pD3DDevice->SetTransform(D3DTS_VIEW, D3DXMatrixLookAtLH(&v, &D3DXVECTOR3(0,0,-90),

                                                                  &D3DXVECTOR3(0,0,0),

                                                                  &D3DXVECTOR3(0,1,0))); 

    // [2] D3DTS_PROJECTION

    D3DXMATRIX p;

    g_pD3DDevice->SetTransform( D3DTS_PROJECTION, D3DXMatrixPerspectiveFovLH( &p,  D3DX_PI/4,  1.0f,

                                                                                                 1.0f,200.0f));

};

 

 

void Render(CMD2X* m)

{

      if(!g_pD3DDevice)return;

 

      set_camera();

 

      static float iCount = 0.0001f;

      iCount += 0.0005f;

 

      // Which Animation, e.g. 0 for stand

      //            |          1 for run

      //            |          2 for jump etc. etc.

      //           \|/

      //            |

      m->Animate(iCount);   // This simply sets up our vertex buffer data in our class instance.

 

      // Clear the back buffer to a blue color

      g_pD3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );

     

      m->RenderFrame();    // Renders our model to the screen.

 

      // After rendering the scene we display it.

      g_pD3DDevice->Present( NULL, NULL, NULL, NULL );

     

     

}

You better site down with a strong cup of coffee and go over some of the stuff... I'm a pencil person, as I usually print out code and scribble on it until I get the jist of it...then modify it on my computer until I know it like the back of my hand.

Well happy coding.

 

 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2024 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.