Sign in to follow this  
gretty

[Win32 C++] Approach to Creating Movement/Animation

Recommended Posts

Hello I have created a Win32 app where you create a series of targets with the left mouse then click a button to fire a laser at the targets, the laser is just a line that moves from one target to the next. Is my method of creating the look of movement or animation the best way that can be done in Win32. Is there a completely different, better, more efficient method you can suggest? Or is my way good but maybe I could imporve with 'blah' tweaks? My present algorthm is really simple: - I have a laser object that draws a rectangular region to make it look like a laser - To give the effect of movement I clear the objects region, update the objects coords, then redraw the object. - I use a timer(like a animation timer/frame rate) to determine when to redraw the laser object at a new position. I am looking for any advice on how my program could be improved in any way, criticism is welcome. Draw Laser Win32 Application - 85kb http://www.mediafire.com/?zguz1nknh5g Header File
#ifndef LASEROBJECTS_H
#define LASEROBJECTS_H

#include <windows.h>
#include <cstdlib>
#include <stdio.h>
#include <queue>

using namespace std;
 

/// Constants ///
#define LA_DRAWLASER 50000
#define LA_TIMER     50001
#define LA_ANIMATION 50002


/// Objects ///
struct target {
       
       // Functions
       target(LONG xPos, LONG yPos, int xOff, int yOff);
       ~target(); 
       void drawTarget(HDC hdc);
       
       // Variables
       POINT pos;    // Object position
       int xOffset;
       int yOffset;
       HRGN objRgn;
};


struct laser {
       
       // Functions
       laser();
       ~laser();
       bool hasTarget();
       void drawController(HDC hdc, HWND hwnd);
       void drawLaser(HDC hdc, HWND hwnd);
       void createTarget(HWND hwnd, int mouseX, int mouseY);
       void moveToTarget(HWND hwnd);
       void initiateDelay(HWND hwnd);
       void ceaseDelay(HWND hwnd);
       void initAnimationTimer(HWND hwnd);
       void ceaseAnimationTimer(HWND hwnd);
       
       // Variables
       POINT base;
       POINT top;
       HRGN objRgn;
       int laserWidth;
       int hSpeed;
       int vSpeed;
       
       bool lButtonDown;
       bool createTargetAllowed;
       bool drawNewTarget;
       bool drawNewLaser;
       UINT delayTimerID;
       UINT animTimerID;
       int targetDelay;
       queue <target*> targetList;
       int targetDimension;
       HPEN laserPen;
       
       int sleepValue;
};


/// Global Functions ///
void createGUI(HWND hwnd, HINSTANCE gInstance, RECT clientR);


#endif






Object Implementation file
#include <windows.h>
#include <cstdlib>
#include <stdio.h>
#include <queue>

#include "laserObjects.h"

using namespace std;

/// Target Object Implementation ///
///                             ///

target::target(LONG xPos, LONG yPos, int xOff, int yOff)
{
   // Constructor
   
   pos.x   = xPos;
   pos.y   = yPos;
   xOffset = xOff;
   yOffset = yOff;
   objRgn  = CreateEllipticRgn(pos.x-xOffset,pos.y-yOffset,pos.x+xOffset,pos.y+yOffset);
   
   // if we failed to create region
   if (!objRgn) {
       MessageBox(NULL,"Failed to create new target region","Error:",MB_OK|MB_ICONERROR);
   }
}


target::~target()
{
   // Destructor
   
   //DeleteObject(objRgn);  Do I need to delete a HRGN?     
}


void target::drawTarget(HDC hdc)
{
   // Post: Draw a black dot on this objects' x,y coords
   
   int success = FillRgn(hdc,objRgn,( CreateSolidBrush(RGB(0,0,0)) ));
   
   if (success == 0) {
               
       MessageBox(NULL,"Failed to fill target region with HBRUSH","Error:",MB_OK|MB_ICONERROR);
   
   }
}



/// Laser Object Implementation ///
///                             ///

