Pygrr - rendering and framerate (part 2)

 Welcome back to "rendering and framerate"! In this conclusion, I will talk about how the stuff we learnt from last entry is applied to Pygrr, and go over some interesting quirks and things. Without further ado, here we go...

For Pygrr's rendering, I have opted to use the standard library of tkinter (click to read about it!). This is a lightweight library that comes pre-installed with Python itself. This means that the users of Pygrr do not have to install any further software after the library itself, and it also means that Pygrr will be able to run on all systems (Linux, Mac & Windows), as they all share tkinter!

Tkinter has a 'window' (also referred to as a 'root') that must be initialised, and also a 'canvas', which can be used to bind gadgets (shapes, buttons, etc) and draw them onto the window. There are 3 functions that the canvas can call which can draw an "oval", a "polygon" and a "line" on it. I will go over this after explaining how the window and canvas is initialised in Pygrr!

There are 3 arguments that the window takes in. This is width, height, and background (colour). These can be called when the user creates the window, by calling the function:

create_window(width=500, height=400, background='white')

As you can see, if the width is missing from that argument, it will be defaulted to 500, the height 400, and the background "white". Some colours are hard-coded with tkinter, so if you input them as a string, the program will translate them into a hex code, which you can learn about in the last post in this entry!

All the supported colours are - image from dftwiki (smith.edu):

I originally forgot these default string colours existed, so hard coded a bunch myself... Oops! Both the window and canvas are created and cached automatically by Pygrr when you call the function. You can also change the window / canvas parameters during runtime by calling the functions:

set_width(new_width)

set_height(new_height)

set_background(new_color)

There's also get versions of those, where you can call get_width() for example, to return the width of the canvas. For simplicity and elegance, the end-user cannot resize the window themselves, by stretching the window on the screen!

You can also call destroy_window() to... Well, destroy the window! This allows for a fresh wipe of the window, and, thus, new 'scenes' in the game.

As we learnt in the last entry, these functions can only display a still screen, because there is no frame system. Well, there's also get and set_framerate(new_framerate), where it is measured in frames per second (FPS). Framerate is defaulted at 30. These work with the user's game loop, in their code, they must set up a loop (infinite or finite), and at the end of the loop, call next_frame(). This will invoke a special timing protocol I coded to pause execution for the correct length of time, and render new stuff.

Creating objects? Tkinter allows for creation of three primitive objects on a canvas, an oval (of given width and height), a polygon (of given array of points), and a line (of given pair of points). Unfortunately, tkinter wasn't going to let me get away that easily with rendering of ovals, such as circles... On every operating system apart from windows, they work flawlessly - however, on windows, upon the oval moving, it leaves a trail of ugly pixels... I couldn't find a way around this, so, I did what every programmer does, and code another program to fix it! This came in the form of a 'circle estimator' that I programmed, which turns a circle into a polygon (array of points), with a given accuracy. Here's some videos of two different accuracies:

 

As you can see, it's very satisfying to watch, and it gets the job done very well. I chose a circle with a greater accuracy than in either of those videos. Here it is:


This fixes those annoying pixels! Now Pygrr's create_oval and circle functions will generate a polygon based off of this circle. Much better!

Pygrr assigns an ID for each object created on the canvas when the object is packed. This means that Pygrr will read all of the graphical data of the object, and draw the shape onto the canvas. When the move functions are thus called, the object will be moved along the canvas, while still changing the coordinates stored in the data of the object. This means that the move and set_position functions do not require a 're-pack' (that is, calling the Object.pack() function again). However, changing the graphical data such as colours, model, width, height, and outline settings of the object requires a re-pack, as tkinter itself does not support changing the graphics of gadgets in runtime. This is unfortunate, but is easy to understand for the user - all they need to do is add one line of code!

Now - if the framerate ever fluctuates (dips or gets bigger) due to computer speed being limited, for example, background tasks can take up different amounts of power at a time, the movement of an object will be messed up. What? Let me show you: if an object, let's call it "player" moves by 1gu (graphical unit) every frame, if the framerate is 50 per second, it will move 50gu a second, whereas, with half the framerate, it will move twice as slowly. This can be combated by inputting a simple multiplication inside the move.



while True:

    player.move_x(10) # this will move at different speeds in different framerates

    player.move_x(10 * pygrr.deltatime) # this will not


How does that work? Deltatime is expressed as the length of time between the last frame and now. This means, if the framerate fluctuates, so will the length of time, and, thus, the distance moved will fluctuate (being multiplied by deltatime). If that sounds confusing, don't worry, I'll show you an example!

Moving 25 * deltatime, at 50 frames per second:


And the same, but at 5 frames per second:


You can see that the resultant speed for the viewer (distance moved per second) is the exact same, even with a much lower framerate.

Another thing I had to deal with was tkinter's funky coordinate system. This stated that (0,0) was in the top right, and (1,1) is in the bottom right... So +y goes down, and +x goes right... That's weird! I spent a good while doing lots of coding maths to make Pygrr interpret these coordinates as a different value, so now, the coordinate (0,0) is in the middle, with +x right, -x left, +y up, -y down. This makes a whole bunch more sense, and is easier to learn!

Anyways, you've read enough now, haha! This concludes "rendering and framerate". I hope you enjoyed! Next stop: input...

Isaac, over and out...

Popular posts from this blog

Blog post #0 - Hello, world!

The principles of Pygrr

An introduction to Pygrr...