Making a render loop

From WxWiki
Jump to navigation Jump to search

While wxWidgets is not an ideal tool for something like a game that requires a high performance render loop, you might still want to use a render loop in your application.

This article will attempt to give examples of how it can be done. Note that there may be more possibilities than explained here, I will try to extend this article in the future.

What not to do

The following ways should not be used, as they can result in obscure errors or crashes that will be very difficult to debug

  • Use a loop
// wxWidgets, like all GUI libraries, handles events in an event loop. This event loop
// is NOT magical, i.e. when the program is looping in your loop then the wxWidgets event
// loop does not get to execute and the app freezes. Also, calling "sleep" makes the thread
// as a whole sleep, and that includes wxWidgets. wxWidgets will not continue executing
// magically if you sleep the main thread.
while(true)
{
    render(); // or Refresh()
    sleep(1);
}
  • Use wxYield() in a loop
// Better than other example above, and may work, but not so good idea.
// it's not elegant and can create re-entrency issues
while(1)
{
    render(); // or Refresh()
    wxYield();
}
  • Use a loop in a separate thread (bad because wxWidgets requires all GUI calls to happen in the main thread). However you can use a loop in a thread that sends events to the main thread regularly, however this is just a more complicated version of the wxTimer example posted below.

Other things to check for if you get weird results

  • Don't use wxPaintDC outside paint events (use wxClientDC instead.)
  • If you catch paint events, you must create a wxPaintDC inside them. Failing to do so will result in the event not being handled properly and weird behavior can ensue.

Using idle events

Pros :

  • doesn't hog the system, as it renders only when it can
  • one of the fastest ways possible in wx (on my MacBook 2GHz I get around 3500 fps with the code below)

Cons :

  • framerate is not necessarly regular
  • can stop rendering when you do something else, for instance open a menu
#include "wx/sizer.h"
#include "wx/wx.h"

class BasicDrawPane : public wxPanel
{
    
public:
    BasicDrawPane(wxFrame* parent);
	
    void paintEvent(wxPaintEvent& evt);
    void paintNow();
    void render( wxDC& dc );
    
    DECLARE_EVENT_TABLE()
};

class MyFrame;

class MyApp: public wxApp
{
    bool render_loop_on;
    bool OnInit();
    void onIdle(wxIdleEvent& evt);
    
    MyFrame* frame;
    BasicDrawPane* drawPane;
public:
    void activateRenderLoop(bool on);
        
};

IMPLEMENT_APP(MyApp)

class MyFrame : public wxFrame
{
public:
    MyFrame() : wxFrame((wxFrame *)NULL, -1,  wxT("Hello wxDC"), wxPoint(50,50), wxSize(400,200))
    {
    }
    void onClose(wxCloseEvent& evt)
    {
        wxGetApp().activateRenderLoop(false);
        evt.Skip(); // don't stop event, we still want window to close
    }
    DECLARE_EVENT_TABLE()
};


BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_CLOSE(MyFrame::onClose)
END_EVENT_TABLE()

bool MyApp::OnInit()
{
    render_loop_on = false;
    
    wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
    frame = new MyFrame();
	
    drawPane = new BasicDrawPane( frame );
    sizer->Add(drawPane, 1, wxEXPAND);
	
    frame->SetSizer(sizer);
    frame->Show();
    
    activateRenderLoop(true);
    return true;
} 

void MyApp::activateRenderLoop(bool on)
{
    if(on && !render_loop_on)
    {
        Connect( wxID_ANY, wxEVT_IDLE, wxIdleEventHandler(MyApp::onIdle) );
        render_loop_on = true;
    }
    else if(!on && render_loop_on)
    {
        Disconnect( wxEVT_IDLE, wxIdleEventHandler(MyApp::onIdle) );
        render_loop_on = false;
    }
}
void MyApp::onIdle(wxIdleEvent& evt)
{
    if(render_loop_on)
    {
        drawPane->paintNow();
        evt.RequestMore(); // render continuously, not only once on idle
    }
}


BEGIN_EVENT_TABLE(BasicDrawPane, wxPanel)
EVT_PAINT(BasicDrawPane::paintEvent)
END_EVENT_TABLE()



