简体   繁体   中英

Choppy online python game

I have programmed a simple clone of Slither.io in python using pygame and sockets and I have three problems:

  1. When I play the game alone on my laptop the game is choppy. Every ten seconds my game get stuck for a while (one milisecond) and then continue. It's not a big problem but it's annoying.
  2. When I play on two computers in my local network, I see the other player (the other snake) is also choppy.
  3. The strangest problem is when I run my server on my main laptop and then run the game on my second laptop the game starts and after few seconds crash. Debugger on client says that pickle data was truncated while receiving data from the server. But when I run the server program on my second laptop and the game on my main laptop everything is OK. Why?

I tried:
Problem 1. change FPS on the client and time.sleep on the server
Problem 2. change time.sleep on the server
Problem 3. change the input value of recv() method


Server code:

import socket
import threading
import pickle
import random
import time
import math

ip = socket.gethostbyname(socket.gethostname())
port = 5555

address = (ip, port)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

snakes = []
colors = [(80, 0, 0), (0, 80, 0), (0, 0, 140), (80, 80, 0), (80, 0, 80), (0, 80, 80)]
food = []

for i in range(0, 300, 1):
    food.append([random.randint(10, 3290), random.randint(10, 2090), (0, 0, 100), 10, 1])

number_of_players = 0

def start(server, address):
    server.bind(address)

def new_clients(server):
    global number_of_players
    server.listen()
    while True:
        conn, addr = server.accept()
        thread = threading.Thread(target=client, args=(conn, addr, number_of_players))
        thread.start()
        number_of_players += 1
        print(f"Active connections: {threading.activeCount() - 1}")

def client(conn, addr, player):
    print(f"New connection: {addr}\n")
    global snakes
    global colors
    global food
    try:
        snakes.append([])
        name = pickle.loads(conn.recv(1024))
        while True:
            verity = True
            x = random.randint(100, 3200)
            y = random.randint(100, 2000)
            for i in range(0, len(snakes), 1):
                if snakes[i] != []:
                    if math.sqrt((snakes[i][0][0] - x)**2 + (snakes[i][0][1] - y)**2) > snakes[i][0][4]*2:
                        for a in range(0, len(snakes[i][1])):
                            if math.sqrt((snakes[i][1][a][0] - x)**2 + (snakes[i][1][a][1] - y)**2) < snakes[i][0][4]*2:
                                verity = False
                            if verity == False:
                                break
                    else:
                        verity = False
                    if verity == False:
                        break
            if verity == True:
                break

        snakes[player] = [[x, y, random.choice(colors), 4, 30, name, 0, 3], []]
        conn.send(pickle.dumps(snakes[player]))
    except (ConnectionAbortedError, ConnectionResetError, EOFError):
        print("Connection lost")
        print(f"Active connections: {threading.activeCount() - 2}")
        conn.close()
        return
    while True:
        try:
            snakes_to_send = []
            for i in range(0, len(snakes), 1):
                if (i != player) and (len(snakes[i]) != 0):
                    snakes_to_send.append(snakes[i])
            conn.send(pickle.dumps([snakes_to_send, food]))

            received = pickle.loads(conn.recv(8192*4))
            snakes[player] = received[0]

            eaten = received[1]
            for i in range(0, len(eaten), 1):
                try:
                    food.remove(eaten[i])
                    if eaten[i][4] != 5:
                        food.append([random.randint(10, 3290), random.randint(10, 2090), (0, 0, 100), 10, 1])
                except ValueError:
                    continue

            dead_snake = received[2]
            if dead_snake != []:
                for i in range(0, len(dead_snake), 1):
                    food.append(dead_snake[i])

            time.sleep(1/1000)
        except (ConnectionAbortedError, ConnectionResetError, EOFError):
            snakes[player] = []
            print("Connection lost")
            print(f"Active connections: {threading.activeCount() - 2}")
            conn.close()
            break

print(f"Server is starting on IP: {ip} and PORT: {port}")
start(server, address)
new_clients(server)

The game:

#import
import pygame
import sys
import math
import random
import pygame_textinput
from client import Client

#pygame initialize
pygame.init()
win = pygame.display.set_mode((1100, 700))
pygame.display.set_caption("Slither.io")

