Pythonでp2p通信対戦を行うゲームの基盤作った

# 始めに
pythonの授業で好きなゲームを作れと言われたので、p2pで通信対戦が行える基盤の作成を行いました。
クライアントとサーバ両方の処理を行う事ができ、様々なゲームに利用が可能になると思っています。

ソースコードは下記にGitHubにアップロードしました。
https://github.com/hayama17/simple_p2p
# 目標
* 自分の操作を送信できる
* 相手の操作を受信できる

この二つが出来れば通信対戦が可能であると考えています。

相手の操作を受信して、自分のクライアントで再現すれば良いわけですから、ターン制のゲームでなくても格ゲーやぷよぷよのような対戦ゲームでも応用が可能であると考えています。

今回はUDP通信を行います。キー入力をただ送信出来ればいいだけなので、TCP通信程のコネクションが必要であるとは考えていません。
ただ、高品質でパケットロスが少ない通信対戦を行う為にはTCP通信が必須である為、勉強していく必要あり



# 使用するモジュール
* socket (通信対戦を行うのでソケットの利用は絶対)
* thread　(サーバとクライアントの処理を非同期で行う為に必要)
* Queue(受信した相手の操作を保存するため)
* pygame(ゲームの用モジュール)


# サーバ
キー入力のデータを受信して、キューにデータを入れる処理を書けばいいです。
問題なのは、一つのプロセスで送信も受信も行う必要がある為、threadを使って処理を分けます。
そうする事でサーバとクライアントの干渉を避けます。

```py
class Server():#サーバー側(受信側の処理)
    def __init__(self,q):
        self.q = q#キューです
        self.host = "localhost"
        self.port = 2222
        self.bufsize = 1024

        self.sock =  socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.bind((self.host, self.port))
        self.thread = threading.Thread(target=self.c2s, daemon=True)
        self.thread.start()
    
    def c2s(self):#サーバー側へ受信
        try:
            while True:
                msg, cli_addr = self.sock.recvfrom(self.bufsize)#受信したら
                msg.decode('utf-8')#データをutf-8にデコードして
                self.q.put(msg)# キューに入れる
                if msg == 'q':#もし「q」のキーボード入力があったら
                    break# サーバを閉じる
                # print(msg)# デバッグ用
        except Exception as e:
            print(e)
```






# クライアント
ゲームの処理とキー入力を送信する処理を記述する

## ゲーム画面の処理
キー入力を画面ただ表示するプログラムを書けばいいです。

```py
WIDTH = 640
HEIGHT = 640

# 色の定義
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

BK_COLOR = WHITE   # 背景色の設定
class Client: 
    def __init__(self,q):# 初期化
        self.q=q
        pygame.init()
        pygame.display.set_caption("main")
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        self.font = pygame.font.Font(None, 55)


    def draw(self):# 画面表示用loopを回す事で画面にを表示させ続ける
        loop = True
        text1 =  self.font.render("",True,WHITE)
        position1 = (1,1)
        self.screen.fill(BK_COLOR)
        pygame.display.flip()
        while loop:
            for event in pygame.event.get():
                    # 「閉じる」ボタンを処理する
                if event.type == pygame.QUIT: loop = False
                if event.type == KEYDOWN:
                    if event.key == 113:
                        loop = False
                    else:
                        self.screen.fill(BK_COLOR)
                        text1 = self.font.render(pygame.key.name(event.key),True,BLACK)
                        position1 = text1.get_rect()
                        position1.left = self.screen.get_rect().left

            self.screen.blit(text1,position1)
            pygame.display.flip()
```
真っ白な画面にキーボードで入力すると、画面に入力したキーが表示されるプログラムです。

## 送信の処理
UDP通信なのでコネクションを気にせずにただ通信すればいいです。
送信用の関数を作成する

```py
    def c2s(self, ip, port, msg):  # サーバー側へ送信
        s =  socket.socket(socket.AF_INET, socket.SOCK_DGRAM)#socketの作成
        serv_address = (ip, port)#送信するサーバの設定
        s.sendto(msg.encode('utf-8'), serv_address)#サーバへ送信
```