BasicDrawPane::BasicDrawPane(wxFrame* parent) :
wxPanel(parent)
{
}


void BasicDrawPane::paintEvent(wxPaintEvent& evt)
{
    wxPaintDC dc(this);
    render(dc);
}

void BasicDrawPane::paintNow()
{
    wxClientDC dc(this);
    render(dc);
}

void BasicDrawPane::render( wxDC& dc )
{
    static int y = 0;
    static int y_speed = 2;
    
    y += y_speed;
    if(y<0) y_speed = 2;
    if(y>200) y_speed = -2;
    
    dc.SetBackground( *wxWHITE_BRUSH );
    dc.Clear();
    dc.DrawText(wxT("Testing"), 40, y); 
}

Using a timer

Pros :

  • Regular framerate
  • Doesn't stop like idle events when you do something else, like open a menu

Cons :

  • Not as fast as idle events
  • Implementation quality depends on your platform; some platforms provide better timers than others.
#include <wx/sizer.h>
#include <wx/wx.h>
#include <wx/timer.h>

class BasicDrawPane;

class RenderTimer : public wxTimer
{
    BasicDrawPane* pane;
public:
    RenderTimer(BasicDrawPane* pane);
    void Notify();
    void start();
};


class BasicDrawPane : public wxPanel
{
    
public:
    BasicDrawPane(wxFrame* parent);
	
    void paintEvent(wxPaintEvent& evt);
    void paintNow();
    void render( wxDC& dc );
    
    DECLARE_EVENT_TABLE()
};

class MyFrame;

class MyApp: public wxApp
{
    bool OnInit();
    
    MyFrame* frame;
public:
        
};


RenderTimer::RenderTimer(BasicDrawPane* pane) : wxTimer()
{
    RenderTimer::pane = pane;
}

void RenderTimer::Notify()
{
    pane->Refresh();
}

void RenderTimer::start()
{
    wxTimer::Start(10);
}

IMPLEMENT_APP(MyApp)

class MyFrame : public wxFrame
{
    RenderTimer* timer;
    BasicDrawPane* drawPane;
    
public:
    MyFrame() : wxFrame((wxFrame *)NULL, -1,  wxT("Hello wxDC"), wxPoint(50,50), wxSize(400,200))
    {
        wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
        drawPane = new BasicDrawPane( this );
        sizer->Add(drawPane, 1, wxEXPAND);
        SetSizer(sizer);
        
        timer = new RenderTimer(drawPane);
        Show();
        timer->start();
    }
    ~MyFrame()
    {
        delete timer;
    }
    void onClose(wxCloseEvent& evt)
    {
        timer->Stop();
        evt.Skip();
    }
    DECLARE_EVENT_TABLE()
};


BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_CLOSE(MyFrame::onClose)
END_EVENT_TABLE()

bool MyApp::OnInit()
{
    frame = new MyFrame();
    frame->Show();

    return true;
} 


BEGIN_EVENT_TABLE(BasicDrawPane, wxPanel)
EVT_PAINT(BasicDrawPane::paintEvent)
END_EVENT_TABLE()



BasicDrawPane::BasicDrawPane(wxFrame* parent) :
wxPanel(parent)
{
}


void BasicDrawPane::paintEvent(wxPaintEvent& evt)
{
    wxPaintDC dc(this);
    render(dc);
}

void BasicDrawPane::paintNow()
{
    wxClientDC dc(this);
    render(dc);
}

void BasicDrawPane::render( wxDC& dc )
{
    static int y = 0;
    static int y_speed = 2;
    
    y += y_speed;
    if(y<0) y_speed = 2;
    if(y>200) y_speed = -2;
    
    dc.SetBackground( *wxWHITE_BRUSH );
    dc.Clear();
    dc.DrawText(wxT("Testing"), 40, y); 
}

Note: When using this method with wxMSW it is very important to include

wxPaintDC dc(this);

in your EVT_PAINT handler. If you do not, your timer (somewhat mysteriously) will not work. Your code may work on other platforms (e.g. wxMAC) but will not work in wxMSW.