laser::laser()
{
   // Default Constructor
   
   base.x = 0;  base.y = 100;
   top.x  = 100; top.y  = 100;
   objRgn = CreateRectRgn(int(base.x-1.5),int(base.y-1.5),int(top.x+1.5),int(top.y+1.5));
   laserWidth = 4;
   hSpeed = 4;
   vSpeed = 4;
   
   
   createTargetAllowed = true;   // Can we create a target
   lButtonDown     = false;      // Is the left mouse button held down
   drawNewTarget   = false;
   drawNewLaser    = false;
   delayTimerID    = INT_MAX;
   animTimerID     = INT_MIN;
   targetDelay     = 500;        // Timer/Delay between creating targets
   targetDimension = 6;          // Width & height of target ellipse
   laserPen        = CreatePen(PS_SOLID,laserWidth,BLACK_PEN);
   
   sleepValue = 200;
}


laser::~laser()
{
   // Destructor 
   
   // delete all dynamic data
   while (!targetList.empty()) {
         
         delete targetList.front();
         targetList.pop();
   
   }  
   
   //DeleteObject(objRgn); Should I delete a HRGN?
   DeleteObject(laserPen);
}


bool laser::hasTarget()
{
   // Post: Returns true if there are targets for the laser to shoot at
   //       else false
            
   if (targetList.size() > 0) { 
       return true;
   } 
   else return false;
}


void laser::drawController(HDC hdc, HWND hwnd)
{
   // Post: Controls & initiates the drawing of the laser & targets
   
   if (hasTarget()) {
            
       if (drawNewTarget) {
          
          targetList.back()->drawTarget(hdc);
          drawNewTarget = false; 
       
       }     
       
       if (drawNewLaser) {
          
           drawLaser(hdc,hwnd);         
       
       }        
   }   
}


void laser::drawLaser(HDC hdc, HWND hwnd)
{
   // Post: Draws line from lasers base point to its top point to appear like
   //       the laser is shooting at the target
           
   if (hasTarget()) {
       
       InvalidateRgn(hwnd,objRgn,true);
       
       moveToTarget(hwnd);
       
       POINT baseLeft, baseRight, topLeft, topRight;
       
       baseLeft.x  = base.x + (laserWidth / 2);  baseLeft.y  = base.y - (laserWidth / 2);
       baseRight.x = base.x - (laserWidth / 2);  baseRight.y = base.y + (laserWidth / 2);
       topLeft.x   = top.x + (laserWidth / 2);   topLeft.y   = top.y - (laserWidth / 2);
       topRight.x  = top.x - (laserWidth / 2);   topRight.y  = top.y + (laserWidth / 2);
       
       POINT laserPnts[] = {baseRight, baseLeft, topLeft, topRight};
       
       objRgn = CreatePolygonRgn(laserPnts,4,WINDING);
       FillRgn(hdc,objRgn, CreateSolidBrush(RGB(0,0,0)) );
       
       // The below Sleep function is a real hack.
       // If I dont make the program sleep for x time, then you dont see the laser line 
       // coz it get erased straight away, so I needed a way to see the laser line for x amt of time
       // can you suggest a better way to fix this?
       // Should I use different animation, drawing & movement method to avoid this?
       // Or is there a simple way to fix this problem?
       Sleep(sleepValue);   
   }
}


void laser::moveToTarget(HWND hwnd)
{
   // Post: Logically move laser top POINT towards a target
   
   int collision = PtInRegion(targetList.front()->objRgn,top.x,top.y);
   
   // if NOT laser collision with target | if laser is NOT at target
   if (collision == 0) 
   {
   
       int xDist = top.x - targetList.front()->pos.x; 
       int yDist = top.y - targetList.front()->pos.y;
       
       // if laser is left of target
       if (xDist < 0) {
           top.x += hSpeed;
       }
       // if laser is right of target
       else if (xDist > 0) {
            top.x -= hSpeed;
       }
       
       // if laser is above target
       if (yDist < 0) {
           top.y += vSpeed;
       }
       // if laser is below target
       else if (yDist > 0) {
            top.y -= vSpeed;
       }
   }
   // Laser is at target
   else {
       // Delete present target & move on to next target if it exists
       InvalidateRgn(hwnd,targetList.front()->objRgn,true);
       delete targetList.front();
       targetList.pop();
   }
}


