浏览代码

Merge pull request #2153 from IvanSavenko/better_time_management

Separate updates from rendering actions
Ivan Savenko 2 年之前
父节点
当前提交
ddf22a757d

+ 0 - 2
client/CMT.cpp

@@ -599,8 +599,6 @@ static void mainLoop()
 	fsChanged([](const JsonNode &newState){  CGuiHandler::pushUserEvent(EUserEvent::FULLSCREEN_TOGGLED); });
 
 	inGuiThread.reset(new bool(true));
-	assert(GH.mainFPSmng);
-	GH.mainFPSmng->init(settings["video"]["targetfps"].Integer());
 
 	while(1) //main SDL events loop
 	{

+ 2 - 0
client/CMakeLists.txt

@@ -31,6 +31,7 @@ set(client_SRCS
 	gui/CIntObject.cpp
 	gui/CursorHandler.cpp
 	gui/InterfaceObjectConfigurable.cpp
+	gui/FramerateManager.cpp
 	gui/NotificationHandler.cpp
 	gui/ShortcutHandler.cpp
 
@@ -162,6 +163,7 @@ set(client_HEADERS
 	gui/CIntObject.h
 	gui/CursorHandler.h
 	gui/InterfaceObjectConfigurable.h
+	gui/FramerateManager.h
 	gui/MouseButton.h
 	gui/NotificationHandler.h
 	gui/Shortcut.h

+ 12 - 5
client/CVideoHandler.cpp

@@ -12,6 +12,7 @@
 
 #include "CMT.h"
 #include "gui/CGuiHandler.h"
+#include "gui/FramerateManager.h"
 #include "renderSDL/SDL_Extensions.h"
 #include "CPlayerInterface.h"
 #include "../lib/filesystem/Filesystem.h"
@@ -370,7 +371,7 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo
 	auto packet_duration = frame->duration;
 #endif
 	double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base);
-	frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.0;
+	frameTime += GH.framerateManager().getElapsedMilliseconds() / 1000.0;
 
 	if (frameTime >= frameEndTime )
 	{
@@ -450,6 +451,7 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 
 	pos.x = x;
 	pos.y = y;
+	frameTime = 0.0;
 
 	while(nextFrame())
 	{
@@ -461,10 +463,15 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 		SDL_RenderCopy(mainRenderer, texture, nullptr, &rect);
 		SDL_RenderPresent(mainRenderer);
 
-		// Wait 3 frames
-		GH.mainFPSmng->framerateDelay();
-		GH.mainFPSmng->framerateDelay();
-		GH.mainFPSmng->framerateDelay();
+#if (LIBAVUTIL_VERSION_MAJOR < 58)
+		auto packet_duration = frame->pkt_duration;
+#else
+		auto packet_duration = frame->duration;
+#endif
+		double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base);
+		uint32_t timeToSleepMillisec = 1000 * (frameDurationSec);
+
+		boost::this_thread::sleep(boost::posix_time::millisec(timeToSleepMillisec));
 	}
 
 	return true;

+ 7 - 5
client/adventureMap/AdventureMapInterface.cpp

@@ -59,7 +59,7 @@ AdventureMapInterface::AdventureMapInterface():
 	shortcuts->setState(EAdventureState::MAKING_TURN);
 	widget->getMapView()->onViewMapActivated();
 
-	addUsedEvents(KEYBOARD);
+	addUsedEvents(KEYBOARD | TIME);
 }
 
 void AdventureMapInterface::onMapViewMoved(const Rect & visibleArea, int mapLevel)
@@ -139,18 +139,20 @@ void AdventureMapInterface::showAll(SDL_Surface * to)
 
 void AdventureMapInterface::show(SDL_Surface * to)
 {
-	handleMapScrollingUpdate();
-
 	CIntObject::show(to);
 	LOCPLINT->cingconsole->show(to);
 }
 
-void AdventureMapInterface::handleMapScrollingUpdate()
+void AdventureMapInterface::tick(uint32_t msPassed)
+{
+	handleMapScrollingUpdate(msPassed);
+}
+
+void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed)
 {
 	/// Width of window border, in pixels, that triggers map scrolling
 	static constexpr uint32_t borderScrollWidth = 15;
 
-	uint32_t timePassed = GH.mainFPSmng->getElapsedMilliseconds();
 	uint32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float();
 	uint32_t scrollDistance = scrollSpeedPixels * timePassed / 1000;
 

+ 2 - 1
client/adventureMap/AdventureMapInterface.h

@@ -75,7 +75,7 @@ private:
 	const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const;
 
 	/// check and if necessary reacts on scrolling by moving cursor to screen edge
-	void handleMapScrollingUpdate();
+	void handleMapScrollingUpdate(uint32_t msPassed);
 
 	void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode);
 
@@ -93,6 +93,7 @@ protected:
 	void activate() override;
 	void deactivate() override;
 
+	void tick(uint32_t msPassed) override;
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
 

+ 17 - 17
client/battle/BattleAnimationClasses.cpp

