Timer class abstracted

Started by
6 comments, last by ApochPiQ 7 years, 3 months ago

Hi all,

To be able to support multiple platforms in the future, I've abstracted my Game Timer class.

Below is the result (just the headers/ interfaces).

When I had just one Timer class (win32 specific), it was a 'normal' (stack) member of my application class.

Meaning it is constructed automatically etc.

Now it is a std::unique_ptr, to give me control over creation for the requested platform.

I.e. if win32 mTimer -> std::make_unique (or reset) <Win32GameTimer> etc.

This all works fine, except for one point.

The windows message WM_ACTIVATE is called earlier then my Init, which sets up the Game timer, in this case win32.

Meaning the application will crash/ nullptr (mTimer).

I found a quick fix by simply adding a nullptr check and calling Start after the initialization.


		case WM_ACTIVATE:
			if(LOWORD(wParam) == WA_INACTIVE)
			{
				mAppPaused = true;
				mTimer->Stop();
			}
			else
			{
				mAppPaused = false;
				if(mTimer) mTimer->Start();
			}


	/** Setup and start GameTimer **/
	if(pPlatform == USE_WIN32) mTimer = std::make_unique<Crealysm::COMMON::Win32GameTimer>();
	mTimer->Start();

This works, but I'm just curious if there are better/ more elegant ways to solve this.

Any input is appreciated.


class IGameTimer
{
public:
	IGameTimer();
	virtual ~IGameTimer();

	virtual float GetTotalTime()	const = 0;
	virtual float GetDeltaTime()	const = 0;

	virtual void Reset() = 0;		// Call before message loop.
	virtual void Start() = 0;		// Call when unpaused.
	virtual void Stop() = 0;		// Call when paused.
	virtual void Tick() = 0;		// Call every frame.

protected:
	double mSecondsPerCount;
	double mDeltaTime;

	bool mStopped;

	// time tracking vars etc. are in platform specific/ inherited classes
};

class Win32GameTimer : public IGameTimer
{
public:
	Win32GameTimer();
	~Win32GameTimer();

	// Abstracted: Win32 implementation
	float GetTotalTime()	const;
	float GetDeltaTime()	const;

	void Reset();						// Call before message loop.
	void Start();						// Call when unpaused.
	void Stop();						// Call when paused.
	void Tick();						// Call every frame.

private:
	// Win32 specific
	__int64 mBaseTime;
	__int64 mPausedTime;
	__int64 mStopTime;
	__int64 mPrevTime;
	__int64 mCurrTime;
};

Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me

Advertisement

Have never used the Windows API so I can't give a helpful answer to your actual question, but is there a particular reason why you can't just use std::chrono to keep track of time? Seems like it would save you the work of making different Timer implementations for different platforms.

This all works fine, except for one point. The windows message WM_ACTIVATE is called earlier then my Init, which sets up the Game timer, in this case win32. Meaning the application will crash/ nullptr (mTimer). I found a quick fix by simply adding a nullptr check and calling Start after the initialization. case

case WM_ACTIVATE:
            if(LOWORD(wParam) == WA_INACTIVE)
            {
                mAppPaused = true;
                mTimer->Stop();
            }
            else
            {
                mAppPaused = false;
                if(mTimer) mTimer->Start();
            }

This looks very wrong. Why couldn't it go to the WA_INACTIVE branch here? I know you'll have a hard time reproducing that crash, but if I am quick enough, it could happen, right?

In my opinion, you shouldn't even try to dispatch event calls if the object isn't known to exist. Why not add a "if (mTimer == nullptr) return;" somewhere above your switch?

PS You do realize that "WM_ACTIVATE" is not portable either? The entire event dispatch should be split for multi-platform code.

As for your multi-platform timer, as a program will be compiled for a specific platform, you always have only one type of timers in the code. The virtual baseclass thus serves no purpose (you can't have a Windows, and a Linux timer object in the same executable).

Instead, make a plain class definition, and use #if / #else / #endif constructs to select the code to use for implementation, based on the platform you're compiling for, ie


void GameTimer::Reset() {
#if defined(WIN32)
  // win32 code
#elif defined(__APPLE__)
  // Apple code
#else
  // linux code
#endif
}

Thanks.
@Ansou: I didn't know about std::chrono so far, will look into it. Did some quick googling, and I'm not sure yet if it offers the same precision/ speed (as platform specific, i.e. QueryperformanceCounter).

@AAlbert: I know that Windows messages are platform specific :) I'm just taking the 1st steps to support other platforms, I don't know yet how that'll work for example, for UNIX. For now I've adopted your suggestion and check for nullptr above the switch (not sure if that makes much of a difference though, because the other windows messages could be needed for other purposes).

Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me


