Refactoring A Tower Defense Game In Python // CODE ROAST

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
it's time for another code roast [Applause] [Music] [Applause] it took me a while to do the second installment of the series i realized the first episode got really long because i basically tried to fix everything so today i'm not going to do a full refactor but i still try to do a pretty thorough analysis of the code this video is sponsored by tab 9 an ai code completion a system that directly integrates into your ide i'm going to use it later on and show you how it works and also talk about some of the features that it offers for this code roast we're going to look at a tower defense game provided by guccillific did i pronounce that correctly is it even possible to pronounce it correctly by humans anyway let's look at the code the tower defense game is a single file that basically contains all the code you see it starts with a couple of imports so here's where the first issue that you're importing everything from tkinter which means that it's really hard to refactor things if you want to move to let's say another gui library also there are a couple of globals [Laughter] i am put yourself together you can do this actually that's not how i should respond to this i noticed that as developers we have a tendency to be very negative about bad code of course we want to improve code we want to write better code but we also have to realize that people are in different stages of the learning process i mean we want to help people and in this case griffik who sent in this code he also said that this is not a project that he worked on recently but it's a few years back so i think this actually shows a lot of courage that you're able to put this out into the world and let other people criticize it and actually that means you put your learning process above your ego which i think is a really great attitude so i salute you gus horrific now let's look at the rest of the code so we have these globals we have a game class that's actually the main class that governs the way that the game runs there is an initializer that creates all the game objects that initialize everything creates canvas etc etc there's a run method that runs the loop so games normally they consist of a loop that runs a number of times per second that's the frame rate and what does the game will do well it updates and paints the game and in this case it uses a timeout method from tk inter to rerun the same method after 50 milliseconds you see in these methods there are a lot of references to global variables like here for example and that makes it hard to separate things into different files but we're going to try to do that anyway and then let's move down further there are some more classes i'll talk more about these classes later is a couple of buttons monsters a representation of the mouse different elements that are part of the game and then if you scroll all the way down to the bottom we have the actual code that starts the game so let's run this and see how it works i'll just launch a new wave to show you what's happening so these are the enemies that are appearing and now i can place these arrow shooter things here i have different types of towers that i can put that are going to kill these enemies and oh that one is getting close i just put a few towers here and now that the wave is complete i can start a next wave let me put a few more towers here because this is clearly not enough it's actually a really nice game i had trouble working on refactoring the code because i was playing this game all the time so let's analyze the code for a bit so obviously we have all these global variables and the wildcard imports that we should generally avoid but there are a couple of other things as well to look at like for example this game class currently has lots of responsibilities it's responsible for managing the generic game loop the game engine system so to say it's responsible for creating all these game objects for updating them drawing them so that's a lot and that's also why it's a pretty long class now also all these classes are in the same file i would recommend to put classes in separate files and because of the global variables this is going to be a lot of work to move all these classes out to different files because there is so much coupling happening here another thing to think about is whether the structure of the classes at the moment makes sense so let's look through the code and see what we have so we have for example a button class which is great but here you see that the button checks whether it's pressed by looking if the x and y value that's probably the mouse coordinates are within the bounding area of the button but it's also responsible for painting itself another problem is that the button directly accesses other objects that are part of the game and that means you can't use this button in any other setting because then perhaps this wave generator object is not going to be here so we need to find a way in the game engine to make these objects be less dependent on each other and i'll talk about that in a minute and there is something else let's see we have also a couple of monsters so i'm just gonna scroll down to show you what's happening there so here's the monster class there's lots and lots of settings it has a update and the move method and some other things that it computes and then we have subclasses that use this basic monster class for different things in some cases you could wonder whether it's really needed to use sub classes or do it in a different way for example here monster 1 is basically exactly the same as monster except that it has a couple of different values for health and for speed and things like that another way to do this would be instead of creating a subclass actually creating an instance of monster that has those particular values and this is always a balance you have to strike between when are you going to use a subclass when are you going to use inheritance versus when are you going to use configurations to make the distinctions between the different things my general advice would be that if you add behavior if you change behavior then inheritance or sub classes makes more sense if it's just changing values like you're doing here then it's perhaps better to use a configuration file or something like that so another thing is that this script directly creates the game instance at the end and that means that if you import this for some reason in another place then it's also going to create this game which is a bad thing so we should put this into a separate if statement to make sure that this only happens when you're running this as the main script so let's fix that right away now that we're here in the code one way to further improve this is actually not putting this in the global scope but actually creating a main function so let's do that as well and then we're just going to copy this and put it right here and then here we're going to call the main function instead so now we've put this into a main function which is a bit cleaner so what i'd like to do in this video is create a basic game engine architecture to separate out the generic game aspects from the more specific tower defense game aspects but before i start doing that let's first fix a few minor things here and there in the code so i'm going to start with the run method in the game class inside this method we're using a run attribute which is for the moment apparently always going to be true and then if that run attribute is true then we're going to call the update and paint method so it's basically a mechanism to kind of start or stop the game loop the problem is that this has a deep indentation level because everything is under this if statement and also we're comparing with a boolean value which is not needed and finally run is in upper case here which generally in python means that it's going to be a constant but this is something that you might want to set to false at some point in the game so i'd suggest to not make this an uppercase variable but actually a lowercase variable there we go and then we actually don't need this is true part because we're already checking with the boolean and one other thing you can do to reduce the indentation level is to actually check whether it's running but check that it's not running so if it's not running then we're going to return and then we're going to take all these lines here and put them at this lower indentation level this is generally a good idea if you're writing code that you handle the special cases first in this case the special case is that we're at the moment not running and then the main part that the method is supposed to do which is updating the game and painting the game and resetting timer is happening at the main indentation level of the method and that's overall is easier to read so now i'm going to the wave generator and there is another example of this in the update method let's see so here you see it's kind of the same thing so if if we're not done but that's also comparison with boolean here then we're going to do all these things so here you can also basically swap that around so we're going to remove the direct boolean comparison and remove this stuff and then we're going to return and let's select these lines and put them at the higher indentation level so we're doing the same thing and we might even be able to shorten this if else statement here as well because this also you can return after this line and then this goes to the higher indentation level as well okay so we have this next wave button so i talked about this check whether it's pressed actually there's two things we can do here to simplify well one is that again we have these nested if statements and then only then we're going to do this but what i'm going to do is add a method that's called is within bounds and that's going to get an x and a y which is also an end and then we can basically return this whole statement here but there's actually a way to write this in a slightly shorter way in python so here's you we're checking that x is larger or equal than self.x and less than self.x2 and y is the same thing python actually supports comparison statements that are a bit closer to how you do it in mathematics so you could also write this as follows there you go so now you can check whether the mouse is within the bounds of the button by simply calling this function and then in the checkpress method we can use that and basically invert this if statement to again reduce the indentation level then we're going to return and then this i can delete and then let's again select these lines and de-indent them because this is python 3 classes don't need to inherit from object anymore so let's delete these and we don't need that anymore and what you can also do because this is python 3 is that these super calls can actually be simplified as well so i can just delete this because it's not needed anymore and here actually if you look at this button this is actually completely superb for us because if we remove this initializer then the super initializer is called automatically so we can actually just remove this entirely and the same goes for the upgrade button and probably for a couple of the other buttons as well so there's one more thing i'd like to fix before we continue with the game engine part and that's that this code contains a couple of these try except blocks that you see here so we have two of them in the update method in the game class so which is here and there's another one which is a bit lower in the i think this is the projectile yeah the projectile so this is also a really bad thing because you're hiding lots of errors by catching basically any exception actually the reason that the tri-xep was put here in this way is that these projectiles when they update themselves they can potentially remove themselves from the list of projectiles because we're in a for loop that's a numbered for loop with an index if a projectile removes itself basically this breaks and you get an exception now there's a simple way actually to fix it and that's to not use these indexed loops through the list of projectiles but simply use the for in so now we're updating the projectile and we can remove all these lines here because we don't need them anymore and we can do the same thing for the monsters so this simplifies the code and it removes the empty accept block and there's one except blonde left and that's this one and this one is a bit strange we're trying this part of the code but only if this results in an exception then we're going to do this so this kind of mixes control flow with exception handling and that's really confusing the reason that this is put into a try except block is that in some cases target doesn't exist but we can actually check for that so instead of putting in this into a try except block let me just delete this and then let's reduce indentation lab again we can actually check that target exists and then of course again we're not going to do the boolean comparison so that looks like this and then the rest we can leave it like this for the moment game engines and game architectures have always been an interest of mine when i was teaching at the university i taught a game programming course for years there's a few common problems that you'll encounter when you try to create a game from scratch one of them is that you'll have lots of objects in your game world that need to be updated and drawn on the screen and the order in which you update them and in which you draw them matters often objects and games also need to communicate with each other for example if you press a spawn button the enemy spawn unit should start spawning enemies and ideally you'd want to do this in a way that these objects don't know about each other because that's going to lead to a lot of coupling this is also a problem that we have to address in this particular game and i'll talk about a few possible solutions another issue is that objects in a game can be part of some kind of structure for example a grid or a list so you need to be able to structure objects in some way and provide some limitations about how and where they can be positioned you're also going to need to process player input like key presses or mouse movements or mouse clicks without breaking the game loop and making sure that you're not ending up with some kind of race condition somewhere and finally when you're creating a game engine you also have to deal with time and how that interacts with the game loop for example you might want to create some scheduling system that allows you to execute a certain command after a given delay and then there is the difference between time in the game versus time in real life that you also have to think about so lots of aspects unfortunately i won't be able to cover all of these things in fact i could probably do a whole series about developing game engines and game engine architectures alone if that's something you'd like me to cover in a future video series let me know in the comments so now what i'm going to do is create a simple game engine and i use tab 9 to help me write this code more quickly tab 9 is an ai assistant that provides smart code completion in your ide it supports over 30 languages including python in 15 ides including vs code and pycharm tab 9 offers both a local model and a cloud model you can choose to run tapline locally only and your code never leaves your machine this also means you can use it to work offline while ensuring maximum security and privacy tap 9 recently launched tab 9 for teams which will learn your teams projects preferences and patterns suggesting even better code completions for you and your team members you can get tap 9 basic as a free extension to your ide of choice if you're a student you can get tap 9 pro free for more information go to tab 9.com students or if you're not a student you can use coupon code ariane to get a discount of the pro plan the links are in the description of this video so what i'm going to do now is create a separate file called game and this is going to form the basis of our simple game engine that we're going to use for this tower defense game so as first step i'm going to copy over this game class and then we're going to refactor that class and make it a bit more generic so this is a copy of the game class and obviously it now contains a lot of tower defense specific stuff that we're going to remove in a minute so the first thing i'm going to do is import tk enter because that's what we're going to need to set up the canvas and then when we refer to tk enter then i have to do it like this so there's a couple of things that the game class depends on it needs a width and a height so you see that it relies on this global map size variable that obviously we don't have here and there is also a fixed title that we probably want to pass as a parameter and a few other things so let's add a few parameters to this initializer to make the game class a bit more generic and what we're going to add is the title which is a string we're going to add a width and we're going to add a height and what we're also going to do is currently the run method calls itself again after 15 milliseconds i'd like to make this time step value an option as well in the constructor and i'm going to give it a default value of 50 milliseconds so now instead of putting the title here i'm just gonna write the title that we got as a parameter and let's see we also need to put tk in front of this because the frame comes from tk then we're going to create the canvas which is also a tk object that we go and for the width we're going to change that to width and the height we're going to change that to the height then we have all these tower defense specific things which we're going to remove here there we go and what remains is that it calls the run method and then it calls the main loop method so i think this is not very clean i'd prefer to call the run method explicitly after i've created the game also because this might result into problems if you create a subclass of game where you're creating the game objects and then you call the run method when you're initializing the game super class and that might lead to all kind of weird race conditions so i'm going to remove this run call here because we're going to call that explicitly after we've initialized the game and the same thing for this main loop we're also not going to call that here so the first thing that i'll do is i'll replace this by the time step which obviously we're going to have to store inside the game class so you see that tab 9 also provides me with a couple of useful code completions here so i think we're all set now in the game class except that initially our running variable should be set to false because we're not calling the run method so this is going to be false and what i'm going to do is split this into two parts there's basically one part that deals with starting the tk inter main loop and another part is actually starting and running the game loop so i'm gonna rename this method and call this underscore run and that's basically going to do the thing that we want the run method to do in as part of the game loop and i'm going to create another function to actually start the game loop and you could also call this start but i'm just calling it run for now so what this does is it sets the running variable to true which tab 9 completes very nicely for me and then it's going to call the underscore run method and it's also starting the main loop there we go so that's the run method we're going to restructure this a little bit and for now i'm going to remove that here the underscore run method updates the game and then paints the game and then it's going to start a new timer but the new timer we're only going to start it if the game is running there we go and as a part of that let me select this and let's indent that this line we don't need and here we're going to put self dot time step and in order to deal with the timer and perhaps canceling it we also need to store the timer id there we go and then this is not calling the this run method but it's going to be calling this run method i'll just remove these commands here because they don't really add much so now we have our new run method and then when we stop the game we're first gonna set running to false then if the timer id is not none then we're going to cancel the timer there we go and you see also here tap 9 really helps in completing this code and making good suggestions to finish my lines which is really helpful and then finally we call the destroy method so cancelling the timer if there is a timer and then we're destroying it that means now the first part of our game class has become kind of a generic version and let's see we have an update method and we have a paint method now because these are at the moment very game specific you'd say it doesn't make a lot of sense to put these methods in there but what i'm going to do is add a little bit of a more generic approach to updating and painting game objects in this example so what i'm going to do is create a so-called game objects that the other classes can then inherit from and what i'm going to use for that is protocols i didn't talk a lot about protocols on my channel yet until now but they're basically a kind of updated version of abstract based classes that also allow for static type checking so what i'm going to do is i'm going to create a class called game object and that's a protocol class and that has two methods update the game and we have a paint the game and we should probably call that game object and do the same thing here and i actually saw tap nine already suggesting that when we paint the game we're going to need one more parameter which is the canvas so let's add that here so now paint has the canvas so now we have a generic structure of a game object and if you want to make a more complete game engine you could extend this class with lots of other features like giving it a x and y position giving it a speed things like that so now what you can do is let game have a list of these game objects so let's create something called objects and and that's of type list of game objects there we go and then we can add a few methods to add and remove game objects so we're going to remove a game object as well there we go so we have adding and removing objects and now actually updating and painting the game is very simple it just needs to update and paint the objects in the list so i'm going to add a line here this updates the game thank you tap nine and then for in self.objects we're going to do object.update and i'll just delete all these things here because we don't need that anymore there we go and for paint this is going to be more or less the same thing so we're going to delete everything on the screen so that's something we can leave in here all is something that belongs to tk and then for in self objects we're going to paint that object there we go and now this we can delete now that looks a lot cleaner and because the game is now responsible for updating and drawing the objects what we can do in the tower defense game itself is to basically add these objects to the list and then game is going to take care of updating and drawing them and actually what you can do then is that the update and paint methods here are basically going to be almost empty because when you initialize a game then you're going to create the game put the objects inside the list and then the game itself is going to be responsible for updating and drawing those objects so what we can do now to use our new game engine is imported here there we have our game class and then our game class here let's also call that something more specific let's call this a tower defense game and that's a subclass of game and then first let's call the super method and pass it the information that we'd like to give it so we have the title and the width and the height so the title is going to be tower defense game or what was it called tower defense ultra mode and for the width and height we're going to use the map size which is a global but now it's easier to separate this out and use something else so that goes the super dot init method and actually in the refactoring it did something wrong so let's remove this because we're going to import game there we go and now we can import game and then it should look a lot better yes there we go and now we can remove a lot of this code from the tower defense game and this is basically all the code that's not specific anymore to this particular game so the canvas we don't need we don't need that and we basically can create our objects that are here and let's also delete this and the run method we don't need that either there we go the end method we don't need either and in the update and pain method we just have to make sure that we call the version of the super class as well so there we go and in the draw methods let's do that as well there we go and one thing i'd like to do to make this a bit cleaner is that i'll move all this initialization code here into a separate method and that way we're sure that the game object is properly created before we start adding things to it so that's an initialize method and then we're going to create the display board the info board tower box all these different objects and now instead of keeping track of all these objects here ourself what we can do is basically add them as a game object by calling this add object method so we're going to add the mouse we're going to add the map and we're going to add the wave generator and the order of things here is actually important so let me put that here because obviously we want the mouse to be shown on top of the map and this is also the order in which these things are being drawn so i just remove these things here and now we don't need to call the update anymore because the game engine is going to take care of that for us and the same thing for painting so the map we don't need to paint that's already going to happen and the mouse is the same thing and this we can also delete because we don't need that anymore that's already dealt with by the game engine so you can see this already cleans up our game class a lot by doing this now before we finish the refactoring work there's one more thing i'd like to show you and that's a common problem that happens in games where objects are dependent on each other what i was showing you is that the next wave button so here it is so you see that next wave button is when it checks if it's pressed it's going to call directly the wave generator to start a new wave now obviously at the moment what we did in the game class is that we actually removed the direct reference to the wave generator inside the game so that means you can't have this direct dependency anymore that was here before in the next wave button so how can we now instruct wave generator to start a new wave when we click the button one thing you could do is basically add an event system to your game so that you could post an event start new wave and then the wave generator would check that this event occurred and then it could handle the event so that's a way to decouple it another way to do it is to use game states instead if you look at tower defense game there's basically three main states there is an idle state when there's basically nothing's happening or the player is maybe placing some towers here and there there's a spawning state when the game is currently spawning enemies and there's a state where you just click the button and you're waiting for a spawn to start so what you can also do to reduce the coupling that's between these objects in the tower defense game is by introducing these states and then let the objects respond to state changes instead of direct calls from other objects and then you can decouple them nicely in their own dependence on the state of the game so let's do something here let's create an enum to represent different states in the game so i'm going to import enum in auto and let's create a game state here so there we go we have three states idle waiting for spawn and spawning and then when we create the tower defense game instance let's also add a state and initially this state is going to be idle and then let's also add a set state method so that other objects in the game can update the state of the game there we go so now what you can do in the next wave button is instead of directly calling the wave generator here you can update the game state let's also make sure that the type is correct here that's a tower defense game and then this is going to update the state there we go i'll remove this line so now we've removed the dependency of next wave button on the wave generator and then in wave generator so that's here what we can do is if self.game.state equals this gamestate wait for spawn then what we're doing is calling the getwave method there we go then the getwave method is going to put the state to spawning and because we're now dealing with states instead of direct coupling with objects we also don't need to set this can press thing anymore because that's something that now fully depends on the state of the game so you can only press the next wave button if the state is idle so i'm going to remove this line because it's not needed anymore so here you see an example of how you can decrease the coupling between the different elements of your game by introducing things like a game state and then make things dependent on that game state instead obviously this refactor is far from finished but having a basic game setup like this already helps and now you can start separating out the various different objects and combine them in a more loosely coupled way and by doing that you'll also start to remove all these global variables that are still in there and in the end that's going to result in a much better architecture of this game so that was it for today i hope you enjoyed this code roast thanks for guccific to supply this game it was really nice to work on this a little bit scary with the global variables if you enjoyed this don't forget to subscribe like the video and see you next time [Music] you
Info
Channel: ArjanCodes
Views: 223,715
Rating: undefined out of 5
Keywords: refactoring, refactoring code, refactoring in software engineering, refactoring martin fowler, refactoring legacy code, refactoring python, refactoring python code, refactoring in sofware engineering, refactoring in software engeneering, refactoring in sofware engeneering, software design, game engine architecture, code roast, game engine code, python game engine, python game, game python, Game loop
Id: 8eWYxNpMjSU
Channel Id: undefined
Length: 36min 48sec (2208 seconds)
Published: Fri Sep 03 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.