@@ -222,7 +222,7 @@ bool DummyAnimation::init()
 	return true;
 }
 
-void DummyAnimation::nextFrame()
+void DummyAnimation::tick(uint32_t msPassed)
 {
 	counter++;
 	if(counter > howMany)
@@ -300,7 +300,7 @@ ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack)
 	return mutPosToGroup[mutPos];
 }
 
-void MeleeAttackAnimation::nextFrame()
+void MeleeAttackAnimation::tick(uint32_t msPassed)
 {
 	size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame();
 	size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
@@ -308,7 +308,7 @@ void MeleeAttackAnimation::nextFrame()
 	if ( currentFrame * 2 >= totalFrames )
 		owner.executeAnimationStage(EAnimationEvents::HIT);
 
-	AttackAnimation::nextFrame();
+	AttackAnimation::tick(msPassed);
 }
 
 MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack)
@@ -379,15 +379,15 @@ bool MovementAnimation::init()
 	return true;
 }
 
-void MovementAnimation::nextFrame()
+void MovementAnimation::tick(uint32_t msPassed)
 {
-	progress += float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000 * progressPerSecond;
+	progress += float(msPassed) / 1000 * progressPerSecond;
 
 	//moving instructions
 	myAnim->pos.x = static_cast<Sint16>(begX + distanceX * progress );
 	myAnim->pos.y = static_cast<Sint16>(begY + distanceY * progress );
 
-	BattleAnimation::nextFrame();
+	BattleAnimation::tick(msPassed);
 
 	if(progress >= 1.0)
 	{
@@ -577,9 +577,9 @@ bool ColorTransformAnimation::init()
 	return true;
 }
 
-void ColorTransformAnimation::nextFrame()
+void ColorTransformAnimation::tick(uint32_t msPassed)
 {
-	float elapsed  = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	float elapsed  = msPassed / 1000.f;
 	float fullTime = AnimationControls::getFadeInDuration();
 	float delta    = elapsed / fullTime;
 	totalProgress += delta;
@@ -699,7 +699,7 @@ void RangedAttackAnimation::emitProjectile()
 	projectileEmitted = true;
 }
 
-void RangedAttackAnimation::nextFrame()
+void RangedAttackAnimation::tick(uint32_t msPassed)
 {
 	// animation should be paused if there is an active projectile
 	if (projectileEmitted)
@@ -716,7 +716,7 @@ void RangedAttackAnimation::nextFrame()
 	else
 		stackAnimation(attackingStack)->playUntil(static_cast<size_t>(-1));
 
-	AttackAnimation::nextFrame();
+	AttackAnimation::tick(msPassed);
 
 	if (!projectileEmitted)
 	{
@@ -790,9 +790,9 @@ CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * att
 	logAnim->debug("Created shooting anim for %s", stack->getName());
 }
 
-void CatapultAnimation::nextFrame()
+void CatapultAnimation::tick(uint32_t msPassed)
 {
-	ShootingAnimation::nextFrame();
+	ShootingAnimation::tick(msPassed);
 
 	if ( explosionEmitted)
 		return;
@@ -988,9 +988,9 @@ bool EffectAnimation::init()
 	return true;
 }
 
-void EffectAnimation::nextFrame()
+void EffectAnimation::tick(uint32_t msPassed)
 {
-	playEffect();
+	playEffect(msPassed);
 
 	if (effectFinished)
 	{
@@ -1020,7 +1020,7 @@ void EffectAnimation::onEffectFinished()
 	effectFinished = true;
 }
 
-void EffectAnimation::playEffect()
+void EffectAnimation::playEffect(uint32_t msPassed)
 {
 	if ( effectFinished )
 		return;
@@ -1029,7 +1029,7 @@ void EffectAnimation::playEffect()
 	{
 		if(elem.effectID == ID)
 		{
-			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
+			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
 
 			if(elem.currentFrame >= elem.animation->size())
 			{
@@ -1113,7 +1113,7 @@ void HeroCastAnimation::emitAnimationEvent()
 	owner.executeAnimationStage(EAnimationEvents::HIT);
 }
 
-void HeroCastAnimation::nextFrame()
+void HeroCastAnimation::tick(uint32_t msPassed)
 {
 	float frame = hero->getFrame();
 

+ 10 - 10
client/battle/BattleAnimationClasses.h

@@ -48,7 +48,7 @@ public:
 
 	bool isInitialized();
 	bool tryInitialize();
-	virtual void nextFrame() {} //call every new frame
+	virtual void tick(uint32_t msPassed) {} //call every new frame
 	virtual ~BattleAnimation();
 
 	BattleAnimation(BattleInterface & owner);
@@ -120,7 +120,7 @@ class ColorTransformAnimation : public BattleStackAnimation
 	float totalProgress;
 
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 
 public:
 	ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell);
@@ -157,7 +157,7 @@ private:
 
 public:
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 
 	MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
 	~MovementAnimation();
@@ -220,7 +220,7 @@ class MeleeAttackAnimation : public AttackAnimation
 public:
 	MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack);
 
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 
@@ -246,7 +246,7 @@ public:
 	~RangedAttackAnimation();
 
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 /// Shooting attack
@@ -275,7 +275,7 @@ public:
 	CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0);
 
 	void createProjectile(const Point & from, const Point & dest) const override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 class CastAnimation : public RangedAttackAnimation
@@ -300,7 +300,7 @@ private:
 	int howMany;
 public:
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 
 	DummyAnimation(BattleInterface & owner, int howManyFrames);
 };
@@ -324,7 +324,7 @@ class EffectAnimation : public BattleAnimation
 
 	void onEffectFinished();
 	void clearEffect();
-	void playEffect();
+	void playEffect(uint32_t msPassed);
 
 public:
 	enum EEffectFlags
@@ -349,7 +349,7 @@ public:
 	 ~EffectAnimation();
 
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 class HeroCastAnimation : public BattleAnimation
@@ -367,6 +367,6 @@ class HeroCastAnimation : public BattleAnimation
 public:
 	HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell);
 
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 	bool init() override;
 };

+ 9 - 5
client/battle/BattleFieldController.cpp

@@ -68,7 +68,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height()));
 
 	updateAccessibleHexes();
