From bd8c9540d470548134d8bf487640a1c1dda126e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E9=B8=BF?= Date: Wed, 21 Feb 2024 00:03:07 +0800 Subject: [PATCH] init. --- sdl_demo.py | 428 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 sdl_demo.py diff --git a/sdl_demo.py b/sdl_demo.py new file mode 100644 index 0000000..47f5bdb --- /dev/null +++ b/sdl_demo.py @@ -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()