init.
This commit is contained in:
commit
bd8c9540d4
|
@ -0,0 +1,428 @@
|
|||
import sdl2.ext
|
||||
|
||||
# from math import sqrt
|
||||
from time import sleep
|
||||
|
||||
# SDL related initializations
|
||||
sdl2.ext.init()
|
||||
width = 1024
|
||||
height = 768
|
||||
window = sdl2.ext.Window("Balls!", size=(width, height))
|
||||
window_surface = window.get_surface()
|
||||
window_pixels = sdl2.ext.PixelView(window_surface)
|
||||
window.show()
|
||||
|
||||
|
||||
# controller class
|
||||
# technically unnecessary, but having keyboard input code directly manipulate physics is really goddamn messy, so should always be separated
|
||||
# like using this as a "middle-man", thus making it possible also to manipulate controls by A.I, demo playback, etc. without extra code
|
||||
class Controller:
|
||||
left = 0
|
||||
right = 0
|
||||
up = 0
|
||||
down = 0
|
||||
|
||||
|
||||
class Ball:
|
||||
# default location is 0 0
|
||||
x = 0
|
||||
y = 0
|
||||
# default velocity is 0 0
|
||||
vel_x = 0
|
||||
vel_y = 0
|
||||
# default colour values give a red ball, unless redefined at initialization
|
||||
r = 255
|
||||
g = 0
|
||||
b = 0
|
||||
# the controller object stores information if we are steering left, right, up, down
|
||||
controller = Controller()
|
||||
|
||||
def __init__(self, size, x, y, sprite_surface):
|
||||
# physics
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.size = size
|
||||
self.radius = size / 2.0
|
||||
self.bounce_x = bounce # assigned from global default value; x axis bounce-back
|
||||
self.bounce_y = bounce # assigned from global default value; y axis bounce-back
|
||||
self.accelerate = accelerate # assigned from global default value; rate of increasing x velocity by controlling
|
||||
self.max_move_vel = (
|
||||
max_move_vel # assigned from global default value; maximum velocity to apply when controlling
|
||||
)
|
||||
self.jump_vel = jump_vel # assigned from global default value; negative y velocity applied for jumping
|
||||
self.floating = (
|
||||
0 # determines whether the object is floating in air or standing on the bottom edge of the screen border
|
||||
)
|
||||
|
||||
# graphics
|
||||
# use the sprite which we got as an argument
|
||||
self.sprite_surface = sprite_surface
|
||||
# if we got None as argument, generate a new, red ball sphere
|
||||
if self.sprite_surface == None:
|
||||
self.sprite_surface = make_ball_sprite(self.size, 255, 0, 0, 0.66)
|
||||
|
||||
# read input from controller object and apply incremental changes to velocity
|
||||
def do_control(self):
|
||||
# if the input for left == 1 and vel is not yet -max_move_vel ("greater than" negative max_move_vel),
|
||||
# accelerate with a negative value (toward left on x axis)
|
||||
if self.controller.left and self.vel_x > -self.max_move_vel:
|
||||
self.vel_x -= self.accelerate
|
||||
# if the input for right == 1 and vel is less than max_move_vel, increase x vel by 1
|
||||
if self.controller.right and self.vel_x < self.max_move_vel:
|
||||
self.vel_x += self.accelerate
|
||||
# if we pressed up and the ball is NOT floating (therefore it's on the ground), "jump" by setting y vel to -20
|
||||
if self.controller.up and not self.floating:
|
||||
self.vel_y = -self.jump_vel
|
||||
|
||||
# apply physics-caused changes to velocity, then manipulate x y position based on x y velocity
|
||||
def do_physics(self):
|
||||
# apply gravity to y vel, incrementally making the object fall faster downwards
|
||||
if self.floating:
|
||||
self.vel_y += gravity
|
||||
|
||||
# horizontal friction when touching floor, scale the vel down
|
||||
if not self.floating:
|
||||
self.vel_x *= friction
|
||||
# when x velocity drops below a set threshold (stop_vel), stop it entirely to prevent a slow eternal slide
|
||||
if abs(self.vel_x) < stop_vel:
|
||||
self.vel_x = 0
|
||||
|
||||
# apply current velocities to position values
|
||||
self.x += self.vel_x
|
||||
self.y += self.vel_y
|
||||
|
||||
# test for collision against screen borders, and perform necessary actions
|
||||
def do_edge_collision(self):
|
||||
################# x collision #################
|
||||
# left edge
|
||||
if self.x - self.radius < 0:
|
||||
# if we crossed over the left screen edge, move the ball right exactly by
|
||||
# the difference between the ball's leftmost point and the left screen edge, so its edge touches the border
|
||||
self.x -= self.x - self.radius
|
||||
# reverse velocity for a "bounce" effect
|
||||
if self.vel_x < 0:
|
||||
self.do_bounce_x()
|
||||
|
||||
# same shit for right edge
|
||||
elif self.x + self.radius > width - 1:
|
||||
# if we crossed over the right screen edge, move the ball back enough so it's inside, its edge touching the border, again
|
||||
self.x -= self.x + self.radius - width
|
||||
# reverse velocity for a "bounce" effect
|
||||
if self.vel_x > 0:
|
||||
self.do_bounce_x()
|
||||
|
||||
################# y collision ####################
|
||||
# top edge
|
||||
if self.y - self.radius < 0:
|
||||
self.y -= self.y - self.radius
|
||||
if self.vel_y < 0:
|
||||
self.do_bounce_y()
|
||||
|
||||
# bottom edge
|
||||
elif self.y + self.radius > height - 1:
|
||||
self.y -= self.y + self.radius - height
|
||||
if self.vel_y > 0:
|
||||
self.do_bounce_y()
|
||||
# since we're touching the floor, we are NOT floating in air; set floating to 0
|
||||
self.floating = 0
|
||||
else:
|
||||
# otherwise we clearly are floating in the air, high as a kite
|
||||
self.floating = 1
|
||||
|
||||
# the below functions could also cause some bouncy squeezy animation to play
|
||||
def do_bounce_x(self):
|
||||
self.vel_x *= self.bounce_x
|
||||
|
||||
def do_bounce_y(self):
|
||||
self.vel_y *= self.bounce_y
|
||||
|
||||
# draw function, to copy the ball's (pre-calculated, pre-drawn) image surface, or "sprite", onto the window surface
|
||||
def draw(self):
|
||||
# the blitsurface function copies sprite_surface to window_surface
|
||||
# it's much faster than setting pixels one by one via PixelView
|
||||
# the "None" could be replaced with: sdl.SDL_Rect(x, y, w, h) if we only wanted to copy part of the sprite
|
||||
# such as if it's actually a sprite sheet graphic, and we're just copying one piece, representing one frame
|
||||
# the SDL_Rect that __is__ there, is the destination coordinates (x, y, w, h) on the screen where we want to draw
|
||||
# you could scale the sprite larger or smaller along x and y axes by inserting different values for w and h there
|
||||
# instead of BlitSurface, we use BlitScaled; exact same, except Scaled will resize the source surface to the given area
|
||||
sdl2.SDL_BlitScaled(
|
||||
self.sprite_surface, # source surface
|
||||
None, # source rectangle
|
||||
window_surface, # target surface
|
||||
sdl2.SDL_Rect( # target rectangle
|
||||
int(self.x - self.size // 2), # target rect. start x
|
||||
int(self.y - self.size // 2), # target rect. start y
|
||||
self.size, # target rect. width
|
||||
self.size, # target rect. height
|
||||
),
|
||||
)
|
||||
# the function is written on multiple lines for the sake of clarity, but could be written on one line all the same;
|
||||
# such as the original blitsurface version of the function, commented out below
|
||||
# sdl2.SDL_BlitSurface(self.sprite_surface, None, window_surface, sdl2.SDL_Rect(int(self.x-self.radius), int(self.y-self.radius), self.size, self.size))
|
||||
|
||||
|
||||
def draw_circle(x, y, radius, r, g, b, target_pixels, shaded):
|
||||
# cast coordinates into integers, since they might be fractional numbers,
|
||||
# but we need integers to know which exact pixels to reference
|
||||
x = int(x)
|
||||
y = int(y)
|
||||
|
||||
# turn a flag on to create an extra pixel if the circle radius would produce even numbered coordinates
|
||||
# (e.g. if the circle is 100 x 100, it must have 4 pixels in the middle)
|
||||
# % 1 gets the fractional part of any number (e.g. 10.75 gives 0.75), abs() gets the absolute value (e.g. -0.1 becomes 0.1)
|
||||
# k must be 1 for even numbered, and 0 for odd numbered coordinates
|
||||
if abs((radius % 1) - 0.5) < 0.5:
|
||||
k = 0
|
||||
else:
|
||||
k = 1
|
||||
|
||||
# now that we don't need the fractional part anymore, we cast the radius to an integer, rounding down
|
||||
# if k is 0, we inferred from the radius that the circle size is closer to an odd number
|
||||
# in this case the rounding down "loses" an odd pixel along each dimension, and so we add 1 to the radius
|
||||
radius = int(radius) + (1 - k)
|
||||
|
||||
# we need these 2ndary variables if the circle is shaded; explained below
|
||||
new_r = r
|
||||
new_g = g
|
||||
new_b = b
|
||||
|
||||
# squared radius for an optimization trick; explained below
|
||||
radius_squared = radius**2
|
||||
|
||||
# process each pixel in a squrae area; not the whole pixel area of the ball, just one quadrant of it
|
||||
for y_pix in range(0, radius):
|
||||
for x_pix in range(0, radius):
|
||||
# get each pixel's distance from the square's centre
|
||||
# distance = math.sqrt(y_pix**2 + x_pix**2)
|
||||
# instead of getting the square root of the distance every time,
|
||||
# we get the square of the radius once at the beginning of the function (above)
|
||||
# and compare the squared values; the mathematical outcome in comparing which is greater, is the exact same! :O
|
||||
# but now we have saved ourselves lots of processing power, and don't need the sqrt library!
|
||||
distance_squared = y_pix**2 + x_pix**2
|
||||
# if the pixel is within a radius, draw it; otherwise leave it empty
|
||||
if distance_squared < radius_squared:
|
||||
# if the circle is shaded, modify r,g,b values using distance/radius ratio and shaded modifier
|
||||
if shaded != 0:
|
||||
# shaded == 1 gives a fully shaded ball, almost black near edges
|
||||
# (think like: 1 * 100% of red is reduced from red if distance/radius == 1.0)
|
||||
# (think like: 1 * 50% of red is reduced from red if distance/radius == 0.5)
|
||||
|
||||
# shaded == 0.5 gives a half-shaded ball, which has half-brightness of original colour near the edges
|
||||
# (think like: 0.5 * 100% == 50% of red is reduced from red if distance/radius == 1.0)
|
||||
# (think like: 0.5 * 50% == 25% of red is reduced from red if distance/radius == 0.5)
|
||||
|
||||
# shaded == 0 is not shaded at all, plain colour
|
||||
|
||||
# please note that we have to calculate a new shaded colour for each pixel,
|
||||
# while preserving the original r, g, b values; hence we need the new_r, new_g, new_b variables
|
||||
new_r = r - int(shaded * r * (distance_squared / radius_squared))
|
||||
new_g = g - int(shaded * g * (distance_squared / radius_squared))
|
||||
new_b = b - int(shaded * b * (distance_squared / radius_squared))
|
||||
|
||||
# we do 4 pixels at a time, just flipping - and + for coordinate offsets from x and y
|
||||
# we do the math above for one quadrant of the image, but write a mirrored pixel into all four quadrants
|
||||
# since the circle image is symmetrical on x and y axes, this saves time and processing power
|
||||
# you could even calculate just 1 octant and draw 8 pixels in all octants, but that would overcomplicate the code for this example
|
||||
|
||||
try:
|
||||
target_pixels[y - y_pix - k][x - x_pix - k] = sdl2.ext.Color(new_r, new_g, new_b)
|
||||
target_pixels[y - y_pix - k][x + x_pix] = sdl2.ext.Color(new_r, new_g, new_b)
|
||||
target_pixels[y + y_pix][x - x_pix - k] = sdl2.ext.Color(new_r, new_g, new_b)
|
||||
target_pixels[y + y_pix][x + x_pix] = sdl2.ext.Color(new_r, new_g, new_b)
|
||||
except:
|
||||
print("Warning: attempting to draw outside of boundaries!")
|
||||
|
||||
|
||||
def make_ball_sprite(size, r, g, b, shaded):
|
||||
# a surface is just data containing pixel graphics, like an image
|
||||
# because SDL_CreateRGBSurface() returns a memory address (pointer) to a surface, instead of a surface itself...
|
||||
p_sprite_surface = sdl2.SDL_CreateRGBSurface(0, size, size, 32, 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF)
|
||||
# ...we need to create another variable from the contents (actual surface) at that memory address
|
||||
sprite_surface = p_sprite_surface.contents
|
||||
# in order to be able to manipulate the pixels of the image, we need this PixelView thing to "unlock" its memory
|
||||
sprite_pixels = sdl2.ext.PixelView(sprite_surface)
|
||||
# with all that shit done, we can finally fucking draw some shit on the image
|
||||
# start coordinates are size // 2, since the x, y given to draw_circle are its middle point
|
||||
draw_circle(size // 2, size // 2, size / 2, r, g, b, sprite_pixels, 0.75)
|
||||
|
||||
# now that we're finished manipulating the surface's pixels, return the surface to whoever called
|
||||
return sprite_surface
|
||||
|
||||
|
||||
def process_events(control):
|
||||
# get_events() gets a "queue" list of all kinds of happenings from keyboard, mouse, whatever
|
||||
# these things are called events, though in other contexts the word can also refer to in-game events (player died, grenade exploded...)
|
||||
events = sdl2.ext.get_events()
|
||||
|
||||
# you can then walk through this queue of events using a for loop, and check for the ones you want
|
||||
# see https://wiki.libsdl.org/SDL_EventType for a complete list
|
||||
for event in events:
|
||||
# function returns False to the while (running) loop, making running = False
|
||||
# and thus exit the loop - and also exit the program
|
||||
if event.type == sdl2.SDL_QUIT:
|
||||
return False
|
||||
|
||||
# SDL_GetKeyboardState(None) creates a data structure that contains status of all keyboard buttons
|
||||
# please keep in mind that get_events() must be called first or else there is nothing
|
||||
keyboard_state = sdl2.SDL_GetKeyboardState(None)
|
||||
|
||||
# the control object given as argument to this function is used to store 1 or 0 for left, right, up, down
|
||||
# depending on if that key element on the keyboard_state list is 1 or 0
|
||||
# for a full list, see list of SDL_SCANCODE_ * in https://wiki.libsdl.org/SDL_Scancode
|
||||
control.left = keyboard_state[sdl2.SDL_SCANCODE_LEFT]
|
||||
control.right = keyboard_state[sdl2.SDL_SCANCODE_RIGHT]
|
||||
control.up = keyboard_state[sdl2.SDL_SCANCODE_UP]
|
||||
control.down = keyboard_state[sdl2.SDL_SCANCODE_DOWN]
|
||||
|
||||
# don't forget to return True to the main loop that calls this function, so running = True
|
||||
return True
|
||||
|
||||
|
||||
# global physics variables
|
||||
gravity = 1 # rate at which vel_y increases every cycle to simulate gravity
|
||||
friction = 0.8 # multiplier for scaling x velocity when colliding with bottom edge
|
||||
bounce = -0.9 # multiplier for reversing velocity when an object collides and bounces off
|
||||
stop_vel = 0.25 # threshold below which velocity is set to 0
|
||||
accelerate = 1 # rate at which vel_x changes when controlling left/right
|
||||
max_move_vel = 8 # stop applying left/right force once max vel reached
|
||||
jump_vel = 20 # y_vel to be applied for jumping (as a negative value -> up)
|
||||
# a quick physics lesson:
|
||||
# https://i.imgur.com/9qKajZI.png
|
||||
# * speed is how fast you are travelling, e.g. car travelling at speed of 20 m/s
|
||||
# * velocity is speed in a given direction, e.g. a car is travelling east at a velocity of 10/ms
|
||||
# velocity is usually a vector, i.e. information represented using two or three axises, so:
|
||||
# a car with a north velocity (y axis) of 10 m/s and east velocity (x axis) of 10 m/s would have an actual speed of ~14.1 m/s
|
||||
# that is why we use the word velocity, and seldom call anything speed
|
||||
|
||||
|
||||
################## MAIN PROGRAM EXECUTION ##########################
|
||||
|
||||
# globals for testing
|
||||
color = sdl2.ext.Color(255, 0, 255)
|
||||
# let's create some balls because why not
|
||||
# 1st ball should auto-generate its own red ball sprite because we give None as the sprite surface argument
|
||||
Ball1 = Ball(100, 200, 200, None)
|
||||
Ball1.vel_x = 0
|
||||
Ball1.vel_y = 0
|
||||
Ball1.bounce_y = 0 # disable the vertical bouncing to make this ball easier to control
|
||||
|
||||
# let's make a blue sprite for the 2nd ball
|
||||
# we purposefully make the sprite smaller than teh ball, to see that BlitScaled will scale the sprite up
|
||||
# to the target drawing area on the screen
|
||||
blue_ball_sprite = make_ball_sprite(15, 100, 100, 255, 0.66)
|
||||
|
||||
# and the 2nd ball that uses it
|
||||
Ball2 = Ball(50, 100, 200, blue_ball_sprite)
|
||||
Ball2.vel_x = -50
|
||||
Ball2.vel_y = -10
|
||||
|
||||
# Let's make a 3rd ball that uses the same sprite image too
|
||||
Ball3 = Ball(100, 400, 200, blue_ball_sprite)
|
||||
Ball3.vel_x = 0
|
||||
Ball3.vel_y = 0
|
||||
|
||||
# just to prove a point, let's edit the blue ball sprite by adding a green square to it
|
||||
# and see that the change shows up in both blue balls, proving they don't store unique copies
|
||||
# but instead, have memory pointers to the same graphic!
|
||||
sdl2.ext.fill(blue_ball_sprite, sdl2.ext.Color(0, 255, 0), (5, 5, 5, 5))
|
||||
# just for the sake of completeness and rehearsal, let's also see how to add just 1 pixel, a red dot at the centre
|
||||
blue_ball_sprite_pixels = sdl2.ext.PixelView(blue_ball_sprite)
|
||||
blue_ball_sprite_pixels[7][7] = sdl2.ext.Color(255, 0, 0)
|
||||
|
||||
|
||||
# could be called main(), run(), execute() or whatever
|
||||
def run():
|
||||
# example of a game loop, demonstrating in which order to execute things
|
||||
running = True
|
||||
while running == True:
|
||||
######################## 1. GET INPUT & PROCESS EVENTS ####################################
|
||||
# get player input into Ball1.controller and check events for quitting,
|
||||
# set running to False if quit, otherwise True
|
||||
# control input could also be stored to some kind of global_controller object,
|
||||
# such as when you want to control a menu, or copy input from a global object
|
||||
# to multiple balls and other things, to simultaneously control many things
|
||||
running = process_events(Ball1.controller)
|
||||
|
||||
######################## 2. LOGIC PROCESSING (physics, AI, etc.) ########################
|
||||
# now that we processed events and stored keyboard state into Ball1.controller,
|
||||
# first alter the Ball1's velocity using its control() function
|
||||
Ball1.do_control()
|
||||
# then work out physics, first gravity, then how friction affects vel values,
|
||||
# and then move the ball's physical location (x, y) based on the final vel values
|
||||
Ball1.do_physics()
|
||||
Ball2.do_physics()
|
||||
Ball3.do_physics()
|
||||
# since the ball has been moved now, we must check if it has collided, and do things like
|
||||
# correct its position if it went through the edge, or change velocity values to "bounce" it off
|
||||
Ball1.do_edge_collision()
|
||||
Ball2.do_edge_collision()
|
||||
Ball3.do_edge_collision()
|
||||
# for countless more balls, it would make more sense for program code to look like this:
|
||||
# list_of_balls = []
|
||||
# list_of_balls.append(Ball(size_here, some_x, some_y, some_sprite or None))
|
||||
# ...
|
||||
# for ball in list_of_balls:
|
||||
# ball.do_some_thing()
|
||||
# ...
|
||||
# for ball in list_of_balls:
|
||||
# ball.do_another_thing()
|
||||
# ...
|
||||
# you would certainly need to do this for things like particles, bullets, etc. which there are 100s or 1000s
|
||||
# because you cannot write individual variables and class instances for each of them
|
||||
|
||||
############ 3. GRAPHICS UPDATE (draw the screen and all objects on top of it) ############
|
||||
# clear screen with black background
|
||||
sdl2.ext.fill(window_surface, color, (0, 0, width, height))
|
||||
# with the balls now in their final positions after all the calculations done above,
|
||||
# draw the balls over the black background
|
||||
Ball1.draw()
|
||||
Ball2.draw()
|
||||
Ball3.draw()
|
||||
|
||||
color2 = sdl2.ext.Color(23, 0, 42)
|
||||
for i in range(0,1000,2):
|
||||
sdl2.ext.draw.line(
|
||||
window_surface,
|
||||
color2,
|
||||
(
|
||||
i,
|
||||
int(Ball1.vel_y),
|
||||
i,
|
||||
int(Ball1.vel_y+100),
|
||||
),
|
||||
1,
|
||||
)
|
||||
sdl2.ext.draw.draw_rect(
|
||||
window_surface,
|
||||
color2,
|
||||
(
|
||||
i,
|
||||
int(Ball1.vel_y),
|
||||
i,
|
||||
int(Ball1.vel_y+100),
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
for i in range(0,1000,2):
|
||||
sdl2.ext.draw.line(
|
||||
window_surface,
|
||||
color2,
|
||||
(
|
||||
int(Ball1.vel_x),
|
||||
i,
|
||||
int(Ball1.vel_x+100),
|
||||
i,
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
# refresh the window
|
||||
window.refresh()
|
||||
sleep(1 / 60) # <- this ...
|
||||
# doesn't actually mean we get 60 FPS, just that we get a 1/60 second delay,
|
||||
# no matter how long the game frame took to process
|
||||
|
||||
|
||||
# program start
|
||||
run()
|
Loading…
Reference in New Issue