-	addUsedEvents(LCLICK | RCLICK | MOVE);
+	addUsedEvents(LCLICK | RCLICK | MOVE | TIME);
 }
 
 void BattleFieldController::activate()
@@ -134,7 +134,7 @@ void BattleFieldController::renderBattlefield(Canvas & canvas)
 
 	renderer.execute(clippedCanvas);
 
-	owner.projectilesController->showProjectiles(clippedCanvas);
+	owner.projectilesController->render(clippedCanvas);
 }
 
 void BattleFieldController::showBackground(Canvas & canvas)
@@ -611,12 +611,16 @@ void BattleFieldController::showAll(SDL_Surface * to)
 	show(to);
 }
 
-void BattleFieldController::show(SDL_Surface * to)
+void BattleFieldController::tick(uint32_t msPassed)
 {
 	updateAccessibleHexes();
-	owner.stacksController->update();
-	owner.obstacleController->update();
+	owner.stacksController->tick(msPassed);
+	owner.obstacleController->tick(msPassed);
+	owner.projectilesController->tick(msPassed);
+}
 
+void BattleFieldController::show(SDL_Surface * to)
+{
 	Canvas canvas(to);
 	CSDL_Ext::CClipRectGuard guard(to, pos);
 

+ 1 - 0
client/battle/BattleFieldController.h

@@ -70,6 +70,7 @@ class BattleFieldController : public CIntObject
 
 	void showAll(SDL_Surface * to) override;
 	void show(SDL_Surface * to) override;
+	void tick(uint32_t msPassed) override;
 public:
 	BattleFieldController(BattleInterface & owner);
 

+ 21 - 14
client/battle/BattleInterfaceClasses.cpp

@@ -202,6 +202,25 @@ const CGHeroInstance * BattleHero::instance()
 	return hero;
 }
 
+void BattleHero::tick(uint32_t msPassed)
+{
+	size_t groupIndex = static_cast<size_t>(phase);
+
+	float timePassed = msPassed / 1000.f;
+
+	flagCurrentFrame += currentSpeed * timePassed;
+	currentFrame += currentSpeed * timePassed;
+
+	if(flagCurrentFrame >= flagAnimation->size(0))
+		flagCurrentFrame -= flagAnimation->size(0);
+
+	if(currentFrame >= animation->size(groupIndex))
+	{
+		currentFrame -= animation->size(groupIndex);
+		switchToNextPhase();
+	}
+}
+
 void BattleHero::render(Canvas & canvas)
 {
 	size_t groupIndex = static_cast<size_t>(phase);
@@ -219,20 +238,6 @@ void BattleHero::render(Canvas & canvas)
 
 	canvas.draw(flagFrame, flagPosition);
 	canvas.draw(heroFrame, heroPosition);
-
-	float timePassed = float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000.f;
-
-	flagCurrentFrame += currentSpeed * timePassed;
-	currentFrame += currentSpeed * timePassed;
-
-	if(flagCurrentFrame >= flagAnimation->size(0))
-		flagCurrentFrame -= flagAnimation->size(0);
-
-	if(currentFrame >= animation->size(groupIndex))
-	{
-		currentFrame -= animation->size(groupIndex);
-		switchToNextPhase();
-	}
 }
 
 void BattleHero::pause()
@@ -354,6 +359,8 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her
 
 	switchToNextPhase();
 	play();
+
+	addUsedEvents(TIME);
 }
 
 HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)

+ 1 - 0
client/battle/BattleInterfaceClasses.h

@@ -114,6 +114,7 @@ public:
 	void setPhase(EHeroAnimType newPhase); //sets phase of hero animation
 
 	void collectRenderableObjects(BattleRenderer & renderer);
+	void tick(uint32_t msPassed) override;
 
 	float getFrame() const;
 	void onPhaseFinished(const std::function<void()> &);

+ 2 - 2
client/battle/BattleObstacleController.cpp

