0
\$\begingroup\$

I'm writing the beginnings of a small game engine in Visual C++. What I have so far doesn't seem to be updating the rendering each frame, but I don't understand where I'm going wrong with it.

The code is mostly the boilerplate that Visual Studio generates. I added the draw() and main_loop() functions.

#include "stdafx.h"
#include "Windows.h"

#include "Keyboard.h";
#include "Photon.h"
#include "Player2D.h"

#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst;                                // current instance
HWND Window;
WCHAR szTitle[MAX_LOADSTRING];                  // The title bar text
WCHAR szWindowClass[MAX_LOADSTRING];            // the main window class name
Player2D player;
Keyboard keyboard;

// Prototypes
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
void draw ();
void main_loop ();

int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
  {
  UNREFERENCED_PARAMETER(hPrevInstance);
  UNREFERENCED_PARAMETER(lpCmdLine);

  // Initialize global strings
  LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
  LoadStringW(hInstance, IDC_PHOTON, szWindowClass, MAX_LOADSTRING);
  MyRegisterClass(hInstance);

  // Perform application initialization:
  if (!InitInstance (hInstance, nCmdShow))
    {
    return FALSE;
    }

  HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PHOTON));

  MSG msg;

  // Main message loop:
  while (GetMessage(&msg, nullptr, 0, 0))
    {
    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
      {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
      }
    }

    return (int) msg.wParam;
  }

// Registers the window class.
ATOM MyRegisterClass(HINSTANCE hInstance)
  {
  WNDCLASSEXW wcex;

  wcex.cbSize = sizeof(WNDCLASSEX);

  wcex.style          = CS_HREDRAW | CS_VREDRAW;
  wcex.lpfnWndProc    = WndProc;
  wcex.cbClsExtra     = 0;
  wcex.cbWndExtra     = 0;
  wcex.hInstance      = hInstance;
  wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PHOTON));
  wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
  wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
  wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_PHOTON);
  wcex.lpszClassName  = szWindowClass;
  wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

  return RegisterClassExW(&wcex);
  }

// In this function, we save the instance handle in a global variable and
// create and display the main program window.
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
  {
  hInst = hInstance; // Store instance handle in our global variable

  Window = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
  CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

  if (!Window)
    {
    return FALSE;
    }

  ShowWindow(Window, nCmdShow);
  UpdateWindow(Window);

  return TRUE;
  }

// Processes messages for the main window.
//  WM_COMMAND  - process the application menu
//  WM_PAINT    - Paint the main window
//  WM_DESTROY  - post a quit message and return
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  {
  main_loop ();

  switch (message)
    {
    case WM_COMMAND:
      {
      int wmId = LOWORD(wParam);
      // Parse the menu selections:
      switch (wmId)
        {
        case IDM_ABOUT:
          DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
          break;

        case IDM_EXIT:
          DestroyWindow(hWnd);
          break;

        default:
          return DefWindowProc(hWnd, message, wParam, lParam);
        }
      }
    break;

    case WM_DESTROY:
      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
    }
  return 0;
  }

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
  {
  UNREFERENCED_PARAMETER(lParam);
  switch (message)
    {
    case WM_INITDIALOG:
      return (INT_PTR)TRUE;

    case WM_COMMAND:
      if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
        EndDialog(hDlg, LOWORD(wParam));
        return (INT_PTR)TRUE;
        }
      break;
    }
  return (INT_PTR)FALSE;
  }

void main_loop ()
  {
  player.x += 1;
  draw ();
  }

void draw ()
  {
  PAINTSTRUCT ps;
  HDC hdc = BeginPaint (Window, &ps);

  // clear background
  RECT screen;
  GetWindowRect (Window, &screen);
  screen.left = 0;
  screen.top = 0;
  COLORREF color_black = RGB (0, 0, 0);
  COLORREF color_red = RGB (255, 0, 0);
  HBRUSH brush_black = CreateSolidBrush (color_black);
  HBRUSH brush_red = CreateSolidBrush (color_red);
  FillRect (hdc, &screen, brush_black);

  player.position_rect ();
  FillRect (hdc, &player.render_rect, brush_red);

  EndPaint (Window, &ps);
  }

