Table of Contents
2.5d example
How to simulate 3d programming using 2d graphics functions.
The Program
The program uses Pygame Framework with the following changes to Game.py. You can just cut and paste the “Game.py” section (below) as a replacement.
One: Something 2d
For this example we need some image, such as “naruto.jpg”. It doesn't matter what this image contains, but you can find some image online. The image we found was a 425×600 jpeg. We added it like this, in the init section:
# 1. Load image self.oimage = pygame.image.load('naruto.jpg') self.rimage = self.oimage self.image = self.oimage self.scale = 1.0 self.imgw = self.image.get_width() self.imgh = self.image.get_height() self.cpx = Screen.WIDTH // 2 self.cpy = Screen.HEIGHT // 2 self.imgx = self.cpx - self.imgw // 2 self.imgy = self.cpy - self.imgh // 2 self.scalex = 1 self.scaley = 1 self.angle = 0
It's a Projection!
In addition to loading the image into “oimage”, which means “original image”, we have a wide variety of information we will need to do the “3d” projection. It's called a projection because we calculate what it should look like and then “project” it onto the 2d surface.
Projection Information
We need the original image, and the image we will display. We will keep track of the position and information about the image and then modify it into image. We need to keep track of the x and y location and the z locastion. But we are calling the z location “scale” here, because it isn't really a z location we are using. Thats why it is called 2.5d. It will “look like” a z axis but it isn't.
Simulating Z?
We also need to keep track of scale in the x and y axis as well. This will allow us to “sorta” rotate the image around the x or y axis. Because scaling an image in that direction kinda-maybe-sorta looks like rotation into the z dimension around the x or y axis. This isn't perfect; if it was a little better then the top of the image would get smaller if it moved away and the bottom would get bigger as it rotated towards you; we didn't account for this effect here. We just wanted to try some cheap effects to simulate 3d that you could use in a game.
Rotation is also broken
We also keep track of rotation around the z axis but it's somewhat broken. It's at this point you will realize the limitations of simple 2.5d programming. Fixing this would require a lot of work.
Classes are better
Because of the large amount of data we need to keep about an image to make it kinda maybe sorta 3d it's better to make this into some kind of class. Maybe a 3d sprite class that we can somehow have a list of extra transform functions to make it look like it's moving in 3d space. For many games this will be a fine solution. A lot of early 2.5d games worked very well like this.
Centerpoint and other Kludges
There are a lot of kludges here. One is using centerpoint to keep track of the center in order to make scaling look like z direction movement. Using kludges like this is the best way to make 2.5d games. You can be inventive and find some way to fix rotation this way too!
Transforms
The key to making this work is the transform functions. We are keeping track of at least six things; X, Y and Z position, and X, Y and Z rotation. Again, Z here is a form of scale or rotation but the idea is the same.
The transforms we used were:
- def transform_r(self, factor):
- A rotational transformation made to look like rotation around z.
- def transform_size(self, factor):
- A scale transform to simulate movement along z.
- def transform_sx(self, factor):
- def transform_sy(self, factor):
- Scale along x and y separately to make it look like rotation around x or y.
- def transform_xy(self, dx, dy):
- Movement along X and Y is easy because it's 2d.
Not really proper projection
These are all really bad kludges because we aren't really projecting things, so you will need to tweak a number of constants to make the game feel more natural. But it can certainly work in a pinch! And to add special effects!
Keyboard Controls
I went with frame by frame detection to keep things 'live' which I felt was a good choice for this demo. The key choices could probably be improved, 1, 2, 3, 4 etc, is not the best choice of keys. You might try:
- OK: wsad and UP, DOWN for movement
- CHANGED: Use YU, HJ, NM for axial rotation (not 12, 34, 56).
Maybe not the most important change, but could be nice.
Game.py
You can cut and paste this into PyGame Framework.
import pygame import time from Screen import * class Game: def __init__(self, window): self.window = window self.screen = window.screen self.font = window.font # Set up game variables (see notes) self.running = True # 1. Load image self.oimage = pygame.image.load('naruto.jpg') self.rimage = self.oimage self.image = self.oimage self.scale = 1.0 self.imgw = self.image.get_width() self.imgh = self.image.get_height() self.cpx = Screen.WIDTH // 2 self.cpy = Screen.HEIGHT // 2 self.imgx = self.cpx - self.imgw // 2 self.imgy = self.cpy - self.imgh // 2 self.scalex = 1 self.scaley = 1 self.angle = 0 def transform_r(self, factor): self.angle += factor self.image = pygame.transform.rotate(self.image, factor) def transform_size(self, factor): w, h = self.oimage.get_size() self.scale = self.scale + factor self.image = pygame.transform.scale(self.oimage, (w * self.scale* self.scalex, h * self.scale*self.scaley)) self.imgw = self.image.get_width() self.imgh = self.image.get_height() self.imgx = self.cpx - self.imgw // 2 self.imgy = self.cpy - self.imgh // 2 def transform_sx(self, factor): w, h = self.oimage.get_size() self.scalex = self.scalex + factor self.transform_size(0) def transform_sy(self, factor): w, h = self.oimage.get_size() self.scaley = self.scaley + factor self.transform_size(0) def transform_xy(self, dx, dy): self.cpx += dx self.cpy += dy self.imgx = self.cpx - self.imgw // 2 self.imgy = self.cpy - self.imgh // 2 def start(self): # Clock for controlling the frame rate clock = pygame.time.Clock() # Variables to keep track of time, if needed frame_counter = 0 time_start = time.time() * 1000 # Main Loop while self.running == True: # Manage time for timed events like animations time_clock = (time.time() * 1000) - time_start frame_counter += 1 if frame_counter > Screen.FPS: frame_counter = 1 # All event handling self.checkEvents() # Frame generation self.screen.fill((0, 0, 0)) # Clear the screen. self.drawGame() pygame.display.update() # update the display. Can also use flip() # Control the frame rate clock.tick(Screen.FPS) def drawGame(self): self.window.screen.blit(self.image, (self.imgx, self.imgy) ) def checkEvents(self): for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False return if event.type == pygame.KEYDOWN: # key down event, process keys. if event.key == pygame.K_q: self.quit_game() if event.key == pygame.K_ESCAPE: self.quit_game() keys = pygame.key.get_pressed() if keys[pygame.K_UP]: self.transform_size(-0.001) if keys[pygame.K_DOWN]: self.transform_size(0.001) if keys[pygame.K_1]: self.transform_sx(-0.003) if keys[pygame.K_2]: self.transform_sx(0.003) if keys[pygame.K_3]: self.transform_sy(-0.003) if keys[pygame.K_4]: self.transform_sy(0.003) if keys[pygame.K_5]: self.transform_r(-1) if keys[pygame.K_6]: self.transform_r(1) if keys[pygame.K_w]: self.transform_xy(0,-1) if keys[pygame.K_s]: self.transform_xy(0,1) if keys[pygame.K_a]: self.transform_xy(-1,0) if keys[pygame.K_d]: self.transform_xy(1,0) def quit_game(self): print("Exiting game...") pygame.quit() quit() exit()