@@ -159,9 +159,9 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere
 	}
 }
 
-void BattleObstacleController::update()
+void BattleObstacleController::tick(uint32_t msPassed)
 {
-	timePassed += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	timePassed += msPassed / 1000.f;
 }
 
 std::shared_ptr<IImage> BattleObstacleController::getObstacleImage(const CObstacleInstance & oi)

+ 1 - 1
client/battle/BattleObstacleController.h

@@ -50,7 +50,7 @@ public:
 	BattleObstacleController(BattleInterface & owner);
 
 	/// called every frame
-	void update();
+	void tick(uint32_t msPassed);
 
 	/// call-in from network pack, add newly placed obstacles with any required animations
 	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & oi);

+ 28 - 10
client/battle/BattleProjectileController.cpp

@@ -58,15 +58,18 @@ void ProjectileMissile::show(Canvas & canvas)
 
 		canvas.draw(image, pos);
 	}
+}
 
-	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+void ProjectileMissile::tick(uint32_t msPassed)
+{
+	float timePassed = msPassed / 1000.f;
 	progress += timePassed * speed;
 }
 
-void ProjectileAnimatedMissile::show(Canvas & canvas)
+void ProjectileAnimatedMissile::tick(uint32_t msPassed)
 {
-	ProjectileMissile::show(canvas);
-	frameProgress += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
+	ProjectileMissile::tick(msPassed);
+	frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
 	size_t animationSize = animation->size(reverse ? 1 : 0);
 	while (frameProgress > animationSize)
 		frameProgress -= animationSize;
@@ -74,9 +77,15 @@ void ProjectileAnimatedMissile::show(Canvas & canvas)
 	frameNum = std::floor(frameProgress);
 }
 
+void ProjectileCatapult::tick(uint32_t msPassed)
+{
+	frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
+	float timePassed = msPassed / 1000.f;
+	progress += timePassed * speed;
+}
+
 void ProjectileCatapult::show(Canvas & canvas)
 {
-	frameProgress += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
 	int frameCounter = std::floor(frameProgress);
 	int frameIndex = (frameCounter + 1) % animation->size(0);
 
@@ -90,9 +99,6 @@ void ProjectileCatapult::show(Canvas & canvas)
 
 		canvas.draw(image, pos);
 	}
-
-	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
-	progress += timePassed * speed;
 }
 
 void ProjectileRay::show(Canvas & canvas)
@@ -135,8 +141,11 @@ void ProjectileRay::show(Canvas & canvas)
 			canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end);
 		}
 	}
+}
 
-	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+void ProjectileRay::tick(uint32_t msPassed)
+{
+	float timePassed = msPassed / 1000.f;
 	progress += timePassed * speed;
 }
 
@@ -217,13 +226,22 @@ void BattleProjectileController::emitStackProjectile(const CStack * stack)
 	}
 }
 
