LoginSignup
31
24

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-01-20

始めに

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

ソースコードは下記にGitHubにアップロードしました。
https://github.com/hayama17/simple_p2p

目標

  • 自分の操作を送信できる
  • 相手の操作を受信できる

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

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

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

使用するモジュール

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

サーバ

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

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)

クライアント

ゲームの処理とキー入力を送信する処理を記述する

ゲーム画面の処理

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

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通信なのでコネクションを気にせずにただ通信すればいいです。
送信用の関数を作成する

    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関数を実行するようにすればいいです。

受信の処理

クライアントで受信処理?となっていますが、サーバで受信してもゲーム画面(クライアント)で反映しなくてはならない為です。

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

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)#表示する

全ての処理を合わせる

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

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

さいごに

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

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

31
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
24