void laser::createTarget(HWND hwnd, int mouseX, int mouseY)
{
   // Post: Creates a new target at cursor position & stores target in queue
   
   if (createTargetAllowed) {
                        
       target *newTarget = new target(mouseX,mouseY,targetDimension,targetDimension);
       targetList.push(newTarget);
       
       drawNewTarget = true;
       InvalidateRect(hwnd,NULL,false); // Send WM_PAINT to WNDPROC queue
       UpdateWindow(hwnd);              // Push WM_PAINT to front of queue
       initiateDelay(hwnd);             // Set timer so we cant create another target for 1/2 second
   
   } 
   
   return;
}


void laser::initiateDelay(HWND hwnd)
{
   // Post: Make it so user cannot create a target for a certain amount of time
   //       using a timer
     
   createTargetAllowed = false;
   delayTimerID = SetTimer(hwnd,LA_TIMER,targetDelay,NULL);
}


void laser::ceaseDelay(HWND hwnd)
{
   // Post: Turn off 'target creation delay' timer
      
   int success = KillTimer(hwnd,LA_TIMER);
   createTargetAllowed = true;
}


void laser::initAnimationTimer(HWND hwnd)
{
   // Post: Create timer to trigger animation of laser movement
   
   animTimerID = SetTimer(hwnd,LA_ANIMATION,80,NULL);
}


void laser::ceaseAnimationTimer(HWND hwnd)
{
   // Post: Turn off animation timer
   
   int success = KillTimer(hwnd,LA_ANIMATION);
   drawNewLaser = false;
   //InvalidateRgn(hwnd,objRgn,true);
   //UpdateWindow(hwnd);
}



/// Functions ///
///           ///

void createGUI(HWND hwnd, HINSTANCE gInstance, RECT clientR)
{
   // Post: Create Laser Application's Controls/Graphical User Interface
        
   char instructions[] = "Click and hold left mouse button down to create targets for the laser to" 
   "hit. Press button to fire laser when you have created your targets. Press control to see issue" 
   "with drawing the laser, you will see that the laser is not drawn long enough. Also the laser "
   "flickers. ";
   
   HWND btDrawLaser = CreateWindowEx(0,"Button","Draw Laser",WS_CHILD|WS_VISIBLE|WS_BORDER|BS_PUSHBUTTON,
                                     20,clientR.bottom-40,100,20,hwnd,(HMENU)LA_DRAWLASER,gInstance,NULL);
   
   HWND instruct = CreateWindowEx(0,"Static",instructions,WS_CHILD|WS_VISIBLE|WS_BORDER,
                                130,clientR.bottom-100,clientR.right-140,90,hwnd,NULL,gInstance,NULL); 
   
   if (!btDrawLaser) {
       MessageBox(hwnd,"Failed to create button to draw laser","Error:",MB_OK|MB_ICONERROR);   
   }
}










Winmain File
/*
  Application: Laser Example
*/

#include <windows.h>
#include <cstdlib>
#include <stdio.h>
#include <queue>

#include "laserObjects.h"

using namespace std;

// Global Variables //
static HINSTANCE gInstance;
laser myLaser;

// Functions List //
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);