ゲームの処理の中でキー入力のイベントがあったらc2s関数を実行するようにすればいいです。
## 受信の処理
クライアントで受信処理？となっていますが、サーバで受信してもゲーム画面(クライアント)で反映しなくてはならない為です。

実際は受信してキューにデータが入った場合の処理になります。

```py
if self.q.empty()==False: #もしキューにデータがあったら？
    self.screen.fill(BK_COLOR)#スクリーンを真っ白にして
    text2 = self.font.render(self.q.get(),True,BLACK)#テキストをレンダリングして
    position2 = text2.get_rect()#場所を決めて
    position2.right = self.screen.get_rect().right
self.screen.blit(text2,position2)#表示する
```

# 全ての処理を合わせる

```py
import socket
from typing import Text
import pygame
from dataclasses import dataclass, field
import threading
import queue
from pygame.constants import *

WIDTH = 640
HEIGHT = 640

# 色の定義
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

BK_COLOR = WHITE   # 背景色の設定

FPS = 60     # Frame per Second 毎秒のフレーム数
@dataclass
class Client:  # クライアント側(送信側の処理)

    def __init__(self,q):
        self.q = q
        pygame.init()
        pygame.display.set_caption("main")
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        self.font = pygame.font.Font(None, 55)

    def c2s(self, ip, port, msg):  # サーバー側へ送信
        s =  socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        serv_address = (ip, port)
        s.sendto(msg.encode('utf-8'), serv_address)

    def draw(self):
        loop = True
        text1 =  self.font.render("",True,WHITE)
        text2 = self.font.render("",True,WHITE)
        position1 = (1,1)
        position2 = (1,1)
        self.screen.fill(BK_COLOR)
        pygame.display.flip()
        while loop:


            for event in pygame.event.get():
                    # 「閉じる」ボタンを処理する
                if event.type == pygame.QUIT: loop = False
                if event.type == KEYDOWN:
                    if event.key == 113:
                        loop = False
                    else:
                        self.screen.fill(BK_COLOR)
                        text1 = self.font.render(pygame.key.name(event.key),True,BLACK)
                        self.c2s("localhost",2222,pygame.key.name(event.key))
                        position1 = text1.get_rect()
                        position1.left = self.screen.get_rect().left
                        
                       

            if self.q.empty()==False: 
                self.screen.fill(BK_COLOR)
                text2 = self.font.render(self.q.get(),True,BLACK)
                position2 = text2.get_rect()
                position2.right = self.screen.get_rect().right
            self.screen.blit(text1,position1)
            self.screen.blit(text2,position2)
            pygame.display.flip()
                        
class Server():#サーバー側(受信側の処理)
    def __init__(self,q):
        self.q = q
        self.host = "localhost"
        self.port = 8080
        self.bufsize = 1024

        self.sock =  socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.bind((self.host, self.port))
        self.thread = threading.Thread(target=self.c2s, daemon=True)
        self.thread.start()
    
    def c2s(self):#サーバー側へ受信
        try:
            while True:
                msg, cli_addr = self.sock.recvfrom(self.bufsize)
                msg.decode('utf-8')
                self.q.put(msg)
                if msg == 'q':
                    break
                # print(msg)
        except Exception as e:
            print(e)

        # self.sock.close()           

    
def main():
    q = queue.Queue()
    client = Client(q) # 盤面の初期化
    server = Server(q)
    client.draw()                  # メインルーチン


if __name__=="__main__":
    main()
```

#　実行
ポートを変えて実行する
![26baf-7so2e.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2352304/6b0cba29-82d3-a603-80d8-ac0b8a231b71.gif)

しっかりキーボード入力が伝わってるのが分かります。


# さいごに

自分がこのソースコードを使い、迷路ゲームの攻略を競う通信対戦ゲームの作成を行いました。
localhostならパケットロスが0だが、有線とwifiを使った場合など送信出来ていないケースがあります。

TCP通信を使った通信対戦を考慮する必要があると考えています。