I've updated it to this after some suggestions from Josh♦:

#include "stdafx.h"
#include "Windows.h"

#include "Keyboard.h";
#include "Photon.h"
#include "Player2D.h"

#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst;                                // current instance
HWND Window;
WCHAR szTitle[MAX_LOADSTRING];                  // The title bar text
WCHAR szWindowClass[MAX_LOADSTRING];            // the main window class name
Player2D player;
Keyboard keyboard;
bool Exit = false;

// Prototypes
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
void draw ();
void main_loop ();

int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
  {
  UNREFERENCED_PARAMETER(hPrevInstance);
  UNREFERENCED_PARAMETER(lpCmdLine);

  // Initialize global strings
  LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
  LoadStringW(hInstance, IDC_PHOTON, szWindowClass, MAX_LOADSTRING);
  MyRegisterClass(hInstance);

  // Perform application initialization:
  if (!InitInstance (hInstance, nCmdShow))
    {
    return FALSE;
    }

  HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PHOTON));

  MSG msg;

  // Main message loop:
  while (!Exit)
    {
    if (PeekMessage (&msg, nullptr, 0, 0, PM_REMOVE))
      {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
      }
    }

    return (int) msg.wParam;
  }

// Registers the window class.
ATOM MyRegisterClass(HINSTANCE hInstance)
  {
  WNDCLASSEXW wcex;

  wcex.cbSize = sizeof(WNDCLASSEX);

  wcex.style          = CS_HREDRAW | CS_VREDRAW;
  wcex.lpfnWndProc    = WndProc;
  wcex.cbClsExtra     = 0;
  wcex.cbWndExtra     = 0;
  wcex.hInstance      = hInstance;
  wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PHOTON));
  wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
  wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
  wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_PHOTON);
  wcex.lpszClassName  = szWindowClass;
  wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

  return RegisterClassExW(&wcex);
  }

// In this function, we save the instance handle in a global variable and
// create and display the main program window.
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
  {
  hInst = hInstance; // Store instance handle in our global variable

  Window = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
  CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

  if (!Window)
    {
    return FALSE;
    }

  ShowWindow(Window, nCmdShow);
  UpdateWindow(Window);

  return TRUE;
  }

// Processes messages for the main window.
//  WM_COMMAND  - process the application menu
//  WM_PAINT    - Paint the main window
//  WM_DESTROY  - post a quit message and return
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  {
  switch (message)
    {
    case WM_COMMAND:
      {
      int wmId = LOWORD(wParam);
      // Parse the menu selections:
      switch (wmId)
        {
        case IDM_ABOUT:
          DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
          break;

        case IDM_EXIT:
          DestroyWindow(hWnd);
          Exit = true;
          break;

        default:
          return DefWindowProc(hWnd, message, wParam, lParam);
        }
      }
    break;

    case WM_PAINT:
      {
      draw ();
      }
    break;

    case WM_DESTROY:
      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
    }
  return 0;
  }

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
  {
  UNREFERENCED_PARAMETER(lParam);
  switch (message)
    {
    case WM_INITDIALOG:
      return (INT_PTR)TRUE;

    case WM_COMMAND:
      if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
        EndDialog(hDlg, LOWORD(wParam));
        return (INT_PTR)TRUE;
        }
      break;
    }
  return (INT_PTR)FALSE;
  }

void draw ()
  {
  // clear background
  RECT screen;
  GetClientRect (Window, &screen);
  InvalidateRect (Window, &screen, true);

  PAINTSTRUCT ps;
  HDC hdc = BeginPaint (Window, &ps);

  COLORREF color_black = RGB (0, 0, 0);
  COLORREF color_red = RGB (255, 0, 0);
  HBRUSH brush_black = CreateSolidBrush (color_black);
  HBRUSH brush_red = CreateSolidBrush (color_red);
  FillRect (hdc, &screen, brush_black);

  player.position_rect ();

  player.x += 1;
  if (player.x > screen.right)
    {
    player.x = 0;
    player.y += 1;
    }

  SetPixel (hdc, player.x, player.y, color_red);

  EndPaint (Window, &ps);
  }

