Making Breakout - Code Structure help requested

Started by
77 comments, last by Kylotan 6 years, 6 months ago
Just now, MarcusAseth said:

I didn't had tried to compile so far (until you asked), otherwise I would have realized that I shouldn't put the ';' at the end of the macro, which was expanding the ';' into LoadImage("Graphics/Background";); :P

Exactly :)

Other stuff, from your hidden code:

  • You don't initialize GameInstance in your App constructor to nullptr; this means that when you get to ~App(), if the game wasn't running, the GameInstance pointer is random garbage. Attempting to delete such a pointer is undefined behavior and will likely crash you. You should consider using std::unique_ptr<Game> to manage this object instead of having to manually manage it.
  • In ~App, you don't actually appear to call SDL_DestroyWindow and friends. You are missing your parentheses.
  • Methods defined in the class definition (IsRunning) are automatically inline; you don't need the keyword. It's largely useless these days anyhow. As a matter of sanity I tend to avoid defining anything in a header, except when needed (templates), because link-time optimization is usually good enough and I like the faster recompiles. Your mileage may vary.
  • This "return Foo() ? (Bar() ? : true : false) : false" construct is horrid. Not only is it redundant (because Bar() is already returning true or false), the nesting makes it onerous to read.
  • I am worried about what the Error() function is actually doing. It very much looks like it conflates recording of the error with presentation or handling of the error, which is not a great long-term pattern.
  • const char* for strings should be reserved, generally, for inter-operation with C APIs in C++. Use std::string and use its c_str() member when you need to pass data to C APIs like SDL.
  • Prefer nullptr to NULL.

 

 

 

Advertisement

Thanks for all the tips and for reading trough my code jpetrie ;)

Resolution of the day: all pointers will be forever initialized to nullptr.

The SDL_DestroyWindow was really careless, but thankfully it gets catched by the compiler

point 4 made me laugh, disgusting people trough code xD

This is my Error() function:


bool Error(const char* err)
{
	std::cout << err << std::endl;
	return false;
}

I either pass the GetError() or a error message written by me, and it always return false.

Though aside from that case where I took the ternary operator TOO FAR, using it like in the code below is "elegant", right? :P


return Window ? true : Error(SDL_GetError());

Anyway I'll be sure to apply all fixes, thanks !

1 minute ago, MarcusAseth said:

Though aside from that case where I took the ternary operator TOO FAR, using it like in the code below is "elegant", right?




 

No.

Just now, jpetrie said:

No.

:D:D:D

 

3 minutes ago, MarcusAseth said:

This is my Error() function

This looks like a questionable habit to develop.

You're munging together three concerns here: logging, error propagation, and error presentation. These should probably be three different things.

Logging can be useful for recording information about some event at the site of that event, for future diagnostics purposes. You should have a dedicated function for logging things, if you want to use that capability. Things you may want to log are not always errors.

Error propagation is about bubbling errors up to the place where they can be reasonably dealt with, and including all the relevant information about the error in that bubbling. Right now all your error handling boils down to is returning booleans, which the implicit assumption that there is only one way for a thing to fail. This doesn't scale, and in fact you've already scaled past it: several of your functions can return via Error() using two different messages. It's impossible for the caller of LoadImage to know why an image failed to load, and if that error is something that can be recovered from or handled by retrying or what.

Error presentation at this level is usually simple, usually the same as logging. But as you build things that are more and more advanced, this kind of "immediately present the error" approach falls apart. Consider if you were trying to tell the user "Error("Could not connect to login server, try again in five minutes.")." This is a typical message you'd like to surface to a user, and as such will usually be rendered in the UI style of your game. But you can't put up a blocking modal dialog deep inside your code like that; you need to have that error information propagated outwards (as above) and then handled by the appropriate UI presentation layer in your code.

I can and will certainly add the Logging() function, but all after that, is just too much for me to consider right now x_x

First I'll focus on getting a playable game.

Also I don't actually think as what I am doing as "error handling", is just like a warning in the console for me to roughly know "around which part of the code I fucked up and should place a breakpoint and hunt for bugs"

4 minutes ago, MarcusAseth said:

Also I don't actually think as what I am doing as "error handling", is just like a warning in the console for me to roughly know "around which part of the code I fucked up and should place a breakpoint and hunt for bugs"

That's reasonable, but then I'd change the name of the Error() function to Log(). :D

You're right to focus on getting a thing that works and is a game first, before worrying about the minutiae of some of my above points. But they are still worth keeping in the back of your mind for consideration as you build new things, because good habits (particularly with respect to dealing with errors in some fashion beyond "oh shit, well, let's just terminate the whole process") are easily to build early.

Small progress today,  trying to get the time stuff right, and probably still isn't, code below.

I've read this whole page again and I am not quite sure of the need/usage for the MAX_FRAMESKIP and that's why I didn't added it to my code x_x

I post the code in case anyone would be so kind to doublecheck :P

GameTimer.h


#pragma once
#include <chrono>
#include <string>
using namespace std::chrono;


class GameTimer
{
	milliseconds MsSinceLastFrame;
	time_point<steady_clock> TimeNow;
public:
	time_point<steady_clock> TimeBegin;
	time_point<steady_clock> PrevUpdateTime;
	time_point<steady_clock> NextUpdateTime;
	
	GameTimer();
	~GameTimer();

	time_point<steady_clock> CurrentTime();
	std::string FPS();
	std::string GameSpeed();

