Python Forum
[PyGame] Structure and Organizing (part 8)
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Structure and Organizing (part 8)
#1
Back to part 7
https://python-forum.io/Thread-PyGame-Fl...ing-part-7


Ideally you should do this in the beginning from scratch. Even if you need to make a "new game template" to start each game with. You should never be throwing stuff in one file like we have on this tutorial. The only reason i did it was to progressively work towards code like this as its overwhelming to start with.

A good structure would be the following. The / at the end indicates its a directory where whats after it indented is in that directory. The following example would have main.py and two directories resources and data in it. In resources directory there would be 3 more subdirectories (fonts, sounds, and images). In those would be the files, etc. The same with data directory, as we have a small program we only have 6 files in data directory. As the program grows, you will split them even further into sub directories, etc. etc.

Output:
ShooterGame/ main.py resources/ fonts/ Arial_custom.tff sounds/ beep.wav game_over.wav images/ enemy.png spaceship.png data/ enemy.py laser.py player.py tools.py control.py start.py
All of our code is in the data directory. start.py would be the root file that starts the game. However main.py is further up the directory and runs start.py. main.py only job is to run start.py and pass any arguments along with it.

This is because then the user can go into ShooterGame directory and start main.py. They dont have to look through files to find which one is the root file. There is only one to choose. A lot of times people throw everything into one directory of hundreds of source files, pyc files, spritesheets, sounds, music, videos, fonts, etc. Most commonly they either name them main.py, run_game.py, or ShooterGame.py (the name of the game). Sometimes you are just guessing which files is the root file to start the game. By using this structure, you avoid that. You want to make it easier for the user to use your game. Then more people will at least attempt to try it. Even if you have built an exe you should still make the source be able to run easy, as not all people will be using the exe.

control.py would have the main game loop. start.pt would run controls main game loop. Then control takes over from there. In start.py you can also have pygme.quit and all that since nothing will come after the main game loop except exiting.

tools.py will include the Tools class. Some of this can be steamed out into a prep module that gets imported by others if needed. More on that later.

player.py would have the player class and laser.py would have the laser class

enemy.py would have both Enemy class and EnemyController class. Any other enemies could go in there as well. Again if you hadd 100 different enemy classes, you should split those as well like we are with the game now.

The fonts, music, sounds, and images go into sub directories in resources directory.

This is the basic layout of splitting this one file into modules.

Why OOP? (Object Oriented Programming)
There are some that believe that by removing OOP from tutorials, it simplifies the concept of teaching pygame. However, the problem with not using OOP, is the code is littered with global keywords, passing variables between functions, and is more organized when using classes and objects. Actually using OOP reduces the amount of code, redundancy and maintenance. While it also increases the readability.

That is exactly why these tutorials start out using classes to house the player class from the start. Otherwise the player attributes would be globals passing back and forth. Things pertaining to player can be implemented into the player class for organization.

A lot of tutorials at the moment do not do this, and the result is spaghetti code that is unreadable and not maintainable. It actually is more amount of lines with more amount of code.

Hopefully if you did not understand classes and objects in the beginning, this tutorial gave a good example of how to use them and why they are so important for game programming. I would urge you to learn more about classes that have not been covered here in our class tutorials. Inheritane is important to know. Pygame Sprites need to be inherited if they are used. This tutorial has done it manually without inheriting these. But these can make it more simple such as Groups, instead of looping an enemies list, just have an enemy class add itself to a group, and then just update the group, or draw the group, etc.

Here is an example of with and without OOP, a small example. This is a youtuber that I redid his code to use OOP. A lot of people were making spaghetti code and needed to be shown the difference of his code since they can relate to his code, of how it can be better with OOP. A small 100 line piece of code is not significant. But as you add this and that, it can easily turn huge. And it is much easier to do it from the start, than trying to fix code to use OOP.


Split File
We are going to go step by step through this process of splitting this one file into many.

To do this we are going to make a github repo to house our code. Its too confusing having different code boxes for each file, and its confusing to understand that way. To learn you dont need to understand how Version Control Systems work. I will provide the path to each branch which is like a snapshot in time as we progress through splitting our file out. You can download each branch by clicking the "Clone or Download" button and selecting ZIP. But you can of course just look through the code as well. You can look through each branch commit to see each addition and deletion of code as shown in the first commit.

Here is the repo as we have our code now
https://github.com/metulburr/ShooterGame..._directory

The first thing we are going to do is move all the resources into their proper directory. We named the our original file "all_in_one_file.py". This will eventually be deleted when we are done. We are basically going to cut and paste code out of there.
https://github.com/metulburr/ShooterGame...oved_files

