Images von Server an Client und zurück senden

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
Raphael_155
User
Beiträge: 5
Registriert: Sonntag 12. September 2021, 06:01

Sonntag 12. September 2021, 06:19

Hallo,
ich habe seit ein paar Tage ein Problem, ich programmiere mithilfe eines Tutorials ein Online Spiel, in dem Video wird gezeigt, wie man ein mit Rechteck bewegen kann aber halt über den Server, also dass alle die den Client haben,(und im Localhost sind) ein Rechteck bewegen könne, das dann der 2. Spieler sehen kann und umgekehrt, ich hoffe das ist einigermaßen verständlich. Das bekomme ich auch hin, das Problem ist, dass ich bei meinem Spiel kein Rechteck sondern Sprites verwenden möchte, doch das bekomme ich einfach nicht hin, das die Images an den Server und wieder zurück gesendet werden. Ich habe dann ewig damit rumgespielt, die Images versucht in str umzuwandeln, aber das funktioniert einfach nicht. Vielleicht weißt einer ja, ob es da ein Modul oder ähnliches gibt...
Liebe Grüße Raphael
__deets__
User
Beiträge: 10308
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 12. September 2021, 07:50

Es ist unüblich, Assets wie Sprites und Audio etc von einem Client an den anderen zu verteilen. Die sind entweder schon da, bei allen Clients. Oder bestenfalls auf dem Server.

Du zeigst keinen Code, darum kann man da nur raten. Aber grundsätzlich sendet man ein Bild genau in der Form über die Leitung, die es auch auf der Platte hat.
Raphael_155
User
Beiträge: 5
Registriert: Sonntag 12. September 2021, 06:01

Sonntag 12. September 2021, 10:14

Hallo,
vielen Dank für die schnelle Rückmeldung, das Problem ist, dass die Sprites sich auf Knopfdruck ja bewegen sollen, also müsste ich doch irgendwie die images an den Server senden und dann ja wieder zurück, vielleicht gibt es da auch irgendeinen Trick... Ich kopier mal meinen Code rein:


Client:

import pygame
from network import Network

width = 500
height = 500
win = pygame.display.set_mode((width, height))
pygame.display.set_caption("Client")
clientNumber = 0


class Player():
def __init__(self, x, y, width, height, color):
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
self.rect = (x, y, width, height)
self.vel = 3

def draw(self, win):
pygame.draw.rect(win, self.color, self.rect)