#connect to the server
ip = "10.0.0.3"
port = 5555
c = Client(ip, port)
c.server()

#fonts
font = pygame.font.SysFont("comicsansms", 20)
big_font = pygame.font.SysFont("comicsansms", 35)

#time
clock = pygame.time.Clock()

class Snake:
    def __init__(self, x, y, color, length, diameter, name, score, step):
        self.x = x
        self.y = y
        self.color = color
        self.length = length
        self.diameter = diameter
        self.name = name
        self.score = score
        self.step = step
        self.pos_list = []

    #move the snake
    def move(self):
        self.mousex, self.mousey = pygame.mouse.get_pos()
        self.difx = self.mousex - 550
        self.dify = self.mousey - 350
        try:
            self.xstep = math.sqrt((self.step**2)/((self.dify/self.difx)**2 + 1))
            self.ystep = (self.dify/self.difx)*self.xstep
        except ZeroDivisionError:
            self.xstep = 0
            self.ystep = self.step
        if math.sqrt(self.difx**2 + self.dify**2) > self.diameter/3:
            if(self.difx > 0):
                self.x += self.xstep
                self.y += self.ystep
            elif(self.difx < 0):
                self.x -= self.xstep
                self.y -= self.ystep
            else:
                if self.dify > 0:
                    self.y += self.ystep
                else:
                    self.y -= self.ystep
        self.pos_list.append([self.x, self.y])
        if len(self.pos_list) == 100:
            del self.pos_list[0]

    #check if the snake isn't out of the area or not crashed
    def collision(self):
        global run
        if (self.x + self.diameter/3 >= 3299) or (self.x - self.diameter/3 <= 1) or (self.y + self.diameter/3 >= 2099) or (self.y - self.diameter/3 <= 1):
            run = False
        for i in range(0, len(other_snakes), 1):
            if math.sqrt((other_snakes[i].x - snake.x)**2 + (other_snakes[i].y - snake.y)**2) < other_snakes[i].diameter*(2/3):
                run = False
        for i in range(0, len(other_segments), 1):
            if math.sqrt((other_segments[i].x - snake.x)**2 + (other_segments[i].y - snake.y)**2) < other_segments[i].diameter*(2/3):
                run = False

    #draw all snakes
    def draw(self):
        pygame.draw.rect(win, self.color, [self.x - snake.x + 550 - self.diameter/2, self.y - snake.y + 350 - self.diameter/2, self.diameter, self.diameter], border_radius=int(self.diameter/2))

class Segment(Snake):
    def __init__(self, x, y, diameter, step, circle):
        self.x = x
        self.y = y
        self.color = (80, 80, 80)
        self.diameter = diameter
        self.step = step
        self.circle = circle
        self.pos_list = []

    def move(self):
        self.diameter = snake.diameter
        try:
            self.x = self.circle.pos_list[-int(self.diameter/self.step)][0]
            self.y = self.circle.pos_list[-int(self.diameter/self.step)][1]
        except IndexError:
            self.x = self.circle.pos_list[0][0]
            self.y = self.circle.pos_list[0][1]
        self.pos_list.append([self.x, self.y])
        if len(self.pos_list) == 100:
            del self.pos_list[0]

class Food(Snake):
    def __init__(self, x, y, color, diameter, points):
        self.x = x
        self.y = y
        self.color = color
        self.diameter = diameter
        self.points = points

other_snakes = []
segments = []
other_segments = []
food = []
eaten = []
dead_snake = []

#get other snakes
def return_snakes():
    global other_snakes
    global other_segments
    global food
    other_snakes = []
    other_segments = []
    food = []
    message = c.receive_message()
    received = message[0]
    for i in range(0, len(received), 1):
        s = Snake(received[i][0][0], received[i][0][1], received[i][0][2], received[i][0][3], received[i][0][4], received[i][0][5], received[i][0][6], snake.step)
        other_snakes.append(s)
        for a in range(0, len(received[i][1]), 1):
            sg = Segment(received[i][1][a][0], received[i][1][a][1], s.diameter, snake.step, 0)
            other_segments.append(sg)
    rec_fd = message[1]
    for i in range(0, len(rec_fd), 1):
        f = Food(rec_fd[i][0], rec_fd[i][1], rec_fd[i][2], rec_fd[i][3], rec_fd[i][4])
        food.append(f)