The game logic updates each frame, and the game appears to render at least once (a black background with a red pixel representing the player's position). Breakpoints in the draw() function are hit on each frame, but the rendering doesn't update as the player moves.

I realize I could have started with a Direct-X project, but I wanted to try my hand at writing my own graphics code (eventually). So far, even basic functionality is eluding me, though.

\$\endgroup\$
  • \$\begingroup\$ It's been a long time since I messed around with Windows GDI, but as I recall some calls don't necessarily execute immediately (don't recall if that's the case here though). Also, what do you mean by "writing my own graphics code"? Do you mean writing code that uses the GDI layer instead of Direct X? \$\endgroup\$ – Pikalek Aug 10 at 3:01
  • \$\begingroup\$ @Pikalek - Exactly. \$\endgroup\$ – Nightmare Games Aug 10 at 4:19
  • \$\begingroup\$ Then I want to call attention to the differences between GDI & DirectX in terms of game dev. Even if you're already familiar with the differences, I think it's important to signpost it for other future readers. \$\endgroup\$ – Pikalek Aug 10 at 14:27
  • \$\begingroup\$ You should take a look at these project templates. \$\endgroup\$ – Chuck Walbourn Sep 9 at 3:09
4
\$\begingroup\$

GetMessage blocks until a message matching the filter is available. Generally you want to use PeekMessage instead to implement a real-time game loop, in a fashion that looks roughly like:

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    } else {
        UpdateAndRender();
    }
}

Note that updating game logic and rendering happens in the main loop, not in the window procedure. Yours is in the window procedure, which isn't a good place for it. The window procedure will not get called until there's a message to handle, and sometimes it can be a long (in computer terms) time between any two messages. That's part of why you are not seeing any rendering updates.

The other reason is that BeginPaint sets the clip region of the device context to the invalidated update region for the paint, which is probably empty after the first draw because you never call InvalidateRect to reset it. Once you know the screen needs to be updated, you should call InvalidateRect.

It's also probably worth looking in to putting your drawing into the WM_PAINT handler of your window procedure since it looks like you're trying to create a proper Win32 application (with menus and dialogs and the like) based on the rest of your code, and WM_PAINT is where painting is "supposed" to happen.

Other issues that are problems but probably not related to the one you asked about:

GetWindowRect gives you the rectangle for the entire window, including the UI chrome and title bar and such. That's larger than the area you want to actually draw into; your drawing will get clipped in this case, but if you use that rectangle for computing certain things or coordinate conversion, you'll notice errors. GetClientRect will give you the client (interior canvas) part of the window.

A RECT also contains left, top, right and bottom coordinates. Not width and height. By getting the window rectangle and then zeroing out the top/left coordinates, you're not just positioning the rectangle at the origin, you're also resizing it.

When you create brushes, you should delete them when you are done. Otherwise you'll eventually exhaust the (relatively small) pool of device objects allowed and rendering will stop working; right now you're leaking two brushes per draw. Consider creating both brushes at startup and deleting them at shutdown, and just reusing them for each draw. It will be faster as well as use fewer objects.

\$\endgroup\$
  • \$\begingroup\$ These are some very good insights. Unfortunately, I don't see a change in behavior after implementing them. \$\endgroup\$ – Nightmare Games Aug 10 at 19:38
  • \$\begingroup\$ Can you update your post with what your code looks like now? (Leave the original ones for context though) \$\endgroup\$ – Josh Aug 10 at 19:57
  • \$\begingroup\$ Ah, I updated earlier but I overwrote the original. \$\endgroup\$ – Nightmare Games Aug 10 at 22:35
  • \$\begingroup\$ In your new example, you call InvalidateRect() during the paint; you want to it sometime after the paint to cause a new WM_PAINT to post (possibly immediately after). You want to return 0 from WM_PAINT's case as well. And I'm not sure offhand what you're setting the background color to, but you may get really ugly flickering because WM_ERASEBKGND may be using a different color. You also don't delete the brushes still, which will only give you a few frames worth of updates before everything stops because you exhaust the pool. \$\endgroup\$ – Josh Aug 10 at 23:11
  • \$\begingroup\$ Thanks for the added help, but I'm unclear on where exactly the InvalidateRect() should go if it's meant to precede the painting. Somewhere inside the WndProc() function? I'm also not sure what I should be calling in the PeekMessage's else case. I only have the draw() function now, and I'm already calling that from the WM_PAINT case. \$\endgroup\$ – Nightmare Games Aug 10 at 23:53