	void RecordTimeSinceLastFrame();
};

GameTimer.cpp


#include "GameTimer.h"



GameTimer::GameTimer()
	:MsSinceLastFrame{0}
{
	TimeBegin = TimeNow = PrevUpdateTime = NextUpdateTime = steady_clock::now();
}


GameTimer::~GameTimer()
{
}

time_point<steady_clock> GameTimer::CurrentTime()
{
	TimeNow = steady_clock::now();
	return TimeNow;
}

std::string GameTimer::FPS()
{
	
	return "| FPS: "+ std::to_string(static_cast<float>(MsSinceLastFrame.count())) + "ms |";
}

std::string GameTimer::GameSpeed()
{
	return "";
}

void GameTimer::RecordTimeSinceLastFrame()
{
	MsSinceLastFrame = duration_cast<milliseconds>(CurrentTime() - PrevUpdateTime);
	PrevUpdateTime = CurrentTime();
}

GameLoop:


void App::GameLoop()
{
	//GameUpdateTickPerSecond   suggested ratio 1/25 seconds = 40ms per udpate
	static duration<uint32_t, std::ratio<1, GameUpdatesPerSec>> GameUpdateTPS{ 1 };

	while (Game->IsRunning())
	{
		Timer->RecordTimeSinceLastFrame();

		while (Timer->CurrentTime() > Timer->NextUpdateTime)
		{
			Game->Update();
			Timer->NextUpdateTime += GameUpdateTPS;
		}

		auto TimeOffset = Timer->CurrentTime() + GameUpdateTPS - Timer->NextUpdateTime;
		auto GameUpdateTPSns = duration_cast<nanoseconds>(GameUpdateTPS);
		
		float Interpolation = float(TimeOffset.count()) / float(GameUpdateTPSns.count());
		Game->Draw(Interpolation);


		/*DEGUB*/LogConsole(std::to_string(Interpolation).c_str());
		std::string AppTitle = "Breakout" + Timer->FPS() + Timer->GameSpeed();
		SDL_SetWindowTitle(Window, AppTitle.c_str());
	}
}

 

I think maybe I should briefly mention something (from my limited understanding) about the chrono library, so maybe other begginners in this section can benefit from it too so that my post/code is less obscure to them and not useful only to me. Also is to review my understanding of it since anyone can correct what I say.

Chrono library is a time library part of the STL, you just need to #include <chrono> in order to use it. Most of the code live inside namespace std::chrono::  .The library doesn't allow inplicit conversions of arithmetic types to durations therefore you can say "seconds MySeconds = 3;//Error" because "3 what?! 3 hours?"

It is mainly composed by 3 things: duration,    time_point,    clocks.

clocks: it has 3 of these, the system_clock which is basically a calender (has functions to convert a time point to a real date), like your Window time, the steady_clock which is basically a stopwatch in nanoseconds (at least on my machine, no idea if this can change) and is great for timing, and lastly the high_resolution_clock which just gives you back one of the other 2 (the one with the shortest tick period).

name_of_one_of_those_3_clocks::now() returns you the current time.

duration: this is a template class on o the form of duration<rap, ratio<num,den>>. It is used to store an arbitrary duration of time, some of the default durations defined in the library are seconds and milliseconds .

Rap is the internal rapresentation used to store the time, it is usually a long long but you can also pass a float or double to it if you want to have your operations on that duration return "fractions of a tick". 

ratio<num,den> is a ratio made up of numerator and denominator used to allow for proper conversion between different duration, seconds default to ratio<1,1> and milliseconds is ratio<1,1000> , so that if you do:


//s is a literal representing seconds, declared with many others in std::literals::chrono_literals 
//but you can just do "using namespace std::chrono;" and it will give you access to those as well
seconds MySeconds = 4s;
milliseconds MyMilliseconds = MySeconds;

//Because of the ratio thing, now MyMilliseconds contains the value 4000

also some default ratios like the one used by milliseconds are provided so that you can do std::milli instead of writing ratio<1,100> 

So in my code in the reply above


static duration<uint32_t, std::ratio<1, GameUpdatesPerSec>> GameUpdateTPS{ 1 };

GameUpdatePerSec is set to 25 and my  GameUpdateTPS contains just 1 of this duration, so that if you convert it to milliseconds or assign one of this to milliseconds you get 40ms ,    1000ms/25 = 40ms.

time_point: this are different than duration because this is a duration from a point in time. You can't just assign a time_point to a duration.

If I have got it right, then:

time_point + time_point returns error

time_point - time_point returns  duration

time_point + duration returns  time_point

time_point - duration returns  time_point

You can always get a duration out of a time_point by calling it's member function time_since_epoch()

Lastly, back to duration, no implicit conversion will happen between a "finer precision" duration and a "coarser precision" duration, so between ms and s for example because you would lose precision, but the implicit conversion will happen the other way around because it is safe to go from coarser to finer.

You can still use duration_cast<Put_To_duration_type_here>(MyTime)  to force that "upward" conversion which will result in truncation of the value stored inside the duration.

And is pretty much the chrono library to my understanding ;)

 

MAX_FRAMESKIP exists so that if updates are too slow, you still get to render every once in a while. I would expect it's unlikely you'll hit this problem.

You don't need to set the window title every time through the game loop.

Chrono is part of the standard library. It's not called the STL, and hasn't been for many years.

This topic is closed to new replies.

Advertisement