#send the snake to server
def send_snake():
    global snake
    global segments
    global food
    to_send = [[[snake.x, snake.y, snake.color, snake.length, snake.diameter, snake.name, snake.score], []]]
    for i in range(0, len(segments), 1):
        to_send[0][1].append([segments[i].x, segments[i].y])
    to_send.append(eaten)
    to_send.append(dead_snake)
    c.send_message(to_send)

#function for ordering snakes by score
def order(o):
    return o["score"]

#draw screen
def screen():
    for i in range(0, 3301, 100):
        pygame.draw.line(win, (60, 60, 60), [i - snake.x + 550, 0 - snake.y + 350], [i - snake.x + 550, 2100 - snake.y + 350], 1)
    for i in range(0, 2101, 100):
        pygame.draw.line(win, (60, 60, 60), [0 - snake.x + 550, i - snake.y + 350], [3300 - snake.x + 550, i - snake.y + 350], 1)

#enter your nickname
textinput = pygame_textinput.TextInput(font_family="comicsansms", max_string_length=12)
while True:
    win.fill((100, 100, 100))
    events = pygame.event.get()
    for event in events:
        keys = pygame.key.get_pressed()
        if (event.type == pygame.QUIT) or keys[pygame.K_ESCAPE] or keys[pygame.K_F4] and pygame.key.get_mods() & pygame.KMOD_ALT:
            sys.exit()
    if keys[pygame.K_RETURN]:
        break
    name = big_font.render("Nickname:", True, (0, 0, 0))
    center = name.get_rect(center=(400, 350))
    win.blit(name, center)
    textinput.update(events)
    win.blit(textinput.get_surface(), (500, 325))
    pygame.display.update()

c.send_message(textinput.get_text())
received = c.receive_message()
snake = Snake(received[0][0], received[0][1], received[0][2], received[0][3], received[0][4], received[0][5], received[0][6], received[0][7])

segment = Segment(snake.x, snake.y, snake.diameter, snake.step, snake)
segments.append(segment)
for i in range(0, snake.length - 2, 1):
    segment = Segment(snake.x, snake.y, snake.diameter, snake.step, segment)
    segments.append(segment)

