始めに
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()
しっかりキーボード入力が伝わってるのが分かります。
さいごに
自分がこのソースコードを使い、迷路ゲームの攻略を競う通信対戦ゲームの作成を行いました。
localhostならパケットロスが0だが、有線とwifiを使った場合など送信出来ていないケースがあります。
TCP通信を使った通信対戦を考慮する必要があると考えています。