First let me go through some stuff that's pretty easy to figure out.
Code: Select all
static constexpr size_t numPixels = 10'000;
Pixel p[ numPixels ];
int i = 0;
int amount = 0;
In the tutorials, chili does explain what constexpr is, don't remember at which point, but it is just a new-ish keyword for C++ since 2011 that allows you to declare variables and assign to them constant values, like the 10'000. The compiler will replace the variable name with the constant during compilation instead of looking in memory for the value.
Because this constexpr variable is declared within the Game class, it needs to be static. When declared like this, it's not stored with the instantiated object, as stated, if it's a simple int or float the variable gets replaced during compilation. In this context, static means it'll have the same value for every instance of Game...which will only ever be one in the chili framework.
Since it is a compile time constant, you can use it when declaring static C-style arrays, Poo p[ numPixels ];
I believe in C++14, you can use the apostrophe to separate digits and it gets ignored by the compiler. It's just a nice way of letting the programmer know what the number is without having to count 10000 vs 10'000.
The type size_t or std::size_t is just an alias for unsigned int when compiling for x86 ( 32bit ) and unsigned long long when compiling for x64 ( 64 bit ). A lot of standard library functions that return a size ( ie, std::vector::size() ) returns a size_t because the size of something will never be negative and a type with size in the name kind of explains what data it holds. I probably use it more often than I should. Unsigned int or uint32_t would work, but as I said, it's an unsigned long long or uint64_t when in 64 bit mode. So instead of switching back and forth between uint32_t and uint64_t, I just use size_t for array indexing or size type parameters.
Code: Select all
for( auto& pixel : p )
{
pixel = Pixel();
}
gfx.BeginFrame();
In C++11, they introduced the auto keyword. This allows you to 1) not need to know what type p is and 2) shorten variable declarations by using auto instead of the fully qualified name. An example would be pre-C++11 people used iterators in their for loops instead of indexes.
Code: Select all
for(std::vector<float>::iterator it = vec.begin(), end = vec.end(); it != end; ++it)
{
// Do stuff with the dereferenced iterator by prepending it with an asterisk
*it += 3.42f; // The operator* is overloaded for iterators to return a reference to the element it points to. It's like a safe pointer.
}
// Same loop using auto to deduce what the type of it should be
for( auto it = vec.begin(), end = vec.end(); it != end; ++it)
{
*it += 3.42f;
}
// Same loop, but using indices
for(size_t i = 0; i < vec.size(); ++i)
{
vec[i] += 3.42f;
}
//C++11 range based for loop
for( auto& elem : vec )
{
elem += 3.42f
}
As you can see, the first auto version is preferable to the full blown std::vector<float>::iterator version.
In your code, you use a C-style array or static array. It's size is known to the compiler, so the use of the range based for loop is permitted. This will not work if you allocate memory from the heap. Chili does cover memory allocations in the first couple episodes of Intermediate.
I put the gfx.BeginFrame call in the Game::Game function to just clear the screen to black once. Calling it each frame would be a waste because you aren't wanting to clear the screen at 60 fps while trying to draw. Ideally, you might put this in a function that gets called when you start a new image.
Code: Select all
// Reclaim the pixels in the buffer by resetting i
i = 0;
if( wnd.mouse.LeftIsPressed() && i < numPixels )
This resets i back to 0 each frame, this way it doesn't just keep increasing forever. You only have so much memory to deal with and you can only allocate a small C-style array on the stack...about 1 megabyte or less. Probably anything greater than or equal to a 512x512 image would cause a "stack overflow".
Code: Select all
// only search from &p[0] to &p[i]
const auto it = std::find_if( p, &p[ i ], [ mx, my ]( const Pixel& pixel )
{
return mx == pixel.x && my == pixel.y;
} );
Here, &p[0] just returns the address of the first element of the array same as just putting p and &p
gets the address at p. p at index i is the last inserted Pixel object.
The std::find_if is a function that loops through from p to p + i looking for matching x and y values to the mouse x and mouse y values. The std::find_if function takes as it's last parameter a predicate function. In this case, I use a lambda function ( also introduced in C++11 ).
A lambda is a function object that can be declared and defined inside of another function. Chili does briefly cover lambdas in the intermediate series...actually all of the things I've used here are introduced or more thoroughly covered in the intermediate series.
There are three parts to a lambda: the capture list, the parameter list and the function body.
The capture list is a list of variables between the square brackets [ ].
The parameter list and function body are the same as any other function.
The capture list can capture specific variables like the [mx,my] I did here. Or capture everything that it uses.
To capture by value, just use the name of the variable like the [mx,my].
To capture everything by value, use the = sign. [ = ]
To capture everything by reference, use the reference operator &. [ & ]
You can and sometimes must capture the 'this' pointer. [ this ]
If you use the = to not have to list a bunch of variables, only those that you use will be copied.
If you use the &, only those that get used will be referenced.
If you want to access a member variable of the current class, you must pass the this pointer.
Code: Select all
if( it == &p[ i ] )
{
p[ i ].x = mx;
p[ i ].y = my;
// Assign whatever selected color is
p[ i ].r = p[ i ].g = p[ i ].b = 255;
++i;
}
}
The std::find_if function like most standard library algorithm functions, return an iterator. In this case, a pointer. For this project, you want to check if there was a match and if there is, than that pixel is already in the array, so no need to store and increment i. Since index i is the last element inserted, we want it to be the same as i, meaning the pixel is not in the list. If it's not in the list, let's go ahead and assign a new pixel and index i and increment i to get ready for the next insertion.
Remember, when you click the left mouse button, several frames go by during that click. You don't want to store the same pixels unless the color has changed or you are using a different tool like the eraser for instance.
I didn't do any advanced heap allocations or other memory management, I still used the same C-style or static array you did. I just made sure that the index i didn't go over the pre-allocated or stack allocated amount of space.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com