Thanks.
@Ansou: I didn't know about std::chrono so far, will look into it. Did some quick googling, and I'm not sure yet if it offers the same precision/ speed (as platform specific, i.e. QueryperformanceCounter).

It is implemented using QueryPerformanceCounter, this is what I use, I could give a code snippet if you like,


static float now_seconds() {
  return std::chrono::duration_cast<std::chrono::microseconds>(clock::now() - application_start_time).count() * 1e-6f;
}?

EDIT:

?Forgot to add the important part in the snippet above:


typedef std::chrono::high_resolution_clock clock;
typedef clock::time_point time_point;
(not sure if that makes much of a difference though, because the other windows messages could be needed for other purposes)

You only showed the case with timers, so that's what my suggestion was about. If you have other cases to handle independent of timer existence, then the solution is to take out timer-related code only, but do it everywhere.

It might get ugly though. An example I just realized exists: In your original code, if the timer did not exist when WM_ACTIVATE arrives, you skip starting the timer, but you do reset "mAppPaused". Can these get out of sync? (mAppPaused == true but timer is running, or mAppPaused == false and timer is stopped).

To avoid problems like this, have one and only one place for anything. If you move the "mAppPaused" handling into the timer, the sync-problem gets localized to the Timer class, which is easier to control and get/keep consistent.

Chrono it is:


class CGameTimer
{
public:
	CGameTimer();
	~CGameTimer();

	uint64_t GetTotalTime()		const;		// in seconds
	float GetDeltaTime()		const;		// in ms

	void Reset();							// Call before message loop.
	void Continue();						// Call when unpaused.
	void Pause();							// Call when paused.
	void Tick();							// Call every frame.

private:
	double mDeltaTime;

	std::chrono::high_resolution_clock::time_point	mStartTime;
	std::chrono::high_resolution_clock::time_point	mStopTime;
	std::chrono::high_resolution_clock::time_point	mPrevTime;

	uint64_t										mTotalTime;

	bool mStopped;
};

void CGameTimer::Reset()
{
	mStartTime	= std::chrono::high_resolution_clock::now();
	mPrevTime	= mStartTime;
	mStopped	= false;
	mTotalTime	= 0;

	CLOG(INFO, "COMMON") << info_timer_reset.c_str();
}

void CGameTimer::Continue()
{
	mStartTime = std::chrono::high_resolution_clock::now();

	if(mStopped)
	{
		mPrevTime	= mStartTime;
		mStopped	= false;
		mTotalTime	= 0;
	}
	CLOG(INFO, "COMMON") << info_timer_start.c_str();
}

void CGameTimer::Pause()
{
	if(!mStopped)
	{
		std::chrono::duration<double> addTime = std::chrono::high_resolution_clock::now() - mStartTime;	
		mTotalTime += static_cast<uint64_t>(addTime.count());

		mStopped  = true;
		CLOG(INFO, "COMMON") << info_timer_stop.c_str();
	}
}

void CGameTimer::Tick()
{
	if(mStopped)
	{
		mDeltaTime = 0.0;
		return;
	}

	std::chrono::high_resolution_clock::time_point currTime = std::chrono::high_resolution_clock::now();

	std::chrono::duration<double, std::milli> delta  = currTime - mPrevTime;
	mDeltaTime = delta.count();
	
	mPrevTime = currTime;

	// force nonnegative: if procesor goes into power save or sth., delta could be negative
	if(mDeltaTime < 0.0)
	{
		mDeltaTime = 0.0;
	}
}

uint64_t CGameTimer::GetTotalTime() const
{
	if(mStopped)
	{
		return mTotalTime;
	}
	else
	{
		std::chrono::duration<double> addTime = std::chrono::high_resolution_clock::now() - mStartTime;	
		return static_cast<uint64_t>(mTotalTime + addTime.count());
	}
}

float CGameTimer::GetDeltaTime() const
{
	return static_cast<float>(mDeltaTime);
}

Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me

You should initialize application-wide data in WinMain, and/or initialize window-specific data while creating the window itself via CreateWindowEx and friends.

You have full control over the lifetime of your timer object. It's kind of silly to not take advantage of that. In other words, make the lifetime equal to or greater than the lifetime of everything that depends on the timer.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

This topic is closed to new replies.

Advertisement