├── tutorials ├── tutorial_2 │ └── README.md └── tutorial_1 │ ├── chase_game.py │ └── README.md ├── examples ├── sprites │ ├── player1.gif │ ├── player2.gif │ └── sprites.py ├── basics │ ├── writing_text.py │ ├── text_align.py │ ├── key_presses.py │ ├── keys_presses_functions.py │ ├── detecting_clicks.py │ ├── drawing_shapes.py │ ├── movement.py │ ├── collisions.py │ ├── buttons_simple.py │ ├── buttons.py │ └── scenes.py ├── games │ ├── apple_catch.py │ ├── chase_game_simple.py │ ├── platformer.py │ ├── flappy_bird.py │ ├── chase_game.py │ ├── aim_trainer.py │ └── pong.py └── lesson_plans │ └── snowglobe.py ├── LICENSE ├── .gitignore ├── README.md └── tphysics.py /tutorials/tutorial_2/README.md: -------------------------------------------------------------------------------- 1 | Coming soon: Tutorial 2 -------------------------------------------------------------------------------- /examples/sprites/player1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebillington/tphysics/HEAD/examples/sprites/player1.gif -------------------------------------------------------------------------------- /examples/sprites/player2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebillington/tphysics/HEAD/examples/sprites/player2.gif -------------------------------------------------------------------------------- /examples/basics/writing_text.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game 2 | 3 | # Create a game object 4 | game = Game("Score Game", "red") 5 | 6 | # Create a score variable 7 | score = 0 8 | 9 | # Game Loop 10 | while True: 11 | 12 | # Write the score using write(x, y, text, colour, size) 13 | game.write(-100, 100, f"Score: {score}", "black", 20) 14 | 15 | # Add 1 to the score 16 | score += 1 17 | 18 | # Update the game 19 | game.update() -------------------------------------------------------------------------------- /examples/basics/text_align.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game 2 | 3 | # Create a game object 4 | game = Game("Score Game", "red") 5 | 6 | # Game Loop 7 | while True: 8 | 9 | # Write aligned text using write(x, y, text, colour, size, align) 10 | game.write(0, 100, "Left align", "black", 20) 11 | game.write(0, 50, "Center align", "black", 20, align="center") 12 | game.write(0, 0, "Right align", "black", 20, align="right") 13 | 14 | # Update the game 15 | game.update() -------------------------------------------------------------------------------- /examples/basics/key_presses.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle 2 | 3 | # Create game object 4 | game = Game("Key Press Game", "light blue") 5 | 6 | # Create player object 7 | player = Rectangle(0, 0, 20, 20, "green") 8 | game.add_shape(player) 9 | 10 | # Game loop 11 | while True: 12 | 13 | #Check whether a specific key is being pressed 14 | if game.ispressed("Right"): 15 | #Change the x speed 16 | player.x += 1 17 | 18 | # Update the game 19 | game.update() -------------------------------------------------------------------------------- /examples/basics/keys_presses_functions.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Circle 2 | 3 | # Create a game object 4 | game = Game("Higher Order Keys Example", "light blue") 5 | 6 | # Create a player 7 | player = Circle(0, 0, 10, "orange") 8 | game.add_shape(player) 9 | 10 | #Create a function to handle the up key press 11 | def up(): 12 | #Move the player up 13 | player.y += 1 14 | 15 | #Pass the function to the game object 16 | game.addkeypress(up, "Up") 17 | 18 | # Game loop 19 | while True: 20 | 21 | # Update the game 22 | game.update() -------------------------------------------------------------------------------- /examples/basics/detecting_clicks.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle 2 | 3 | # Create game object 4 | game = Game("Click Game", "light blue") 5 | 6 | # Create a player 7 | player = Rectangle(0, 0, 20, 20, "green") 8 | game.add_shape(player) 9 | 10 | #Create a function to handle the click 11 | def click(x, y): 12 | 13 | # Move player to click location 14 | player.x = x 15 | player.y = y 16 | 17 | #Add the click listener 18 | game.addclick(click) 19 | 20 | # Game loop 21 | while True: 22 | 23 | # Update the game 24 | game.update() -------------------------------------------------------------------------------- /examples/basics/drawing_shapes.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle, Circle 2 | 3 | #Create a new game object and store it in a variable 4 | game = Game("Basic Game", "light blue") 5 | 6 | #Create a player Rectangle(x, y, width, height, colour) 7 | player = Rectangle(-100, 100, 20, 50, "orange") 8 | game.add_shape(player) 9 | 10 | #Create an obstacle Circle(x, y, radius, colour) 11 | obstacle = Circle(100, 100, 50, "green") 12 | game.add_shape(obstacle) 13 | 14 | # Game loop 15 | while True: 16 | 17 | # Render the next frame 18 | game.update() -------------------------------------------------------------------------------- /examples/basics/movement.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle, Circle 2 | 3 | #Create a new game object and store it in a variable 4 | game = Game("Basic Game", "light blue") 5 | 6 | #Create a player Rectangle(x, y, width, height, colour) 7 | player = Rectangle(0, 0, 20, 20, "orange") 8 | game.add_shape(player) 9 | 10 | # Store the direction 11 | direction = 1 12 | 13 | # Game loop 14 | while True: 15 | 16 | # Move the player by direction 17 | player.x += direction 18 | 19 | # If player goes above x=100 or below x=-100, flip direction 20 | if player.x > 100 or player.x < -100: 21 | direction = direction = direction * -1 22 | 23 | # Render the next frame 24 | game.update() -------------------------------------------------------------------------------- /examples/sprites/sprites.py: -------------------------------------------------------------------------------- 1 | #Imports 2 | from tphysics import Sprite, Game 3 | 4 | #Create a new game 5 | g = Game("Sprite Game", "light blue") 6 | 7 | #Create two sprites for each of the characters 8 | pOne = Sprite("player1.gif", g.window, -100, 0) 9 | pTwo = Sprite("player2.gif", g.window, 100, 0) 10 | 11 | #Add the sprites to the game 12 | g.add_sprite(pOne) 13 | g.add_sprite(pTwo) 14 | 15 | #Game loop 16 | while True: 17 | 18 | #Check for key presses 19 | if g.ispressed("w"): 20 | pOne.move(0, 1) 21 | if g.ispressed("s"): 22 | pOne.move(0, -1) 23 | if g.ispressed("a"): 24 | pOne.move(-1, 0) 25 | if g.ispressed("d"): 26 | pOne.move(1, 0) 27 | if g.ispressed("Up"): 28 | pTwo.move(0, 1) 29 | if g.ispressed("Down"): 30 | pTwo.move(0, -1) 31 | if g.ispressed("Left"): 32 | pTwo.move(-1, 0) 33 | if g.ispressed("Right"): 34 | pTwo.move(1, 0) 35 | 36 | # Render next frame 37 | g.update() -------------------------------------------------------------------------------- /examples/basics/collisions.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle, Circle 2 | from random import randint 3 | 4 | # Create game object 5 | game = Game("Collision Game", "light blue") 6 | 7 | # Create player 8 | player = Rectangle(0, 0, 20, 20, "green") 9 | game.add_shape(player) 10 | 11 | # Create obstacle at random position 12 | obstacle = Circle(randint(-300,300), randint(-300,300), 5, "red") 13 | game.add_shape(obstacle) 14 | 15 | # Game loop 16 | while True: 17 | 18 | # Check for key presses and move the player 19 | if game.ispressed("Left"): 20 | player.x -= 1 21 | if game.ispressed("Right"): 22 | player.x += 1 23 | if game.ispressed("Down"): 24 | player.y -= 1 25 | if game.ispressed("Up"): 26 | player.y += 1 27 | 28 | # Check for a collision 29 | if player.collide(obstacle): 30 | # Move the obstacle to a rand location between -300 and 300 31 | obstacle.x = randint(-300,300) 32 | obstacle.y = randint(-300,300) 33 | 34 | # Render the next frame 35 | game.update() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Billy Rebecchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/basics/buttons_simple.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Button, Rectangle 2 | 3 | # Instantiate a game object 4 | game = Game("test", "blue") 5 | 6 | # Create a player rectangle and add to the game 7 | player = Rectangle(0, 100, 20, 20, "orange") 8 | game.add_shape(player) 9 | 10 | # Create 2 buttons with different labels, positions and colours 11 | button_a = Button(-100, 0, 150, 20, "A Button", button_colour="green", text_colour="white", padding=12) 12 | button_b = Button(100, 0, 150, 20, "B Button", button_colour="red", text_colour="black", padding=12) 13 | 14 | # Add buttons to the game∂ 15 | game.add_button(button_a) 16 | game.add_button(button_b) 17 | 18 | # Define a click handler function 19 | def click(x,y): 20 | 21 | # Check for A Button press and if so, move player left 22 | if button_a.check_click(x,y): 23 | player.x -= 10 24 | 25 | # Check for B Button press and if so, move player left 26 | if button_b.check_click(x,y): 27 | player.x += 10 28 | 29 | # Add click handler to the game as a higher order function 30 | game.addclick(click) 31 | 32 | # Game loop 33 | while True: 34 | 35 | # Render the next frame of the game (no logic as all logic is in click handler) 36 | game.update() -------------------------------------------------------------------------------- /examples/basics/buttons.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle, Button 2 | 3 | # Create a game object 4 | game = Game("test", "blue") 5 | 6 | # Create a player and add to the game 7 | player = Rectangle(0, 0, 20, 20, "yellow") 8 | game.add_shape(player) 9 | 10 | # Create a pause button 11 | pause_button = Button(300, 300, 50, 20, "Pause", button_colour="green") 12 | game.add_button(pause_button) 13 | 14 | # Set global variable playing to true 15 | global playing 16 | playing = True 17 | 18 | def click(x,y): 19 | 20 | # Check if pause button clicked 21 | if pause_button.check_click(x,y): 22 | 23 | # Get the global variable playing and flip it 24 | global playing 25 | playing = not playing 26 | 27 | # Switch the colour of the button 28 | if playing: 29 | pause_button.rect.fill_colour = "green" 30 | else: 31 | pause_button.rect.fill_colour = "red" 32 | 33 | # Add the click handler to the game 34 | game.addclick(click) 35 | 36 | # Game loop 37 | while True: 38 | 39 | # Check if we are playing 40 | if playing: 41 | 42 | # Check key presses 43 | if game.ispressed("Up"): 44 | player.y += 1 45 | if game.ispressed("Down"): 46 | player.y -= 1 47 | if game.ispressed("Right"): 48 | player.x += 1 49 | if game.ispressed("Left"): 50 | player.x -= 1 51 | 52 | # If not playing 53 | else: 54 | 55 | # Show pause message 56 | game.write(0, 20, "Game is Paused", "white", 20, align="centre") 57 | 58 | # Render the next frame 59 | game.update() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | docs -------------------------------------------------------------------------------- /examples/games/apple_catch.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle, Circle 2 | from random import randint 3 | 4 | # Create a new game object 5 | game = Game("Apple Catch", "light blue") 6 | 7 | # Create a player rectangle 8 | player = Rectangle(0, -200, 60, 30, "brown") 9 | game.add_shape(player) 10 | 11 | # Create a circle for the apple 12 | apple = Circle( randint(-250,250), 300, 10, "red") 13 | game.add_shape(apple) 14 | 15 | # Set initial score to zero 16 | score = 0 17 | 18 | # Initialise the player and apple speeds 19 | player_speed = 2 20 | apple_speed = 3 21 | 22 | # Game loop 23 | while True: 24 | 25 | # Move the player in response to key presses 26 | if game.ispressed("Right"): 27 | player.x += player_speed 28 | if game.ispressed("Left"): 29 | player.x -= player_speed 30 | 31 | # Make the apple fall on every frame 32 | apple.y -= apple_speed 33 | 34 | # Check if the player has caught the apple 35 | if player.collide(apple): 36 | 37 | # Generate a new apple position 38 | apple.x = randint(-250,250) 39 | apple.y = 300 40 | 41 | # Increase the score and speed 42 | score += 1 43 | player_speed += 1 44 | apple_speed += 1 45 | 46 | # Check if the apple has fallen off the bottom of the screen 47 | if apple.y < -350: 48 | 49 | # Generate a new apple position 50 | apple.x = randint(-250,250) 51 | apple.y = 300 52 | 53 | # Reset the score and speeds 54 | score = 0 55 | player_speed = 2 56 | apple_speed = 3 57 | 58 | # Show the score in game 59 | game.write(-250, 250, f"Score: {score}", "black", 20) 60 | 61 | # Render the next frame 62 | game.update() 63 | -------------------------------------------------------------------------------- /examples/games/chase_game_simple.py: -------------------------------------------------------------------------------- 1 | # This is an example game built to show a simple use of tphysics 2 | # For a more complex example, making use of screen width and height, see chase_game.py 3 | 4 | from tphysics import * 5 | from random import randint 6 | 7 | game = Game("Chase Game") 8 | window_width, window_height = game.get_window_size() 9 | 10 | player = Rectangle(0, 0, 20, 20, "yellow") 11 | game.add_shape(player) 12 | player_speed = 3 13 | 14 | enemy = Rectangle(randint(-300, 300), randint(-300, 300), 10, 10, "red") 15 | game.add_shape(enemy) 16 | enemy_speed = 2 17 | 18 | pickup = Circle(randint(-300, 300), randint(-300, 300), 5, "purple") 19 | game.add_shape(pickup) 20 | 21 | score = 0 22 | high_score = 0 23 | 24 | while True: 25 | 26 | if game.ispressed("Left"): 27 | player.x -= player_speed 28 | if game.ispressed("Right"): 29 | player.x += player_speed 30 | if game.ispressed("Down"): 31 | player.y -= player_speed 32 | if game.ispressed("Up"): 33 | player.y += player_speed 34 | 35 | if enemy.x < player.x: 36 | enemy.x += enemy_speed 37 | elif enemy.x > player.x: 38 | enemy.x -= enemy_speed 39 | if enemy.y < player.y: 40 | enemy.y += enemy_speed 41 | elif enemy.y > player.y: 42 | enemy.y -= enemy_speed 43 | 44 | if player.collide(enemy): 45 | score = 0 46 | player.x = 0 47 | player.y = 0 48 | enemy.x = randint(-300, 300) 49 | enemy.y = randint(-300, 300) 50 | 51 | if player.collide(pickup): 52 | score += 1 53 | pickup.x = randint(-300, 300) 54 | pickup.y = randint(-300, 300) 55 | 56 | if score > high_score: 57 | high_score = score 58 | 59 | game.write(-300, 300, f"Score: {score}", "black", 20) 60 | game.write(-300, 270, f"High Score: {high_score}", "black", 20) 61 | 62 | game.update() -------------------------------------------------------------------------------- /examples/games/platformer.py: -------------------------------------------------------------------------------- 1 | #Imports 2 | from tphysics import Rectangle, Game 3 | 4 | #Create a new game 5 | g = Game("Platformer", "grey") 6 | 7 | #Create the player 8 | player = Rectangle(0, -250, 20, 20) 9 | g.add_shape(player) 10 | 11 | #Create the ground 12 | ground = Rectangle(0, -290, 600, 20) 13 | ground.fill_colour = "blue" 14 | g.add_shape(ground) 15 | 16 | #Fields 17 | gravity = 0.5 18 | fallspeed = -10 19 | jumpspeed = 12 20 | xspeed = 0 21 | global yspeed 22 | yspeed = 0 23 | global jumping 24 | jumping = False 25 | 26 | #Create platforms 27 | platforms = [] 28 | platforms.append(Rectangle(-100, -200, 50, 10)) 29 | platforms.append(Rectangle(-150, 50, 50, 10)) 30 | platforms.append(Rectangle(200, 0, 50, 10)) 31 | platforms.append(Rectangle(-200, 200, 50, 10)) 32 | platforms.append(Rectangle(0, -50, 50, 10)) 33 | for p in platforms: 34 | p.fill_colour = "white" 35 | g.add_shape(p) 36 | 37 | #Handle jumps 38 | def jump(): 39 | global jumping 40 | global yspeed 41 | #If not jumping, jump 42 | if not jumping: 43 | yspeed = jumpspeed 44 | jumping = True 45 | 46 | #Function to resolve platform collisions 47 | def platform(p): 48 | while p.collide(player): 49 | player.y += 0.1 50 | 51 | #Add key listeners 52 | g.addkeypress(jump, "space") 53 | 54 | #Game loop 55 | while True: 56 | 57 | #Handle left and right key presses 58 | if g.ispressed("Left"): 59 | xspeed = -5 60 | elif g.ispressed("Right"): 61 | xspeed = 5 62 | else: 63 | xspeed = 0 64 | 65 | #Move the player 66 | player.x += xspeed 67 | player.y += yspeed 68 | 69 | #Gravity 70 | if yspeed > fallspeed: 71 | yspeed -= gravity 72 | 73 | #If the player collided with the ground 74 | if player.collide(ground): 75 | jumping = False 76 | platform(ground) 77 | 78 | #Check each of the platforms for collisions 79 | for p in platforms: 80 | if p.collide(player): 81 | jumping = False 82 | platform(p) 83 | 84 | # Render next frame 85 | g.update() 86 | -------------------------------------------------------------------------------- /examples/lesson_plans/snowglobe.py: -------------------------------------------------------------------------------- 1 | from tphysics import * 2 | from random import randint 3 | 4 | # Create a new game object 5 | game = Game("Snowglobe", "dark blue") 6 | 7 | # Get the screen width and height 8 | screen_width, screen_height = game.get_window_size() 9 | 10 | # Create an empty list to hold our snowflakes, and the speed of each snowflake 11 | snowflakes = [] 12 | speeds = [] 13 | 14 | # Run the code inside the loop 300 times, to create 300 snowflakes and add each to the list 15 | for i in range(300): 16 | 17 | # Generate a random horizontal position for this snowflake 18 | # between left and right side of screen, based on screen_width 19 | # Remember that an x value of 0 is in the centre of the screen 20 | # so we have to use half the screen width to the left and right 21 | x = randint(-int(screen_width/2),int(screen_width/2)) 22 | 23 | # Starting at the top of the screen, generate a y value randomly 24 | # between 30 and 500, to make each snowflake start at a different height 25 | y = int(screen_height/2) + randint(30, 500) 26 | 27 | # Generate a random size for the snowflake 28 | size = randint(5, 10) 29 | 30 | # Generate a random speed and store it in the speeds list 31 | speeds.append(randint(2,5)) 32 | 33 | # Create the snowflake circle object, add it to the game and snowflakes list 34 | snowflake = Circle(x,y,size,"white") 35 | game.add_shape(snowflake) 36 | snowflakes.append(snowflake) 37 | 38 | # Game loop - this is an infinite loop which we use to update our snowflakes every frame 39 | while True: 40 | 41 | # Iterate from i = 0 to the length of the snowflakes list, to access each snowflake individually 42 | for i in range(len(snowflakes)): 43 | 44 | # Change the y position of the snowflake by its speed 45 | snowflakes[i].y -= speeds[i] 46 | 47 | # Check if the snowflake has fallen off the bottom of the screen 48 | if snowflakes[i].y < -int(screen_height/2) - snowflakes[i].radius: 49 | 50 | # If it has, generate a brand new position, radius and speed 51 | snowflakes[i].x = randint(-int(screen_width/2),int(screen_width/2)) 52 | snowflakes[i].y = int(screen_height/2) + randint(30, 500) 53 | snowflakes[i].radius = randint(5, 10) 54 | speeds[i] = randint(2,5) 55 | 56 | # Generate the next frame and show it 57 | game.update() 58 | -------------------------------------------------------------------------------- /examples/games/flappy_bird.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle, Circle 2 | from random import randint 3 | 4 | # Instantiate a tphysics game object and store in game variable 5 | game = Game("Flappy Bird", "light blue") 6 | 7 | # Instantiate a circle for the player and add it to the game 8 | player = Circle(-300, 0, 20, "yellow") 9 | game.add_shape(player) 10 | 11 | # Set the initial player speed and max speed 12 | player_speed = 0 13 | max_speed = -10 14 | 15 | # Generate a top pipe at a random y position 16 | top_pipe = Rectangle(300, randint(200,750) , 20, 800, "green") 17 | game.add_shape(top_pipe) 18 | 19 | # Generate the bottom pipe, setting position to leave a gap of 150 between this and top pipe 20 | bottom_pipe = Rectangle(300, top_pipe.y - 950 , 20, 800, "green") 21 | game.add_shape(bottom_pipe) 22 | 23 | # Set score to zero 24 | score = 0 25 | 26 | # Game loop 27 | while True: 28 | 29 | # If the player is not at max velocity, increase the speed 30 | if player_speed > max_speed: 31 | player_speed -= 0.3 32 | 33 | # If the space key is pressed, set upward velocity of 6 34 | if game.ispressed("space") and player_speed < -2: 35 | player_speed = 6 36 | 37 | # Move the player by its speed 38 | player.y += player_speed 39 | 40 | # Move the pipes to the left 41 | top_pipe.x -= 2 42 | bottom_pipe.x -= 2 43 | 44 | # Check for a collision with either pipe or player falling off bottom 45 | if player.collide(top_pipe) or player.collide(bottom_pipe) or player.y < -400: 46 | 47 | # Reset the player position, speed and score 48 | player.y = 0 49 | player_speed = 0 50 | score = 0 51 | 52 | # Generate a new position for the pipes 53 | top_pipe.x = 450 54 | bottom_pipe.x = 450 55 | top_pipe.y = randint(200,600) 56 | bottom_pipe.y = top_pipe.y - 950 57 | 58 | # Check if the pipes have gone off the left of the screen 59 | if top_pipe.x < -450: 60 | 61 | # If they have, increase the score 62 | score += 1 63 | 64 | # Generate a new position for the pipes 65 | top_pipe.x = 450 66 | bottom_pipe.x = 450 67 | top_pipe.y = randint(200,600) 68 | bottom_pipe.y = top_pipe.y - 950 69 | 70 | # Show the score in the top left corner at (-300,300) 71 | game.write(-300, 300, f"Score: {score}", "black", 20) 72 | 73 | # Render the next frame 74 | game.update() 75 | -------------------------------------------------------------------------------- /tutorials/tutorial_1/chase_game.py: -------------------------------------------------------------------------------- 1 | from tphysics import Game, Rectangle, Circle 2 | from random import randint 3 | 4 | # Create a new game window 5 | game = Game("Chase Game", "black") 6 | 7 | # Create the player game object in the middle of the screen 8 | player = Rectangle(0, 0, 20, 20, "light blue") 9 | game.add_shape(player) 10 | 11 | # Create a coin with a random position between -250 and 250 on x and y 12 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 13 | game.add_shape(coin) 14 | 15 | # Create enemy with a random position 16 | enemy = Circle(randint(-250,250), randint(-250,250), 10, "red") 17 | game.add_shape(enemy) 18 | 19 | # Set the initial score to zero 20 | score = 0 21 | 22 | # Set the player and enemy speed 23 | player_speed = 2 24 | enemy_speed = 1 25 | 26 | # Game loop which will contain all of our logic 27 | while True: 28 | 29 | # Check key presses 30 | if game.ispressed("Right"): 31 | player.x += player_speed 32 | if game.ispressed("Left"): 33 | player.x -= player_speed 34 | if game.ispressed("Up"): 35 | player.y += player_speed 36 | if game.ispressed("Down"): 37 | player.y -= player_speed 38 | 39 | # Enemy movement checks 40 | if enemy.x < player.x: 41 | enemy.x += enemy_speed 42 | if enemy.x > player.x: 43 | enemy.x -= enemy_speed 44 | if enemy.y < player.y: 45 | enemy.y += enemy_speed 46 | if enemy.y > player.y: 47 | enemy.y -= enemy_speed 48 | 49 | # Check if the player has collected the coin 50 | if player.collide(coin): 51 | 52 | # If so, add to score and generate new coin position 53 | score += 1 54 | coin.x = randint(-250, 250) 55 | coin.y = randint(-250, 250) 56 | 57 | # Check if the score is divisible by 5 58 | if score % 5 == 0: 59 | 60 | # If so, increase player and enemy speed 61 | player_speed += 1 62 | enemy_speed += 1 63 | 64 | # Check if the enemy has hit the player 65 | if enemy.collide(player): 66 | 67 | # Reset the player position and score 68 | score = 0 69 | player.x = 0 70 | player.y = 0 71 | 72 | # Generate new coin and enemy positions 73 | coin.x = randint(-250,250) 74 | coin.y = randint(-250,250) 75 | enemy.x = randint(-250,250) 76 | enemy.y = randint(-250,250) 77 | 78 | # Reset player and enemy speed 79 | player_speed = 2 80 | enemy_speed = 1 81 | 82 | # Show the score 83 | game.write(-300, 300, f"Score: {score}", "white", 20) 84 | 85 | # Each time the game loop runs, at the end, render a new frame 86 | game.update() 87 | -------------------------------------------------------------------------------- /examples/games/chase_game.py: -------------------------------------------------------------------------------- 1 | # This is an example game built to show a simple use of tphysics 2 | # This example makes use of some advanced concepts in terms of using 3 | # Screen Width and Screen Height as markers, to stop the player, enemy 4 | # or pickup from going off of the screen. 5 | # For a simpler example, see chase_game_simple.py 6 | 7 | from tphysics import * 8 | from random import randint 9 | 10 | game = Game("Chase Game", "light blue", fullscreen=True) 11 | window_width, window_height = game.get_window_size() 12 | 13 | player = Rectangle(0, 0, 30, 30, "yellow") 14 | game.add_shape(player) 15 | player_speed = 3 16 | 17 | enemy = Rectangle( 18 | randint(int(-window_width/2) + 30, int(window_width/2) - 30), 19 | randint(int(-window_height/2) + 30, int(window_height/2) - 30), 20 | 20, 21 | 20, 22 | "red" 23 | ) 24 | game.add_shape(enemy) 25 | enemy_speed = 2 26 | 27 | pickup = Circle( 28 | randint(int(-window_width/2) + 30, int(window_width/2) - 30), 29 | randint(int(-window_height/2) + 30, int(window_height/2) - 30), 30 | 5, 31 | "purple" 32 | ) 33 | game.add_shape(pickup) 34 | 35 | score = 0 36 | high_score = 0 37 | 38 | while True: 39 | 40 | if game.ispressed("Left"): 41 | player.x -= player_speed 42 | if game.ispressed("Right"): 43 | player.x += player_speed 44 | if game.ispressed("Down"): 45 | player.y -= player_speed 46 | if game.ispressed("Up"): 47 | player.y += player_speed 48 | 49 | if player.x > int(window_width/2) - player.width: 50 | player.x = int(window_width/2) - player.width 51 | if player.x < -int(window_width/2) + player.width: 52 | player.x = -int(window_width/2) + player.width 53 | if player.y > int(window_height/2) - player.height: 54 | player.y = int(window_height/2) - player.height 55 | if player.y < -int(window_height/2) + player.height: 56 | player.y = -int(window_height/2) + player.height 57 | 58 | if enemy.x < player.x: 59 | enemy.x += enemy_speed 60 | elif enemy.x > player.x: 61 | enemy.x -= enemy_speed 62 | if enemy.y < player.y: 63 | enemy.y += enemy_speed 64 | elif enemy.y > player.y: 65 | enemy.y -= enemy_speed 66 | 67 | if player.collide(enemy): 68 | score = 0 69 | player.x = 0 70 | player.y = 0 71 | enemy.x = randint(int(-window_width/2) + 30, int(window_width/2) - 30) 72 | enemy.y = randint(int(-window_height/2) + 30, int(window_height/2) - 30) 73 | pickup.x = randint(int(-window_width/2) + 30, int(window_width/2) - 30) 74 | pickup.y = randint(int(-window_height/2) + 30, int(window_height/2) - 30) 75 | 76 | if player.collide(pickup): 77 | score += 1 78 | pickup.x = randint(int(-window_width/2) + 30, int(window_width/2) - 30) 79 | pickup.y = randint(int(-window_height/2) + 30, int(window_height/2) - 30) 80 | 81 | if score > high_score: 82 | high_score = score 83 | 84 | game.write(-window_width/2 + 30, window_height/2 - 50, f"Score: {score}", "black", 28) 85 | game.write(-window_width/2 + 30, window_height/2 - 80, f"High Score: {high_score}", "black", 28) 86 | 87 | game.update() -------------------------------------------------------------------------------- /examples/basics/scenes.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | from tphysics import * 3 | 4 | # Create the game 5 | game = Game("Scene Demo") 6 | 7 | # Create scenes and add to the game 8 | menu_scene = Scene("menu", "indigo") 9 | game_scene = Scene("game", "light blue") 10 | game.add_scene(menu_scene) 11 | game.add_scene(game_scene) 12 | 13 | # Switch to the menu scene 14 | game.load_scene(menu_scene) 15 | 16 | # Create the menu items and add them to the scene 17 | play_button = Button(0, 50, 100, 50, "Play", button_colour="green") 18 | menu_scene.add_button(play_button) 19 | 20 | quit_button = Button(0, -50, 100, 50, "Quit", button_colour="red") 21 | menu_scene.add_button(quit_button) 22 | 23 | # Create the game scene 24 | player = Rectangle(0, 0, 20, 20, "yellow") 25 | game_scene.add_shape(player) 26 | player_speed = 3 27 | 28 | enemy = Rectangle(randint(-300, 300), randint(-300, 300), 10, 10, "red") 29 | game_scene.add_shape(enemy) 30 | enemy_speed = 2 31 | 32 | pickup = Circle(randint(-300, 300), randint(-300, 300), 5, "purple") 33 | game_scene.add_shape(pickup) 34 | 35 | score = 0 36 | high_score = 0 37 | 38 | # Click handler 39 | def click(x,y): 40 | 41 | # Check if the menu scene is currently active 42 | if game.current_scene == menu_scene: 43 | 44 | # Check if play button clicked 45 | if play_button.check_click(x,y): 46 | 47 | # Load the game scene 48 | game.load_scene(game_scene) 49 | 50 | # Check if quit button clicked 51 | if quit_button.check_click(x,y): 52 | 53 | # Quit 54 | game.quit() 55 | 56 | # Add click handler 57 | game.addclick(click) 58 | 59 | # Game loop 60 | while True: 61 | 62 | # Check if the game scene is loaded 63 | if game.current_scene == game_scene: 64 | 65 | if game.ispressed("Left"): 66 | player.x -= player_speed 67 | if game.ispressed("Right"): 68 | player.x += player_speed 69 | if game.ispressed("Down"): 70 | player.y -= player_speed 71 | if game.ispressed("Up"): 72 | player.y += player_speed 73 | 74 | if enemy.x < player.x: 75 | enemy.x += enemy_speed 76 | elif enemy.x > player.x: 77 | enemy.x -= enemy_speed 78 | if enemy.y < player.y: 79 | enemy.y += enemy_speed 80 | elif enemy.y > player.y: 81 | enemy.y -= enemy_speed 82 | 83 | if player.collide(enemy): 84 | game.load_scene(menu_scene) 85 | score = 0 86 | player.x = 0 87 | player.y = 0 88 | enemy.x = randint(-300, 300) 89 | enemy.y = randint(-300, 300) 90 | 91 | if player.collide(pickup): 92 | score += 1 93 | pickup.x = randint(-300, 300) 94 | pickup.y = randint(-300, 300) 95 | 96 | if score > high_score: 97 | high_score = score 98 | 99 | game.write(-300, 300, f"Score: {score}", "black", 20) 100 | game.write(-300, 270, f"High Score: {high_score}", "black", 20) 101 | 102 | # Game render 103 | game.update() 104 | -------------------------------------------------------------------------------- /examples/games/aim_trainer.py: -------------------------------------------------------------------------------- 1 | from tphysics import * 2 | from random import randint 3 | 4 | # Create game object 5 | game = Game("Aim Training", "white", fullscreen=True) 6 | 7 | # Get screen size 8 | screen_width, screen_height = game.get_window_size() 9 | 10 | # Create a circle target in the bounds of the screen 11 | target = Circle( 12 | randint(-int(screen_width/2) + 100, int(screen_width/2) - 100), 13 | randint(-int(screen_height/2) + 100, int(screen_height/2) - 100), 14 | 10, 15 | "red" 16 | ) 17 | game.add_shape(target) 18 | 19 | # Track the score and make it a global variable 20 | global score 21 | score = 0 22 | 23 | # Track the high score 24 | high_score = 0 25 | 26 | # Track the number of targets left in the game 27 | global targets_remaining 28 | targets_remaining = 30 29 | 30 | # Create a click handler 31 | def click(x,y): 32 | 33 | # Check if the click collides with the target 34 | if Point(x,y).collide(target): 35 | 36 | # Take one from number of remaining targets 37 | global targets_remaining 38 | targets_remaining -= 1 39 | 40 | # Calculate the value based on the radius of the target 41 | target_value = 150 - target.radius 42 | 43 | # Fetch the score variable and add the correct amount 44 | global score 45 | score += target_value 46 | 47 | # Check if we still have targets left 48 | if targets_remaining > 0: 49 | 50 | # Generate a new location for the target and reset radius 51 | target.x = randint(-int(screen_width/2) + 100, int(screen_width/2) - 100) 52 | target.y = randint(-int(screen_height/2) + 100, int(screen_height/2) - 100) 53 | target.radius = 10 54 | 55 | # If we don't have any targets left 56 | else: 57 | 58 | # Remove the target from the game 59 | game.remove_shape(target) 60 | 61 | # Add the click handler to the game 62 | game.addclick(click) 63 | 64 | # Game loop 65 | while True: 66 | 67 | # Check if we need to increase the size of the target 68 | if target.radius < 100: 69 | target.radius += 1 70 | 71 | # Check if high score needs updating 72 | if score > high_score: 73 | high_score = score 74 | 75 | # Show the score 76 | game.write( 77 | -int(screen_width/2) + 20, 78 | int(screen_height/2) - 30, 79 | f"Score: {score}", 80 | "black", 81 | 20 82 | ) 83 | 84 | # Show the high score 85 | game.write( 86 | -int(screen_width/2) + 20, 87 | int(screen_height/2) - 60, 88 | f"High Score: {high_score}", 89 | "black", 90 | 20 91 | ) 92 | 93 | # Check if game has finished 94 | if targets_remaining == 0: 95 | 96 | # Write end screen text 97 | game.write(-200, 0, f"Your score: {score} - Press 'r' to play again", "black", 30) 98 | 99 | # Check for r press 100 | if game.ispressed("r"): 101 | 102 | # Set score to 0 103 | score = 0 104 | 105 | # Reset number of targets 106 | targets_remaining = 30 107 | 108 | # Generate a new location for the target and reset radius 109 | target.x = randint(-int(screen_width/2) + 100, int(screen_width/2) - 100) 110 | target.y = randint(-int(screen_height/2) + 100, int(screen_height/2) - 100) 111 | target.radius = 10 112 | 113 | # Add target back to game 114 | game.add_shape(target) 115 | 116 | # Render the next frame 117 | game.update() 118 | -------------------------------------------------------------------------------- /examples/games/pong.py: -------------------------------------------------------------------------------- 1 | #Imports 2 | from tphysics import Game, Rectangle, Circle 3 | from random import randint 4 | 5 | #Create the game window 6 | g = Game("Pong Game", "grey") 7 | 8 | #Create a list of walls 9 | walls = [Rectangle(-290, 0, 20, 600), Rectangle(290, 0, 20, 600), Rectangle(0, 290, 600, 20), Rectangle(0, -290, 600, 20)] 10 | 11 | #Set the walls fill colour to black and add them to the game 12 | for w in walls: 13 | #Set the colour 14 | w.fill_colour = "black" 15 | #Add the wall to the game 16 | g.add_shape(w) 17 | 18 | #Create the centre line 19 | centre = Rectangle(0, 0, 5, 600) 20 | #Set the centre square colour 21 | centre.fill_colour = "black" 22 | 23 | #Create variable to hold whether game is running 24 | global running 25 | running = False 26 | 27 | #Create the players 28 | p1 = Rectangle(-250, 0, 30, 100) 29 | p2 = Rectangle(250, 0, 30, 100) 30 | #Set the player colours 31 | p1.fill_colour = "red" 32 | p2.fill_colour = "green" 33 | 34 | #Create the ball 35 | ball = Circle(0, 0, 10) 36 | #Set the ball colour 37 | ball.fill_colour = "white" 38 | 39 | #Add the shapes 40 | g.add_shape(p1) 41 | g.add_shape(p2) 42 | g.add_shape(centre) 43 | g.add_shape(ball) 44 | 45 | #Set the paddle speed 46 | paddle_speed = 10 47 | 48 | #Create functions to deal with key presses 49 | def space(): 50 | global running 51 | running = not running 52 | 53 | #Create a function to reset the game 54 | def reset(): 55 | p1.y = 0 56 | p2.y = 0 57 | ball.x = 0 58 | ball.y = 0 59 | speed[1] = randint(-3, 3) 60 | 61 | #Add the functions to key listeners 62 | g.addkeypress(space, "space") 63 | 64 | #Set the speed of the ball 65 | speed = [3, randint(-3, 3)] 66 | xdirection = 1 67 | 68 | player_one_score = 0 69 | player_two_score = 0 70 | 71 | #Game loop 72 | while True: 73 | 74 | #If the game is running 75 | if running: 76 | 77 | #Handle key presses 78 | if g.ispressed("w"): 79 | p1.y += paddle_speed 80 | while p1.collide(walls[2]): 81 | p1.y -= 1 82 | if g.ispressed("s"): 83 | p1.y -= paddle_speed 84 | while p1.collide(walls[3]): 85 | p1.y += 1 86 | if g.ispressed("Up"): 87 | p2.y += paddle_speed 88 | while p2.collide(walls[2]): 89 | p2.y -= 1 90 | if g.ispressed("Down"): 91 | p2.y -= paddle_speed 92 | while p2.collide(walls[3]): 93 | p2.y += 1 94 | 95 | #Change the balls position 96 | ball.x += speed[0] * xdirection 97 | ball.y += speed[1] 98 | 99 | #If the ball collides with a horizontal wall, reset 100 | for i in range(2): 101 | if walls[i].collide(ball): 102 | running = False 103 | reset() 104 | # Check which wall was hit and increase score 105 | if i == 0: 106 | player_two_score += 1 107 | if i == 1: 108 | player_one_score += 1 109 | #If the ball collides with a top wall, reverse direction 110 | for i in range(2, 4): 111 | if walls[i].collide(ball): 112 | speed[1] = -speed[1] 113 | 114 | #If the ball hits the top of a paddle 115 | if p1.collide(ball) == 1 or p2.collide(ball) == 1: 116 | speed[0] += 1 117 | xdirection = -xdirection 118 | 119 | #If the ball hits the top of a paddle 120 | if p1.collide(ball) == 2 or p2.collide(ball) == 2: 121 | speed[0] += 1 122 | xdirection = -xdirection 123 | 124 | #If the ball hits the bottom of a paddle 125 | if p1.collide(ball) == 3 or p2.collide(ball) == 3: 126 | speed[0] += 1 127 | speed[1] = -speed[1] 128 | 129 | #If the ball hits the corner of a paddle 130 | if p1.collide(ball) == 4 or p2.collide(ball) == 4: 131 | speed[0] += 1 132 | xdirection = -xdirection 133 | speed[1] = -speed[1] 134 | 135 | # If game isn't running 136 | else: 137 | g.write(-85, 300, "Press Space to Start", "black", 20) 138 | 139 | # Show player scores 140 | g.write(-300, 300, f"P1 Score: {player_one_score}", "black", 20) 141 | g.write(200, 300, f"P2 Score: {player_two_score}", "black", 20) 142 | 143 | # Render the next frame 144 | g.update() 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tphysics 2 | 3 | tphysics is a cross platform physics engine built for educational purposes. 4 | 5 | The entire engine is contained in a single file and uses Turtle graphics for rendering, making it an excellent candidate for use in the classroom. 6 | 7 | The code is thoroughly commented with aims to providing a suitable library for people interested in learning the basics of Game Engineering. 8 | 9 | ### Resources 10 | 11 | Teacher's introduction to tphysics in the classroom: https://www.youtube.com/watch?v=6z4ZEE0VqHU 12 | 13 | How tphysics renders using Turtle graphics: https://youtu.be/QQJT1oDcXUQ 14 | 15 | tphysics tutorials: [Watch the Playlist on YouTube](https://www.youtube.com/watch?v=QQJT1oDcXUQ&list=PLMr7li1270gySPa4xz8PVVug1z0bCTil_) 16 | 17 | ### Why tphysics 18 | 19 | Over time I have found that students had a hard time learning games programming. They are able to be creative in blocky environments such as Scratch, which are designed specifically for learning the fundamentals of games programming, but when moving into textual languages and engines such as Unity, they find it much more difficult. 20 | 21 | Learners aren't able to be creative as they are having to learn the complexities of an engine at the same time as a textual language. I found that they were often copying verbatim from a video tutorial and their ability to make maningful changes and improvements to final code was limited. 22 | 23 | tphysics is designed to bridge the gap from a blocky style environment and more feature complete engines. All of the code is contained in a single file which means learners can take a look under the hood and since tphysics only uses simple rectangles and circles, learners develop an intuitive sense of abstraction, as well as a very pure understanding of collision detection. 24 | 25 | ### Supported versions: 26 | * Python 3+ 27 | 28 | ## Contents 29 | 30 | ### [Installation](#install) 31 | 32 | ### [Examples](#tphysics-examples) 33 | 34 | ### [Learn to use tphysics](#using-tphysics) 35 | 36 | * [Creating a game object](#create-a-new-game-object) 37 | * [Rendering the game window](#updating-the-game-window) 38 | * [Creating shapes](#drawing-shapes) 39 | * [Colour](#colour-and-fill) 40 | * [Key presses](#detecting-key-presses) 41 | * [Checking for collisions](#collision-detection) 42 | * [Writing text](#writing-text) 43 | * [Making buttons](#making-buttons) 44 | * [Mouse clicks](#detecting-mouse-clicks) 45 | 46 | ## Install 47 | 48 | ### Using as a script 49 | 50 | Using as a python script is extremely simple and can be done on almost any computer, provided the Python installation includes TKinter. This makes `tphysics` ideal for use in schools as you can just copy the code from this repository and import it directly into your code. 51 | 52 | 1. Open [tphysics.py](https://github.com/thebillington/tphysics/blob/master/tphysics.py) and `copy` the file contents. 53 | 2. Create a new python file in your IDE (likely `IDLE` if you are on school computers) and paste the contents 54 | 3. Save the file as `tphysics.py` - if you save it as anything else, it won't work! 55 | 4. Create a new file in *the same directory/folder* as your `tphysics.py` file and test with `from tphysics import *` 56 | 57 | ### Installing with PIP 58 | 59 | > [!WARNING] 60 | > The following section was no longer relevant as I have updated tphysics to a single file library, to make it simple to download and use on school computers. There are plans to release tphysics officially via `PyPi` but currently, tphysics CANNOT be installed to your python installation with `pip`. 61 | 62 | ## tphysics Examples 63 | 64 | This section is currently being fleshed out. Remember that if you want to run any examples, you must make sure you execute the script from the *same directory* as your `tphysics.py` script. 65 | 66 | You can check out the `examples` folder for a full list of up to date examples. 67 | 68 | ## Using tphysics 69 | 70 | Getting started with tphysics is easy. First, select which **classes** you are going to need and import them. 71 | The following example will make use of the circle, square and game classes. 72 | 73 | #### Create a new game object 74 | 75 | First, create a new game object with the desired title, width, height and colour: 76 | 77 | ```python 78 | #Imports 79 | from tphysics import Game 80 | 81 | #Create a new game object and store it in a variable 82 | game = Game("Basic Game", "light blue") 83 | ``` 84 | 85 | You can make the game window full screen by using `fullscreen=True`: 86 | 87 | ```python 88 | #Imports 89 | from tphysics import Game 90 | 91 | #Create a new game object and store it in a variable 92 | game = Game("Basic Game", "light blue", fullscreen=True) 93 | ``` 94 | 95 | #### Updating the game window 96 | 97 | In order to update any changes you make between frames, you must create a game loop that calls the **update** function. When the update function is called, it renders all of the changes that have happened since the last time you called it and creates a new *frame*: 98 | 99 | ```python 100 | from tphysics import Game 101 | 102 | #Create a new game object and store it in a variable 103 | game = Game("Basic Game", "light blue") 104 | 105 | #Game loop 106 | while True: 107 | 108 | #Update the game 109 | game.update() 110 | ``` 111 | 112 | #### Drawing shapes 113 | 114 | Once you have created a game it is extremely simple to draw shapes. Simply create a new shape, store it in a variable and add it to the game: 115 | 116 | ```python 117 | from tphysics import Game, Rectangle, Circle 118 | 119 | #Create a new game object and store it in a variable 120 | game = Game("Basic Game", "light blue") 121 | 122 | #Create a player Rectangle(x, y, width, height, colour) 123 | player = Rectangle(-100, 100, 20, 50, "orange") 124 | game.add_shape(player) 125 | 126 | #Create an obstacle Circle(x, y, radius, colour) 127 | obstacle = Circle(100, 100, 50, "green") 128 | game.add_shape(obstacle) 129 | 130 | # Game loop 131 | while True: 132 | 133 | # Render the next frame 134 | game.update() 135 | ``` 136 | 137 | Shapes will be drawn in the order they are added. 138 | 139 | It is easy to move shapes by accessing the `x` and `y` properties: 140 | 141 | ```python 142 | from tphysics import Game, Rectangle, Circle 143 | 144 | #Create a new game object and store it in a variable 145 | game = Game("Basic Game", "light blue") 146 | 147 | #Create a player Rectangle(x, y, width, height, colour) 148 | player = Rectangle(0, 0, 20, 20, "orange") 149 | game.add_shape(player) 150 | 151 | # Store the direction 152 | direction = 1 153 | 154 | # Game loop 155 | while True: 156 | 157 | # Move the player by direction 158 | player.x += direction 159 | 160 | # If player goes above x=100 or below x=-100, flip direction 161 | if player.x > 100 or player.x < -100: 162 | direction = direction * -1 163 | 164 | # Render the next frame 165 | game.update() 166 | ``` 167 | 168 | #### Colour and fill 169 | 170 | Changing the colour of objects is easy and can be done by directly accessing the **fill_colour** and **line_colour** variables in your shape. 171 | Any pre-defined colour accepted by [TKinter](https://www.tcl.tk/man/tcl8.4/TkCmd/colors.htm) is allowed. RGB values are also accepted: 172 | 173 | ```python 174 | player.fill_colour = "blue" 175 | obstacle.line_colour = "#00FFF7" 176 | ``` 177 | 178 | It is also easy to enable/disable the line being drawn or the shape being filled: 179 | 180 | ```python 181 | player.fill = False 182 | obstacle.line = False 183 | ``` 184 | 185 | Please note: With the current implementation disabling both of these will draw a shape with an outline the same colour as the fill colour. 186 | 187 | #### Detecting key presses 188 | 189 | Detecting key presses in tphysics is extremely simple. 190 | 191 | The best way to check whether a specific key is pressed is by using the **ispressed** function on your game object. Currently only the alphanumeric, space and arrow keys are supported: 192 | 193 | ```python 194 | from tphysics import Game, Rectangle 195 | 196 | # Create game object 197 | game = Game("Key Press Game", "light blue") 198 | 199 | # Create player object 200 | player = Rectangle(0, 0, 20, 20, "green") 201 | game.add_shape(player) 202 | 203 | # Game loop 204 | while True: 205 | 206 | #Check whether a specific key is being pressed 207 | if game.ispressed("Right"): 208 | #Change the x speed 209 | player.x += 1 210 | 211 | # Update the game 212 | game.update() 213 | ``` 214 | 215 | You can also create a function that you want to handle the key press and pass this to your game object along with the name of the key you want to detect. This is a more advanced way of handling key presses as it requires the passing of `higher order functions`: 216 | 217 | ```python 218 | from tphysics import Game, Circle 219 | 220 | # Create a game object 221 | game = Game("Higher Order Keys Example", "light blue") 222 | 223 | # Create a player 224 | player = Circle(0, 0, 10, "orange") 225 | game.add_shape(player) 226 | 227 | #Create a function to handle the up key press 228 | def up(): 229 | #Move the player up 230 | player.y += 1 231 | 232 | #Pass the function to the game object 233 | game.addkeypress(up, "Up") 234 | 235 | # Game loop 236 | while True: 237 | 238 | # Update the game 239 | game.update() 240 | ``` 241 | 242 | For a full list of available key names, check out the [TK documentation](https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm). 243 | 244 | #### Collision detection 245 | 246 | There is currently thorough collision detection support for circles and rectangles. Shape rotation as of the current time is unsupported. 247 | 248 | In order to check collision between two shapes you must use the collide function: 249 | 250 | ```python 251 | from tphysics import Game, Rectangle, Circle 252 | from random import randint 253 | 254 | # Create game object 255 | game = Game("Collision Game", "light blue") 256 | 257 | # Create player 258 | player = Rectangle(0, 0, 20, 20, "green") 259 | game.add_shape(player) 260 | 261 | # Create obstacle at random position 262 | obstacle = Circle(randint(-300,300), randint(-300,300), 5, "red") 263 | game.add_shape(obstacle) 264 | 265 | # Game loop 266 | while True: 267 | 268 | # Check for key presses and move the player 269 | if game.ispressed("Left"): 270 | player.x -= 1 271 | if game.ispressed("Right"): 272 | player.x += 1 273 | if game.ispressed("Down"): 274 | player.y -= 1 275 | if game.ispressed("Up"): 276 | player.y += 1 277 | 278 | # Check for a collision 279 | if player.collide(obstacle): 280 | # Move the obstacle to a rand location between -300 and 300 281 | obstacle.x = randint(-300,300) 282 | obstacle.y = randint(-300,300) 283 | 284 | # Render the next frame 285 | game.update() 286 | ``` 287 | 288 | The collide function will return **False** for no collision, and **True** for any collision of two same type shapes (circle-circle and rectangle-rectangle). 289 | 290 | There is also support for circle-rectangle collision detection, which will return 0 for no collision and a non-zero value for a collision. 291 | The non-zero values can be used to identify where on the square the centre of the circle collided as per the below chart (1 = center, 2 = alongside, 3 = above/below, 4 = corner, 0 = no collision): 292 | 293 | ``` 294 | | | 295 | 4 3 4 296 | | | 297 | _ _ _______ _ _ 298 | | | 299 | 2 | 1 | 2 300 | _ _ |_______| _ _ 301 | 302 | | | 303 | 4 3 4 304 | | | 305 | ``` 306 | 307 | In future iterations this will be improved to identify which individual corner or side the circle collided with. 308 | 309 | #### Writing Text 310 | 311 | You can write text by passing your desired text into the `write` function: 312 | 313 | ```python 314 | from tphysics import Game 315 | 316 | # Create a game object 317 | game = Game("Score Game", "red") 318 | 319 | # Create a score variable 320 | score = 0 321 | 322 | # Game Loop 323 | while True: 324 | 325 | # Write the score using write(x, y, text, colour, size) 326 | game.write(-100, 100, f"Score: {score}", "black", 20) 327 | 328 | # Add 1 to the score 329 | score += 1 330 | 331 | # Update the game 332 | game.update() 333 | 334 | ``` 335 | 336 | You can align your text using the `align="left"`, `align="center`, or `align="right"` parameters. 337 | 338 | Text is aligned to the left by default. 339 | 340 | ```python 341 | from tphysics import Game 342 | 343 | # Create a game object 344 | game = Game("Score Game", "red") 345 | 346 | # Game Loop 347 | while True: 348 | 349 | # Write aligned text using write(x, y, text, colour, size, align) 350 | game.write(-100, 100, "Left align", "black", 20) 351 | game.write(-100, 40, "Center align", "black", 20, align="center") 352 | game.write(-100, 10, "Right align", "black", 20, align="right") 353 | 354 | # Update the game 355 | game.update() 356 | 357 | ``` 358 | 359 | #### Making Buttons 360 | 361 | Buttons can be created and added to the game the same way that rectangles are, however you also need to pass some text. 362 | 363 | When you create a button, tphysics will automatically use the best text size to fit inside your button. 364 | 365 | ```python 366 | from tphysics import Game, Button, Rectangle 367 | 368 | # Instantiate a game object 369 | game = Game("test", "blue") 370 | 371 | # Create a player rectangle and add to the game 372 | player = Rectangle(0, 100, 20, 20, "orange") 373 | game.add_shape(player) 374 | 375 | # Create 2 buttons with different labels, positions and colours 376 | button_a = Button(-100, 0, 150, 20, "A Button", button_colour="green", text_colour="white", padding=10) 377 | button_b = Button(100, 0, 150, 20, "B Button", button_colour="red", text_colour="black", padding=10) 378 | 379 | # Add buttons to the game 380 | game.add_button(button_a) 381 | game.add_button(button_b) 382 | 383 | # Define a click handler function 384 | def click(x,y): 385 | 386 | # Check for A Button press and if so, move player left 387 | if button_a.check_click(x,y): 388 | player.x -= 10 389 | 390 | # Check for B Button press and if so, move player left 391 | if button_b.check_click(x,y): 392 | player.x += 10 393 | 394 | # Add click handler to the game as a higher order function 395 | game.addclick(click) 396 | 397 | # Game loop 398 | while True: 399 | 400 | # Render the next frame of the game (no logic as all logic is in click handler) 401 | game.update() 402 | ``` 403 | 404 | #### Detecting mouse clicks 405 | 406 | Mouse click detection is handled in a very similar way to key presses. 407 | Simply create a function that you want to handle your click and pass it in to the click listener: 408 | 409 | ```python 410 | from tphysics import Game, Rectangle 411 | 412 | # Create game object 413 | game = Game("Click Game", "light blue") 414 | 415 | # Create a player 416 | player = Rectangle(0, 0, 20, 20, "green") 417 | game.add_shape(player) 418 | 419 | #Create a function to handle the click 420 | def click(x, y): 421 | 422 | # Move player to click location 423 | player.x = x 424 | player.y = y 425 | 426 | #Add the click listener 427 | game.addclick(click) 428 | 429 | # Game loop 430 | while True: 431 | 432 | # Update the game 433 | game.update() 434 | ``` 435 | 436 | By default the addclick function sets clicks to the left mouse button. You can specify a left or right click using 1 or 2 respectively: 437 | 438 | ```python 439 | #Left click listener 440 | g.addclick(click, 1) 441 | 442 | #Right click listener 443 | g.addclick(click, 2) 444 | ``` 445 | -------------------------------------------------------------------------------- /tphysics.py: -------------------------------------------------------------------------------- 1 | # IMPORTS 2 | import math 3 | import turtle 4 | from turtle import Turtle 5 | from tkinter import TclError 6 | from tkinter.font import Font 7 | from time import sleep 8 | import sys 9 | from functools import partial 10 | import logging 11 | 12 | # SHAPES 13 | 14 | #Define a class to hold opur shape 15 | class Shape: 16 | 17 | #Types 18 | POINT = "point" 19 | RECT = "rectangle" 20 | CIRCLE = "circle" 21 | 22 | #Implement our init function 23 | def __init__(self, x, y, type): 24 | self.x = x 25 | self.y = y 26 | self.type = type 27 | 28 | self.line_colour = "black" 29 | self.fill_colour = "red" 30 | self.fill = True 31 | self.line = True 32 | 33 | #Define pythagoral function 34 | def pythagoras(self, s): 35 | 36 | #Return the distance between the shapes 37 | return math.sqrt(math.pow(abs(self.x - s.x) , 2) + math.pow(abs(self.y - s.y) , 2)) 38 | 39 | #Create our rectangle class 40 | class Point(Shape): 41 | 42 | #Define our initialize function 43 | def __init__(self, x, y): 44 | 45 | #Create our derivative shape 46 | super(Point, self).__init__(x, y, Shape.POINT) 47 | 48 | #Define pythagoral function 49 | def collide(self, s): 50 | 51 | #Circle collision 52 | if s.type == Shape.CIRCLE: 53 | 54 | #Check whether the distance between the shapes is larger than the radius of the circle 55 | return self.pythagoras(s) < s.radius 56 | 57 | #Square collision 58 | if s.type == Shape.RECT: 59 | 60 | #Pass to rectangle collision 61 | return s.collide(self) 62 | 63 | #Create our rectangle class 64 | class Rectangle(Shape): 65 | 66 | #Define our initialize function 67 | def __init__(self, x, y, width, height, colour = "red"): 68 | 69 | #Create our derivative shape 70 | super(Rectangle, self).__init__(x, y, Shape.RECT) 71 | self.width = width 72 | self.height = height 73 | self.fill_colour = colour 74 | 75 | #Define a function to get the corners 76 | def update_corners(self): 77 | 78 | #Fetch the x, y, width and height 79 | x = self.x 80 | y = self.y 81 | width = self.width 82 | height = self.height 83 | 84 | #Create our corners 85 | self.corners = [Point(x - width / 2, y + height / 2), Point(x - width / 2, y - height / 2), Point(x + width / 2, y + height / 2), Point(x + width / 2, y - height / 2)] 86 | 87 | #define a collice function 88 | def collide(self, s): 89 | 90 | #If s is a rectangle 91 | if s.type == Shape.RECT: 92 | 93 | #Craete our collide x variable 94 | col_x = abs(self.x - s.x) < (self.width / 2) + (s.width / 2) 95 | #Create our collide y variables 96 | col_y = abs(self.y - s.y) < (self.height / 2) + (s.height / 2) 97 | #return col_x and col_y 98 | return (col_x and col_y) 99 | 100 | #Square Circle collision 101 | if s.type == Shape.CIRCLE: 102 | 103 | #Update the corners 104 | self.update_corners() 105 | 106 | #Check if there is an overlap on the x or y 107 | x_overlap = abs(self.x - s.x) < self.width / 2 108 | y_overlap = abs(self.y - s.y) < self.height / 2 109 | 110 | #If they have overlapped on x and y, return true 111 | if x_overlap and y_overlap: 112 | return 1 113 | 114 | #If it has overlapped on just the y 115 | if y_overlap: 116 | #If the x distance has overlapped return true 117 | if abs(self.x - s.x) < s.radius + self.width / 2: 118 | return 2 119 | 120 | #If it has overlapped on just the x 121 | if x_overlap: 122 | #If the y distance has overlapped return true 123 | if abs(self.y - s.y) < s.radius + self.height / 2: 124 | return 3 125 | 126 | #If we have overlapped on any corner, return true 127 | for c in self.corners: 128 | if c.pythagoras(s) < s.radius: 129 | return 4 130 | 131 | #Not collided 132 | return 0 133 | 134 | #If the shape is a point 135 | if s.type == Shape.POINT: 136 | 137 | #Update the corners 138 | self.update_corners() 139 | 140 | #Check whether the point lies inside the rectangle 141 | return (s.x > self.corners[1].x and s.y > self.corners[1].y) and (s.x < self.corners[2].x and s.y < self.corners[2].y) 142 | 143 | return("Collision not implemented") 144 | 145 | #Create my circle class 146 | class Circle(Shape): 147 | 148 | #Define init for my circle 149 | def __init__(self, x, y, radius, colour = "yellow"): 150 | 151 | #Create our derivative shape 152 | super(Circle, self).__init__(x, y, Shape.CIRCLE) 153 | #Set the radious 154 | self.radius = radius 155 | 156 | #Set the default fill colour 157 | self.fill_colour = colour 158 | 159 | def collide(self, s): 160 | 161 | #If the s is a circle 162 | if s.type == Shape.CIRCLE: 163 | return self.pythagoras(s) < self.radius + s.radius 164 | 165 | #If it's a rectangle 166 | if s.type == Shape.RECT: 167 | return s.collide(self) 168 | 169 | #If the shape is a point 170 | if s.type == Shape.POINT: 171 | return s.collide(self) 172 | 173 | return("Collision not implemented") 174 | 175 | # ENGINE 176 | 177 | #Create a game object to hold all of our physics 178 | class Game: 179 | 180 | #Initialize 181 | def __init__(self, name, colour = "grey", fullscreen=False, sleep_time = 0.01): 182 | 183 | #Create a screen and set the name 184 | self.window = turtle.Screen() 185 | self.window.title(name) 186 | self.window.bgcolor(colour) 187 | if fullscreen: 188 | self.window.setup(1.0, 1.0) 189 | 190 | #Create a turtle to do our drawing 191 | self.t = turtle.Turtle() 192 | 193 | #Hide the turtle 194 | self.t.hideturtle() 195 | 196 | #Disable the tracer 197 | self.window.tracer(0, 0) 198 | 199 | #Create a list of shapes 200 | self.shapes = [] 201 | 202 | # Create a list of text objects 203 | self.text = [] 204 | 205 | # Create a list of button objects 206 | self.buttons = [] 207 | 208 | #Create a key listener 209 | self.keylistener = KeyListener(self.window) 210 | 211 | #Create an empty list of sprites 212 | self.sprites = [] 213 | 214 | # Set the sleep time 215 | self.sleep = sleep_time 216 | 217 | # Deafault scene 218 | self.default_scene_name = "default" 219 | self.scenes = { 220 | self.default_scene_name: Scene(self.default_scene_name) 221 | } 222 | self.current_scene = self.scenes[self.default_scene_name] 223 | 224 | # Function to return the width and height of the window 225 | def get_window_size(self): 226 | return (self.window.window_width(), self.window.window_height()) 227 | 228 | # Function to add a new scene 229 | def add_scene(self, scene): 230 | self.scenes[scene.name] = scene 231 | 232 | # Function to load a scene 233 | def load_scene(self, scene): 234 | if not scene.name in self.scenes: 235 | self.scenes[scene.name] = scene 236 | self.current_scene = self.scenes[scene.name] 237 | self.window.bgcolor(self.current_scene.bg_colour) 238 | 239 | # Function to remove a scene 240 | def remove_scene(self, scene): 241 | self.scenes.pop(scene.name, None) 242 | 243 | #Define a function to add a shape 244 | def add_shape(self, shape): 245 | 246 | #Add the shape 247 | self.current_scene.add_shape(shape) 248 | 249 | # Define a function to remove a shape 250 | def remove_shape(self, shape): 251 | 252 | # Remove the shape pointer from the list 253 | self.current_scene.remove_shape(shape) 254 | 255 | #Define a function to add a button 256 | def add_button(self, shape): 257 | 258 | #Add the button 259 | self.current_scene.add_button(shape) 260 | 261 | # Define a function to remove a button 262 | def remove_button(self, button): 263 | 264 | # Remove the button pointer from the list 265 | self.current_scene.remove_button(button) 266 | 267 | #Create a function to iterate over each of the shapes and draw them on screen 268 | def update(self): 269 | 270 | # Check for game exit 271 | try: 272 | 273 | #Clear the canvas 274 | self.t.clear() 275 | 276 | #For each of the shapes in the dictionary, draw them 277 | for s in self.current_scene.shapes: 278 | 279 | #Check the type of the shape 280 | if s.type == Shape.RECT: 281 | self.rectangle(s) 282 | if s.type == Shape.CIRCLE: 283 | self.circle(s) 284 | 285 | # For each of the buttons in the list 286 | for b in self.current_scene.buttons: 287 | 288 | # Draw the button 289 | self.button(b) 290 | 291 | # For each of the shapes in the list, render 292 | for t in self.text: 293 | self.render_text(t) 294 | 295 | #Update the screen 296 | self.window.update() 297 | 298 | # Wipe the text list ready for the next frame 299 | self.text = [] 300 | 301 | # Sleep 302 | sleep(self.sleep) 303 | 304 | except TclError as e: 305 | print("Program exited successfully.") 306 | sys.exit() 307 | 308 | # Add quit handler 309 | def quit(self): 310 | self.window.bye() 311 | 312 | #Create a function to allow us to draw a rectangle 313 | def rectangle(self, s): 314 | 315 | #Check whether the line should be drawn 316 | if s.line: 317 | #Set the colour of the line 318 | self.t.color(s.line_colour) 319 | else: 320 | #Set the colour of the line to the fill colour 321 | self.t.color(s.fill_colour) 322 | 323 | #Move the pen to the correct position 324 | self.t.penup() 325 | self.t.goto(s.x - (s.width/2), s.y + (s.height/2)) 326 | self.t.pendown() 327 | 328 | #If the shape should be filled 329 | if s.fill: 330 | #Start the fill 331 | self.t.begin_fill() 332 | 333 | #Draw the rectangle 334 | for i in range(2): 335 | self.t.forward(s.width) 336 | self.t.right(90) 337 | self.t.forward(s.height) 338 | self.t.right(90) 339 | 340 | #If the shape should be filled 341 | if s.fill: 342 | #Set the colour and end the fill 343 | self.t.color(s.fill_colour) 344 | self.t.end_fill() 345 | 346 | #Create a function to allow us to draw a circle 347 | def circle(self, s): 348 | 349 | #Check whether the line should be drawn 350 | if s.line: 351 | #Set the colour of the line 352 | self.t.color(s.line_colour) 353 | else: 354 | #Set the colour of the line to the fill colour 355 | self.t.color(s.fill_colour) 356 | 357 | #Move the pen to the correct position 358 | self.t.penup() 359 | self.t.goto(s.x, s.y - s.radius) 360 | self.t.pendown() 361 | 362 | #If the shape should be filled 363 | if s.fill: 364 | #Start the fill 365 | self.t.begin_fill() 366 | 367 | #Draw the circle 368 | self.t.circle(s.radius) 369 | 370 | #If the shape should be filled 371 | if s.fill: 372 | #Set the colour and end the fill 373 | self.t.color(s.fill_colour) 374 | self.t.end_fill() 375 | 376 | # Create a function to render a button 377 | def button(self, b): 378 | 379 | # Draw the rectangle of the button 380 | self.rectangle(b.rect) 381 | 382 | # Render the text 383 | self.render_text(b.font) 384 | 385 | # Create a function that lets us draw text to the screen 386 | def render_text(self, text_object): 387 | 388 | # Move to the correct location 389 | self.t.penup() 390 | self.t.goto(text_object.x, text_object.y) 391 | self.t.pendown() 392 | 393 | # Set the colour 394 | self.t.color(text_object.colour) 395 | 396 | # Write the text 397 | self.t.write(text_object.text, align=text_object.align, font=("Arial", text_object.size, "normal")) 398 | 399 | # Function to add text to be rendered on the next frame 400 | def write(self, x, y, text, colour, size, align="left"): 401 | 402 | # Add a text object to the text list 403 | self.text.append( 404 | Text(x, y, text, colour, size, align) 405 | ) 406 | 407 | #Create a function to add a mouse click 408 | def addclick(self, f, m=1): 409 | 410 | #Add the function to the click listener 411 | self.window.onclick(f, m) 412 | 413 | #Create a function to add a key listener 414 | def addkeypress(self, f, key): 415 | 416 | #Add the key and start listening 417 | self.window.onkey(f, key) 418 | self.window.listen() 419 | 420 | #Create a function to check whether a key is currently being pressed 421 | def ispressed(self, k): 422 | 423 | #Return the key listener check 424 | return self.keylistener.isPressed(k) 425 | 426 | # SCENE 427 | 428 | # Create a class to represent a scene, which is a collection of shapes and buttons 429 | class Scene: 430 | 431 | # Constructor 432 | def __init__(self, name, bg_colour = "grey"): 433 | 434 | self.name = name 435 | self.bg_colour = bg_colour 436 | self.shapes = [] 437 | self.buttons = [] 438 | 439 | #Define a function to add a shape 440 | def add_shape(self, shape): 441 | 442 | #Add the shape 443 | self.shapes.append(shape) 444 | 445 | # Define a function to remove a shape 446 | def remove_shape(self, shape): 447 | 448 | # Remove the shape pointer from the list 449 | self.shapes.remove(shape) 450 | 451 | #Define a function to add a button 452 | def add_button(self, shape): 453 | 454 | #Add the button 455 | self.buttons.append(shape) 456 | 457 | # Define a function to remove a button 458 | def remove_button(self, button): 459 | 460 | # Remove the button pointer from the list 461 | self.buttons.remove(button) 462 | 463 | # Override default equality check 464 | def __eq__(self, other): 465 | 466 | # Return if the name matches (can only have one scene with same name loaded, since they live in a dictionary) 467 | return self.name == other.name 468 | 469 | 470 | # TEXT 471 | 472 | # Create a class to store information about text 473 | class Text: 474 | 475 | # Constructor 476 | def __init__(self, x, y, text, colour, size, align="left"): 477 | self.x = x 478 | self.y = y 479 | self.text = text 480 | self.colour = colour 481 | self.size = size 482 | self.align = align 483 | 484 | # KEYS 485 | 486 | #Create a key listener class 487 | class KeyListener: 488 | 489 | #List of keys 490 | keys = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "space", "Up", "Down", "Left", "Right"] 491 | 492 | #Constructor 493 | def __init__(self, window): 494 | 495 | #Store the window 496 | self.window = window 497 | 498 | #Run the setup function for the dictionary 499 | self.setup() 500 | 501 | #Setup the key listeners 502 | self.createListeners() 503 | 504 | #Function to initialize the key listener 505 | def setup(self): 506 | 507 | #Create a dictionary to store the keys that are currently being pressed 508 | self.pressed = {} 509 | 510 | #For each of the keys, set them to false in the dictionary 511 | for k in KeyListener.keys: 512 | self.pressed[k] = False 513 | 514 | #Function to handle key down events 515 | def keyDown(self, k): 516 | 517 | #Add this key to the list of pressed keys 518 | self.pressed[k] = True 519 | 520 | #Function to handle key up events 521 | def keyUp(self, k): 522 | 523 | #Add this key to the list of pressed keys 524 | self.pressed[k] = False 525 | 526 | #Function to add key listeners for each key 527 | def createListeners(self): 528 | 529 | #For each of the keys, add a listener 530 | for k in KeyListener.keys: 531 | 532 | #Set the current key 533 | fp = partial(self.keyDown, k) 534 | fr = partial(self.keyUp, k) 535 | 536 | #Add the on key listeners 537 | self.window.onkeypress(fp, k) 538 | self.window.onkeyrelease(fr, k) 539 | 540 | #Start the listener 541 | self.window.listen() 542 | 543 | #Function to check whether a key is pressed 544 | def isPressed(self, k): 545 | 546 | #Return whether the key is pressed or not 547 | return self.pressed[k] 548 | 549 | # BUTTONS 550 | 551 | # Create a class to represent a button, comprised of text and a rectangle 552 | class Button: 553 | 554 | # Constructor 555 | def __init__(self, x, y, width, height, text, button_colour="white", text_colour="black", padding=10): 556 | 557 | # Create the rectangle, adding the padding 558 | self.rect = Rectangle(x, y, width + (2 * padding), height + (2 * padding), button_colour) 559 | 560 | # Set up variables for a binary search to find the appropriate font size 561 | min_size = 2 562 | max_size = 200 563 | 564 | # Set the tolerance for how close the actual width/height has to be to the font width/height 565 | tolerance = 2 566 | 567 | # Set up a loop to perform a binary search; ln(max_size) = ln(200) = 5.3, so 10 should be double maximum required count 568 | for i in range(10): 569 | 570 | # Create a font based on the current font size check, being midway between min and max 571 | font_size = int((min_size + max_size) / 2) 572 | font_config = Font(font=("Arial", font_size, "normal")) 573 | 574 | # Get the height of the font, and the width of specified text 575 | font_ascent = font_config.metrics("ascent") 576 | font_linespace = font_config.metrics("linespace") 577 | text_width = font_config.measure(text) 578 | 579 | # If the font height is too big, reduce max size 580 | if font_ascent > height: 581 | max_size = font_size 582 | 583 | # Otherwise if we have found a font within width tolerance, continue 584 | elif abs(width - text_width) < tolerance: 585 | break 586 | 587 | # Otherwise, if the text width is too big, reduce max size (right pointer for binary search) 588 | elif width - text_width < 0: 589 | max_size = font_size 590 | 591 | # Otherwise, if the text width is too small, increase minimum size (left pointer for binary search) 592 | else: 593 | min_size = font_size 594 | 595 | # Offset the x and y as required (this is based on the height and width of the final font) 596 | x += 2 597 | y -= int((font_linespace - font_ascent) / 2) + int(font_ascent / 2) 598 | 599 | # Create the font to be rendered 600 | self.font = Text(x, y, text, text_colour, font_size, align="center") 601 | 602 | # Function to check whether a given x y coordinate collides with the button 603 | def check_click(self, x, y): 604 | 605 | # Return the point to rectangle collision check using the specified point 606 | return self.rect.collide(Point(x,y)) 607 | 608 | # SPRITES 609 | 610 | #Create an object to hold a sprite 611 | class Sprite: 612 | 613 | #Constructor 614 | def __init__(self, img, window, x, y): 615 | 616 | #Store the image resource 617 | self.img = img 618 | 619 | #Store the window 620 | self.window = window 621 | 622 | #Add the image to the window 623 | window.addshape(self.img) 624 | 625 | #Create a turtle object 626 | self.turtle = Turtle() 627 | self.turtle.penup() 628 | 629 | #Set the image for the turtle 630 | self.turtle.shape(self.img) 631 | 632 | #Set the x and y position of the turtle 633 | self.x = x 634 | self.y = y 635 | 636 | #Set the turtle ot visible by default 637 | self.visible = True 638 | 639 | #Set the initial position 640 | self.move(0, 0) 641 | 642 | #Function to move the turtle 643 | def move(self, x, y): 644 | 645 | #Change the values of the position 646 | self.x += x 647 | self.y += y 648 | 649 | #Move the sprite 650 | self.turtle.goto(self.x, self.y) 651 | 652 | #Functions to set the sprite visible or invisible 653 | def setvisibile(self): 654 | self.visible = True 655 | self.turtle.showturtle() 656 | def setinvisible(self): 657 | self.visible = False 658 | self.turtle.hideturtle() 659 | -------------------------------------------------------------------------------- /tutorials/tutorial_1/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to the first tutorial in our *learning tphysics* series. 4 | 5 | In this tutorial we will be building a simple chase game, where the player controls their character using the arrow keys whilst an enemy chases them down. The player will need to collect coins as they go, but be careful! The game gets faster over time, making it more difficult as you collect more coins. 6 | 7 | > [!TIP] 8 | > This tutorial will work best if you follow each step and write the code out for yourself. Where you see a tip like this one, try to solve the problem before moving ahead! You can easily copy and paste the final code, but doing so will miss out on the point of this tutorial, which is to learn. If you just want to look at some examples, then you can head over to the [examples folder](https://github.com/thebillington/tphysics/tree/master/examples). 9 | 10 | ## Learning outcomes 11 | 12 | By the end of this tutorial, you will know how to: 13 | 14 | 1. Create a `tphysics` game window and add shapes. 15 | 2. Use a `game loop` to update our game state. 16 | 3. Detect key presses and respond to them. 17 | 4. Check for collisions and respond to them. 18 | 5. Generate random numbers and use them as positions. 19 | 5. Keep track of a score and output it in game. 20 | 21 | ## Creating a game window and player controller 22 | 23 | We will start by creating our game window and game loop. 24 | 25 | ```python 26 | from tphysics import Game, Rectangle 27 | 28 | # Create a new game window 29 | game = Game("Chase Game", "black") 30 | 31 | # Game loop which will contain all of our logic 32 | while True: 33 | 34 | # Each time the game loop runs, at the end, render a new frame 35 | game.update() 36 | ``` 37 | 38 | A game loop is an infinite loop which runs forever. This means that the code inside of the loop will continue until we press the *x* button to close the game. 39 | 40 | Currently we haven't written any logic to go inside the game loop, so the only thing we need to do is render a new frame each time the game loop runs. If you run your code, you should see that we have an empty game window. 41 | 42 | Now let's create a rectangle for the player and add it to the game. 43 | 44 | ```python 45 | from tphysics import Game, Rectangle 46 | 47 | # Create a new game window 48 | game = Game("Chase Game", "black") 49 | 50 | # Create the player game object in the middle of the screen 51 | player = Rectangle(0, 0, 20, 20, "light blue") 52 | game.add_shape(player) 53 | 54 | # Game loop which will contain all of our logic 55 | while True: 56 | 57 | # Each time the game loop runs, at the end, render a new frame 58 | game.update() 59 | ``` 60 | 61 | If you run the code again, you should see that we have a red square in the middle of the game window. 62 | 63 | > [!TIP] 64 | > Can you change the colour of the player? 65 | 66 | To get it to move, every frame (each time the loop runs) we need to change the position of the player. 67 | 68 | We can do this using the `player.x` and `player.y` properties. 69 | 70 | ```python 71 | from tphysics import Game, Rectangle 72 | 73 | # Create a new game window 74 | game = Game("Chase Game", "black") 75 | 76 | # Create the player game object in the middle of the screen 77 | player = Rectangle(0, 0, 20, 20, "light blue") 78 | game.add_shape(player) 79 | 80 | # Game loop which will contain all of our logic 81 | while True: 82 | 83 | # Move the player by adding 1 to the x and y positions 84 | player.x = player.x + 1 85 | player.y += 1 86 | 87 | # Each time the game loop runs, at the end, render a new frame 88 | game.update() 89 | ``` 90 | 91 | We can now get our player to move, but in order to move based on key presses, we have to check if a key is pressed. 92 | 93 | We can do this using the `game.ispressed()` function. This function will return `True` if a key is pressed, or `False` if it isn't. 94 | 95 | Let's check if the right key is pressed and if it is, increase the `x` position of the player. 96 | 97 | ```python 98 | from tphysics import Game, Rectangle 99 | 100 | # Create a new game window 101 | game = Game("Chase Game", "black") 102 | 103 | # Create the player game object in the middle of the screen 104 | player = Rectangle(0, 0, 20, 20, "light blue") 105 | game.add_shape(player) 106 | 107 | # Game loop which will contain all of our logic 108 | while True: 109 | 110 | # Check if the right key is pressed 111 | if game.ispressed("Right"): 112 | 113 | # Move the player x position 114 | player.x += 1 115 | 116 | # Each time the game loop runs, at the end, render a new frame 117 | game.update() 118 | ``` 119 | 120 | > [!TIP] 121 | > You will now need to add in checks for the other direction keys and move the player. If you get stuck, then you can look ahead, however it is important to try and solve the problem by yourself first! 122 | 123 | ## Creating a coin to collect 124 | 125 | Now that we have a character with a movement controller, we need to add a coin for the player to collect. 126 | 127 | This will increase the score and the player will get a higher score by collecting more coins. 128 | 129 | In order to stop the coin from generating in the same position each time, we will have to generate a random number. 130 | 131 | Let's start by importing the `randint` function from the `random` library. We will also need to import `Circle` from tphysics. 132 | 133 | ```python 134 | from tphysics import Game, Rectangle, Circle 135 | from random import randint 136 | 137 | # Create a new game window 138 | game = Game("Chase Game", "black") 139 | 140 | # Create the player game object in the middle of the screen 141 | player = Rectangle(0, 0, 20, 20, "light blue") 142 | game.add_shape(player) 143 | 144 | # Game loop which will contain all of our logic 145 | while True: 146 | 147 | # Check key presses 148 | if game.ispressed("Right"): 149 | player.x += 1 150 | if game.ispressed("Left"): 151 | player.x -= 1 152 | if game.ispressed("Up"): 153 | player.y += 1 154 | if game.ispressed("Down"): 155 | player.y -= 1 156 | 157 | # Each time the game loop runs, at the end, render a new frame 158 | game.update() 159 | ``` 160 | 161 | Now we need to create a new Circle with a random position. 162 | 163 | ```python 164 | from tphysics import Game, Rectangle, Circle 165 | from random import randint 166 | 167 | # Create a new game window 168 | game = Game("Chase Game", "black") 169 | 170 | # Create the player game object in the middle of the screen 171 | player = Rectangle(0, 0, 20, 20, "light blue") 172 | game.add_shape(player) 173 | 174 | # Create a coin with a random position between -250 and 250 on x and y 175 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 176 | game.add_shape(coin) 177 | 178 | # Game loop which will contain all of our logic 179 | while True: 180 | 181 | # Check key presses 182 | if game.ispressed("Right"): 183 | player.x += 1 184 | if game.ispressed("Left"): 185 | player.x -= 1 186 | if game.ispressed("Up"): 187 | player.y += 1 188 | if game.ispressed("Down"): 189 | player.y -= 1 190 | 191 | # Each time the game loop runs, at the end, render a new frame 192 | game.update() 193 | ``` 194 | 195 | Now that we have our coin spawning in a random position, we need to check if the player has collected the coin. 196 | 197 | We do this by using the `player.collide()` function. 198 | 199 | Every shape has access to its own `collide` function which allows it to check if it has collided with any other shape. 200 | 201 | ```python 202 | from tphysics import Game, Rectangle, Circle 203 | from random import randint 204 | 205 | # Create a new game window 206 | game = Game("Chase Game", "black") 207 | 208 | # Create the player game object in the middle of the screen 209 | player = Rectangle(0, 0, 20, 20, "light blue") 210 | game.add_shape(player) 211 | 212 | # Create a coin with a random position between -250 and 250 on x and y 213 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 214 | game.add_shape(coin) 215 | 216 | # Game loop which will contain all of our logic 217 | while True: 218 | 219 | # Check key presses 220 | if game.ispressed("Right"): 221 | player.x += 1 222 | if game.ispressed("Left"): 223 | player.x -= 1 224 | if game.ispressed("Up"): 225 | player.y += 1 226 | if game.ispressed("Down"): 227 | player.y -= 1 228 | 229 | # Check if the player has collected the coin 230 | if player.collide(coin): 231 | 232 | # Generate a new random position for the coin 233 | coin.x = randint(-250, 250) 234 | coin.y = randint(-250, 250) 235 | 236 | # Each time the game loop runs, at the end, render a new frame 237 | game.update() 238 | ``` 239 | 240 | Whenever the `player.collide(coin)` function returns as `True`, we generate a new random position for the coin. 241 | 242 | # Tracking the score 243 | 244 | Now that we have a coin to collect, we can keep track of a score. 245 | 246 | To do this, we need to create a new variable in our setup code called `score` and give it a vlue of `1`. 247 | 248 | Whenever we collect a coin, we will then add one to the score. 249 | 250 | ```python 251 | from tphysics import Game, Rectangle, Circle 252 | from random import randint 253 | 254 | # Create a new game window 255 | game = Game("Chase Game", "black") 256 | 257 | # Create the player game object in the middle of the screen 258 | player = Rectangle(0, 0, 20, 20, "light blue") 259 | game.add_shape(player) 260 | 261 | # Create a coin with a random position between -250 and 250 on x and y 262 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 263 | game.add_shape(coin) 264 | 265 | # Set the initial score to zero 266 | score = 0 267 | 268 | # Game loop which will contain all of our logic 269 | while True: 270 | 271 | # Check key presses 272 | if game.ispressed("Right"): 273 | player.x += 1 274 | if game.ispressed("Left"): 275 | player.x -= 1 276 | if game.ispressed("Up"): 277 | player.y += 1 278 | if game.ispressed("Down"): 279 | player.y -= 1 280 | 281 | # Check if the player has collected the coin 282 | if player.collide(coin): 283 | 284 | # If so, add to score and generate new coin position 285 | score += 1 286 | coin.x = randint(-250, 250) 287 | coin.y = randint(-250, 250) 288 | 289 | # Each time the game loop runs, at the end, render a new frame 290 | game.update() 291 | ``` 292 | 293 | Once we have a score to keep track of, we can use the `game.write()` function to show it to the player. 294 | 295 | `game.write` needs to know the position (x,y), text, colour and size of the text to write. 296 | 297 | ```python 298 | from tphysics import Game, Rectangle, Circle 299 | from random import randint 300 | 301 | # Create a new game window 302 | game = Game("Chase Game", "black") 303 | 304 | # Create the player game object in the middle of the screen 305 | player = Rectangle(0, 0, 20, 20, "light blue") 306 | game.add_shape(player) 307 | 308 | # Create a coin with a random position between -250 and 250 on x and y 309 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 310 | game.add_shape(coin) 311 | 312 | # Set the initial score to zero 313 | score = 0 314 | 315 | # Game loop which will contain all of our logic 316 | while True: 317 | 318 | # Check key presses 319 | if game.ispressed("Right"): 320 | player.x += 1 321 | if game.ispressed("Left"): 322 | player.x -= 1 323 | if game.ispressed("Up"): 324 | player.y += 1 325 | if game.ispressed("Down"): 326 | player.y -= 1 327 | 328 | # Check if the player has collected the coin 329 | if player.collide(coin): 330 | 331 | # If so, add to score and generate new coin position 332 | score += 1 333 | coin.x = randint(-250, 250) 334 | coin.y = randint(-250, 250) 335 | 336 | # Show the score 337 | game.write(-300, 300, f"Score: {score}", "white", 20) 338 | 339 | # Each time the game loop runs, at the end, render a new frame 340 | game.update() 341 | ``` 342 | 343 | Notice that we have used a formatted string in order to show the score. 344 | 345 | `f"Score: {score}"` 346 | 347 | By using a formatted string, we can replace `{score}` with the actual value held in the score variable. 348 | 349 | # Creating the enemy 350 | 351 | Now that we have a way to collect coins and track the score, it is time to add difficulty. 352 | 353 | We can do this by adding an enemy that chases the player. 354 | 355 | We want our enemy to generate at a random position to prevent predictability. 356 | 357 | ```python 358 | from tphysics import Game, Rectangle, Circle 359 | from random import randint 360 | 361 | # Create a new game window 362 | game = Game("Chase Game", "black") 363 | 364 | # Create the player game object in the middle of the screen 365 | player = Rectangle(0, 0, 20, 20, "light blue") 366 | game.add_shape(player) 367 | 368 | # Create a coin with a random position between -250 and 250 on x and y 369 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 370 | game.add_shape(coin) 371 | 372 | # Create enemy with a random position 373 | enemy = Circle(randint(-250,250), randint(-250,250), 10, "red") 374 | game.add_shape(enemy) 375 | 376 | # Set the initial score to zero 377 | score = 0 378 | 379 | # Game loop which will contain all of our logic 380 | while True: 381 | 382 | # Check key presses 383 | if game.ispressed("Right"): 384 | player.x += 1 385 | if game.ispressed("Left"): 386 | player.x -= 1 387 | if game.ispressed("Up"): 388 | player.y += 1 389 | if game.ispressed("Down"): 390 | player.y -= 1 391 | 392 | # Check if the player has collected the coin 393 | if player.collide(coin): 394 | 395 | # If so, add to score and generate new coin position 396 | score += 1 397 | coin.x = randint(-250, 250) 398 | coin.y = randint(-250, 250) 399 | 400 | # Show the score 401 | game.write(-300, 300, f"Score: {score}", "white", 20) 402 | 403 | # Each time the game loop runs, at the end, render a new frame 404 | game.update() 405 | ``` 406 | 407 | In order to get the enemy to chase the player, we will need it to keep track of the player x position. 408 | 409 | We can do this using inequality operators, also known as the `less than` and `greater than` operators. 410 | 411 | ```python 412 | from tphysics import Game, Rectangle, Circle 413 | from random import randint 414 | 415 | # Create a new game window 416 | game = Game("Chase Game", "black") 417 | 418 | # Create the player game object in the middle of the screen 419 | player = Rectangle(0, 0, 20, 20, "light blue") 420 | game.add_shape(player) 421 | 422 | # Create a coin with a random position between -250 and 250 on x and y 423 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 424 | game.add_shape(coin) 425 | 426 | # Create enemy with a random position 427 | enemy = Circle(randint(-250,250), randint(-250,250), 10, "red") 428 | game.add_shape(enemy) 429 | 430 | # Set the initial score to zero 431 | score = 0 432 | 433 | # Game loop which will contain all of our logic 434 | while True: 435 | 436 | # Check key presses 437 | if game.ispressed("Right"): 438 | player.x += 1 439 | if game.ispressed("Left"): 440 | player.x -= 1 441 | if game.ispressed("Up"): 442 | player.y += 1 443 | if game.ispressed("Down"): 444 | player.y -= 1 445 | 446 | # Check if the enemy is to the left of the player 447 | if enemy.x < player.x: 448 | 449 | # Move the enemy to the right 450 | enemy.x += 1 451 | 452 | # Check if the player has collected the coin 453 | if player.collide(coin): 454 | 455 | # If so, add to score and generate new coin position 456 | score += 1 457 | coin.x = randint(-250, 250) 458 | coin.y = randint(-250, 250) 459 | 460 | # Show the score 461 | game.write(-300, 300, f"Score: {score}", "white", 20) 462 | 463 | # Each time the game loop runs, at the end, render a new frame 464 | game.update() 465 | ``` 466 | 467 | > [!TIP] 468 | > You will now need to add in checks for the enemy being to the right, above and below the player. If you get stuck, then you can look ahead, however it is important to try and solve the problem by yourself first! 469 | 470 | # Checking for enemy collision 471 | 472 | Once we have the enemy movement controller working, we need to check for player and enemy collisions. 473 | 474 | We do this in the same way we checked for player and coin collisions. 475 | 476 | When the enemy hits the player, we will want to generate a new position for the enemy and coin. 477 | 478 | We will also set the player position back to the centre and the score to zero. 479 | 480 | ```python 481 | from tphysics import Game, Rectangle, Circle 482 | from random import randint 483 | 484 | # Create a new game window 485 | game = Game("Chase Game", "black") 486 | 487 | # Create the player game object in the middle of the screen 488 | player = Rectangle(0, 0, 20, 20, "light blue") 489 | game.add_shape(player) 490 | 491 | # Create a coin with a random position between -250 and 250 on x and y 492 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 493 | game.add_shape(coin) 494 | 495 | # Create enemy with a random position 496 | enemy = Circle(randint(-250,250), randint(-250,250), 10, "red") 497 | game.add_shape(enemy) 498 | 499 | # Set the initial score to zero 500 | score = 0 501 | 502 | # Game loop which will contain all of our logic 503 | while True: 504 | 505 | # Check key presses 506 | if game.ispressed("Right"): 507 | player.x += 1 508 | if game.ispressed("Left"): 509 | player.x -= 1 510 | if game.ispressed("Up"): 511 | player.y += 1 512 | if game.ispressed("Down"): 513 | player.y -= 1 514 | 515 | # Enemy movement checks 516 | if enemy.x < player.x: 517 | enemy.x += 1 518 | if enemy.x > player.x: 519 | enemy.x -= 1 520 | if enemy.y < player.y: 521 | enemy.y += 1 522 | if enemy.y > player.y: 523 | enemy.y -= 1 524 | 525 | # Check if the player has collected the coin 526 | if player.collide(coin): 527 | 528 | # If so, add to score and generate new coin position 529 | score += 1 530 | coin.x = randint(-250, 250) 531 | coin.y = randint(-250, 250) 532 | 533 | # Check if the enemy has hit the player 534 | if enemy.collide(player): 535 | 536 | # Reset the player position and score 537 | score = 0 538 | player.x = 0 539 | player.y = 0 540 | 541 | # Generate new coin and enemy positions 542 | coin.x = randint(-250,250) 543 | coin.y = randint(-250,250) 544 | enemy.x = randint(-250,250) 545 | enemy.y = randint(-250,250) 546 | 547 | # Show the score 548 | game.write(-300, 300, f"Score: {score}", "white", 20) 549 | 550 | # Each time the game loop runs, at the end, render a new frame 551 | game.update() 552 | ``` 553 | 554 | # Increasing difficulty over time 555 | 556 | Currently the player and enemy move with the same speed. 557 | 558 | This isn't very fun as once the enemy has caught up, it is impossible to get away. 559 | 560 | In order to make the game more interesting, we will add `player_speed` and `enemy_speed` variables with different values. 561 | 562 | ```python 563 | from tphysics import Game, Rectangle, Circle 564 | from random import randint 565 | 566 | # Create a new game window 567 | game = Game("Chase Game", "black") 568 | 569 | # Create the player game object in the middle of the screen 570 | player = Rectangle(0, 0, 20, 20, "light blue") 571 | game.add_shape(player) 572 | 573 | # Create a coin with a random position between -250 and 250 on x and y 574 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 575 | game.add_shape(coin) 576 | 577 | # Create enemy with a random position 578 | enemy = Circle(randint(-250,250), randint(-250,250), 10, "red") 579 | game.add_shape(enemy) 580 | 581 | # Set the initial score to zero 582 | score = 0 583 | 584 | # Set the player and enemy speed 585 | player_speed = 2 586 | enemy_speed = 1 587 | 588 | # Game loop which will contain all of our logic 589 | while True: 590 | 591 | # Check key presses 592 | if game.ispressed("Right"): 593 | player.x += player_speed 594 | if game.ispressed("Left"): 595 | player.x -= player_speed 596 | if game.ispressed("Up"): 597 | player.y += player_speed 598 | if game.ispressed("Down"): 599 | player.y -= player_speed 600 | 601 | # Enemy movement checks 602 | if enemy.x < player.x: 603 | enemy.x += enemy_speed 604 | if enemy.x > player.x: 605 | enemy.x -= enemy_speed 606 | if enemy.y < player.y: 607 | enemy.y += enemy_speed 608 | if enemy.y > player.y: 609 | enemy.y -= enemy_speed 610 | 611 | # Check if the player has collected the coin 612 | if player.collide(coin): 613 | 614 | # If so, add to score and generate new coin position 615 | score += 1 616 | coin.x = randint(-250, 250) 617 | coin.y = randint(-250, 250) 618 | 619 | # Show the score 620 | game.write(-300, 300, f"Score: {score}", "white", 20) 621 | 622 | # Each time the game loop runs, at the end, render a new frame 623 | game.update() 624 | ``` 625 | 626 | Now that we have variables to track the player and enemy speed, we can increase it over time. 627 | 628 | To do so, we will use the modulus operator to check if the score is divisible by 5. 629 | 630 | `score % 5 == 0` - this code returns `True` if the score is divisible by 5, as the modulus operator finds the remaineder of a division. 631 | 632 | The remainder of 24 divided by 5 is 4, so we know that it isn't divisible. 633 | 634 | The remainder of 25 divided by 5 is 0, so we know that it is divisible, as it has a remainder of zero. 635 | 636 | If the score is divisible by 5 after the player has collected a coin, then we will increase the player and enemy speed. 637 | 638 | ```python 639 | from tphysics import Game, Rectangle, Circle 640 | from random import randint 641 | 642 | # Create a new game window 643 | game = Game("Chase Game", "black") 644 | 645 | # Create the player game object in the middle of the screen 646 | player = Rectangle(0, 0, 20, 20, "light blue") 647 | game.add_shape(player) 648 | 649 | # Create a coin with a random position between -250 and 250 on x and y 650 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 651 | game.add_shape(coin) 652 | 653 | # Create enemy with a random position 654 | enemy = Circle(randint(-250,250), randint(-250,250), 10, "red") 655 | game.add_shape(enemy) 656 | 657 | # Set the initial score to zero 658 | score = 0 659 | 660 | # Set the player and enemy speed 661 | player_speed = 2 662 | enemy_speed = 1 663 | 664 | # Game loop which will contain all of our logic 665 | while True: 666 | 667 | # Check key presses 668 | if game.ispressed("Right"): 669 | player.x += player_speed 670 | if game.ispressed("Left"): 671 | player.x -= player_speed 672 | if game.ispressed("Up"): 673 | player.y += player_speed 674 | if game.ispressed("Down"): 675 | player.y -= player_speed 676 | 677 | # Enemy movement checks 678 | if enemy.x < player.x: 679 | enemy.x += enemy_speed 680 | if enemy.x > player.x: 681 | enemy.x -= enemy_speed 682 | if enemy.y < player.y: 683 | enemy.y += enemy_speed 684 | if enemy.y > player.y: 685 | enemy.y -= enemy_speed 686 | 687 | # Check if the player has collected the coin 688 | if player.collide(coin): 689 | 690 | # If so, add to score and generate new coin position 691 | score += 1 692 | coin.x = randint(-250, 250) 693 | coin.y = randint(-250, 250) 694 | 695 | # Check if the score is divisible by 5 696 | if score % 5 == 0: 697 | 698 | # If so, increase player and enemy speed 699 | player_speed += 1 700 | enemy_speed += 1 701 | 702 | # Check if the enemy has hit the player 703 | if enemy.collide(player): 704 | 705 | # Reset the player position and score 706 | score = 0 707 | player.x = 0 708 | player.y = 0 709 | 710 | # Generate new coin and enemy positions 711 | coin.x = randint(-250,250) 712 | coin.y = randint(-250,250) 713 | enemy.x = randint(-250,250) 714 | enemy.y = randint(-250,250) 715 | 716 | # Show the score 717 | game.write(-300, 300, f"Score: {score}", "white", 20) 718 | 719 | # Each time the game loop runs, at the end, render a new frame 720 | game.update() 721 | ``` 722 | 723 | This has now increased difficulty over time, but when we lose, the speeds don't reset back down. 724 | 725 | To do this, when the enemy hits the player, we will need to reset the player and enemy speed. 726 | 727 | ```python 728 | from tphysics import Game, Rectangle, Circle 729 | from random import randint 730 | 731 | # Create a new game window 732 | game = Game("Chase Game", "black") 733 | 734 | # Create the player game object in the middle of the screen 735 | player = Rectangle(0, 0, 20, 20, "light blue") 736 | game.add_shape(player) 737 | 738 | # Create a coin with a random position between -250 and 250 on x and y 739 | coin = Circle(randint(-250,250), randint(-250,250), 5, "yellow") 740 | game.add_shape(coin) 741 | 742 | # Create enemy with a random position 743 | enemy = Circle(randint(-250,250), randint(-250,250), 10, "red") 744 | game.add_shape(enemy) 745 | 746 | # Set the initial score to zero 747 | score = 0 748 | 749 | # Set the player and enemy speed 750 | player_speed = 2 751 | enemy_speed = 1 752 | 753 | # Game loop which will contain all of our logic 754 | while True: 755 | 756 | # Check key presses 757 | if game.ispressed("Right"): 758 | player.x += player_speed 759 | if game.ispressed("Left"): 760 | player.x -= player_speed 761 | if game.ispressed("Up"): 762 | player.y += player_speed 763 | if game.ispressed("Down"): 764 | player.y -= player_speed 765 | 766 | # Enemy movement checks 767 | if enemy.x < player.x: 768 | enemy.x += enemy_speed 769 | if enemy.x > player.x: 770 | enemy.x -= enemy_speed 771 | if enemy.y < player.y: 772 | enemy.y += enemy_speed 773 | if enemy.y > player.y: 774 | enemy.y -= enemy_speed 775 | 776 | # Check if the player has collected the coin 777 | if player.collide(coin): 778 | 779 | # If so, add to score and generate new coin position 780 | score += 1 781 | coin.x = randint(-250, 250) 782 | coin.y = randint(-250, 250) 783 | 784 | # Check if the score is divisible by 5 785 | if score % 5 == 0: 786 | 787 | # If so, increase player and enemy speed 788 | player_speed += 1 789 | enemy_speed += 1 790 | 791 | # Check if the enemy has hit the player 792 | if enemy.collide(player): 793 | 794 | # Reset the player position and score 795 | score = 0 796 | player.x = 0 797 | player.y = 0 798 | 799 | # Generate new coin and enemy positions 800 | coin.x = randint(-250,250) 801 | coin.y = randint(-250,250) 802 | enemy.x = randint(-250,250) 803 | enemy.y = randint(-250,250) 804 | 805 | # Reset player and enemy speed 806 | player_speed = 2 807 | enemy_speed = 1 808 | 809 | # Show the score 810 | game.write(-300, 300, f"Score: {score}", "white", 20) 811 | 812 | # Each time the game loop runs, at the end, render a new frame 813 | game.update() 814 | ``` 815 | 816 | I hope you have enjoyed this tutorial. If so, you can find more tutorials on YouTube in the [tphysics playlist](https://www.youtube.com/watch?v=QQJT1oDcXUQ&list=PLMr7li1270gySPa4xz8PVVug1z0bCTil_). --------------------------------------------------------------------------------