-void BattleProjectileController::showProjectiles(Canvas & canvas)
+void BattleProjectileController::render(Canvas & canvas)
 {
 	for ( auto projectile: projectiles)
 	{
 		if ( projectile->playing )
 			projectile->show(canvas);
 	}
+}
+
+void BattleProjectileController::tick(uint32_t msPassed)
+{
+	for ( auto projectile: projectiles)
+	{
+		if ( projectile->playing )
+			projectile->tick(msPassed);
+	}
 
 	vstd::erase_if(projectiles, [&](const std::shared_ptr<ProjectileBase> & projectile){
 		return projectile->progress > 1.0f;

+ 9 - 2
client/battle/BattleProjectileController.h

@@ -28,6 +28,7 @@ struct ProjectileBase
 {
 	virtual ~ProjectileBase() = default;
 	virtual void show(Canvas & canvas) =  0;
+	virtual void tick(uint32_t msPassed) = 0;
 
 	Point from; // initial position on the screen
 	Point dest; // target position on the screen
@@ -42,6 +43,7 @@ struct ProjectileBase
 struct ProjectileMissile : ProjectileBase
 {
 	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 
 	std::shared_ptr<CAnimation> animation;
 	int frameNum;  // frame to display from projectile animation
@@ -51,7 +53,7 @@ struct ProjectileMissile : ProjectileBase
 /// Projectile for spell - render animation moving in straight line from origin to destination
 struct ProjectileAnimatedMissile : ProjectileMissile
 {
-	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 	float frameProgress;
 };
 
@@ -59,6 +61,7 @@ struct ProjectileAnimatedMissile : ProjectileMissile
 struct ProjectileCatapult : ProjectileBase
 {
 	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 
 	std::shared_ptr<CAnimation> animation;
 	float frameProgress;
@@ -68,6 +71,7 @@ struct ProjectileCatapult : ProjectileBase
 struct ProjectileRay : ProjectileBase
 {
 	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 
 	std::vector<CCreature::CreatureAnimation::RayColor> rayConfig;
 };
@@ -102,7 +106,10 @@ public:
 	BattleProjectileController(BattleInterface & owner);
 
 	/// renders all currently active projectiles
-	void showProjectiles(Canvas & canvas);
+	void render(Canvas & canvas);
+
+	/// updates positioning / animations of all projectiles
+	void tick(uint32_t msPassed);
 
 	/// returns true if stack has projectile that is yet to hit target
 	bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const;

+ 15 - 7
client/battle/BattleStacksController.cpp

@@ -335,13 +335,12 @@ void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
 	}
 
 	stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
-	stackAnimation[stack->unitId()]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000);
 }
 
-void BattleStacksController::update()
+void BattleStacksController::tick(uint32_t msPassed)
 {
 	updateHoveredStacks();
-	updateBattleAnimations();
+	updateBattleAnimations(msPassed);
 }
 
 void BattleStacksController::initializeBattleAnimations()
@@ -352,21 +351,30 @@ void BattleStacksController::initializeBattleAnimations()
 			elem->tryInitialize();
 }
 
-void BattleStacksController::stepFrameBattleAnimations()
+void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed)
 {
+	for (auto stack : owner.curInt->cb->battleGetAllStacks(false))
+	{
+		if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
+			continue;
+
+		stackAnimation[stack->unitId()]->incrementFrame(msPassed / 1000.f);
+	}
+
 	// operate on copy - to prevent potential iterator invalidation due to push_back's
 	// FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing)
+
 	auto copiedVector = currentAnimations;
 	for (auto & elem : copiedVector)
 		if (elem && elem->isInitialized())
-			elem->nextFrame();
+			elem->tick(msPassed);
 }
 
-void BattleStacksController::updateBattleAnimations()
+void BattleStacksController::updateBattleAnimations(uint32_t msPassed)
 {
 	bool hadAnimations = !currentAnimations.empty();
 	initializeBattleAnimations();
-	stepFrameBattleAnimations();
+	tickFrameBattleAnimations(msPassed);
 	vstd::erase(currentAnimations, nullptr);
 
 	if (hadAnimations && currentAnimations.empty())

+ 3 - 3
client/battle/BattleStacksController.h

@@ -91,9 +91,9 @@ class BattleStacksController
 	void removeExpiredColorFilters();
 
 	void initializeBattleAnimations();
-	void stepFrameBattleAnimations();
+	void tickFrameBattleAnimations(uint32_t msPassed);
 
-	void updateBattleAnimations();
+	void updateBattleAnimations(uint32_t msPassed);
 	void updateHoveredStacks();
 
 	std::vector<const CStack *> selectHoveredStacks();
@@ -138,7 +138,7 @@ public:
 	const CStack* getActiveStack() const;
 	const std::vector<uint32_t> getHoveredStacksUnitIds() const;
 
-	void update();
+	void tick(uint32_t msPassed);
 
 	/// returns position of animation needed to place stack in specific hex
 	Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const;

+ 12 - 59
client/gui/CGuiHandler.cpp

@@ -14,6 +14,7 @@
 #include "CIntObject.h"
 #include "CursorHandler.h"
 #include "ShortcutHandler.h"
+#include "FramerateManager.h"
 
 #include "../CGameInfo.h"
 #include "../render/Colors.h"
@@ -100,7 +101,7 @@ void CGuiHandler::init()
 {
 	screenHandlerInstance = std::make_unique<ScreenHandler>();
 	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
-	mainFPSmng = new CFramerateManager(settings["video"]["targetfps"].Integer());
+	framerateManagerInstance = std::make_unique<FramerateManager>(settings["video"]["targetfps"].Integer());
 
 	isPointerRelativeMode = settings["general"]["userRelativePointer"].Bool();
 	pointerSpeedMultiplier = settings["general"]["relativePointerSpeedMultiplier"].Float();
@@ -193,7 +194,7 @@ void CGuiHandler::totalRedraw()
 
 void CGuiHandler::updateTime()
 {
-	int ms = mainFPSmng->getElapsedMilliseconds();
+	int ms = framerateManager().getElapsedMilliseconds();
 	std::list<CIntObject*> hlp = timeinterested;
 	for (auto & elem : hlp)
 	{
@@ -692,10 +693,9 @@ void CGuiHandler::renderFrame()
 		disposed.clear();
 	}
 
-	mainFPSmng->framerateDelay(); // holds a constant FPS
+	framerateManager().framerateDelay(); // holds a constant FPS
 }
 
-
 CGuiHandler::CGuiHandler()
 	: lastClick(-500, -500)
 	, lastClickTime(0)
@@ -705,7 +705,6 @@ CGuiHandler::CGuiHandler()
 	, mouseButtonsMask(0)
 	, continueEventHandling(true)
 	, curInt(nullptr)
-	, mainFPSmng(nullptr)
 	, statusbar(nullptr)
 {
 	terminate_cond = new CondSh<bool>(false);
@@ -713,15 +712,21 @@ CGuiHandler::CGuiHandler()
 
 CGuiHandler::~CGuiHandler()
 {
-	delete mainFPSmng;
 	delete terminate_cond;
 }
 
 ShortcutHandler & CGuiHandler::shortcutsHandler()
 {
+	assert(shortcutsHandlerInstance);
 	return *shortcutsHandlerInstance;
 }
 
+FramerateManager & CGuiHandler::framerateManager()
+{
+	assert(framerateManagerInstance);
+	return *framerateManagerInstance;
+}
+
 void CGuiHandler::moveCursorToPosition(const Point & position)
 {
 	SDL_WarpMouseInWindow(mainWindow, position.x, position.y);
@@ -783,7 +788,7 @@ void CGuiHandler::drawFPSCounter()
 	static SDL_Rect overlay = { 0, 0, 64, 32};
 	uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10);
 	SDL_FillRect(screen, &overlay, black);
-	std::string fps = std::to_string(mainFPSmng->getFramerate());
+	std::string fps = std::to_string(framerateManager().getFramerate());
 	graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(10, 10));
 }
 
@@ -823,55 +828,3 @@ void CGuiHandler::onScreenResize()
 
 	totalRedraw();
 }
-
-
-CFramerateManager::CFramerateManager(int newRate)
-	: rate(0)
-	, rateticks(0)
-	, fps(0)
-	, accumulatedFrames(0)
-	, accumulatedTime(0)
-	, lastticks(0)
-	, timeElapsed(0)
-{
-	init(newRate);
-}
-
-void CFramerateManager::init(int newRate)
-{
-	rate = newRate;
-	rateticks = 1000.0 / rate;
-	this->lastticks = SDL_GetTicks();
-}
-
-void CFramerateManager::framerateDelay()
-{
-	ui32 currentTicks = SDL_GetTicks();
-
-	timeElapsed = currentTicks - lastticks;
-	accumulatedFrames++;
-
-	// FPS is higher than it should be, then wait some time
-	if(timeElapsed < rateticks)
-	{
-		int timeToSleep = (uint32_t)ceil(this->rateticks) - timeElapsed;
-		boost::this_thread::sleep(boost::posix_time::milliseconds(timeToSleep));
-	}
-
-	currentTicks = SDL_GetTicks();
-	// recalculate timeElapsed for external calls via getElapsed()
-	// limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint)
-	timeElapsed = std::min<ui32>(currentTicks - lastticks, 100);
-
-	lastticks = SDL_GetTicks();
-
-	accumulatedTime += timeElapsed;
-
-	if(accumulatedFrames >= 100)
-	{
-		//about 2 second should be passed
-		fps = static_cast<int>(ceil(1000.0 / (accumulatedTime / accumulatedFrames)));
-		accumulatedTime = 0;
-		accumulatedFrames = 0;
-	}
-}

+ 7 - 23
client/gui/CGuiHandler.h

@@ -23,7 +23,7 @@ union SDL_Event;
 struct SDL_MouseMotionEvent;
 
 class ShortcutHandler;
-class CFramerateManager;
+class FramerateManager;
 class IStatusBar;
 class CIntObject;
 class IUpdateable;
@@ -44,32 +44,11 @@ enum class EUserEvent
 	FORCE_QUIT, //quit client without question
 };
 
