def __init__(self, *, width, height, **kwargs): """Constructor Kwargs: width(int): width of the scene in pixels height(int): height of the scene in pixels kwargs(dict, optional): Arbitrary keyword arguments """ super().__init__(**kwargs) # Set scene dimensions self._window_width = width self._window_height = height # Set up the actual EntityManager self._ent_mgr = EntityManager() # set gravity self._ent_mgr.gravity = spot_get("sv_gravity") # Board self._board = Board(width, height, self._ent_mgr) # Players (and their related information) will be held in here self._players = {} # Register entities self._ent_mgr.register_class("ent_player", PlayerPaddle) self._ent_mgr.register_class("ent_ball", Ball) # Paddle impulse, top speed and artificial friction self._paddle_impulse = spot_get("sv_paddle_impulse") self._paddle_max_velocity = spot_get("sv_paddle_max_velocity") self._paddle_friction = spot_get("sv_paddle_friction") # Create the actual ball self.create_ball() # Current state self._state = self.ST_WAITING_FOR_PLAYER # Set up tick interval on server self._tickrate = 1.0 / spot_get("tickrate") pyglet.clock.schedule_interval(self.tick, self._tickrate) # this method wis called each time the ball # collides with either the left or the right boundary # on the board self._ent_mgr.add_collision_handler(Ball.CTYPE, Board.BOUNDARY_CTYPE_LEFT, begin=self._scored_left) self._ent_mgr.add_collision_handler(Ball.CTYPE, Board.BOUNDARY_CTYPE_RIGHT, begin=self._scored_right)
class Scene(ming.Server): """ Scene server implementation This bad boy handles the game per se, by keeping and controlling entities and pumping responses to clients so they can do their rendering. """ # Maximum number of players (clients) allowed to join the game MAX_PLAYERS = 2 # States ST_WAITING_FOR_PLAYER = 100 ST_BEGIN = 101 ST_PLAYING = 102 ST_SCORE = 103 ST_GAME_SET = 104 def __init__(self, *, width, height, **kwargs): """Constructor Kwargs: width(int): width of the scene in pixels height(int): height of the scene in pixels kwargs(dict, optional): Arbitrary keyword arguments """ super().__init__(**kwargs) # Set scene dimensions self._window_width = width self._window_height = height # Set up the actual EntityManager self._ent_mgr = EntityManager() # set gravity self._ent_mgr.gravity = spot_get("sv_gravity") # Board self._board = Board(width, height, self._ent_mgr) # Players (and their related information) will be held in here self._players = {} # Register entities self._ent_mgr.register_class("ent_player", PlayerPaddle) self._ent_mgr.register_class("ent_ball", Ball) # Paddle impulse, top speed and artificial friction self._paddle_impulse = spot_get("sv_paddle_impulse") self._paddle_max_velocity = spot_get("sv_paddle_max_velocity") self._paddle_friction = spot_get("sv_paddle_friction") # Create the actual ball self.create_ball() # Current state self._state = self.ST_WAITING_FOR_PLAYER # Set up tick interval on server self._tickrate = 1.0 / spot_get("tickrate") pyglet.clock.schedule_interval(self.tick, self._tickrate) # this method wis called each time the ball # collides with either the left or the right boundary # on the board self._ent_mgr.add_collision_handler(Ball.CTYPE, Board.BOUNDARY_CTYPE_LEFT, begin=self._scored_left) self._ent_mgr.add_collision_handler(Ball.CTYPE, Board.BOUNDARY_CTYPE_RIGHT, begin=self._scored_right) def _scored_left(self, space, arbiter, *args, **kwargs): """ the ball has collided with the left boundary """ player = [p for p in self._players.values() if p.number == 2][0] player.score += 1 # bump the score self._scored() return False # tell pymunk to ignore the collision def _scored_right(self, space, arbiter, *args, **kwargs): """ the ball has collided with the right boundary """ player = [p for p in self._players.values() if p.number == 1][0] player.score += 1 # bump the score self._scored() return False # tell pymunk to ignore the collision def _scored(self): """ At this point , the ball has collided with either the right or left wall """ # Set state to 'score' state self._state = self.ST_SCORE # Wait for 3 seconds before unfreezing the board # If one of the players has reached max score, then switch # to set state, otherwise, go back to round state pyglet.clock.schedule_once(self._round_goback, 3) def _round_goback(self, dt): if any([player.score == spot_get("sv_score_max") for player in self._players.values()]): self._state = self.ST_GAME_SET else: self._state = self.ST_PLAYING @property def state(self): """Get current state in server""" return self._state def broadcast_update(self): """Send an update to all clients""" if len(self._players): # The actual response response = Response() # Set the answer as accepted response.status = Response.STATUS_OK response.reason = Response.REASON_UPDATE # Set state response.state = self._state for player in self._players.values(): # Current player player_me = player # Get host and port from him host = player.host port = player.port if self._state == self.ST_PLAYING or self.state == self.ST_SCORE or self.state == self.ST_BEGIN: # Set player information response.set_player_info( name="you", score=player_me.score, number=player_me.number, position=(int(player_me.position.x), int(player_me.position.y)), velocity=(int(player_me.velocity.x), int(player_me.velocity.y)), ) # Set opponent (foe) information if player.foe is not None: player_foe = self._players[player.foe] response.set_player_info( name="foe", score=player_foe.score, number=player_foe.number, position=(int(player_foe.position.x), int(player_foe.position.y)), velocity=(int(player_foe.velocity.x), int(player_foe.velocity.y)), ) # Set ball information response.set_ball_info( position=(int(self._ball.position.x), int(self._ball.position.y)), velocity=(int(self._ball.velocity.x), int(self._ball.velocity.y)), ) # Send the packet to the client self.send(response.data, host, port) def _reset_player(self, player): """Reset values on a player""" # Calculate initial position player_position_x, player_position_y = spot_get("paddle_position_start") # player 2's position on the other side of the screen if player.number == 2: player_position_x = self._window_width - player_position_x # Set initial position for this player player.position = player_position_x, player_position_y # Reset physics on this player # player.reset_forces() player.velocity = (0, 0) if self._state == self.ST_BEGIN: # Reset score player.score = 0 # Ready state for this player player.ready = False def reset_players(self): """Reset values on players""" for player in self._players.values(): self._reset_player(player) def reset_ball(self): """Reset ball position""" # Set initial position for this ball # self._ball.reset_forces() self._ball.velocity = (0, 0) self._ball.position = spot_get("ball_position_start") # FIXME: do something better # Set initial impulse on the ball self._ball.apply_impulse((-1500, 0)) def create_ball(self): """Create a ball to play""" # New PlayerPaddle for a client self._ball = self._ent_mgr.create_entity("ent_ball") # Reset values on ball self.reset_ball() # Increase/maintain ball velocity each second # TODO: move this to tick() pyglet.clock.schedule_interval(self.increase_ball_velocity, 1.0) def create_player(self, host, port): """Create a PlayerPaddle for a client Args: host(str): client address port(int): client port """ # New PlayerPaddle for a client player = self._ent_mgr.create_entity("ent_player", host=host, port=port, number=len(self._players) + 1) # Add this player to the server self._players[player.uuid] = player # Reset its values self._reset_player(self._players[player.uuid]) # Return the entity return player def destroy_player(self, player_id): """Get rid of a player""" del self._players[player_id] def update_players(self): """Update information on players""" # Change state depending on the number of players present if len(self._players) < self.MAX_PLAYERS: self._state = self.ST_WAITING_FOR_PLAYER else: self._state = self.ST_BEGIN # Update each player's foes for uuid, player in self._players.items(): foes = [p.uuid for p in self._players.values() if p.uuid != uuid] if len(foes): player.foe = foes[0] else: player.foe = None def increase_ball_velocity(self, dt): """Increase/maintain a constant velocity for the ball""" if self._state == self.ST_PLAYING: # In order to have a decent/pleasurable gameplay # the ball needs to maintain a certain pace, so it # becomes "pushed" constantly until it reaches its # top speed self._ball.velocity = (1.02 * self._ball.velocity.x, 1.02 * self._ball.velocity.y) # Very much like table hockey games the ball gets # pushed until it gains its minimun speed of 200.0 if abs(self._ball.velocity.x) < 200.0: if self._ball.velocity.x > 0: self._ball.apply_impulse((200, self._ball.velocity.y)) elif self._ball.velocity.x < 0: self._ball.apply_impulse((-200, self._ball.velocity.y)) def tick(self, dt): """Run simulation on server and broadcast an update to all clients During each tick, the server processes incoming user commands, runs a physical simulation step, checks the game rules, and updates all object states. """ # Process incoming user commands self.pump() ################################# # Run a physical simulation step: ################################# if self._state == self.ST_PLAYING: # FIXME: This is working, it caps the velocity to 0 in x # so it won't move sideways no matter what for player in self._players.values(): # cancel horizontal velocity player.velocity = 0, player.velocity.y # artificial friction maybe? player.apply_impulse((0, -self._paddle_friction * player.velocity.y)) # Physics are performed based on a fixed time step # or time scale from which all bodies on a scene # are ruled. This is done for consistent client-server # physics. self._ent_mgr.step(self._tickrate) # Tell the EntityManager to deliver all # pending messages (if there are any) self._ent_mgr.dispatch_messages() elif self._state == self.ST_BEGIN: # If all players are ready, then move on if all([p.ready for p in self._players.values()]): self._state = self.ST_PLAYING # Broadcast latest snapshot to all clients self.broadcast_update() def on_data_received(self, data, host, port): """Pump network requests from clients Args: data(dict): incoming raw data host(str): client address port(int): client port """ # # Get a nice Request from raw data # request = Request(data=data) # # By default, the server will not be OK with the incoming request # response = Response() response.status = Response.STATUS_UNAUTHORIZED response.reason = Response.REASON_CONN_REFUSED # TODO: Check for protocol version # if request.proto_version != Packet.PROTO_VERSION: # # Send the packet to the client # response.reason.REASON_VERSION_NOT_SUPPORTED # self.send(response.data, host, port) # return ####################################### # The actual pump ####################################### if request.command is not None: if request.player_id is None: # # Client is trying to establish a connection # if request.command == Request.CMD_CONNECT: if len(self._players) < self.MAX_PLAYERS: response.status = Response.STATUS_OK response.reason = Response.REASON_CONN_GRANTED response.player_id = self.create_player(host, port).uuid self.update_players() # Send the packet to the client self.send(response.data, host, port) elif request.player_id in self._players: # # Request is valid and going to be processed # # First of all, get the players' entities player_me = self._players[request.player_id] # FIXME: this will get better if self._players[request.player_id].foe is not None: foe_uuid = self._players[request.player_id].foe player_foe = self._players[foe_uuid] # Get player's command command = request.command # TODO: document this if self._state == self.ST_PLAYING: # +move command if command == Request.CMD_MV_UP: player_me.apply_impulse((0, self._paddle_impulse)) # -move command elif command == Request.CMD_MV_DN: player_me.apply_impulse((0, -self._paddle_impulse)) # TODO: document this if self._state == self.ST_BEGIN: # +ready command if command == Request.CMD_READY: player_me.ready = True # disconnect command if command == Request.CMD_DISCONNECT: self.destroy_player(request.player_id) self.update_players()