Jump to content
  • Advertisement
Sign in to follow this  
cozzie

Timer class abstracted

This topic is 543 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

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;
};

Share this post


Link to post
Share on other sites
Advertisement

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
}
Edited by Alberth

Share this post


Link to post
Share on other sites

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).

Share this post


Link to post
Share on other sites
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;
Edited by imoogiBG

Share this post


Link to post
Share on other sites
(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.

Edited by Alberth

Share this post


Link to post
Share on other sites

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);
}

Edited by cozzie

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!