-// A fps manager which holds game updates at a constant rate
-class CFramerateManager
-{
-private:
-	double rateticks;
-	ui32 lastticks;
-	ui32 timeElapsed;
-	int rate;
-	int fps; // the actual fps value
-	ui32 accumulatedTime;
-	ui32 accumulatedFrames;
-
-public:
-	CFramerateManager(int newRate); // initializes the manager with a given fps rate
-	void init(int newRate); // needs to be called directly before the main game loop to reset the internal timer
-	void framerateDelay(); // needs to be called every game update cycle
-	ui32 getElapsedMilliseconds() const {return this->timeElapsed;}
-	ui32 getFrameNumber() const { return accumulatedFrames; }
-	ui32 getFramerate() const { return fps; };
-};
-
 // Handles GUI logic and drawing
 class CGuiHandler
 {
 public:
-	CFramerateManager * mainFPSmng; //to keep const framerate
+
 	std::list<std::shared_ptr<IShowActivatable>> listInt; //list of interfaces - front=foreground; back = background (includes adventure map, window interfaces, all kind of active dialogs, and so on)
 	std::shared_ptr<IStatusBar> statusbar;
 
@@ -97,6 +76,7 @@ private:
 	CIntObjectList textInterested;
 
 	std::unique_ptr<IScreenHandler> screenHandlerInstance;
+	std::unique_ptr<FramerateManager> framerateManagerInstance;
 
 	void handleMouseButtonClick(CIntObjectList & interestedObjs, MouseButton btn, bool isPressed);
 	void processLists(const ui16 activityFlag, std::function<void (std::list<CIntObject*> *)> cb);
@@ -113,11 +93,15 @@ public:
 public:
 	//objs to blit
 	std::vector<std::shared_ptr<IShowActivatable>> objsToBlit;
+
 	/// returns current position of mouse cursor, relative to vcmi window
 	const Point & getCursorPosition() const;
 
 	ShortcutHandler & shortcutsHandler();
+	FramerateManager & framerateManager();
 
+	/// Returns current logical screen dimensions
+	/// May not match size of window if user has UI scaling different from 100%
 	Point screenDimensions() const;
 
 	/// returns true if at least one mouse button is pressed

+ 2 - 1
client/gui/CursorHandler.cpp

@@ -12,6 +12,7 @@
 #include "CursorHandler.h"
 
 #include "CGuiHandler.h"
+#include "FramerateManager.h"
 #include "../renderSDL/CursorSoftware.h"
 #include "../renderSDL/CursorHardware.h"
 #include "../render/CAnimation.h"
@@ -250,7 +251,7 @@ void CursorHandler::updateSpellcastCursor()
 {
 	static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
 
-	frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	frameTime += GH.framerateManager().getElapsedMilliseconds() / 1000.f;
 	size_t newFrame = frame;
 
 	while (frameTime >= frameDisplayDuration)

+ 57 - 0
client/gui/FramerateManager.cpp

@@ -0,0 +1,57 @@
+/*
+ * FramerateManager.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "FramerateManager.h"
+
+FramerateManager::FramerateManager(int targetFrameRate)
+	: targetFrameTime(Duration(boost::chrono::seconds(1)) / targetFrameRate)
+	, lastFrameIndex(0)
+	, lastFrameTimes({})
+	, lastTimePoint (Clock::now())
+{
+	boost::range::fill(lastFrameTimes, targetFrameTime);
+}
+
+void FramerateManager::framerateDelay()
+{
+	Duration timeSpentBusy = Clock::now() - lastTimePoint;
+
+	// FPS is higher than it should be, then wait some time
+	if(timeSpentBusy < targetFrameTime)
+		boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy);
+
+	// compute actual timeElapsed taking into account actual sleep interval
+	// limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint)
+	TimePoint currentTicks = Clock::now();
+	Duration timeElapsed = currentTicks - lastTimePoint;
+	if(timeElapsed > boost::chrono::milliseconds(100))
+		timeElapsed = boost::chrono::milliseconds(100);
+
+	lastTimePoint = currentTicks;
+	lastFrameIndex = (lastFrameIndex + 1) % lastFrameTimes.size();
+	lastFrameTimes[lastFrameIndex] = timeElapsed;
+}
+
+ui32 FramerateManager::getElapsedMilliseconds() const
+{
+	return lastFrameTimes[lastFrameIndex] / boost::chrono::milliseconds(1);
+}
+
+ui32 FramerateManager::getFramerate() const
+{
+	Duration accumulatedTime = std::accumulate(lastFrameTimes.begin(), lastFrameTimes.end(), Duration());
+
+	auto actualFrameTime = accumulatedTime / lastFrameTimes.size();
+	if(actualFrameTime == actualFrameTime.zero())
+		return 0;
+
+	return std::round(boost::chrono::duration<double>(1) / actualFrameTime);
+};

+ 40 - 0
client/gui/FramerateManager.h

@@ -0,0 +1,40 @@
+/*
+ * FramerateManager.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+/// Framerate manager controls current game frame rate by constantly trying to reach targeted frame rate
+class FramerateManager
+{
+	using Clock = boost::chrono::high_resolution_clock;
+	using TimePoint = Clock::time_point;
+	using Duration = Clock::duration;
+
+	/// cyclic buffer of durations of last frames
+	std::array<Duration, 60> lastFrameTimes;
+
+	Duration targetFrameTime;
+	TimePoint lastTimePoint;
+
+	/// index of last measured frome in lastFrameTimes array
+	ui32 lastFrameIndex;
+
+public:
+	FramerateManager(int targetFramerate);
+
+	/// must be called every frame
+	/// updates framerate calculations and executes sleep to maintain target frame rate
+	void framerateDelay();
+
+	/// returns duration of last frame in seconds
+	ui32 getElapsedMilliseconds() const;
+
+	/// returns current estimation of frame rate
+	ui32 getFramerate() const;
+};

+ 8 - 5
client/mapView/MapView.cpp

@@ -55,6 +55,8 @@ BasicMapView::BasicMapView(const Point & offset, const Point & dimensions)
 	pos += offset;
 	pos.w = dimensions.x;
 	pos.h = dimensions.y;
+
+	addUsedEvents(TIME);
 }
 
 void BasicMapView::render(Canvas & target, bool fullUpdate)
@@ -64,21 +66,22 @@ void BasicMapView::render(Canvas & target, bool fullUpdate)
 	tilesCache->render(controller->getContext(), targetClipped, fullUpdate);
 }
 
-void BasicMapView::show(SDL_Surface * to)
+void BasicMapView::tick(uint32_t msPassed)
 {
-	controller->updateBefore(GH.mainFPSmng->getElapsedMilliseconds());
+	controller->tick(msPassed);
+}
 
+void BasicMapView::show(SDL_Surface * to)
+{
 	Canvas target(to);
 	CSDL_Ext::CClipRectGuard guard(to, pos);
 	render(target, false);
 
-	controller->updateAfter(GH.mainFPSmng->getElapsedMilliseconds());
+	controller->afterRender();
 }
 
 void BasicMapView::showAll(SDL_Surface * to)
 {
-	controller->updateBefore(0);
-
 	Canvas target(to);
 	CSDL_Ext::CClipRectGuard guard(to, pos);
 	render(target, true);

+ 1 - 0
client/mapView/MapView.h

@@ -37,6 +37,7 @@ public:
 	BasicMapView(const Point & offset, const Point & dimensions);
 	~BasicMapView() override;
 
+	void tick(uint32_t msPassed) override;
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
 };

+ 2 - 2
client/mapView/MapViewController.cpp

@@ -89,7 +89,7 @@ std::shared_ptr<IMapRendererContext> MapViewController::getContext() const
 	return context;
 }
 
-void MapViewController::updateBefore(uint32_t timeDelta)
+void MapViewController::tick(uint32_t timeDelta)
 {
 	// confirmed to match H3 for
 	// - hero embarking on boat (500 ms)
@@ -158,7 +158,7 @@ void MapViewController::updateBefore(uint32_t timeDelta)
 	}
 }
 
-void MapViewController::updateAfter(uint32_t timeDelta)
+void MapViewController::afterRender()
 {
 	if(movementContext)
 	{

+ 2 - 2
client/mapView/MapViewController.h

@@ -83,8 +83,8 @@ public:
 	void setViewCenter(const int3 & position);
 	void setViewCenter(const Point & position, int level);
 	void setTileSize(const Point & tileSize);
-	void updateBefore(uint32_t timeDelta);
-	void updateAfter(uint32_t timeDelta);
+	void tick(uint32_t timePassed);
+	void afterRender();
 
 	void activateAdventureContext(uint32_t animationTime);
 	void activateAdventureContext();

+ 6 - 1
client/widgets/Images.cpp

@@ -320,6 +320,8 @@ CShowableAnim::CShowableAnim(int x, int y, std::string name, ui8 Flags, ui32 fra
 	pos.h = anim->getImage(0, group)->height();
 	pos.x+= x;
 	pos.y+= y;
+
+	addUsedEvents(TIME);
 }
 
 CShowableAnim::~CShowableAnim()
@@ -391,11 +393,14 @@ void CShowableAnim::show(SDL_Surface * to)
 	if ( flags & BASE )// && frame != first) // FIXME: results in graphical glytch in Fortress, upgraded hydra's dwelling
 		blitImage(first, group, to);
 	blitImage(frame, group, to);
+}
 
+void CShowableAnim::tick(uint32_t msPassed)
+{
 	if ((flags & PLAY_ONCE) && frame + 1 == last)
 		return;
 
-	frameTimePassed += GH.mainFPSmng->getElapsedMilliseconds();
+	frameTimePassed += msPassed;
 
 	if(frameTimePassed >= frameTimeTotal)
 	{

+ 1 - 0
client/widgets/Images.h

@@ -190,6 +190,7 @@ public:
 	//show current frame and increase counter
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
+	void tick(uint32_t msPassed) override;
 };
 
 /// Creature-dependend animations like attacking, moving,...

+ 6 - 13
client/windows/CCastleInterface.cpp

@@ -68,7 +68,7 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town
 	  area(nullptr),
 	  stateTimeCounter(BUILD_ANIMATION_FINISHED_TIMEPOINT)
 {
-	addUsedEvents(LCLICK | RCLICK | HOVER);
+	addUsedEvents(LCLICK | RCLICK | HOVER | TIME);
 	pos.x += str->pos.x;
 	pos.y += str->pos.y;
 
@@ -163,16 +163,6 @@ void CBuildingRect::clickRight(tribool down, bool previousState)
 	}
 }
 
-SDL_Color multiplyColors(const SDL_Color & b, const SDL_Color & a, double f)
-{
-	SDL_Color ret;
-	ret.r = static_cast<uint8_t>(a.r * f + b.r * (1 - f));
-	ret.g = static_cast<uint8_t>(a.g * f + b.g * (1 - f));
-	ret.b = static_cast<uint8_t>(a.b * f + b.b * (1 - f));
-	ret.a = static_cast<uint8_t>(a.a * f + b.b * (1 - f));
-	return ret;
-}
-
 void CBuildingRect::show(SDL_Surface * to)
 {
 	uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT;
@@ -213,9 +203,12 @@ void CBuildingRect::show(SDL_Surface * to)
 
 		border->draw(to, pos.x, pos.y);
 	}
+}
 
-	if(stateTimeCounter < BUILD_ANIMATION_FINISHED_TIMEPOINT)
-		stateTimeCounter += GH.mainFPSmng->getElapsedMilliseconds();
+void CBuildingRect::tick(uint32_t msPassed)
+{
+	CShowableAnim::tick(msPassed);
+	stateTimeCounter += msPassed;
 }
 
 void CBuildingRect::showAll(SDL_Surface * to)

+ 1 - 0
client/windows/CCastleInterface.h

@@ -69,6 +69,7 @@ public:
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
 	void mouseMoved (const Point & cursorPosition) override;
+	void tick(uint32_t msPassed) override;
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
 };