int WINAPI WinMain(HINSTANCE gInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    //Step 1: Registering the Window Class
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = gInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(DKGRAY_BRUSH);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = "Custom Class";
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    // if registration of main class fails
    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, "Window Registration Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    // Step 2: Creating the Window
    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        "Custom Class",
        "Laser Example",
        WS_CAPTION|WS_MINIMIZEBOX|WS_VISIBLE|WS_OVERLAPPED|WS_SYSMENU,
        CW_USEDEFAULT, CW_USEDEFAULT, 600, 500,
        NULL, NULL, gInstance, NULL);

    if(hwnd == NULL)
    {
        MessageBox(NULL, "Window Creation Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }
    
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);
    
    // Step 3: The Message Loop
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    
    switch(msg)
    {
        case WM_CREATE:
        {                  
             // Create GUI
             RECT clientR;
             GetClientRect(hwnd,&clientR);
             createGUI(hwnd,gInstance,clientR);
        }    
        break;
        case WM_LBUTTONDOWN:
        {
             RECT clientR;
             GetClientRect(hwnd,&clientR);
             POINT cursor;
             cursor.x = LOWORD(lParam); cursor.y = HIWORD(lParam);
             
             // if mouse has clicked INSIDE client area
             int collision = PtInRect(&clientR,cursor);
             if (collision != 0) {
                 
                 myLaser.createTarget(hwnd,LOWORD(lParam),HIWORD(lParam));
                 myLaser.lButtonDown = true;
             
             }
        }
        break;
        case WM_LBUTTONUP:
        {
             myLaser.lButtonDown = false;
        }     
        break;
        case WM_KEYDOWN:
        {
             switch(LOWORD(wParam)) {
                        
                  case VK_CONTROL:
                  {    // Just to show problem with drawing laser
                       // it is not drawn long enough
                       if (myLaser.sleepValue == 200)
                       {
                           myLaser.sleepValue = 0;
                       }
                       else myLaser.sleepValue = 200;
                       
                       MessageBox(hwnd,"Problem with drawing laser should occur now","",MB_OK);
                  }
                  break;
                  default:
                  break;
             }
        }          
        break;
        case WM_COMMAND:
        {
             switch(LOWORD(wParam)) {
                        
                  case LA_DRAWLASER:
                  {    // Fire laser at coordinates
                       // if laser has a target to shoot at & we are not already shooting
                       if (myLaser.hasTarget() && !myLaser.drawNewLaser) {
                           
                           myLaser.drawNewLaser = true;
                           myLaser.initAnimationTimer(hwnd);
                           InvalidateRect(hwnd,NULL,false);
                           UpdateWindow(hwnd);
                       
                       }
                       
                       SetFocus(hwnd);
                  }     
                  break;    
                  default:
                  break;
             }
        }
        break;
        case WM_TIMER:
        {
             
             switch(LOWORD(wParam)) {
                        
                  case LA_TIMER:
                  {    // Laser can create another target now
                       
                       myLaser.ceaseDelay(hwnd);
                       
                       // if left mouse button is still held down
                       if (myLaser.lButtonDown) {
                          
                          RECT clientR;
                          GetClientRect(hwnd,&clientR);
                          POINT cursor;
                          GetCursorPos(&cursor);
                          ScreenToClient(hwnd,&cursor);
                          
                          // if mouse is INSIDE client area
                          int collision = PtInRect(&clientR,cursor);
                          if (collision != 0) {
                              
                              myLaser.createTarget(hwnd,cursor.x,cursor.y);
                          
                          }
                       }
                  } 
                  break; 
                  case LA_ANIMATION:
                  {    // Draw new laser position
                       if (myLaser.hasTarget()) {
                           
                           InvalidateRect(hwnd,NULL,false);
                           UpdateWindow(hwnd);
                           
                       }
                       else myLaser.ceaseAnimationTimer(hwnd);
                  } 
                  break;   
                  default:
                  break;
             }
        } 
        break;
        case WM_PAINT:
        {
             HDC hdc;
             PAINTSTRUCT ps;
             hdc = BeginPaint(hwnd,&ps);
             
             myLaser.drawController(hdc,hwnd);
             
             EndPaint(hwnd,&ps);
        }     
        break;
        case WM_ERASEBKGND: 
              //return 1;
        break;
        case WM_CLOSE:
        {
            myLaser.ceaseDelay(hwnd);
            myLaser.ceaseAnimationTimer(hwnd);
            //myLaser.~laser();
            DestroyWindow(hwnd);
        }
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default: 
        break;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}






Share this post


Link to post
Share on other sites

Quote:

Is my method of creating the look of movement or animation the best way that can be done in Win32. Is there a completely different, better, more efficient method you can suggest? Or is my way good but maybe I could imporve with 'blah' tweaks?

Search for "double buffering" and "dirty rectangles" for more generic approaches that would allow you to build a simple GDI-based game engine. The dirty rectangles method works well for GDI since it does minimum redrawing. And you could change it to a "dirty regions" approach to get what you need for this app.

To get rid of the flickering, here's one inefficient way that uses a memory bitmap as a temporary off-screen buffer to combine the two steps of the laser redrawing into one. This is an example of double buffering. Ideally your memory bitmap would be the size of your window and you would only draw to it, and then use a "dirty regions" list to tell your paint message handler which parts of the memory bitmap need to be updated to the window.

In laser::drawLaser, make the following code mods:

- comment out InvalidateRgn call line
- comment out CreatePolygonRgn call line
- comment out FillRgn call line

- add the following code after the FillRgn call line:

HRGN newRgn = CreatePolygonRgn(laserPnts,4,WINDING); // laser's new look
RgnRedraw(hwnd, hdc, objRgn, newRgn); // "undraw" old and draw new
DeleteObject(objRgn); // always delete or destroy "Create"d GDI objects
objRgn = newRgn;


Add the following new function somewhere above laser::drawLaser:

void RgnRedraw(HWND hWnd, HDC hDC, HRGN hRgnOld, HRGN hRgnNew,
bool buffered=true, COLORREF color=RGB(0, 0, 0))
{
if( !buffered )
{
InvalidateRgn(hWnd, hRgnOld, TRUE);
HBRUSH hBrush = CreateSolidBrush(color);
FillRgn(hDC, hRgnNew, hBrush);
DeleteObject(hBrush);
return;
}

// Step 1: Setup

RECT rOld, rNew, rBlt;

GetRgnBox(hRgnOld, &rOld); // bounding box for the region's old look
GetRgnBox(hRgnNew, &rNew); // bounding box for the region's new look

UnionRect(&rBlt, &rOld, &rNew); // minimum rect containing the two regions

int bw = rBlt.right - rBlt.left; // that rect's width
int bh = rBlt.bottom - rBlt.top; // that rect's height

HDC hMC = CreateCompatibleDC(hDC);
HBITMAP hBlt = CreateCompatibleBitmap(hDC, bw, bh);
ULONG_PTR pBFill = GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
HGDIOBJ hMCBmp = SelectObject(hMC, hBlt);

// Step 2: Get a copy of the pixels that will be modified

BitBlt(hMC, 0, 0, bw, bh, hDC, rBlt.left, rBlt.top, SRCCOPY);

// Step 3: Get rid of the region's old look
// Equivalent to the non-buffered method's InvalidateRgn call

// translate region to the memory device context's coord system
OffsetRgn(hRgnOld, -rBlt.left, -rBlt.top);

// don't modify any pixels outside of the region
SelectClipRgn(hMC, hRgnOld);

if( pBFill == 0 )
{
// NOTE: assumes the following about the window's "erase background"
// message handler:
// - it will use the DC passed to it (which the Windows default
// message processor should do)
// - it will not call this function directly or call a function
// that indirectly
// results in a call to this function (which would cause an
// infinite loop)

SendMessage(hWnd, WM_ERASEBKGND, (WPARAM)hMC, 0);
}
else
{
// assumes no system color indices > 16 bits
HBRUSH hBFill = pBFill <= 65536 ? GetSysColorBrush((int)pBFill-1) :
(HBRUSH)pBFill;
FillRgn(hMC, hRgnOld, hBFill);
}

OffsetRgn(hRgnOld, rBlt.left, rBlt.top);
SelectClipRgn(hMC, NULL);

// Step 4: Put in the region's new look
// Equivalent to the non-buffered method's FillRgn call

HBRUSH hBrush = CreateSolidBrush(color);
OffsetRgn(hRgnNew, -rBlt.left, -rBlt.top);
FillRgn(hMC, hRgnNew, hBrush);
OffsetRgn(hRgnNew, rBlt.left, rBlt.top);
DeleteObject(hBrush);

// Step 5: Put the modified pixels back

BitBlt(hDC, rBlt.left, rBlt.top, bw, bh, hMC, 0, 0, SRCCOPY);

// Step 6: Cleanup

SelectObject(hMC, hMCBmp);
DeleteObject(hBlt);
DeleteDC(hMC);
}

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this