while True:
    run = True
    while run:
        eaten = []
        #quit game
        for event in pygame.event.get():
            keys = pygame.key.get_pressed()
            if (event.type == pygame.QUIT) or keys[pygame.K_ESCAPE] or keys[pygame.K_F4] and pygame.key.get_mods() & pygame.KMOD_ALT:
                sys.exit()

        return_snakes()

        snakes = [{"name": snake.name, "score": snake.score}]
        for i in range(0, len(other_snakes), 1):
            snakes.append({"name": other_snakes[i].name, "score": other_snakes[i].score})

        win.fill((100, 100, 100))
        screen()

        #draw all food
        for i in range(0, len(food), 1):
            food[i].draw()

        #draw the other snakes and describe
        for i in range(0, len(other_segments), 1):
            other_segments[i].draw()

        for i in range(0, len(other_snakes), 1):
            other_snakes[i].draw()

            name = font.render(other_snakes[i].name, True, (0, 0, 0))
            center = name.get_rect(center=(other_snakes[i].x - snake.x + 550, other_snakes[i].y - snake.y + 350))
            win.blit(name, center)

        #move, draw snake and check if the snake is alive
        snake.move()
        for i in range(0, len(segments), 1):
            segments[i].move()
        for i in range(0, len(segments), 1):
            segments[i].draw()
        snake.draw()
        snake.collision()

        #check if the snake ate food and add score
        for i in range(len(food) - 1, -1, -1):
            if math.sqrt((food[i].x - snake.x)**2 + (food[i].y - snake.y)**2) < (snake.diameter/2 + food[i].diameter/2):
                snake.score += food[i].points
                snake.length = int(snake.score/10) + 4
                if snake.length - 1 > len(segments):
                    segment = Segment(snake.x, snake.y, snake.diameter, snake.step, segment)
                    segments.append(segment)
                snake.diameter = int(snake.score/50)*0.5 + 30
                eaten.append([food[i].x, food[i].y, food[i].color, food[i].diameter, food[i].points])

        #describe the snake
        name = font.render(snake.name, True, (0, 0, 0))
        center = name.get_rect(center=(550, 350))
        win.blit(name, center)

        send_snake()

        #sort the snakes by score
        snakes.sort(key=order, reverse=True)

        #create the table
        for i in range(0, len(snakes), 1):
            table = font.render(str(snakes[i]["name"]), True, (0, 0, 0))
            win.blit(table, (900, 50 + i*30))
        for i in range(0, len(snakes), 1):
            table = font.render(str(snakes[i]["score"]), True, (0, 0, 0))
            win.blit(table, (1050, 50 + i*30))
        for i in range(0, len(snakes), 1):
            table = font.render(str(i + 1) + ".", True, (0, 0, 0))
            win.blit(table, (870, 50 + i*30))

        pygame.display.update()
        clock.tick(140)

    return_snakes()

    #create food from head of dead snake
    ds = [snake.x, snake.y, snake.color, 15, 5]
    dead_snake.append(ds)

    #create food from segments of dead snake
    for i in range(0, len(segments), 1):
        ds = [segments[i].x, segments[i].y, snake.color, 15, 5]
        dead_snake.append(ds)

    send_snake()
    dead_snake = []

    clock.tick(140)

    c.client.close()

    while True:
        #quit game
        for event in pygame.event.get():
            keys = pygame.key.get_pressed()
            if (event.type == pygame.QUIT) or keys[pygame.K_ESCAPE] or keys[pygame.K_F4] and pygame.key.get_mods() & pygame.KMOD_ALT:
                sys.exit()
        if keys[pygame.K_SPACE]:
            c = Client(ip, port)
            c.server()
            c.send_message(snake.name)
            received = c.receive_message()
            snake = Snake(received[0][0], received[0][1], received[0][2], received[0][3], received[0][4], received[0][5], received[0][6], received[0][7])
            segments = []
            segment = Segment(snake.x, snake.y, snake.diameter, snake.step, snake)
            segments.append(segment)
            for i in range(0, snake.length - 2, 1):
                segment = Segment(snake.x, snake.y, snake.diameter, snake.step, segment)
                segments.append(segment)
            break

        win.fill((100, 100, 100))
        screen()

        #press space bar to continue
        cont = big_font.render("Press space bar to continue", True, (0, 0, 0))
        center = cont.get_rect(center=(550, 350))
        win.blit(cont, center)

        pygame.display.update()
        clock.tick(140)

Client class file:

import socket
import pickle

ip = "10.0.0.3"
port = 5555

class Client:
    def __init__(self, ip, port):
        self.ip = ip
        self.port = port
        self.address = (self.ip, self.port)
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def server(self):
        try:
            self.client.connect(self.address)
            print(f"Successfully connected to {self.address}")
        except:
            pass

    def send_message(self, message):
        self.client.sendall(pickle.dumps(message))

    def receive_message(self):
        return pickle.loads(self.client.recv(8192*4))

And this is pygame_textinput library I'm using here.

Your third problem (truncated pickle data) is because you are using TCP, and you are unpickling whatever recv returns. You might be thinking that whenever you send something, and the receiver calls recv , returns the exact same thing, but actually it doesn't. TCP splits your data up into packets, so the receiver might not receive all the data at the same time.

For example, if you send "abcdefgh" and then separately send "ijkl", it's allowed for the first receive to return "abcd" and the second to return "efghijkl". Or the first one could return "ab" and the second one could return "cde" and the third one could return "fghijkl", or so on.

You have to design a way for the receiver to know when to stop receiving. For example, if you sent "8abcdefgh" and then "4ijkl", the receiver could get "8abcdefgh4ij", and then it knows "8abcdefgh" is one "send" (because it starts with 8 and then 8 more bytes) and it knows the "4ij" is the beginning of the next "send" but it's not the whole thing (because it starts with 4 but there aren't 4 more bytes).

Another way is to send a special character like a newline (enter key) after each message. This probably doesn't work with pickles because pickles can have newlines in them. But you could choose another byte that pickles don't have, like 0xFF. Then the receiver knows to keep on receiving until it sees the byte 0xFF.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM