EDIT: So I found old threads that shed light on this matter here: http://www.gamedev.net/community/forums/topic.asp?topic_id=298637 However, I still am wondering how to implement a timer that would allow me to figure out how much time passed between each of my loop iterations. ——————————— Original Post: I am learning C# using Windows forms and I came to an obivous problem — since Windows Forms is event driven, I cannot implement a «game loop.» I tried to simulate something like it using the System.Timers.Timer class and tics every 10 miliseconds, but that ends up running like crap (1-10 FPS). In Win32 and C++, handling the events was simply a part of the gameloop (the old while(new messages) translate/dispatch message then do other stuff) while here the event handling loop is «hidden» from me so I cannot implement it the same was as I would have in the past. So my question is — how do I implement a game loop in a C# windows program that would be independant of the event-handling? Here’s my current code; very crude since I am just learning C#:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
//vars
public LittleBall Ball;
public System.Timers.Timer Timer;
public float TimeElapsed;
public float LastTime;
//constructor
public Form1()
{
InitializeComponent();
Ball = new LittleBall();
//set timer to run every 10 milliseconds; however, the event doesn't get processed often
//enough hence resulting in inconsistant timer updates
Timer = new System.Timers.Timer(10);
Timer.Elapsed += new System.Timers.ElapsedEventHandler(TimerEventProcessor);
Timer.Enabled = true;
TimeElapsed = 0;
LastTime = 0;
}
//timer handler
private void TimerEventProcessor(Object myObject,
System.Timers.ElapsedEventArgs myEventArgs)
{
LastTime = TimeElapsed;
TimeElapsed += myEventArgs.SignalTime.Millisecond;
}
//the ball/circle we move on screen
public class LittleBall
{
public float PosX;
public float PosY;
public System.Drawing.Size Size;
//constructor
public LittleBall()
{
Size = new System.Drawing.Size(10,10);
PosX = 10.0f;
PosY = 10.0f;
}
//draw function
public void Draw(PaintEventArgs e)
{
Graphics graphics = e.Graphics;
Pen myPen = new Pen(Color.Black);
// Draw the button in the form of a circle
graphics.DrawEllipse(myPen, this.PosX, this.PosY, this.Size.Width, this.Size.Height);
myPen.Dispose();
}
}
//my key handler
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
float Movement = (TimeElapsed - LastTime) / 100;
if(e.KeyCode == Keys.Up)
Ball.PosY -= Movement;
if (e.KeyCode == Keys.Down)
Ball.PosY += Movement;
if (e.KeyCode == Keys.Left)
Ball.PosX -= Movement;
if (e.KeyCode == Keys.Right)
Ball.PosX += Movement;
this.Invalidate();
}
//my OnPaint ovveride that draws the ball
protected override void OnPaint(PaintEventArgs e)
{
Ball.Draw(e);
}
}
}
[Edited by — Koobazaur on February 23, 2008 7:30:01 PM]
Comrade, Listen! The Glorious Commonwealth’s first Airship has been compromised! Who is the saboteur? Who can be saved? Uncover what the passengers are hiding and write the grisly conclusion of its final hours in an open-ended, player-driven adventure. Dziekujemy! — Karaski: What Goes Up…
There are a few sites (And I imagine old threads) that discuss this. The way I am familiar with is using Application.OnIdle to trigger the game loop:
[STAThread] static void Main() { // Initialization // create new form 'MainWindow' System.Windows.Forms.Application.Idle += new EventHandler(MainWindow.OnApplicationIdle); System.Windows.Forms.Application.Run(MainWindow); while (MainWindow.Created) { } } private void OnApplicationIdle(object sender, EventArgs e) { while (AppStillIdle()) { // LOOP } } private static bool AppStillIdle() { pkMessage msg; return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0); } [StructLayout(LayoutKind.Sequential)] public struct pkMessage { public IntPtr hWnd; public Message msg; public IntPtr wParam; public IntPtr lParam; public uint time; public System.Drawing.Point p; } [System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously [DllImport("User32.dll", CharSet = CharSet.Auto)] public static extern bool PeekMessage(out pkMessage msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
[edit:]
As for time, simply putting some code at the beginning of the game loop to determine the current time (and compare to the last loop) is likely sufficient. DateTime.Now.Ticks provides a fairly low resolution option. System.Diagnostics.StopWatch provides a little higher resolution option, and there are libraries out there that can help if you really need super-precise high resolution timing.
using System; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Runtime.InteropServices; using System; using System.Runtime.InteropServices; public class MainWindow : Form { static Bitmap backbuffer; const int windowWidth = 640; const int windowHeight = 480; static int a = 5; public MainWindow() { this.ClientSize = new Size( windowWidth, windowHeight ); // The OptimizedDoubleBuffer style has no effect due to the way // we control this game (drawing would have to be in OnPaint for // it to benefit us at all.) this.SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.Opaque, true ); // Create the backbuffer backbuffer = new Bitmap( windowWidth, windowHeight ); } public void Render() { Graphics g = Graphics.FromImage( backbuffer ); // clear the background. normally if you weren't using a // backbuffer, this would cause severe flicker. g.Clear( Color.White ); // just draw something. a line that moves a++; g.DrawLine( new Pen( Color.Blue, 5 ), new Point( 5, 5 ), new Point( a, 500 ) ); // Flip the backbuffer Graphics gFrontBuffer = Graphics.FromHwnd( this.Handle ); gFrontBuffer.DrawImage( backbuffer, new Point( 0, 0 ) ); } // We're not painting in OnPaint(). /* protected override void OnPaint( PaintEventArgs e ) { } */ static void Main() { // why not to use while(Created){FullRender();Application.DoEvents();} //http://blogs.msdn.com/tmiller/archive/2003/11/07/57524.aspx // Also here are records of people burned by it (scroll to bottom) //http://msdn.microsoft.com/en-us/library/system.windows.forms.application.doevents.aspx // alternative approaches to DoEvents loop //http://blogs.msdn.com/tmiller/archive/2003/11/24/57532.aspx // Using traditional PeekMessage() loop // Create-up a window. MainWindow mw = new MainWindow(); mw.Show(); // (style init stuff is in its constructor) Win32.NativeMessage msg = new Win32.NativeMessage(); while( true ) { if( !mw.Created ) { // Need this here for the application to shutdown gracefully. break; } if( Win32.PeekMessage( out msg, mw.Handle, 0, 0, (uint)Win32.PM.REMOVE ) ) { if( msg.message == (uint)Win32.WindowsMessage.WM_QUIT ) { Console.WriteLine( "QUITTING..." ); break; } else { Win32.TranslateMessage( ref msg ); Win32.DispatchMessage( ref msg ); } } else { // Run the game simulation // Render current state mw.Render(); System.Threading.Thread.Sleep( 2 ); } } Application.Exit(); } } public static class Win32 { // From http://www.pinvoke.net/default.aspx/user32/PeekMessage.html [StructLayout( LayoutKind.Sequential )] public struct NativeMessage { public IntPtr hwnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public System.Drawing.Point point; } [DllImport( "user32.dll" )] [return: MarshalAs( UnmanagedType.Bool )] public static extern bool PeekMessage( out NativeMessage lpMsg, IntPtr hwnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg ); public enum PM : uint { NOREMOVE = 0, REMOVE = 1 } [DllImport( "user32.dll" )] public static extern bool TranslateMessage( [In] ref NativeMessage lpMsg ); [DllImport( "user32.dll" )] public static extern IntPtr DispatchMessage( [In] ref NativeMessage lpmsg ); #region all the sys metrics and WM_ windows message defs // from winuser.h /* * GetSystemMetrics() codes */ public enum SystemMetrics : uint { SM_CXSCREEN = 0, SM_CYSCREEN = 1, SM_CXVSCROLL = 2, SM_CYHSCROLL = 3, SM_CYCAPTION = 4, SM_CXBORDER = 5, SM_CYBORDER = 6, SM_CXDLGFRAME = 7, SM_CXFIXEDFRAME = 7,/* ;win40 name change */ SM_CYDLGFRAME = 8, SM_CYFIXEDFRAME = 8,/* ;win40 name change */ SM_CYVTHUMB = 9, SM_CXHTHUMB = 10, SM_CXICON = 11, SM_CYICON = 12, SM_CXCURSOR = 13, SM_CYCURSOR = 14, SM_CYMENU = 15, SM_CXFULLSCREEN = 16, SM_CYFULLSCREEN = 17, SM_CYKANJIWINDOW = 18, SM_MOUSEPRESENT = 19, SM_CYVSCROLL = 20, SM_CXHSCROLL = 21, SM_DEBUG = 22, SM_SWAPBUTTON = 23, SM_RESERVED1 = 24, SM_RESERVED2 = 25, SM_RESERVED3 = 26, SM_RESERVED4 = 27, SM_CXMIN = 28, SM_CYMIN = 29, SM_CXSIZE = 30, SM_CYSIZE = 31, SM_CXFRAME = 32, SM_CXSIZEFRAME = 32, /* ;win40 name change */ SM_CYFRAME = 33, SM_CYSIZEFRAME = 33,/* ;win40 name change */ SM_CXMINTRACK = 34, SM_CYMINTRACK = 35, SM_CXDOUBLECLK = 36, SM_CYDOUBLECLK = 37, SM_CXICONSPACING = 38, SM_CYICONSPACING = 39, SM_MENUDROPALIGNMENT = 40, SM_PENWINDOWS = 41, SM_DBCSENABLED = 42, SM_CMOUSEBUTTONS = 43, SM_SECURE = 44, SM_CXEDGE = 45, SM_CYEDGE = 46, SM_CXMINSPACING = 47, SM_CYMINSPACING = 48, SM_CXSMICON = 49, SM_CYSMICON = 50, SM_CYSMCAPTION = 51, SM_CXSMSIZE = 52, SM_CYSMSIZE = 53, SM_CXMENUSIZE = 54, SM_CYMENUSIZE = 55, SM_ARRANGE = 56, SM_CXMINIMIZED = 57, SM_CYMINIMIZED = 58, SM_CXMAXTRACK = 59, SM_CYMAXTRACK = 60, SM_CXMAXIMIZED = 61, SM_CYMAXIMIZED = 62, SM_NETWORK = 63, SM_CLEANBOOT = 67, SM_CXDRAG = 68, SM_CYDRAG = 69, SM_SHOWSOUNDS = 70, SM_CXMENUCHECK = 71, /* Use instead of GetMenuCheckMarkDimensions()! */ SM_CYMENUCHECK = 72, SM_SLOWMACHINE = 73, SM_MIDEASTENABLED = 74, SM_MOUSEWHEELPRESENT = 75, SM_XVIRTUALSCREEN = 76, SM_YVIRTUALSCREEN = 77, SM_CXVIRTUALSCREEN = 78, SM_CYVIRTUALSCREEN = 79, SM_CMONITORS = 80, SM_SAMEDISPLAYFORMAT = 81, SM_IMMENABLED = 82, SM_CXFOCUSBORDER = 83, SM_CYFOCUSBORDER = 84, SM_TABLETPC = 86, SM_MEDIACENTER = 87, SM_STARTER = 88, SM_SERVERR2 = 89, SM_CMETRICS = 90, SM_REMOTESESSION = 0x1000, SM_SHUTTINGDOWN = 0x2000, SM_REMOTECONTROL = 0x2001, SM_CARETBLINKINGENABLED = 0x2002 } public enum WindowsMessage : uint { /* * Window Messages */ WM_NULL = 0x0000, WM_CREATE = 0x0001, WM_DESTROY = 0x0002, WM_MOVE = 0x0003, WM_SIZE = 0x0005, WM_ACTIVATE = 0x0006, WM_SETFOCUS = 0x0007, WM_KILLFOCUS = 0x0008, WM_ENABLE = 0x000A, WM_SETREDRAW = 0x000B, WM_SETTEXT = 0x000C, WM_GETTEXT = 0x000D, WM_GETTEXTLENGTH = 0x000E, WM_PAINT = 0x000F, WM_CLOSE = 0x0010, //#ifndef _WIN32_WCE WM_QUERYENDSESSION = 0x0011, WM_QUERYOPEN = 0x0013, WM_ENDSESSION = 0x0016, //#endif WM_QUIT = 0x0012, WM_ERASEBKGND = 0x0014, WM_SYSCOLORCHANGE = 0x0015, WM_SHOWWINDOW = 0x0018, WM_WININICHANGE = 0x001A, WM_DEVMODECHANGE = 0x001B, WM_ACTIVATEAPP = 0x001C, WM_FONTCHANGE = 0x001D, WM_TIMECHANGE = 0x001E, WM_CANCELMODE = 0x001F, WM_SETCURSOR = 0x0020, WM_MOUSEACTIVATE = 0x0021, WM_CHILDACTIVATE = 0x0022, WM_QUEUESYNC = 0x0023, WM_GETMINMAXINFO = 0x0024, WM_PAINTICON = 0x0026, WM_ICONERASEBKGND = 0x0027, WM_NEXTDLGCTL = 0x0028, WM_SPOOLERSTATUS = 0x002A, WM_DRAWITEM = 0x002B, WM_MEASUREITEM = 0x002C, WM_DELETEITEM = 0x002D, WM_VKEYTOITEM = 0x002E, WM_CHARTOITEM = 0x002F, WM_SETFONT = 0x0030, WM_GETFONT = 0x0031, WM_SETHOTKEY = 0x0032, WM_GETHOTKEY = 0x0033, WM_QUERYDRAGICON = 0x0037, WM_COMPAREITEM = 0x0039, WM_COMPACTING = 0x0041, WM_COMMNOTIFY = 0x0044, /* no longer suported */ WM_WINDOWPOSCHANGING = 0x0046, WM_WINDOWPOSCHANGED = 0x0047, WM_POWER = 0x0048, /* * wParam for WM_POWER window message and DRV_POWER driver notification #define PWR_OK 1 #define PWR_FAIL (-1) #define PWR_SUSPENDREQUEST 1 #define PWR_SUSPENDRESUME 2 #define PWR_CRITICALRESUME 3 */ WM_COPYDATA = 0x004A, WM_CANCELJOURNAL = 0x004B, /* * lParam of WM_COPYDATA message points to... typedef struct tagCOPYDATASTRUCT { ULONG_PTR dwData; DWORD cbData; PVOID lpData; } COPYDATASTRUCT, *PCOPYDATASTRUCT; */ //#if(WINVER >= 0x0400) WM_NOTIFY = 0x004E, WM_INPUTLANGCHANGEREQUEST = 0x0050, WM_INPUTLANGCHANGE = 0x0051, WM_TCARD = 0x0052, WM_HELP = 0x0053, WM_USERCHANGED = 0x0054, WM_NOTIFYFORMAT = 0x0055, WM_CONTEXTMENU = 0x007B, WM_STYLECHANGING = 0x007C, WM_STYLECHANGED = 0x007D, WM_DISPLAYCHANGE = 0x007E, WM_GETICON = 0x007F, WM_SETICON = 0x0080, //#endif /* WINVER >= 0x0400 */ WM_NCCREATE = 0x0081, WM_NCDESTROY = 0x0082, WM_NCCALCSIZE = 0x0083, WM_NCHITTEST = 0x0084, WM_NCPAINT = 0x0085, WM_NCACTIVATE = 0x0086, WM_GETDLGCODE = 0x0087, //#ifndef _WIN32_WCE WM_SYNCPAINT = 0x0088, //#endif WM_NCMOUSEMOVE = 0x00A0, WM_NCLBUTTONDOWN = 0x00A1, WM_NCLBUTTONUP = 0x00A2, WM_NCLBUTTONDBLCLK = 0x00A3, WM_NCRBUTTONDOWN = 0x00A4, WM_NCRBUTTONUP = 0x00A5, WM_NCRBUTTONDBLCLK = 0x00A6, WM_NCMBUTTONDOWN = 0x00A7, WM_NCMBUTTONUP = 0x00A8, WM_NCMBUTTONDBLCLK = 0x00A9, //#if(_WIN32_WINNT >= 0x0500) WM_NCXBUTTONDOWN = 0x00AB, WM_NCXBUTTONUP = 0x00AC, WM_NCXBUTTONDBLCLK = 0x00AD, //#endif /* _WIN32_WINNT >= 0x0500 */ //#if(_WIN32_WINNT >= 0x0501) WM_INPUT = 0x00FF, //#endif /* _WIN32_WINNT >= 0x0501 */ WM_KEYFIRST = 0x0100, WM_KEYDOWN = 0x0100, WM_KEYUP = 0x0101, WM_CHAR = 0x0102, WM_DEADCHAR = 0x0103, WM_SYSKEYDOWN = 0x0104, WM_SYSKEYUP = 0x0105, WM_SYSCHAR = 0x0106, WM_SYSDEADCHAR = 0x0107, //#if(_WIN32_WINNT >= 0x0501) WM_UNICHAR = 0x0109, WM_KEYLAST = 0x0109, //#define UNICODE_NOCHAR 0xFFFF //#if(WINVER >= 0x0400) WM_IME_STARTCOMPOSITION = 0x010D, WM_IME_ENDCOMPOSITION = 0x010E, WM_IME_COMPOSITION = 0x010F, WM_IME_KEYLAST = 0x010F, //#endif /* WINVER >= 0x0400 */ WM_INITDIALOG = 0x0110, WM_COMMAND = 0x0111, WM_SYSCOMMAND = 0x0112, WM_TIMER = 0x0113, WM_HSCROLL = 0x0114, WM_VSCROLL = 0x0115, WM_INITMENU = 0x0116, WM_INITMENUPOPUP = 0x0117, WM_MENUSELECT = 0x011F, WM_MENUCHAR = 0x0120, WM_ENTERIDLE = 0x0121, //#if(WINVER >= 0x0500) //#ifndef _WIN32_WCE WM_MENURBUTTONUP = 0x0122, WM_MENUDRAG = 0x0123, WM_MENUGETOBJECT = 0x0124, WM_UNINITMENUPOPUP = 0x0125, WM_MENUCOMMAND = 0x0126, //#ifndef _WIN32_WCE //#if(_WIN32_WINNT >= 0x0500) WM_CHANGEUISTATE = 0x0127, WM_UPDATEUISTATE = 0x0128, WM_QUERYUISTATE = 0x0129, WM_CTLCOLORMSGBOX = 0x0132, WM_CTLCOLOREDIT = 0x0133, WM_CTLCOLORLISTBOX = 0x0134, WM_CTLCOLORBTN = 0x0135, WM_CTLCOLORDLG = 0x0136, WM_CTLCOLORSCROLLBAR = 0x0137, WM_CTLCOLORSTATIC = 0x0138, //#define MN_GETHMENU 0x01E1 WM_MOUSEFIRST = 0x0200, WM_MOUSEMOVE = 0x0200, WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202, WM_LBUTTONDBLCLK = 0x0203, WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205, WM_RBUTTONDBLCLK = 0x0206, WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208, WM_MBUTTONDBLCLK = 0x0209, //#if (_WIN32_WINNT >= 0x0400) || (_WIN32_WINDOWS > 0x0400) WM_MOUSEWHEEL = 0x020A, //#endif //#if (_WIN32_WINNT >= 0x0500) WM_XBUTTONDOWN = 0x020B, WM_XBUTTONUP = 0x020C, WM_XBUTTONDBLCLK = 0x020D, //#endif //#if (_WIN32_WINNT >= 0x0500) WM_MOUSELAST = 0x020D, //#if(_WIN32_WINNT >= 0x0400) /* Value for rolling one detent */ //#define WHEEL_DELTA 120 //#define GET_WHEEL_DELTA_WPARAM(wParam) ((short)HIWORD(wParam)) /* Setting to scroll one page for SPI_GET/SETWHEELSCROLLLINES */ //#define WHEEL_PAGESCROLL (UINT_MAX) //#endif /* _WIN32_WINNT >= 0x0400 */ WM_PARENTNOTIFY = 0x0210, WM_ENTERMENULOOP = 0x0211, WM_EXITMENULOOP = 0x0212, //#if(WINVER >= 0x0400) WM_NEXTMENU = 0x0213, WM_SIZING = 0x0214, WM_CAPTURECHANGED = 0x0215, WM_MOVING = 0x0216, //#endif /* WINVER >= 0x0400 */ //#if(WINVER >= 0x0400) WM_POWERBROADCAST = 0x0218, /* #ifndef _WIN32_WCE #define PBT_APMQUERYSUSPEND 0x0000 #define PBT_APMQUERYSTANDBY 0x0001 #define PBT_APMQUERYSUSPENDFAILED 0x0002 #define PBT_APMQUERYSTANDBYFAILED 0x0003 #define PBT_APMSUSPEND 0x0004 #define PBT_APMSTANDBY 0x0005 #define PBT_APMRESUMECRITICAL 0x0006 #define PBT_APMRESUMESUSPEND 0x0007 #define PBT_APMRESUMESTANDBY 0x0008 #define PBTF_APMRESUMEFROMFAILURE 0x00000001 #define PBT_APMBATTERYLOW 0x0009 #define PBT_APMPOWERSTATUSCHANGE 0x000A #define PBT_APMOEMEVENT 0x000B #define PBT_APMRESUMEAUTOMATIC 0x0012 #endif */ //#endif /* WINVER >= 0x0400 */ //#if(WINVER >= 0x0400) WM_DEVICECHANGE = 0x0219, //#endif /* WINVER >= 0x0400 */ WM_MDICREATE = 0x0220, WM_MDIDESTROY = 0x0221, WM_MDIACTIVATE = 0x0222, WM_MDIRESTORE = 0x0223, WM_MDINEXT = 0x0224, WM_MDIMAXIMIZE = 0x0225, WM_MDITILE = 0x0226, WM_MDICASCADE = 0x0227, WM_MDIICONARRANGE = 0x0228, WM_MDIGETACTIVE = 0x0229, WM_MDISETMENU = 0x0230, WM_ENTERSIZEMOVE = 0x0231, WM_EXITSIZEMOVE = 0x0232, WM_DROPFILES = 0x0233, WM_MDIREFRESHMENU = 0x0234, //#if(WINVER >= 0x0400) WM_IME_SETCONTEXT = 0x0281, WM_IME_NOTIFY = 0x0282, WM_IME_CONTROL = 0x0283, WM_IME_COMPOSITIONFULL = 0x0284, WM_IME_SELECT = 0x0285, WM_IME_CHAR = 0x0286, //#endif /* WINVER >= 0x0400 */ //#if(WINVER >= 0x0500) WM_IME_REQUEST = 0x0288, //#endif /* WINVER >= 0x0500 */ //#if(WINVER >= 0x0400) WM_IME_KEYDOWN = 0x0290, WM_IME_KEYUP = 0x0291, //#endif /* WINVER >= 0x0400 */ //#if((_WIN32_WINNT >= 0x0400) || (WINVER >= 0x0500)) WM_MOUSEHOVER = 0x02A1, WM_MOUSELEAVE = 0x02A3, //#endif //#if(WINVER >= 0x0500) WM_NCMOUSEHOVER = 0x02A0, WM_NCMOUSELEAVE = 0x02A2, //#endif /* WINVER >= 0x0500 */ //#if(_WIN32_WINNT >= 0x0501) WM_WTSSESSION_CHANGE = 0x02B1, WM_TABLET_FIRST = 0x02c0, WM_TABLET_LAST = 0x02df, //#endif /* _WIN32_WINNT >= 0x0501 */ WM_CUT = 0x0300, WM_COPY = 0x0301, WM_PASTE = 0x0302, WM_CLEAR = 0x0303, WM_UNDO = 0x0304, WM_RENDERFORMAT = 0x0305, WM_RENDERALLFORMATS = 0x0306, WM_DESTROYCLIPBOARD = 0x0307, WM_DRAWCLIPBOARD = 0x0308, WM_PAINTCLIPBOARD = 0x0309, WM_VSCROLLCLIPBOARD = 0x030A, WM_SIZECLIPBOARD = 0x030B, WM_ASKCBFORMATNAME = 0x030C, WM_CHANGECBCHAIN = 0x030D, WM_HSCROLLCLIPBOARD = 0x030E, WM_QUERYNEWPALETTE = 0x030F, WM_PALETTEISCHANGING = 0x0310, WM_PALETTECHANGED = 0x0311, WM_HOTKEY = 0x0312, //#if(WINVER >= 0x0400) WM_PRINT = 0x0317, WM_PRINTCLIENT = 0x0318, //#endif /* WINVER >= 0x0400 */ //#if(_WIN32_WINNT >= 0x0500) WM_APPCOMMAND = 0x0319, //#endif /* _WIN32_WINNT >= 0x0500 */ //#if(_WIN32_WINNT >= 0x0501) WM_THEMECHANGED = 0x031A, //#endif /* _WIN32_WINNT >= 0x0501 */ //#if(WINVER >= 0x0400) WM_HANDHELDFIRST = 0x0358, WM_HANDHELDLAST = 0x035F, WM_AFXFIRST = 0x0360, WM_AFXLAST = 0x037F, //#endif /* WINVER >= 0x0400 */ WM_PENWINFIRST = 0x0380, WM_PENWINLAST = 0x038F, //#if(WINVER >= 0x0400) WM_APP = 0x8000, //#endif /* WINVER >= 0x0400 */ /* * NOTE: All Message Numbers below 0x0400 are RESERVED. * * Private Window Messages Start Here: */ WM_USER = 0x0400 } #endregion }
One of my past wonders was:
How can i render Direct 11(or…) on a .net form?
Its actually relatively simple, these are the steps:
When you create the project, create a new file called main.cpp, and an entrance for the program.
To start the .net form:
If you just declare an instance of the .net form inside the cpp file, it WILL go wrong, because it needs to be declared in a managed envoirement.
To do that, you need to do so:
#include <vcclr.h> // Include this one, containing a template class called gcroot.
public class ManagedGlobals
{
public:
gcroot<Editor ^> MainEditor;
};ManagedGlobals MG;
Now you have a wrapper for your form, to start it in the appropriate way:
int main(array<System::String ^> ^args)
{
// Enabling Windows XP visual effects before any controls are created
Application::EnableVisualStyles();
Application::SetCompatibleTextRenderingDefault(false);// Create the main window and run it
MG.MainEditor = gcnew Editor();
//Application::Run(MG.MainEditor);MG.MainEditor->Show();
while (MG.MainEditor->Created)
{
Application::DoEvents();
RenderFrame();
}
return 0;
}
As you see i have a while loop simply saying:
While the editor(in this case) is open, do the following:
Let the application(editor) do all the stuff it has to do right now, check for input blablabla…
Then after that render the frame.
To render in a, lets say a frame, we can do so:
–In the startup of the editor–
HWND pHandle = (HWND)RenderPanel->Handle.ToPointer();
InitD3D(pHandle);
InitGraphics();
Here we’ve got the handle of the panel, and sent it to a function called InitD3D.
InitD3D:
void InitD3D(HWND hWnd)
{….
// In the DXGI_SWAP_CHAIN_DESC
scd.OutputWindow = hWnd;
…..
}
Then for each of the frames, the panel is treated as a window, meaning that the output will appear in that panel.
Good Luck!
Provide feedback
Saved searches
Use saved searches to filter your results more quickly
Sign up
Oct 9th, 2013, 02:29 PM
#1
[Vb.Net] Managed Game Loop
In this thread I will discuss how to create a managed game loop in a proper manner. First I will identify ways that are most commonly used and explain why they should not be used. Then I will explain how to set up the proper managed game loop as well as why it works the way it does.
In game programming, one of the biggest and most basic concepts that should be applied is the game loop. A game loop is simply something that keeps the game in perpetual motion. Think of a normal windows form application for a second, in most scenarios the program remains idle until a user invokes some sort of an event such as clicking a button. In game programming, the game is constantly moving. In other words the game assumes that something is always happening and doesn’t wait for user interaction for the program to continue.
Something that most beginners use as their game loop is the System.Windows.Forms.Timer class. This is OK in the sense that the timer does provide a simple means of raising an event(the Tick event) at user-defined intervals, however these timers are notoriously unreliable at lower intervals. The Interval property of the Timer gets or sets the time measured in milliseconds that invokes the Tick event. Therefore the lower the Interval, the less time between ticks. While theoretically the timer’s Interval can be at one, realistically the timer starts to become inaccurate when it’s Interval is set at about 50. The reason for this lies in the fact that this timer is not a high precision timer. This translates to over a period of time being off x amount of milliseconds(which is sufficient for most applications) after each Tick, there starts to be greater period of gaps. In games, those gaps are what are known as lag.
So if using the System.Windows.Forms.Timer is not a suitable solution, then why not set up a loop that runs as fast as the CPU will allow it to run. Running this type of loop is what is known as running a «Busy Wait» or «Busy Loop». The technique is often used(incorrectly) in normal windows form applications to just kill time until something has happened. One of the reasons why this is a terrible solution is because it kills the performance of the computer and can raise the CPU usage to almost %100! The other reason for this not being a suitable solution is the concept of a locked frame rate. The frame rate is the speed at which the game is refreshed measured in frames-per-second(FPS). Having a locked frame rate allows for a consistent animation of the game. Using a busy loop does not produce a precise and predictable frame rates which result in choppy animation.
The busy loop and the System.Windows.Forms.Timer is out of the question for our game loop. So what do we use? From my experience there are two solutions. One, which is the simpler of the two(but less accurate), is using a high precision timer. In the .NET framework you can find a high precision timer in the System.Diagnostics.Stopwatch class. This class, unlike the System.Windows.Forms.Timer class, does not provide a Tick event or some equivalent; so it is our job to create a method that will keep track of the frame rate and lock in the frames-per-second. This should be done in a separate thread to achieve maximum accuracy. The second solution is to use the QueryPerformance APIs. The QueryPerformance API’s consist of two APIs: the QueryPerformanceCounter function and the QueryPerformanceFrequency function. Using both of these functions will return a high-resolution time stamp that cannot be surpassed.
Here is an example of using the System.Diagnostics.Stopwatch:
Code:
Public Class GameLoop Inherits ComponentModel.Component Private _enabled As Boolean Public Property Enabled As Boolean Get Return _enabled End Get Set(ByVal value As Boolean) If Not _enabled.Equals(value) Then _enabled = value Me.OnEnabledChanged() End If End Set End Property Private _interval As Single Public Property Interval As Single Get Return _interval End Get Set(ByVal value As Single) If Not _interval.Equals(value) Then _interval = value Me.OnIntervalChanged() End If End Set End Property Protected Overridable Sub OnEnabledChanged() RaiseEvent EnabledChanged(Me, EventArgs.Empty) End Sub Protected Overridable Sub OnIntervalChanged() RaiseEvent IntervalChanged(Me, EventArgs.Empty) End Sub Public Event EnabledChanged(ByVal sender As Object, ByVal e As EventArgs) Public Event IntervalChanged(ByVal sender As Object, ByVal e As EventArgs) Public Event Tick(ByVal sender As Object, ByVal e As GameLoopEventArgs) Private Sub GameLoop_EnabledChanged(sender As Object, e As EventArgs) Handles Me.EnabledChanged If _enabled Then Dim game_thread As Threading.Thread = New Threading.Thread(AddressOf ThreadedGameLoop) game_thread.Start() End If End Sub Private Sub GameLoop_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed If _enabled Then Me.Enabled = Not _enabled End If End Sub Private Sub ThreadedGameLoop() Dim s As New Stopwatch Do s.Restart() While _enabled AndAlso s.ElapsedMilliseconds < _interval End While s.Stop() RaiseEvent Tick(Me, New GameLoopEventArgs() With {.ActualFPS = s.ElapsedMilliseconds, .TargetFPS = _interval}) Loop Until Not _enabled End Sub Public Sub Start() If Not _enabled Then Me.Enabled = Not _enabled End If End Sub Public Sub [Stop]() If _enabled Then Me.Enabled = Not _enabled End If End Sub Sub New() _interval = 16.6 End Sub Sub New(ByVal interval As Single) _interval = interval End Sub End Class Public Class GameLoopEventArgs Inherits EventArgs Private _actualFPS As Single Public Property ActualFPS As Single Get Return _actualFPS End Get Set(ByVal value As Single) If Not _actualFPS.Equals(value) Then _actualFPS = value Me.OnActualFPSChanged() End If End Set End Property Private _targetFPS As Single Public Property TargetFPS As Single Get Return _targetFPS End Get Set(ByVal value As Single) If Not _targetFPS.Equals(value) Then _targetFPS = value Me.OnTargetFPSChanged() End If End Set End Property Protected Overridable Sub OnActualFPSChanged() RaiseEvent ActualFPSChanged(Me, EventArgs.Empty) End Sub Protected Overridable Sub OnTargetFPSChanged() RaiseEvent TargetFPSChanged(Me, EventArgs.Empty) End Sub Public Event ActualFPSChanged(ByVal sender As Object, ByVal e As EventArgs) Public Event TargetFPSChanged(ByVal sender As Object, ByVal e As EventArgs) End ClassIn this example, I create a new component which is similar to a System.Timers.Timer class. Whenever the game loop is meant to be running a new thread is created and will not exit the thread until the game loop is set to stop. Inside of the loop I reset the stopwatch(which tracks how much time has elapsed) and do nothing until the time that has elapsed is equal to or greater than the desired interval. Once I reach the desired elapse time I stop the stopwatch and raise an event to signal that the user needs to update/draw/render the game and I also report the Actual vs. Target elapsed milliseconds.
The frame rate will vary depending on what device you are targeting. In this example, you will see that the frames-per-second is locked in at 60FPS(interval = 16.6) if the programmer simply creates a new GameLoop without passing any arguments. This is because the example targets computers that have a screen refresh rate of 60Hz. So how do you determine what the frame rate should be set at?
The simplest way is to check what the maximum refresh rate will be and set the frame rate equal to or less than that refresh rate. In the United States, the refresh rate for most monitors is 60Hz while in Europe the refresh rate for most monitors is 50Hz. If you have a television that has a refresh rate of 120Hz and you want to develop specifically for that TV, then lock your FPS at or below 120. You can get the exact refresh rate by using the ManagementObjectSearcher class.
My last bit of advice is that if you want to target multiple screens then the general rule of thumb is to have the FPS set between 30 and 60.
Last edited by dday9; May 26th, 2016 at 10:41 AM.
Oct 17th, 2016, 09:04 PM
#2
Member
Re: [Vb.Net] Managed Game Loop
But isn’t this essentially just a:
Code:
Public Event Tick() Dim Interval as Long = CLng(stopwatch.Frequency / 60) Dim NextTime as Long = 0 Public Sub Start() NextTime = Stopwatch.GetTimeStamp + Interval While true If Stopwatch.GetTimeStamp >= NextTime Then RaiseEvent Tick NextTime += Interval End If End While End SubJust with some threading and event sugar?
This still uses essentially the same CPU as using the while loop by itself (which is a lot).Edit: Here is my solution and performance difference on my PC.
http://pastebin.com/NnvJQCmK@60FPS or (1000 / 60) Milliseconds
While Loop = ~14.5%
GameLoop = ~14.5%
MicroTimer = ~00.7%Specs:
CPU = Core i7 2600K
GPU = GTX 680
Ram = 8GB DDR3 1333
Last edited by TizzyT; Oct 18th, 2016 at 08:43 AM.
Reason: Added link to my MicroTimer, Added my PC specs