blog post cover

The Refactoring process in Rings code

Presenting the Rings project and talking about some refactoring that I did


Sumário

Hi There! I would like to introduce to you one of my (and Back to Pixels) project Rings. And perhaps show some big refactorings that I did some days ago.

Presenting Rings

Rings is a pygame video-game project that initially was inspired by Enter the Gungeon and Nuclear Throne (even the sprite animation test was done using its sprites). But with time (and some studies) I realized that this scope was too big for my first game, and by the time I was pretty into Vampire Survivors, so I decided to redirect Rings to be a "stand-still" rogue-like game. Aiming to try some video-game software architecture and push to the limits that python can do as a game development language!

The name rings was referring to the upgrades in the game, that would be rings that you could use, a maximum of 10.

But a complete game is still a big scope for someone that studies and has a full-time job. So, my current idea is to develop each feature of a simple game development framework just to experiment with some module architectures.

And here today I will show you what was Rings was before and after some cool structural changes that I made based on some simple things that I've learned recently.

How It Was And Where It Is Going

Since I'm just developing feature to feature, my architecture thing was not that important, so I kept the code struct as the simple pygame tutorial would give you, which is, extremely bad, but with the simple check-events, update entities, render entities order.

But after seeing some DaFluffyPotato live streams I start to recognize a pattern in his code. He put everything into a Game class, and passes this game class to everyone inside it (this part is pretty dangerous but ok), and inside this class, he has a world attribute (of the class World) and an Input attribute (of the class Input) and so on for every major thing in his game. He also had some classic structs such as a list of entities and a list of projectiles.

Anyways, this architecture can help a lot such in debugging (because um can tear apart things easily) and in modularity.

So that's where I aimed to when refactoring Rings for (almost) the first time.

Some Bad Code

Here I will show you how the code was (it is a little bit longer, so maybe I will change it in the future)

1configs = json.load(open('config.json')) 2 3pygame.init() 4pygame.mixer.pre_init(44100, -16, 2, 512) 5pygame.mixer.set_num_channels(32) 6pygame.display.set_caption("Rings") 7pygame.mouse.set_visible(False) 8 9FONT = pygame.font.Font("res/Pixellari.ttf", 22) 10 11global debugging 12game_time = 0 13base_screen_size = configs['resolution'] 14screen = pygame.display.set_mode( 15 (base_screen_size[0],base_screen_size[1]),0,32 16) 17display = pygame.Surface((300,200)) 18clock = pygame.time.Clock() 19screen_real_size = display.get_size() 20camera = Vector(0,0) 21 22player = Player(Vector(10,10)) 23player.load_animations() 24 25TILE_SIZE = 20 26 27game_map = [...] 28 29cursor_img = pygame.transform.scale( 30 pygame.image.load('res/mouse.png').convert(), (44, 44) 31) 32 33cursor_img.set_colorkey((255, 0, 0)) 34 35camera_speed = 20 36debugging = True 37running = True 38 39while running: 40 41 for event in pygame.event.get(): 42 check_events(event,player) 43 44 mx, my = pygame.mouse.get_pos() 45 46 true_mx = mx 47 true_my = my 48 49 mx -= (screen.get_width() - base_screen_size[0]) // 3 50 my -= (screen.get_height() - base_screen_size[1]) // 3 51 mx /= base_screen_size[0] / display.get_width() 52 my /= base_screen_size[1] / display.get_height() 53 54 camera.x = player.position.x + 7 - screen_real_size[0] /2 # the +7 is half of the size of the player, to certralyze 55 camera.y = player.position.y + 7 - screen_real_size[1] /2 56 57 player.is_not_walking = not player.is_moving_right and not player.is_moving_up and not player.is_moving_down and not player.is_moving_left 58 59 60 scroll = camera 61 scroll.x = int(scroll.x) 62 scroll.y = int(scroll.y) 63 64 display.fill((30,30,30)) 65 66 collision_tiles = [] 67 tile_rects =[] 68 69 y = 0 70 71 for row in game_map: 72 73 x= 0 74 75 for tile in row: 76 #<big tiles logic and draw> 77 for t in row: 78 #<more logic and draw world stuff> 79 80 player.update(mx, collision_tiles,camera) 81 player.draw(display,debugging) 82 83 screen.blit(pygame.transform.scale(display, base_screen_size), 84 85 ((screen.get_width() - base_screen_size[0]) // 2, 86 87 (screen.get_height() - base_screen_size[1]) // 2)) 88 89 screen.blit(cursor_img, (true_mx // 3 * 3 + 1, true_my // 3 * 3 + 1)) 90 91 if debugging: 92 93 utils.draw_text(FONT, "FPS: " + str(int(clock.get_fps())), screen, (10,10)) 94 95 pygame.display.update() 96 clock.tick(60)

I removed some unnecessary parts

Some Less-bad Code

1class Game: 2 _entities: list 3 game_time: int 4 player: Player 5 window: Window 6 world: World 7 mouse: Mouse 8 camera: Camera 9 10 def __init__(self): 11 configs = json.load(open("config.json")) 12 self.window = Window(configs) 13 self.mouse = Mouse(self.window) 14 self.world = World() 15 self.player = Player(self) 16 self.player.load_animations() 17 self.camera = Camera( 18 self.player, 19 self.window.screen_real_size 20 ) 21 22 23 self._entities = [] 24 self._entities.append(self.player) 25 self.running = True 26 self.clock = pygame.time.Clock() 27 self.FONT = pygame.font.Font("res/Pixellari.ttf", 22) 28 29 30 def update(self): 31 self.world.update() 32 self.mouse.update() 33 self.camera.update() 34 for entitiy in self._entities: 35 entitiy.update() 36 37 pygame.display.update() 38 self.clock.tick(60) 39 40 def draw(self): 41 self.window.display.fill((30, 30, 30)) 42 self.world.draw(self.window.display, self.camera.position) 43 44 for entitiy in self._entities: 45 entitiy.draw(self.window.display, self.camera.position) 46 47 48 self.window.blit_displays() 49 self.mouse.draw(self.window.screen) 50 51 def run(self): 52 while self.running: 53 self.check_events() 54 self.update() 55 self.draw() 56 57game = Game().run()

Even with this "better" code, there are always things to improve everywhere. Such as an Input class to handle all the input commands and logic (that I hid in this example), or some particles and sound managers (that I didn't implement yet).

As Uncle Bob says in "Clean Architecture":

Good (clean and well architectured) code, should look like a well-written poem

The new rings architecture looks like some Ok poem, but I think it could be better, mainly in the Player class (the animation system is kind of messed up). In the end, all I did was hide things properly and reallocate them where they should be.

All the world logic and drawing are inside the World class, for example, and the same thing applies to all the other pieces of the game. It's clear that I don't have much to organize, but it was a good starting point to do all these refactorings.

What Rings Have Until Now?

Rings started as an experiment, just me trying to implement some cool game-development features. So, the project doesn't have much, just some split, badly implemented simple features, like particles, animations, collisions, and sparks (that I just copied from DaFlufy). But those simple things that I have will be re-write and documented here in the future.

For now, the game looks something like this:

game-screen-shot.webp

I used the Nuclear Throne animation to test some stuff ;)

Conclusion

As I said, Rings are only the start, and I pretend to turn it into a simple rogue-like game, and that's some of the things I will bring her to the game-studio blog.

I will bring some cool implementations and mainly focus on game-design architecture.

Hope you like it and Thanks for reading! You can follow all Rings articles in tag/rings

You can check out this project on my RenatoCesraF/Rings

Recomendations