0
\$\begingroup\$

After following a few suggestions from Josh here on Stack and doing a bunch of research, here's what my working code looks like:

#include "stdafx.h"

#include "Keyboard.h";
#include "Photon.h"
#include "Player2D.h"

#include <windows.h>

#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst;                                // current instance
HWND Window;
WCHAR szTitle[MAX_LOADSTRING];                  // The title bar text
WCHAR szWindowClass[MAX_LOADSTRING];            // the main window class name

Player2D player;
Keyboard keyboard;

bool Exit = false;

COLORREF color_black = RGB (0, 0, 0);
COLORREF color_white = RGB (255, 255, 255);
COLORREF color_red = RGB (255, 0, 0);

// Prototypes
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

void draw ();
void main_loop ();

int APIENTRY wWinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
  {
  UNREFERENCED_PARAMETER(hPrevInstance);
  UNREFERENCED_PARAMETER(lpCmdLine);

  // Initialize global strings
  LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
  LoadStringW(hInstance, IDC_PHOTON, szWindowClass, MAX_LOADSTRING);
  MyRegisterClass(hInstance);

  // Perform application initialization:
  if (!InitInstance (hInstance, nCmdShow))
    {
    return FALSE;
    }

  HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PHOTON));

  MSG msg;

  // Main message loop:
  while (!Exit)
    {
    while (PeekMessage (&msg, nullptr, 0, 0, PM_REMOVE))
      {
      DispatchMessage(&msg);
      }
    main_loop ();
    }

  return (int) msg.wParam;
  }

// Registers the window class.
ATOM MyRegisterClass(HINSTANCE hInstance)
  {
  WNDCLASSEXW wcex;

  wcex.cbSize = sizeof(WNDCLASSEX);

  wcex.style          = CS_HREDRAW | CS_VREDRAW;
  wcex.lpfnWndProc    = WndProc;
  wcex.cbClsExtra     = 0;
  wcex.cbWndExtra     = 0;
  wcex.hInstance      = hInstance;
  wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PHOTON));
  wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
  wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
  wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_PHOTON);
  wcex.lpszClassName  = szWindowClass;
  wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

  return RegisterClassExW(&wcex);
  }

// In this function, we save the instance handle in a global variable and
// create and display the main program window.
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
  {
  hInst = hInstance; // Store instance handle in our global variable

  Window = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
  CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

  if (!Window)
    {
    return FALSE;
    }

  ShowWindow(Window, nCmdShow);
  UpdateWindow(Window);

  return TRUE;
  }

// Processes messages for the main window.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  {
  switch (message)
    {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
    }
  return 0;
  }

void main_loop ()
  {
  player.x += 1;
  keyboard.get_state ();
  keyboard.handle_input (player);
  draw ();
  }

void draw ()
  {
  HDC hdc = GetDC (Window);

  SetPixel (hdc, player.x - 20, player.y, color_white);
  SetPixel (hdc, player.x, player.y, color_red);
  Sleep (5);
  }

This allows me to set pixels individually and render in a loop while still operating inside a window.

From here, I can work on speeding up the draw step and moving away from SetPixel().

Parts I added: - An arbitrary Sleep() after the rendering is finished to make sure what's happening is slow enough to be visible.

Parts I changed:

  • PostMessage() to PeekMessage().
  • Calling main_loop from inside wWinMain() instead of WndProc().
  • Calling draw() from main_loop() instead of from WndProc's WM_PAINT case.
  • Changed the red rectangle to a single red pixel. It's a more basic test case and eliminates some ambiguity.

Parts I didn't need:

  • Clearing the screen - This was causing a lot of uneven flickering, I'm guessing because I was trying to use rectangle painting to do it. I'll bring it back later with a better method.
  • WM_PAINT and painting in general - Without the rectangles, it turns out I didn't need this logic at all. SetPixel() apparently doesn't require it.
  • The menu - Unnecessary fluff for what I'm working on.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.