def move(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.x -= self.vel
if keys[pygame.K_RIGHT]:
self.x += self.vel
if keys[pygame.K_UP]:
self.y -= self.vel
if keys[pygame.K_DOWN]:
self.y += self.vel

self.update()
def update(self):
self.rect = (self.x, self.y, self.width, self.height)

def read_pos(str):
str = str.split(",")
return int(str[0]), int(str[1])


def make_pos(tup):
return str(tup[0]) + "," + str(tup[1])

def redrawWindow(win, player, player2):
win.fill((255, 255, 255))
player.draw(win)
player2.draw(win)
pygame.display.update()


def main():
run = True
n = Network()
startPos = read_pos(n.getPos())
p = Player(startPos[0],startPos[1],100, 100, (0, 255, 0))
p2 = Player(0,0,100, 100, (255, 0, 0))
clock = pygame.time.Clock()

while run:
clock.tick(60)
p2Pos = read_pos(n.send(make_pos((p.x,p.y))))
p2.x = p2Pos[0]
p2.y = p2Pos[1]
p2.update()

for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.quit()
p.move()
redrawWindow(win,p,p2)


main()



Server:

import socket
from _thread import *
import sys

server = "192.168.178.25"
port = 5555

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
s.bind((server, port))
except socket.error as e:
str(e)

s.listen(2)
print("Waiting for connection, server started....")


def read_pos(str):
str = str.split(",")
return int(str[0]), int(str[1])

def make_pos(tup):
return str(tup[0]) + "," + str(tup[1])

pos = [(0,0),(100,100)]
def threaded_client(conn,player):
conn.send(str.encode(make_pos(pos[player])))
reply = ""
while True:
try:
data = read_pos(conn.recv(2048).decode())
pos[player] = data

if not data:
print("Disconnected")
break
else:
if player == 1:
reply = pos[0]
else:
reply = pos[1]
print("Recieved: ", data)
print("Sending : ", reply)

conn.sendall(str.encode(make_pos(reply)))

except:
break
print("Lost connection")
conn.close()

currentPlayer = 0
while True:
conn, addr = s.accept()
print("Conneced to:", addr)

start_new_thread(threaded_client, (conn,currentPlayer))
currentPlayer += 1


Network:

import socket


class Network:
def __init__(self):
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server = "192.168.178.25"
self.port = 5555
self.addr = (self.server, self.port)
self.pos = self.connect()
def getPos(self):
return self.pos

def connect(self):
try:
self.client.connect(self.addr)
return self.client.recv(2048).decode()

except:
pass

def send(self, data):
try:
self.client.send(str.encode(data))
return self.client.recv(2048).decode()

except socket.error as e:
print(e)



So, das ist der Code mit den Rechtecken... Wie gesagt, will ich die Rechtecke durch Sprites ersetzten.
Vielen Dank und Liebe Grüße
__deets__
User
Beiträge: 10308
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 12. September 2021, 10:34

Nein, das ist falsch. Man schickt keine Sprites. Man schickt einfach die Information, wo der Spieler ich befindet. Das wars. Oder glaubst du, dass bei Fortnite jeder Spieler an alle anderen Spieler das Bild aus dessen Perspektive verschickt? Das geht nicht. Stattdessen wird bei jedem Spieler eine Vorstellung der Welt mit Positionen und anderen Zuständen vorgehalten, und dann einfach dargestellt.
Sirius3
User
Beiträge: 15002
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 12. September 2021, 11:03

Deine Schreibweise ist inkonsistent, mal schreibst Du Variablennamen oder Funktionen camelCase, mal mit Unterstrich. In Python werden sie nach Konvention immer komplett klein_mit_unterstrich geschrieben.
Benutze keine globalen Variablen. `win` ist bei Dir eine globale Variable, obwohl Du sie außer in main nirgends benutzt. clientNumber wird definiert, aber nirgends benutzt.
`read_pos` überdeckt die eingebaute Klasse str, das sollte nicht passieren.
Benutze keine kryptischen Abkürzungen. `tup`? `n` für Network und `p`, `p2` für die Player sind eindeutig zu kurz.

Beim Network sollten IP und Port nicht mitten irgendwo im Code stehen, sondern zumindest als Konstanten am Anfang der Datei.
Nackte excepts sind immer schlecht, weil man es damit unmöglich macht, Programmierfehler zu finden. Hier ist es besonders schlimm, weil bei einem Fehler das Programm in einem Inkonsistenten Zustand ist, und der Folgefehler an einer ganz anderen Stelle auftritt.
Dein Socketcode ist so kaputt, wie 99.99% der Beispiele im Netz auch. Wenn man nicht weiß, wie man low-level Sockets programmiert, dann sollte man das nicht in einem Tutorial verbreiten und wenn immer möglich sollte man auf einer höheren Stufe ansetzen.

Das gleiche gilt natürlich auf für den Server-Code.
Hier hast Du wirklich globale Variablen, die zudem noch von verschiedenen Threads benutzt werden, also etwas, was man doppelt nicht machen sollte.
Der _ deutet an, dass man das Modul _thread nicht verwenden sollte, statt dessen gibt es threading.
`s` ist kein guter Name für einen server_socket, nach einem except-Block muß das Programm wieder in einem benutzbaren Zustand sein, was nach einem fehlgeschlagenen `bind` sicher nicht der Fall ist. Was soll das `str(e)`, das nicht weiter verwendet wird?
Beim `server`-Argument von `bind` gibt es eigentlich nur zwei sinnvolle Werte 127.0.0.1 für lokale Verbindungen und 0.0.0.0 für beliebige Verbindungen.

read_pos und make_pos sind hier zum zweiten Mal definiert. Wenn man ein "Protokoll" implementiert, dann in einem Modul, das sowohl vom Server als auch vom Client benutzt werden.

Zum eigentlichen Problem: Du hast bisher ein sehr einfaches Protokoll, das nur eine Position eines zweiten Spielers übertragen kann. Das mußt Du erweitern, um Dinge wie Registrieren mit Name und Bild, Abfragen der anderen Spieler mit Name und Bild, Events, wenn ein weiterer Spieler das Spiel betritt oder verläßt, usw.
Raphael_155
User
Beiträge: 5
Registriert: Sonntag 12. September 2021, 06:01

Sonntag 12. September 2021, 11:35

OK,
danke für das Feedback, aber wo muss ich dann die Sprites "einfügen"? Muss ich nur die Positionen von Client zu Server senden dann wieder zurück und dann die Sprites im Client bliten? Oder muss ich die Images vom Client an den Server und dann im Server bliten?
Benutzeravatar
__blackjack__
User
Beiträge: 9095
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Sonntag 12. September 2021, 11:46

@Raphael_155: (Etwas spät, einiges wurde bereits gesagt, ich bin aber zu faul das jetzt noch raus zu kürzen. 🙂)

Die `Network`-Klasse wird ja nur vom Client verwendet, da macht das nicht viel Sinn die in ein eigenes Modul auszulagern.

Die Klasse an sich macht nicht so wirklich Sinn, weil sie eigentlich nur das Socket-Objekt kapselt und nur eine Methode besitzt die nach dem erstellen des Objekts aufgerufen wird. Sie ”kapselt” noch den Rückgabewert von `Network.connect()`, dass aber nur weil die `__init__()` diesen Wert nicht als Rückgabwert haben kann. Der `connect()`-Code bräuchte nicht in einer eigenen Methode stehen, denn das wird nur von der `__init__()` aufgerufen und `getPos()` als trivialer Getter ist ”unpythonisch”, da kann man auch einfach direkt auf `pos` zugreifen.

Ausnahmebehandlung die einfach so tut als wäre die Ausnahme gar nicht passiert, und einfach weiter macht, ist tatsächlich manchmal sinnvoll, aber nicht wenn der Code gar nicht sinnvoll weiter machen kann. Wenn die Verbindung zum Server nicht klappt, geht ja das Spiel nicht wirklich, da kann man nicht einfach weitermachen.

Ausserdem sollte man keine nackten ``except:`` ohne konkrete Aushnahmen verwenden, denn das behandelt *alle* Ausnahmen, auch solche, die man an der Stelle gar nicht erwartet hat. Wenn das durch einen Programmierfehler ausgelöst wurde, dann möchte man das wissen, denn sonst findet man den nie oder nur sehr mühsam, wenn man die unerwartete Ausnahme behandelt als wäre sie eine andere, für die die Behandlung Sinn macht.

Beide Ausnahmebehandlungen in der `Network`-Klasse sollten einfach ersatzlos gestrichen werden. Denn wenn Verbindungsaufbau oder Senden/Empfangen nicht funktioniert hat, dann kann man nicht einfach so weitermachen als wäre nichts passiert, denn dann ist das Programm in einem Zustand in dem es sehr wahrscheinlich ist, dass es nur noch aus Folgefehlern bei der Kommunikation besteht.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

`width` und `height` auf Modulebene sind Konstanten. `win` ist eine Variable, die gehört nicht auf Modulebene. `clientNumber` wird auf Modulebene definiert, aber nirgends verwendet.

Namen sollte man nicht kryptisch abkürzen. Wenn man `window` meint, dann sollte man nicht nur `win` schreiben. Insbesondere weil das ja auch ein sinnvoller Begriff im Kontext eines Spiels ist, kann das verwirrend werden. Und auch einbuchstabige Namen sind ausser für `i` und `j` für ganze Zahlen die als Laufindex verwendet werden, oder `x`, `y`, und `z` für Koordinaten, in der Regel sehr schlechte Namen. Auch das anhängen von Nummern an Namen ist nicht gut. Dann will man entweder bessere Namen, oder gar keine einzelnen Werte sondern eine Datenstruktur. Oft eine Liste.

Ich sehe beim Client nirgends ein `pygame.init()`‽ Und das Gegenstück `pygame.quit()` wird an einer falschen Stelle aufgerufen, denn danach kommt noch ein neuzeichnen des Fensters, was nach dem `pygame.quit()` aber nicht mehr geht. Das würde ich durch ein ``try``/``finally`` lösen. Dann kann man auch `run` als Variable loswerden.

Grunddatentypen gehören nicht in Namen. Bei `str` verdeckst Du den eingebauten Datentyp `str`. Und bei `tup` muss das gar kein Tupel sein, da geht jeder Sequenztyp der eine Position enthält.

`read_pos()` liest keine Position und `make_pos()` erstellt keine Position. Das sind Funktionen die eine Position aus einer Zeichenkette erstellen, oder eine Zeichenkette aus einer Position. Das sollte man an den Namen erkennen können.

Die `Player`-Klasse enthält redundante Daten. Einmal `rect` und dann noch mal dessen Einzelteile als Attribute, und Du brauchst Code und musst aufpassen den immer an der passenden Stelle aufzurufen, damit diese Werte nicht auseinander laufen. Diese Daten sollten nur einmal gespeichert sein. Am besten unter `rect` und da auch nicht als Tupel, sondern als `pygame.Rect`, weil das eine Reihe nützlicher Eigenschaften hat, gegenüber dem ”dummen” Tupel. Zum Beispiel kann man das verschieben oder die Position neu setzen.

Nun noch zu einem Fehler: Tausende von Beispielen im Netz, und erschreckenderweise sogar Bücher, machen Socketprogrammierung falsch. TCP ist ein Datenstrom und kennt keine Nachrichten. Wenn man über eine TCP-Verbindung Nachrichten übermitteln will, dann muss man dafür ein Protokoll implementieren, dass diese Nachrichten erkennt, und voneinander trennen kann. Gegebenfalls auch Nachrichten oder Nachrichtenteile puffern kann, bis sie verarbeitet werden können.

Die `send()`-Methode auf Sockets muss nicht alles senden was man übergeben hat. Das lässt sich noch einfach beheben in dem man `sendall()` verwendet.

Aber `recv()` empfängt nicht was auf der anderen Seite als ein `sendall()` verschickt wurde, sondern einen beliebigen Ausschnitt vom aktuellen Anfang des Datenstroms. Code muss im Extremfall damit klar kommen können, das jeder `recv()`-Aufruf ein einzelnes Byte liefert. Sonst ist das Fehlerhaft. Auch wenn das scheinbar funktioniert, bei schönem Wetter und auf dem lokalen Rechner, das ist so halt nicht garantiert von der API.

Zwischenstand für den Client (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import socket

import pygame

SCREEN_SIZE = (500, 500)

GREEN = (0, 255, 0)
RED = (255, 0, 0)


def connect():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(("192.168.178.25", 5555))
    #
    # FIXME Das ist fehlerhaft, so funktionieren TCP-Sockets nicht.
    #
    return client_socket, client_socket.recv(2048).decode()


def communicate(client_socket, data):
    client_socket.client.sendall(str.encode(data))
    #
    # FIXME Das ist fehlerhaft, so funktionieren TCP-Sockets nicht.
    #
    return client_socket.client.recv(2048).decode()


def parse_position(text):
    return list(map(int, text.split(",")))


def convert_position_to_text(position):
    return ",".join(map(str, position))


class Player:
    def __init__(self, rect, color):
        self.rect = rect
        self.color = color
        self.velocity = 3

    @property
    def position(self):
        return self.rect.topleft

    @position.setter
    def position(self, value):
        self.rect.topleft = value

    def draw(self, surface):
        pygame.draw.rect(surface, self.color, self.rect)

    def move(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.rect.move_ip(-self.velocity, 0)
        if keys[pygame.K_RIGHT]:
            self.rect.move_ip(self.velocity, 0)
        if keys[pygame.K_UP]:
            self.rect.move_ip(0, -self.velocity)
        if keys[pygame.K_DOWN]:
            self.rect.move_ip(0, self.velocity)


def redraw_window(surface, players):
    surface.fill((255, 255, 255))
    for player in players:
        player.draw(surface)
    pygame.display.update()


def main():
    pygame.init()
    try:
        screen = pygame.display.set_mode(SCREEN_SIZE)
        pygame.display.set_caption("Client")

        client_socket, start_position_text = connect()
        player_size = (100, 100)
        player = Player(
            pygame.Rect(parse_position(start_position_text), player_size),
            GREEN,
        )
        opponent = Player(pygame.Rect((0, 0), player_size), RED)

        clock = pygame.time.Clock()
        while True:
            clock.tick(60)
            opponent.position = parse_position(
                communicate(
                    client_socket, convert_position_to_text(player.position)
                )
            )

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return

            player.move()
            redraw_window(screen, [player, opponent])

    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
“When we write programs that "learn", it turns out that we do and they don't.” — Alan J. Perlis
Sirius3
User
Beiträge: 15002
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 12. September 2021, 12:02

@Raphael_155: wo hat denn der Server ein Fenster, wo er Sprites bliten kann? Nein, die Darstellung findet auf dem Client statt. Der Server ist nur dazu da, die Positionen zwischen den Clients auszutauschen, bzw. später für die ganze Spielelogik.
nezzcarth
User
Beiträge: 1334
Registriert: Samstag 16. April 2011, 12:47

Sonntag 12. September 2021, 12:06

Raphael_155 hat geschrieben:
Sonntag 12. September 2021, 11:35
danke für das Feedback, aber wo muss ich dann die Sprites "einfügen"?
Deine Assets (Bilder, Audio-Files, Videos, etc.) sind etwas überspitzt gesagt nur dafür da, dass es für die User schick ist. Für die eigentliche Technik des Spiels sind sie aber überwiegend ziemlich irrelevant. Intern arbeitet man überall wo es geht mit Abstraktionen wie z. B. Positionsdaten, Verweisen auf Dateien in einem überall gleichen Asset-Ordner etc. Und das ist auch, was die Clients untereinander, vermittelt durch den Server, austauschen. Und im besten Fall so minimalistisch wie möglich. Client-A sendet also die Positionsdaten an den Server, der verteilt sie an die Clients B und C, die das damit assoziierte Sprite lokal laden (bzw. bereits vorhalten) und mit den empfangenen (Positions)daten dessen Darstellung aktualisieren. Und umgekehrt.
Raphael_155
User
Beiträge: 5
Registriert: Sonntag 12. September 2021, 06:01

Sonntag 12. September 2021, 12:27

Ah ok,
also jetzt habe ich das von der Logik verstanden, nur die Position schickt der Client an den Server, der Server verteilt die Position dann zum Beispiel an Client B und Client b rendert das dann.
Was ich aber ehrlicherweise bei den Rechtecken nicht verstehe, wo die gedrawt werden. Nach dem selben Prinzip müsste ich das dann ja auch machen, aber wenn der Client die Position zurück bekommt, wo drawt er dann gegnerische Rechteck, also in welcher Code Zeile bei meinem Code? Bei der Update Funktion?
Raphael_155
User
Beiträge: 5
Registriert: Sonntag 12. September 2021, 06:01

Sonntag 12. September 2021, 12:36

Ah und wo sendet der Client die Position zum Server, da steh ich grad auch auf dem Schlauch...
Antworten