By doing this however our program no longer works.
Error:
Traceback (most recent call last): File "all_in_one_file.py", line 257, in <module> TOOLS = Tools(screen) File "all_in_one_file.py", line 211, in __init__ self.sound_init() File "all_in_one_file.py", line 246, in sound_init self.laser = self.load_sound_file('beep.wav', directory_of_sounds) File "all_in_one_file.py", line 253, in load_sound_file return pg.mixer.Sound(fullname) pygame.error: Unable to open file './beep.wav'
This is because before we had all the files in the same directory and now we have them in sub-directories. You could simply add resources/sounds/beep.wav to each and every location. But this is not the best approach. We want a tool to already know where the sound files are already when we call them. And we want to just call something like SOUNDS['beep'].play() in any module instead of hard coding paths everywhere. Then if the path changes, you change it in one spot instead of all of your modules.

https://github.com/metulburr/ShooterGame...work_again

There is going to be a lot of boilerplate code (loading resources), However most of this new code can be swapped to new games without any change to our new tools. You can literally just copy and paste these modules to a new game and start over and over. This is where classes and modules really shine.

So now we made a couple new modules tools.py and prepare.py.

tools.py is what is sounds like. It contains tools for handling the game, in this case loading all graphics and sound effects from the any given directory. In GFX it returns a dictionary with the key being the filename and only allows what is in the parameters of accept of the function. In SFX it returns a dictionary of a pygame Sound object of the key being the filename and also only allows what is in the parameter of the accept.

prepare.py is a module that will be imported into almost every other module. This is for static data...stuff that doesnt change throughout the game. This is so you can call all images and sound effect from any module...all you have to do is import prepare into that module. You dont have to fuss with sound or image directories. You just put the image in its directory an sound in its directory, import prepare, and call it in the form of a dictionary with the filename as key without the filetype. It also sets default prep stuff, like pygame init() and setting sound volume, etc.

prepare.py will get initialized only once regardless of how many times you import it in other modules. So it will only make 1 display, and load all graphics once, etc.

We also changed all the code in all_in_one_file.py pertaining to loading sounds or images, or andthing set in prepare.py etc. just to make the program run.

At this point we are going to remove the Tools class. Everything is going to dissolved into a new module called labels. Everything left in there is some form of a label so it makes sense to make a label class.

https://github.com/metulburr/ShooterGame..._to_labels

We added the standard gitignore. This is only for github to ignore my compiled pyc files etc. Otherwise you would see that as well in there. But we moved the tools class to labels and created a super class and sub class for each type of label. This exact method can change depending on your game. You can expand on the labels as you wish. Because of this we had to import labels and call the labels like this...
game_over_label = label.GameOverText(prepare.SCREEN_RECT)
score_label = label.TopLeftText((10,10), 'Score: {}', None, prepare.SCREEN_RECT)
damage_label = label.TopLeftText((10,30), 'Damage: {}', None, prepare.SCREEN_RECT)
TopleftText is a subclass of Label. Everything in label class is passed to all subclasses. For example each sub class did not have to create a font object or rect, the super class did that. However if you need to each sub class can override these attributes if needed. What is in the sub class is what is overriden. We need to draw it and we need to update the text on the fly when the score changes. So we need to make the text different for each label, etc.

But we have removed some of the code from our one large file to a module. This is the key to separating a large body of code into manageable parts.


Now we are going to do the same thing with player, laser, and enemy classes. Move them out of the main file and into their own modules.
https://github.com/metulburr/ShooterGame...to_modules

As you can see our main file is now about 35 lines long. If you want to change player code, go to the player.py module. If you want to change enemy code then go to enemy.py file, etc.

The enemy module contains both the enemy class and the enemycontroller. The EnemyController class currently handles the topleft labels, but that can be rerouted to the main module by returning from its update, etc.

This is all pretty much the same code, just moving it into separate modules to be imported.

Now we are going to go one step further and move the main file into data as well and make a run file for users to easily find.

https://github.com/metulburr/ShooterGame..._into_data

Here we literally cut and paste all_in_one_file.py into data directory and renamed it to control.py. Named it that due to it being control of the whole game as the main game loop is in it etc. But since we are not directly running control, we need to make this a relative import by doing this instead
from . import (
    prepare,
    tools,
    label,
    enemy,
    player,
)
but because this file runs the whole game, all we need to do is import this module and it will run the game. So our new run_game.py in the root directory only imports this module.

This opens up more code here such as sys.argv or just plainly allows the user to easily find how to run this game. There is no question on what file to run. They do not have to look through data directory to find what file to run, it is there in the root directory for them to run. If you build an exe file, you would put it there also. Those that wish to run the source would run run_game.py and those that wish to run the exe would run the exe, etc.

Go to Part 9
Recommended Tutorials:
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyGame] Flair and Organizing (part 7) metulburr 0 4,946 Nov-22-2017, 05:07 AM
Last Post: metulburr

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020