「Python(パイソン)」はAIやディープラーニングなど、最先端分野で活用されることが多いプログラミング言語です。一方、Pythonは「ゲーム開発」にも使える、非常に便利な言語だということをご存知でしょうか。
Pythonは実行速度が遅めなので、C++やC#のようにプロフェッショナルなゲーム開発現場で活用されることはありません。
しかし専用ライブラリを利用すれば、2Dグラフィックスの単純なゲームなら問題なく作れます。
さらに、Pythonは生産性が高い言語なので、C++やC#より簡潔なソースコードでゲーム開発をすることが可能です。
そこで本記事では、Pythonでゲームを作るために必要な知識と、「Pygame」というライブラリで実際にシンプルなゲームを作るソースコードをご紹介します!
目次
「Python(パイソン)」は、1991年に登場した汎用プログラミング言語です。プログラムの機能を小さな単位でまとめる「オブジェクト指向」の徹底や、インデント(字下げ)によってコードのブロック(かたまり)を決める「オフサイドルール」が採用されていることが大きな特徴といえます。
Pythonは他のプログラミング言語と比較して、非常に簡潔なコーディングができるため、初心者でも習得しやすいことやプログラムの生産性が高いことが魅力です。そんなPythonについて、まずは以下3つの観点から基本的な知識を整理しておきましょう。
Pythonは機械学習や深層学習など、最先端のAI分野でとくに活用されているプログラミング言語です。そのほかにも、データ収集やデータ解析なども得意なので、金融関連やブロックチェーン、ウェブ情報収集のためのクローニングやスクレイピングなどにも活用されています。つまり、Pythonは科学技術計算の分野で幅広く採用されているということです。
これは、Pythonが学習しやすい言語であるうえに、プログラムの生産性が高いことが理由です。たとえば、同じプログラムを作成する場合でも、Pythonと比べてC++やJavaなどのプログラミング言語では作業工数が大幅に増えます。Pythonはその利便性の高さから、プログラミング経験がない他分野の専門家にも扱いやすく、最先端分野での導入実績が増えたという経緯があります。
Pythonは極めて実用性の高いプログラミング言語ですが、実行速度の速さが求められる用途には向いていません。大きな理由のひとつが、Pythonが「インタプリタ方式」の言語であることです。
インタプリタ方式は、ソースコードを事前に機械語に翻訳しておく「コンパイル方式」とは異なり、実行時に翻訳していく方式です。コンパイル方式と比べて、柔軟なコーディングをしやすいことがメリットですが、翻訳の処理が必要なので実行速度は控えめになります。
以上の点から、Pythonは安定した実行速度が求められる場面では、あまり採用されていません。たとえば基幹業務システムの開発や、レスポンスタイム保証が必要とされる業務では、C言語・C++・Javaなどが主流となります。
ゲーム開発の分野では、C++やC#というプログラミング言語が使用されています。C++はC言語にさまざまな機能を追加した発展版で、C#はC++をさらに改良したものです。実行速度はC++のほうが有利ですが、使いやすさや生産性の観点から近年ではC#が使われることも増えています。
C++とC#は汎用ゲームエンジンとの相性も抜群です。世界中で活用されているゲームエンジンには「Unreal Engine」と「Unity」がありますが、Unreal EngineはC++、UnityはC#でプログラミングを行います。ゲームクリエイター・ゲームプログラマーを目指すなら、C++とC#はいずれも学習する価値が高い言語だといえるでしょう。
一方でPythonは、前述した「実行速度」が理由で、一般的にはゲーム開発分野では採用されていません。しかし、Pythonは初心者でも習得しやすいプログラミング言語なので、学習目的でPythonをゲーム開発に活用する人も多いです。一部のライブラリは3Dグラフィックスの機能も提供しているので、うまく使いこなせばPythonでも本格的な3Dゲームが作れます。
Pythonでゲーム開発を行う方法は以下3つです。それぞれの特徴やメリット、開発できるゲームの種類について確認していきましょう。
それぞれ解説します。
ごく単純なCUIゲームであれば、後述するグラフィックライブラリやゲームエンジンを使わなくても、Pythonでゲームが作れます。CUIゲームとは、Pythonで通常のプログラムを実行したときに表示される、「コマンドプロンプト」の画面で動作するゲームのことです。
コマンドプロンプトにはテキストしか表示されないため、CUIゲームでは画像や3Dグラフィックスは使えません。しかし、CUIゲームの開発を学ぶと、Pythonの基本だけではなく応用テクニックが身につくうえに、2D・3Dゲーム開発に役立つノウハウも身につきます。また、レトロな雰囲気のゲームを作りたい場合は、あえてCUIゲームの開発にチャレンジするのもおすすめです。
グラフィックライブラリ(GUIフレームワーク)を活用するのが、Pythonのゲーム開発で一般的です。グラフィックライブラリやGUIフレームワークは、Pythonで2Dグラフィックスを表示するための機能です。ほとんどがグラフィカルなGUIアプリ制作のためのライブラリですが、ゲーム開発のために作られたものもあります。
グラフィックライブラリを活用すると、画像を活用した2Dゲームが簡単に作れます。Pythonの知識を駆使する必要があるため、自然とプログラミングの応用スキルが身につくことも魅力です。ただし、Pythonのグラフィックライブラリは基本的に3Dグラフィックをサポートしていないため、市販のゲームのようなリアルなゲームを作るのは困難です。
Pythonで本格的な3Dゲームを開発したいのであれば、Python用のゲームエンジンを使う必要があります。ゲームエンジンとは、前述したグラフィックライブラリの機能に加えて、3Dグラフィックス・音声・コントローラー入力など、ゲーム開発に欠かせない機能を網羅したシステムのことです。
ゲームエンジンを活用すると、モダンでリアルなゲームが作れます。Pythonで3Dゲームの開発を学んでおけば、ゲーム開発の専門知識が身につくので、将来的にUnreal EngineやUnityに移行するときもスムーズに習得できるでしょう。
ただし、ゲームエンジンは仕様が複雑なうえに、3Dグラフィックスや数学などに関する知識も求められるため、先ほどの2Dゲーム開発と比べると格段にハードルが上がります。そのため、まずはグラフィックライブラリでゲーム開発の基本的なスキルを身につけてから、ゲームエンジンにチャレンジしてみるとよいでしょう。
Pythonの代表的なグラフィックライブラリ・GUIフレームワークの中から、以下の5つをご紹介します。それぞれのライブラリの特徴を踏まえ、どのライブラリにチャレンジするか選びましょう。
Pythonでゲームを作るためのグラフィックライブラリとして、最も有名なのが「Pygame」です。主にゲーム開発のためのライブラリで、C言語で書かれたマルチメディアライブラリである、「SDLライブラリ」上に構築されています。Pygameは、ハードウェアに近い部分をラッピングしているため、専門知識がなくても高度なグラフィック処理が簡単に行えます。
なお、Pygameには3Dグラフィックスの機能は搭載されていないので、市販タイトルのようなリアルな3Dゲームは作れません。しかし、2Dグラフィックスだけでも、工夫次第で魅力的なシューティングゲームやアクションゲームを作れますし、ゲーム開発のテクニックを身につけるには十分なライブラリです。
Pygameは2000年ごろに登場し、これまで20年以上も広く活用されてきたので、さまざまな情報を簡単に入手できることも魅力です。Pythonで手軽にゲームを開発したいなら、まずはPygameにチャレンジしてみることをおすすめします。IDE(統合開発環境)に「Microsoft Visual Studio」を使用している場合は、Pygameは標準機能として搭載されているので、インストールの手間なしで簡単に使えます。
「Kivy」は、PythonでGUIアプリを開発するためのライブラリです。Windows・Linux・macOSに加えて、AndroidやiOSなどのモバイル端末に対応しています。さらに、マウスやキーボードに加えて、マルチタッチイベントなど多彩な入力方法に対応しているため、クロスプラットフォームのアプリ開発ができることも魅力です。
GUIデザインツール「Kivy Designer」もあるので、凝ったデザインの2Dゲームも作れます。また、Kivyはオープンソースライブラリなので、ライセンスまわりの細かな規約を気にする必要がありません。ただし、KivyはPygameと比べると国内での知名度が低いので、ドキュメントや情報の検索が難しいことが難点です。
「Tkinter」は、Pythonに標準で備わっているGUIフレームワークです。WindowsやUnixなどで動作し、仕様が単純でシンプルなコーディングができることが魅力。ただしPygameやKivyと比べると、Tkinterはできることが限られているため、凝ったゲームを作りたい場合は向いていません。
「Arcade」は、Pygameと同じ2Dゲーム用ライブラリです。Pygameとは異なり、ArcadeはOpenGLを採用しているので、Pygameよりも細かなグラフィック操作が可能となっています。
ただし、Arcadeは国内での知名度が低く情報を入手するのが難しいため、基本的にはPygameを利用するのがおすすめです。
「Pyglet」は、クロスプラットフォーム対応のグラフィックライブラリです。マルチウィンドウやマルチモニター、多様な画像・動画・音楽のファイルに対応していることが特徴です。しかし、使いやすさ・情報の得やすさなどの点でやはりPygameに軍配が上がるため、初心者の方はできるだけPygameを選ぶほうが無難です。
Pythonで使える主なゲームエンジンの中から下記の3つをご紹介します。よりハイレベルな2Dゲームや、高度なグラフィックスを駆使した3Dゲームを開発したい方は、これらのゲームエンジンにチャレンジしてみるといいでしょう。
「Cocos2d」は、2Dゲームの開発ができるゲームエンジンです。Cocos2dのオリジナル版はPythonで開発されましたが、ほかのプログラミング言語やプラットフォームにも提供されています。たとえば、C++の「Cocos2d-X」やC#の「Cocos2d-XNA」などです。そのため、知名度の高いゲームエンジンだといえるでしょう。
Cocos2dはPythonで最も広く活用されている3Dゲームエンジンなので、国内で得られる情報が多く使いやすいことも魅力です。そのため、Pythonで手軽に3Dゲーム開発にチャレンジしたい人は、ぜひCocos2dにチャレンジしてみましょう。
「Panda3D」は、ウォルト・ディズニー社が「Toontown Online」というMMORPGを開発するために、生み出したゲームエンジンです。高機能で操作方法も簡潔なので、ハイレベルな3DゲームをPythonで開発したい場合に最適です。ただし、Panda3Dは国内でほとんど情報が得られないので、3Dゲームを作るなら後述するGodatを選ぶほうがいいでしょう。
「Godot」はGodot Engine communityが提供している、オープンソースのゲームエンジンです。PCはもちろん、スマホや家庭用ゲーム機など、さまざまなプラットフォームで動作します。
アニメーション作成用のGUIを搭載し、スケルタルアニメーション・ブレンディング・モーフィングなどを編集して、3Dモデルを任意のアニメーションで動かせることが魅力。3D物理エンジンには「Bullet」を採用するなど、ハイレベルな3Dゲーム開発にも対応できる、本格的な仕様となっています。
近年では、Unreal EngineやUnityと並び、世界中で徐々にシェアを増やしつつあります。そのため、Cocos2dにある程度慣れてきたら、ステップアップとしてGodotにチャレンジしてみるのもよいでしょう。
初心者の方がPythonでゲーム開発ができるようになるためには、以下3つのコツを意識することが大切です。
それぞれ解説します。
ゲームプログラミングを始める前に、Pythonの基礎知識を習得しておきましょう。ゲームプログラミングで最も重要な要素は、基本的なプログラミングスキルです。ゲーム開発では、「何か特別なテクニックを使う」シーンは意外と少なく、基礎の組み合わせでさまざまな機能を実装していくことがほとんどです。そのため、Pythonの基本を理解していなければ、ゲーム開発もできません。
Pythonはプログラミング経験がない初心者でも、短期間で習得しやすい言語です。ネット上の解説サイトや動画などを参考にして、条件分岐やループ、コレクションやファイル入出力などの知識を身に付けていきましょう。言い換えれば、すでにPythonをある程度習得している人は、それほど苦労せずにゲームプログラミングも習得できるということです。
Pythonの基礎知識が身に付いたら、単純なCUIゲームもしくは2Dゲームのソースコードを探して、IDE(統合開発環境)にコピペして実行してみましょう。ソースコードは必ずしも手入力する必要はなく、時間短縮のためにコピペでOK。ただし、単に実行するだけではなく、ソースコードをよく読んで中身を理解することが重要です。
「どの部分にどのような役割があるか」を意識しながら、ソースコードを検証しましょう。たとえばキャラクターの制御は、「X座標」「Y座標」の設定や、描画の更新などの部分に分かれています。ソースコードと実行結果の関係が分かれば、「どのように書けば目的の実行結果が得られるか」も理解できるでしょう。あとは少しずつオリジナルのコードが書けるように、練習を重ねるだけです。
なお本記事の後半では、最も知名度が高いグラフィックライブラリである「Pygame」で、実際にゲームを開発するためのソースコードをご紹介します。
ソースコードの中身をある程度理解できたら、一部を改変してオリジナルの動作に変更してみましょう。ソースコードに直接記載されている「数字」を変更するのが、とくに効果的です。たとえば、座標値や速度などを変えると、動作の変化が目で見てわかります。ソースの改変により、以前は「いまいち分からなかった」部分も、役割が可視化されるので理解が深まります。
コードの機能や役割が分かれば、同じようなコードを他の部分に転用して、機能を拡張することも可能です。たとえば、プレイヤーに「ジャンプ」の機能を追加したら、同様のコードを敵の部分にも追加すると、敵もジャンプできるようになります。ゲームプログラミングの上達速度は、「いかに多くのソースコードに触れ、改変したか」で決まると言っても過言ではありません。どんどんチャレンジしていきましょう。
ここからは、実際にPygameを使って簡単な2Dゲームが作れるようになるまでの過程を、以下5つのステップに分けてサンプルコード付きで解説します。なお今回は、ゲームプログラミングの題材として、最も一般的であり、なおかつさまざまなテクニックが学べる「シューティングゲーム」の作り方を下記の流れでご紹介します。
まずはウインドウの表示から行いましょう。Pygameでは以下のサンプルコードのように、ごく簡単にウィンドウの表示およびサイズ変更ができます。
//サンプルプログラム
# 「sys」の機能を使用する import sys # 「pygame」の機能を使用する import pygame # 「pygame.locals」のすべての定数をインポートする from pygame.locals import * # main関数を定義する def main(): # pygameを初期化する pygame.init() # ウィンドウサイズを指定する screen = pygame.display.set_mode((1280, 720)) # ウィンドウのタイトルを指定する pygame.display.set_caption("Game Programming") # ゲーム本体の処理は「無限ループ」で行う while(True): # 画面を「青」で塗りつぶす screen.fill((0,32,128)) # fill関数の引数は「R(赤)」「G(緑)」「B(青)」 # 画面を更新して描画する pygame.display.update() # pygameのイベントを処理する for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() if __name__ == "__main__": main() # main関数を実行する
//実行結果
基本的な流れはソースコードおよびコメントのとおりです。まずPygameの「pygame.init関数」を呼び出し、Pygame全体の初期化処理を行います。これを呼び出さないと、Pygameを正常に起動できません。続いて「pygame.display.set_mode関数」の引数で、画面の横側と縦側のサイズを指定します。あとは無限ループ内でゲーム本体の処理を行いましょう。
ただしゲームは、毎フレームごとに描画処理を行います。前フレームの描画内容を削除するために、「screen.fill関数」でRGBカラーを指定して画面を塗りつぶします。そのうえで「pygame.display.update関数」を呼び出し、画面を更新することがポイントです。
なおPygameの終了処理は、「pygame.event.get関数」でイベントを取得し、ループで全イベントを処理することで行います。ウィンドウの「×」ボタンをクリックすると、「QUITメッセージ」が送られるので、これをループ内で受け取ったときに「pygame.quit関数」および「sys.exit関数」を呼び出し、プログラムの終了処理を行います。
ゲームプログラミングでとくに重要なのが「画像の表示」ですが、Pygameならごく単純なソースコードで行えます。まずプロジェクトフォルダ(「pyファイル」があるディレクトリ)に以下の画像を「Sky.png」という名称で保存してください。
それから以下のサンプルコードを実行すると、画面上に雲模様の画像を表示できます。
//サンプルプログラム
# 「sys」の機能を使用する import sys # 「pygame」の機能を使用する import pygame # 「pygame.locals」のすべての定数をインポートする from pygame.locals import * # main関数を定義する def main(): # pygameを初期化する pygame.init() # ウィンドウサイズを指定する screen = pygame.display.set_mode((1280, 720)) # ウィンドウのタイトルを指定する pygame.display.set_caption("Game Programming") # 背景画像を読み込む image = pygame.image.load("Sky.png") # 背景画像をウィンドウと同じサイズに拡大する image = pygame.transform.scale(image, (1280, 720)) # 画像のサイズを取得する rect = image.get_rect() # ゲーム本体の処理は「無限ループ」で行う while(True): # 画面を「青」で塗りつぶす screen.fill((0,32,128)) # fill関数の引数は「R(赤)」「G(緑)」「B(青)」 # 背景画像を描画する screen.blit(image, rect) # 秒間60フレーム描画を実現するために16ミリ秒待機する pygame.time.wait(16) # 画面を更新して描画する pygame.display.update() # pygameのイベントを処理する for event in pygame.event.get(): # 「QUIT」メッセージが届いたらゲームを終了する if event.type == QUIT: pygame.quit() sys.exit() # 「ESCAPE」キーが押されたらゲームを終了する if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() if __name__ == "__main__": main() # main関数を実行する
//実行結果
先ほどのサンプルコードとの違いは、画像の読み込み・拡大・描画の処理を追加していることです。まず「pygame.image.load関数」の引数に、読み込みたい画像ファイルを指定します。続いて、ウインドウと同じサイズに拡大するために「pygame.transform.scale関数」を使用し、「image.get_rect関数」で画像サイズを取得します。
無限ループ内で実際に描画するときは、「screen.blit関数」を呼び出し、引数で画像データとサイズを指定することがポイントです。なお今回は、ゲームで一般的な秒間60フレーム描画を実現するために、「pygame.time.wait関数」で16ミリ秒待機しています。さらにソースコード後半にあるように、「ESCAPE」キーが押されたときもゲームを終了するようにしました。
ゲームではいくつもの画像を重ねて描画することが基本。画像の上に画像を重ねて描画する方法は、以下のサンプルコードのとおりです。まず以下の画像を「Box.png」という名前で保存し、サンプルコードを実行しましょう。
//サンプルプログラム
# 「sys」の機能を使用する import sys # 「pygame」の機能を使用する import pygame # 「pygame.locals」のすべての定数をインポートする from pygame.locals import * # main関数を定義する def main(): # pygameを初期化する pygame.init() # ウィンドウサイズを指定する screen = pygame.display.set_mode((1280, 720)) # ウィンドウのタイトルを指定する pygame.display.set_caption("Game Programming") # 背景画像を読み込む imageBackground = pygame.image.load("Sky.png") # 背景画像をウィンドウと同じサイズに拡大する imageBackground = pygame.transform.scale(imageBackground, (1280, 720)) # 背景画像のサイズを取得する rectBackground = imageBackground.get_rect() # ボックス画像を読み込む imageBox = pygame.image.load("Box.png") # ボックス画像のサイズを取得する rectBox = imageBox.get_rect() # ボックス画像の中心座標を設定する rectBox.center = (300, 300) # ゲーム本体の処理は「無限ループ」で行う while(True): # 画面を「青」で塗りつぶす screen.fill((0,32,128)) # fill関数の引数は「R(赤)」「G(緑)」「B(青)」 # 背景画像を描画する screen.blit(imageBackground, rectBackground) # ボックス画像を描画する screen.blit(imageBox, rectBox) # 秒間60フレーム描画を実現するために16ミリ秒待機する pygame.time.wait(16) # 画面を更新して描画する pygame.display.update() # pygameのイベントを処理する for event in pygame.event.get(): # 「QUIT」メッセージが届いたらゲームを終了する if event.type == QUIT: pygame.quit() sys.exit() # 「ESCAPE」キーが押されたらゲームを終了する if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() if __name__ == "__main__": main() # main関数を実行する
//実行結果
基本的な流れはこれまでと同じです。重ねて表示する画像を読み込み、下から順番に描画していきましょう。なお、ボックスの画像は位置を指定して描画しています。「rectBox.center = (300, 300)」のように、「center変数」に描画したい座標を設定すると、その座標を中心として画像を描画できます。
ゲーム開発に欠かせないのが、リアルタイムのキー入力操作への対応です。オブジェクト指向を活用した発展的な内容になるものの、以下のサンプルコードのようにプログラムの基本を作れば、今後の機能拡張がスムーズに行いやすくなるので、ぜひチャレンジしてみましょう。
//サンプルプログラム
# 「sys」の機能を使用する import sys # 「pygame」の機能を使用する import pygame # 「pygame.locals」のすべての定数をインポートする from pygame.locals import * # 「IntEnum」の機能を使用する from enum import IntEnum # 背景画像を管理するクラス class Background: # コンストラクタ def __init__(self): # 背景画像を読み込む self.image = pygame.image.load("Sky.png") # 背景画像をウィンドウと同じサイズに拡大する self.image = pygame.transform.scale(self.image, (1280, 720)) # 背景画像のサイズを取得する self.rect = self.image.get_rect() # 描画関数 def render(self, screen): # 背景画像を描画する screen.blit(self.image, self.rect) # キャラクター画像を管理するクラス class Character: # コンストラクタ def __init__(self): # ボックス画像を読み込む self.image = pygame.image.load("Box.png") # ボックス画像のサイズを取得する self.rect = self.image.get_rect() # 描画関数 def render(self, position, screen): # ボックス画像の中心座標を設定する self.rect.center = position # ボックス画像を描画する screen.blit(self.image, self.rect) # プレイヤーを管理するクラス class Player: # キーを定義する列挙型 class Keys(IntEnum): LEFT = 0 RIGHT = 1 UP = 2 DOWN = 3 MAX = 4 # キー状態を格納する配列を生成する keys = [False] * Keys.MAX # コンストラクタ def __init__(self): # プレイヤーの初期座標 (self.x, self.y) = (300, 300) # プレイヤーの移動速度 self.speed = 10.0 # 更新関数 def update(self, events): # pygameのイベントを処理する for event in events: if event.type == KEYDOWN: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = True; if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = True; if event.key == K_UP: self.keys[self.Keys.UP] = True; if event.key == K_DOWN: self.keys[self.Keys.DOWN] = True; if event.type == KEYUP: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = False; if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = False; if event.key == K_UP: self.keys[self.Keys.UP] = False; if event.key == K_DOWN: self.keys[self.Keys.DOWN] = False; # キー入力の状態に応じて自機を移動させる if self.keys[self.Keys.LEFT] == True: self.x -= self.speed; if self.keys[self.Keys.RIGHT] == True: self.x += self.speed; if self.keys[self.Keys.UP] == True: self.y -= self.speed; if self.keys[self.Keys.DOWN] == True: self.y += self.speed; # main関数を定義する def main(): # pygameを初期化する pygame.init() # ウィンドウサイズを指定する screen = pygame.display.set_mode((1280, 720)) # ウィンドウのタイトルを指定する pygame.display.set_caption("Game Programming") # Backgroundクラスのインスタンスを生成する background = Background() # Characterクラスのインスタンスを生成する character = Character() # Playerクラスのインスタンスを生成する player = Player() # ゲーム本体の処理は「無限ループ」で行う while(True): # Pygameのイベントを取得する events = pygame.event.get() # 画面を「青」で塗りつぶす screen.fill((0,32,128)) # fill関数の引数は「R(赤)」「G(緑)」「B(青)」 # 背景画像を表示する background.render(screen); # プレイヤーの状態を更新する player.update(events); # プレイヤーの状態を更新する character.render((player.x, player.y), screen); # 画面を更新して描画する pygame.display.update() # pygameのイベントを処理する for event in events: # 「QUIT」メッセージが届いたらゲームを終了する if event.type == QUIT: pygame.quit() sys.exit() # 「ESCAPE」キーが押されたらゲームを終了する if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() # 秒間60フレーム描画を実現するために16ミリ秒待機する pygame.time.wait(16) if __name__ == "__main__": main() # main関数を実行する
//実行結果
プログラム開始直後は、赤いボックスは画面左側に表示されます
キー入力を続けると、赤いボックスが以下のように動きます
上記のサンプルプログラムでは、プレイヤーがキーボードの矢印キーを入力すると、赤いボックスが上下左右に動きます。背景画像・キャラクター画像・プレイヤー管理など、要素ごとに細かくクラスを分けていることもポイントです。こうすることで、それぞれの役割を明確化できるうえに、機能も拡張しやすくなります。
BackgroundクラスとCharacterクラスの中身は、先ほどとほとんど変わりません。Playerクラスは新たな要素で、プレイヤーの移動を管理するためのものです。キー入力を管理するために、クラス内に「Keys列挙体」と「keys配列」を用意し、「update関数」でpygameのイベントに応じてキーの状態を保持しています。
update関数では、pygameのキー入力イベントを処理しています。ただし、KEYDOWNイベントは押しっぱなしのときはイベントが発生しないため、連続したキャラの動きを実現できません。そこでkeys配列に、KEYDOWNイベント発生時に「True」、KEYUPイベント発生時に「False」を格納し、自機の移動処理を個別に行うようにすると、スムーズな移動が可能となります。
シューティングゲームやアクションゲームなどを作るときは、「背景のスクロール」も重要な要素です。以下2つの画像をそれぞれ「Sky.png」「Fighter.png」という名前で保存し、サンプルコードを実行してみましょう。
//サンプルプログラム
# 「sys」の機能を使用する import sys # 「pygame」の機能を使用する import pygame # 「pygame.locals」のすべての定数をインポートする from pygame.locals import * # 「IntEnum」の機能を使用する from enum import IntEnum # 背景画像を管理するクラス class Background: # コンストラクタ def __init__(self): # 背景画像のサイズを設定する self.size = 600 # 背景画像の表示位置を初期化する self.pos = 0.0 # 背景画像の移動速度を設定する self.speed = 50.0 # 背景画像を読み込む self.image = pygame.image.load("Sky.png") # 背景画像をウィンドウと同じサイズに拡大する self.image = pygame.transform.scale(self.image, (self.size, self.size)) # 背景画像(1枚目)のサイズを取得する self.rectA = self.image.get_rect() # 背景画像(2枚目)のサイズを取得する self.rectB = self.image.get_rect() # 背景画像(3枚目)のサイズを取得する self.rectC = self.image.get_rect() # 描画関数 def render(self, screen, time): # 背景画像をスクロールさせる self.pos += time / 1000.0 * self.speed # 背景画像をスクロール量が画像サイズを上回ったら、スクロール位置をゼロに戻す if self.pos >= self.size: self.pos = 0.0 # 背景画像の描画位置を設定する self.rectA.topleft = (0, self.pos) self.rectB.topleft = (0, self.pos + self.size) self.rectC.topleft = (0, self.pos - self.size) # 背景画像を3枚つなげて描画する screen.blit(self.image, self.rectA) screen.blit(self.image, self.rectB) screen.blit(self.image, self.rectC) # 戦闘機の画像を管理するクラス class Fighter: # コンストラクタ def __init__(self): # 戦闘機の画像サイズを設定する self.size = 48 # プレイヤーの機体の色を設定する (self.playerR, self.playerG, self.playerB) = (24, 129, 55) # 戦闘機の画像を読み込む image = pygame.image.load("Fighter.png").convert_alpha() # 戦闘機の画像を指定サイズに縮小、プレイヤー画像として格納する self.playerImage = pygame.transform.scale(image, (self.size, self.size)) # プレイヤーの機体の色を設定する for x in range(self.size): for y in range(self.size): # アルファ値は保持したいのでまず取得し、色の変更時はそのアルファ値を指定する alpha = self.playerImage.get_at((x, y))[3] self.playerImage.set_at((x, y), (self.playerR, self.playerG, self.playerB, alpha)) # プレイヤーの機体画像の矩形情報を取得する self.playerRect = self.playerImage.get_rect() # 描画関数 def render(self, position, screen): # プレイヤーの画像の中心座標を設定する self.playerRect.center = position # プレイヤーの画像を描画する screen.blit(self.playerImage, self.playerRect) # プレイヤーを管理するクラス class Player: # キーを定義する列挙型 class Keys(IntEnum): LEFT = 0 RIGHT = 1 UP = 2 DOWN = 3 MAX = 4 # キー状態を格納する配列を生成する keys = [False] * Keys.MAX # コンストラクタ def __init__(self, windowX, windowY): # ウィンドウサイズを保持する (self.windowX, self.windowY) = (windowX, windowY) # プレイヤーの初期座標 (self.x, self.y) = (windowX / 2.0, windowY) # プレイヤーの移動速度 self.speed = 2.5 # 更新関数 def update(self, events): # pygameのイベントを処理する for event in events: if event.type == KEYDOWN: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = True; if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = True; if event.key == K_UP: self.keys[self.Keys.UP] = True; if event.key == K_DOWN: self.keys[self.Keys.DOWN] = True; if event.type == KEYUP: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = False; if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = False; if event.key == K_UP: self.keys[self.Keys.UP] = False; if event.key == K_DOWN: self.keys[self.Keys.DOWN] = False; # キー入力の状態に応じて自機を移動させる if self.keys[self.Keys.LEFT] == True: self.x -= self.speed; if self.keys[self.Keys.RIGHT] == True: self.x += self.speed; if self.keys[self.Keys.UP] == True: self.y -= self.speed; if self.keys[self.Keys.DOWN] == True: self.y += self.speed; # プレイヤーの位置を制限する if self.x < 0.0: self.x = 0.0 if self.y < 0.0: self.y = 0.0 if self.x > self.windowX: self.x = self.windowX if self.y > self.windowY: self.y = self.windowY # main関数を定義する def main(): # pygameを初期化する pygame.init() # ウィンドウサイズを指定する(フルスクリーンモードで実行) screen = pygame.display.set_mode((480, 640), FULLSCREEN) # フレームレート制御のためのClockオブジェクトを生成する clock = pygame.time.Clock() # ウィンドウのタイトルを指定する pygame.display.set_caption("Game Programming") # Backgroundクラスのインスタンスを生成する background = Background() # Fighterクラスのインスタンスを生成する fighter = Fighter() # Playerクラスのインスタンスを生成する player = Player(screen.get_width(), screen.get_height()) # ゲーム本体の処理は「無限ループ」で行う while(True): # 秒間60フレーム描画を実現するために、「clock.tick_busy_loop関数」を利用する time = clock.tick_busy_loop(60) # Pygameのイベントを取得する events = pygame.event.get() # 画面を「青」で塗りつぶす screen.fill((0,32,128)) # fill関数の引数は「R(赤)」「G(緑)」「B(青)」 # 背景画像を表示する background.render(screen, time); # プレイヤーの状態を更新する player.update(events); # 戦闘機を描画する fighter.render((player.x, player.y), screen); # 画面を更新して描画する pygame.display.update() # pygameのイベントを処理する for event in events: # 「QUIT」メッセージが届いたらゲームを終了する if event.type == QUIT: pygame.quit() sys.exit() # 「ESCAPE」キーが押されたらゲームを終了する if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() if __name__ == "__main__": main() # main関数を実行する
//実行結果
今回のサンプルプログラムには、今後機能を拡張するための変更を多く加えています。とくに重要なポイントは、プレイヤーの画像クラスを「Fighterクラス」として、コンストラクタで画像の色を変更していることです。「set_at関数」では、ピクセルごとの色を変更できます。たとえばプレイヤーを緑、敵を赤などにすることで、1枚の画像を複数のキャラクターに使いまわすことができ、画像作成の手間を省けます。
また背景画像をスクロールするために、同じ画像を縦方向に3枚並べて描画し、スクロール量が画像サイズを超えたときに元の位置に戻していることもポイントです。フレームレート制御に関しては、「clock.tick_busy_loop関数」の引数に「60」を指定することで、より正確な秒間60フレーム描画を実現しています。
ここからは、Pygameでより本格的なゲームを作るために必要な、以下2つのステップについて解説します。
実際にゲームを形にするためには、プレイヤーだけではなく、敵キャラクターなども複数描画する必要があります。そのためには、オブジェクト管理を「クラスの継承」を用いて効率的に行うようにしたり、機能拡張に強いクラス構造を構成したりするなど、応用的なテクニックが必要になります。たとえば以下のようなサンプルコードが考えられるでしょう。
//サンプルプログラム
# 「sys」の機能を使用する import sys # 「math」の機能を使用する import math # 「random」の機能を使用する import random # 「pygame」の機能を使用する import pygame # 「pygame.locals」のすべての定数をインポートする from pygame.locals import * # 「IntEnum」の機能を使用する from enum import IntEnum # 背景画像を管理するクラス class Background: # コンストラクタ def __init__(self): # 背景画像のサイズを設定する self.size = 600 # 背景画像の表示位置を初期化する self.pos = 0.0 # 背景画像の移動速度を設定する self.speed = 50.0 # 背景画像を読み込む self.image = pygame.image.load("Sky.png") # 背景画像をウィンドウと同じサイズに拡大する self.image = pygame.transform.scale(self.image, (self.size, self.size)) # 背景画像(1枚目)のサイズを取得する self.rectA = self.image.get_rect() # 背景画像(2枚目)のサイズを取得する self.rectB = self.image.get_rect() # 背景画像(3枚目)のサイズを取得する self.rectC = self.image.get_rect() # 描画関数 def render(self, screen, time): # 背景画像をスクロールさせる self.pos += time / 1000.0 * self.speed # 背景画像をスクロール量が画像サイズを上回ったら、スクロール位置をゼロに戻す if self.pos >= self.size: self.pos = 0.0 # 背景画像の描画位置を設定する self.rectA.topleft = (0, self.pos) self.rectB.topleft = (0, self.pos + self.size) self.rectC.topleft = (0, self.pos - self.size) # 背景画像を3枚つなげて描画する screen.blit(self.image, self.rectA) screen.blit(self.image, self.rectB) screen.blit(self.image, self.rectC) # 戦闘機の画像を管理するクラス class Fighter: # コンストラクタ def __init__(self): # 各画像サイズを設定する self.playerSize = 48 self.enemySize = 36 # 各機体の色を設定する (self.playerR, self.playerG, self.playerB) = ( 24, 129, 55) (self.enemyR, self.enemyG, self.enemyB ) = (255, 0, 0) # 戦闘機の画像を読み込む image = pygame.image.load("Fighter.png").convert_alpha() # 戦闘機の画像を指定サイズに縮小して格納する self.playerImage = pygame.transform.scale(image, (self.playerSize, self.playerSize)) self.enemyImage = pygame.transform.scale(image, (self.enemySize, self.enemySize)) # 自機の色を設定する for x in range(self.playerSize): for y in range(self.playerSize): # アルファ値は保持したいのでまず取得し、色の変更時はそのアルファ値を指定する alpha = self.playerImage.get_at((x, y))[3] self.playerImage.set_at((x, y), (self.playerR, self.playerG, self.playerB, alpha)) # 敵機の色を設定する for x in range(self.enemySize): for y in range(self.enemySize): alpha = self.enemyImage.get_at((x, y))[3] self.enemyImage.set_at((x, y), (self.enemyR, self.enemyG, self.enemyB, alpha)) # 機体画像の矩形情報を取得する self.playerRect = self.playerImage.get_rect() self.enemyRect = self.enemyImage.get_rect() # 描画関数 def render(self, objectData, screen): #オブジェクトが「Player」の場合 if objectData.type == "Player": # プレイヤーの画像の中心座標を設定する self.playerRect.center = objectData.pos # プレイヤーの画像を回転する image = pygame.transform.rotate(self.playerImage, objectData.angle) # プレイヤーの画像を描画する screen.blit(image, self.playerRect) #オブジェクトが「Enemy」の場合 if objectData.type == "Enemy": # 敵の画像の中心座標を設定する self.enemyRect.center = objectData.pos # 敵の画像を回転する image = pygame.transform.rotate(self.enemyImage, objectData.angle) # 敵の画像を描画する screen.blit(image, self.enemyRect) class ObjectData: # コンストラクタ def __init__(self, type, pos, angle): # オブジェクトの種類(クラス名と同じ) self.type = type # オブジェクトの座標値 self.pos = pos # オブジェクトの角度 self.angle = angle # オブジェクトを管理するクラス class ObjectBase: # クラス変数を初期化する objects = [] (windowX, windowY) = (0, 0) # コンストラクタ def __init__(self, pos, speed): # 初期座標を設定する (self.x, self.y) = pos # 移動速度を設定する self.speed = speed # 角度を設定する self.angle = 0.0 def get(): # オブジェクト描画データを格納する配列を生成する data = [] # 全オブジェクトの描画データを格納する for obj in ObjectBase.objects: data.append(ObjectData(type(obj).__name__, (obj.x, obj.y), obj.angle + 90)) return data # 更新関数 def update(events): for obj in ObjectBase.objects: obj.update(events) # 三角関数を用いてオブジェクトを正確に動かす def move(self): self.x += -math.cos(math.radians(self.angle)) * self.speed self.y += +math.sin(math.radians(self.angle)) * self.speed # オブジェクトの位置を調整する def adjust(self): if self.x < 0.0: self.x = 0.0 if self.y < 0.0: self.y = 0.0 if self.x > ObjectBase.windowX: self.x = ObjectBase.windowX if self.y > ObjectBase.windowY: self.y = ObjectBase.windowY # プレイヤーを管理するクラス class Player(ObjectBase): # キーを定義する列挙型 class Keys(IntEnum): LEFT = 0 RIGHT = 1 UP = 2 DOWN = 3 MAX = 4 # キー状態を格納する配列を生成する keys = [False] * Keys.MAX # キー入力によって得られる自機角度のテーブル angles = [ [315, 0, 45], [270, -1, 90], [225, 180, 135], ] # コンストラクタ def __init__(self, pos, speed): # 親クラスのコンストラクタを呼び出す super().__init__(pos, speed) # 更新関数 def update(self, events): # pygameのイベントを処理する for event in events: if event.type == KEYDOWN: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = True; if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = True; if event.key == K_UP: self.keys[self.Keys.UP] = True; if event.key == K_DOWN: self.keys[self.Keys.DOWN] = True; if event.type == KEYUP: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = False if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = False if event.key == K_UP: self.keys[self.Keys.UP] = False if event.key == K_DOWN: self.keys[self.Keys.DOWN] = False # 自機の角度を初期化する (angleX, angleY) = (1, 1) # キー入力の状態に応じて自機を移動させる if self.keys[self.Keys.LEFT] == True: angleX -= 1 if self.keys[self.Keys.RIGHT] == True: angleX += 1 if self.keys[self.Keys.UP] == True: angleY -= 1 if self.keys[self.Keys.DOWN] == True: angleY += 1 # キー入力からプレイヤーの角度を算出する self.angle = self.angles[angleX][angleY] # オブジェクトを動かす if self.angle != -1: self.move() else: self.angle = 270.0 # 位置を調整する self.adjust() # プレイヤーを管理するクラス class Enemy(ObjectBase): # コンストラクタ def __init__(self, pos, speed): # 親クラスのコンストラクタを呼び出す super().__init__(pos, speed) # 敵の角度はランダムに決める self.angle = random.uniform(0.0, 360.0) # 敵の角度差分もランダムに決める self.add = random.uniform(-3.0, 3.0) # 更新関数 def update(self, events): # 敵機を回転させる self.angle += self.add # オブジェクトを動かす self.move() # 位置を調整する self.adjust() # オブジェクトを出現させる def spawn(): # 自機を出現させる ObjectBase.objects.append(Player((ObjectBase.windowX / 2.0, ObjectBase.windowY), 2.5)) # 敵機を出現させる ObjectBase.objects.append(Enemy((0.0, 0.0), 1.0)) ObjectBase.objects.append(Enemy((ObjectBase.windowX * (1.0 - 0.25), 100.0), 1.0)) ObjectBase.objects.append(Enemy((ObjectBase.windowX * (1.0 - 0.50), 100.0), 1.0)) ObjectBase.objects.append(Enemy((ObjectBase.windowX * (1.0 - 0.75), 100.0), 1.0)) ObjectBase.objects.append(Enemy((ObjectBase.windowX, 0.0), 1.0)) # main関数を定義する def main(): # pygameを初期化する pygame.init() # ウィンドウサイズを指定する(フルスクリーンモードで実行) screen = pygame.display.set_mode((480, 640), FULLSCREEN) # フレームレート制御のためのClockオブジェクトを生成する clock = pygame.time.Clock() # ウィンドウのタイトルを指定する pygame.display.set_caption("Game Programming") # Backgroundクラスのインスタンスを生成する background = Background() # Fighterクラスのインスタンスを生成する fighter = Fighter() # ObjectBaseクラスにウィンドウサイズを設定する ObjectBase.windowX = screen.get_width() ObjectBase.windowY = screen.get_height() # オブジェクトの出現処理を行う spawn(); # ゲーム本体の処理は「無限ループ」で行う while(True): # 秒間60フレーム描画を実現するために、「clock.tick_busy_loop関数」を利用する time = clock.tick_busy_loop(60) # Pygameのイベントを取得する events = pygame.event.get() # 画面を「青」で塗りつぶす screen.fill((0,32,128)) # fill関数の引数は「R(赤)」「G(緑)」「B(青)」 # 背景画像を表示する background.render(screen, time); # すべてのオブジェクトを更新する ObjectBase.update(events) # 全オブジェクトを描画する for obj in ObjectBase.get(): fighter.render(obj, screen); # 画面を更新して描画する pygame.display.update() # pygameのイベントを処理する for event in events: # 「QUIT」メッセージが届いたらゲームを終了する if event.type == QUIT: pygame.quit() sys.exit() # 「ESCAPE」キーが押されたらゲームを終了する if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() if __name__ == "__main__": main() # main関数を実行する
//実行結果
重要なポイントは、「ObjectBaseクラス」を作り、それを継承した「Playerクラス」と「Enemyクラス」で、自機と敵機の動作や描画を管理していることです。すべてのオブジェクトを格納するリストは、ObjectBaseクラス内にObjectBase型として配置しています。こうすることにより、継承先の子クラスのインスタンスをすべて、「objectsリスト」で管理可能です。
オブジェクトを出現させるときは、「ObjectBase.objects.append」として、引数で出現させるオブジェクトを指定します。オブジェクトの更新もObjectBaseクラスに担い、内部ですべてのリストのupdate関数を実行させる形式です。なお、オブジェクトを描画する部分では、render関数に「ObjectData型」を渡しています。これは、描画に必要なオブジェクトの情報だけを抜き出したもので、座標や角度などを画像クラスに伝えるためのものです。
一見すると複雑なように見える構成ですが、このようにすると今後の機能拡張が行いやすくなります。なぜなら、クラスの継承関係を駆使した「ポリフォーリズム」の採用により、機能拡張に追随できるソースコードになっているからです。詳細は次のステップで詳しく確認しましょう。
最後に自機と敵機が弾丸を発射できるようにしたうえで、機体および弾丸の「当たり判定」も実装しましょう。ここまで作ることができれば、シューティングゲーム開発の基本が完成したといえます。まず、以下の細い画像を「Bullet.png」という名称で保存し、サンプルコードを実行してみてください。
//サンプルプログラム
# 「sys」の機能を使用する import sys # 「math」の機能を使用する import math # 「random」の機能を使用する import random # 「pygame」の機能を使用する import pygame # 「pygame.locals」のすべての定数をインポートする from pygame.locals import * # 「IntEnum」の機能を使用する from enum import IntEnum # 背景画像を管理するクラス class GameSystem: # スコア値 score = 0 # 背景画像を管理するクラス class Background: # コンストラクタ def __init__(self): # 背景画像のサイズを設定する self.size = 600 # 背景画像の表示位置を初期化する self.pos = 0.0 # 背景画像の移動速度を設定する self.speed = 50.0 # 背景画像を読み込む self.image = pygame.image.load("Sky.png") # 背景画像をウィンドウと同じサイズに拡大する self.image = pygame.transform.scale(self.image, (self.size, self.size)) # 背景画像のサイズを取得する self.rects = [self.image.get_rect(), self.image.get_rect(), self.image.get_rect()] # 描画関数 def render(self, screen, time): # 背景画像をスクロールさせる self.pos += time * self.speed # 背景画像をスクロール量が画像サイズを上回ったら、スクロール位置をゼロに戻す if self.pos >= self.size: self.pos = 0.0 # 背景画像の描画位置を設定する self.rects[0].topleft = (0, self.pos) self.rects[1].topleft = (0, self.pos + self.size) self.rects[2].topleft = (0, self.pos - self.size) # 背景画像を3枚つなげて描画する for rect in self.rects: screen.blit(self.image, rect) class ImageBase: # コンストラクタ def __init__(self, type, playerSize, enemySize, playerColor, enemyColor): # 画像タイプを設定する self.type = type; # サイズを設定する self.playerSize = playerSize self.enemySize = enemySize # 色を設定する self.playerColor = playerColor self.enemyColor = enemyColor # 画像を読み込む image = pygame.image.load(self.type + ".png").convert_alpha() # 画像を指定サイズに縮小して格納する self.playerImage = pygame.transform.scale(image, (self.playerSize[0], self.playerSize[1])) self.enemyImage = pygame.transform.scale(image, (self.enemySize[0], self.enemySize[1])) # プレイヤー側の色を設定する for x in range(self.playerSize[0]): for y in range(self.playerSize[1]): # アルファ値は保持したいのでまず取得し、色の変更時はそのアルファ値を指定する alpha = self.playerImage.get_at((x, y))[3] self.playerImage.set_at((x, y), (self.playerColor[0], self.playerColor[1], self.playerColor[2], alpha)) # 敵側の色を設定する for x in range(self.enemySize[0]): for y in range(self.enemySize[1]): # アルファ値は保持したいのでまず取得し、色の変更時はそのアルファ値を指定する alpha = self.enemyImage.get_at((x, y))[3] self.enemyImage.set_at((x, y), (self.enemyColor[0], self.enemyColor[1], self.enemyColor[2], alpha)) # 画像の矩形情報を取得する self.playerRect = self.playerImage.get_rect() self.enemyRect = self.enemyImage.get_rect() # 描画関数 def render(self, objectData, screen): #オブジェクトが「Player」の場合 if objectData.type == "Player" + self.type: # プレイヤーの画像の中心座標を設定する self.playerRect.center = objectData.pos # プレイヤーの画像を回転する image = pygame.transform.rotate(self.playerImage, objectData.angle) # プレイヤーの画像を描画する screen.blit(image, self.playerRect) #オブジェクトが「Enemy」の場合 if objectData.type == "Enemy"+ self.type: # 敵の画像の中心座標を設定する self.enemyRect.center = objectData.pos # 敵の画像を回転する image = pygame.transform.rotate(self.enemyImage, objectData.angle) # 敵の画像を描画する screen.blit(image, self.enemyRect) # 戦闘機の画像を管理するクラス class Fighter(ImageBase): # コンストラクタ def __init__(self): # 親クラスのコンストラクタを呼び出す super().__init__("Fighter", (48, 48), (36, 36), (24, 129, 55), (255, 0, 0)) # 弾丸の画像を管理するクラス class Bullet(ImageBase): # コンストラクタ def __init__(self): # 親クラスのコンストラクタを呼び出す super().__init__("Bullet", (8, 32), (8, 32), (24, 129, 55), (255, 0, 0)) class ObjectData: # コンストラクタ def __init__(self, type, pos, angle): # オブジェクトの種類(クラス名と同じ) self.type = type # オブジェクトの座標値 self.pos = pos # オブジェクトの角度 self.angle = angle # オブジェクトを管理するクラス class ObjectBase: # クラス変数を初期化する objects = [] (windowX, windowY) = (0, 0) # コンストラクタ def __init__(self, pos, speed): # オブジェクトの種類(クラス名)を取得する self.type = type(self).__name__ # 初期座標を設定する (self.x, self.y) = pos # 移動速度を設定する self.speed = speed # 角度を設定する self.angle = 0.0 # サイズを設定する self.size = (0, 0) # 撃破時のスコアを設定する self.score = 0 def get(): # 全オブジェクトの当たり判定を行う for objA in ObjectBase.objects: for objB in ObjectBase.objects: if objA != objB: objA.intersects(objB) # オブジェクト描画データを格納する配列を生成する data = [] # 全オブジェクトの描画データを格納する for obj in ObjectBase.objects: data.append(ObjectData(obj.type, (obj.x, obj.y), obj.angle + 90)) return data # 更新関数 def update(events, time): for obj in ObjectBase.objects: obj.update(events, time) # 三角関数を用いてオブジェクトを正確に動かす def move(self): self.x += -math.cos(math.radians(self.angle)) * self.speed self.y += +math.sin(math.radians(self.angle)) * self.speed # オブジェクトの位置を調整する def adjust(self, erase = False): # 範囲外オブジェクトを保持する if erase == False: if self.x < 0.0: self.x = 0.0 if self.y < 0.0: self.y = 0.0 if self.x > ObjectBase.windowX: self.x = ObjectBase.windowX if self.y > ObjectBase.windowY: self.y = ObjectBase.windowY # 範囲外オブジェクトは削除する else: if self.x < 0.0 or self.y < 0.0 or self.x > ObjectBase.windowX or self.y > ObjectBase.windowY: ObjectBase.objects.remove(self) # 矩形を設定する def set_rect(self): self.left = self.x - self.size[0] / 2.0 self.right = self.x + self.size[0] / 2.0 self.top = self.y - self.size[1] / 2.0 self.bottom = self.y + self.size[1] / 2.0 # 衝突判定を行う def intersects(self, other): # 両者の矩形を設定する self.set_rect() other.set_rect() # 当たり判定を行う collide = self.left <= other.right and self.right >= other.left and self.top <= other.bottom and self.bottom >= other.top # 衝突している場合 if collide == True: # 自機と敵が衝突した場合はプレイヤーの敗北 if self.type == "PlayerFighter": if other.type == "EnemyFighter" or other.type == "EnemyBullet": ObjectBase.objects.remove(self) # 自機の弾丸と敵機が衝突した場合はプレイヤーに加点 if self.type == "PlayerBullet": if other.type == "EnemyFighter": # 自機の弾丸と敵機を消去する ObjectBase.objects.remove(self) ObjectBase.objects.remove(other) # プレイヤーに加点する GameSystem.score += other.score # 新たな敵機を出現させる ObjectBase.objects.append(EnemyFighter((ObjectBase.windowX * (1.0 - random.uniform(0.1, 0.9)), random.uniform(0.0, 100.0)), random.uniform(1.0, 3.0))) # プレイヤー機を管理するクラス class PlayerFighter(ObjectBase): # キーを定義する列挙型 class Keys(IntEnum): LEFT = 0 RIGHT = 1 UP = 2 DOWN = 3 MAX = 4 # キー状態を格納する配列を生成する keys = [False] * Keys.MAX # キー入力によって得られる自機角度のテーブル angles = [ [315, 0, 45], [270, -1, 90], [225, 180, 135], ] # コンストラクタ def __init__(self, pos, speed): # 親クラスのコンストラクタを呼び出す super().__init__(pos, speed) # 当たり判定のサイズを設定する self.size = (8, 8) # 更新関数 def update(self, events, time): # pygameのイベントを処理する for event in events: if event.type == KEYDOWN: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = True; if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = True; if event.key == K_UP: self.keys[self.Keys.UP] = True; if event.key == K_DOWN: self.keys[self.Keys.DOWN] = True; # 「Z」を押した場合は弾丸を発射する if event.key == K_z: ObjectBase.objects.append(PlayerBullet((self.x, self.y), 10.0, self.angle)) if event.type == KEYUP: if event.key == K_LEFT: self.keys[self.Keys.LEFT] = False if event.key == K_RIGHT: self.keys[self.Keys.RIGHT] = False if event.key == K_UP: self.keys[self.Keys.UP] = False if event.key == K_DOWN: self.keys[self.Keys.DOWN] = False # 自機の角度を初期化する (angleX, angleY) = (1, 1) # キー入力の状態に応じて自機を移動させる if self.keys[self.Keys.LEFT] == True: angleX -= 1 if self.keys[self.Keys.RIGHT] == True: angleX += 1 if self.keys[self.Keys.UP] == True: angleY -= 1 if self.keys[self.Keys.DOWN] == True: angleY += 1 # キー入力からプレイヤーの角度を算出する self.angle = self.angles[angleX][angleY] # オブジェクトを動かす if self.angle != -1: self.move() # プレイヤーの描画角度は上向きに固定する self.angle = 270.0 # 位置を調整する self.adjust() # 敵機を管理するクラス class EnemyFighter(ObjectBase): # コンストラクタ def __init__(self, pos, speed): # 親クラスのコンストラクタを呼び出す super().__init__(pos, speed) # 敵の角度はランダムに決める self.angle = random.uniform(0.0, 360.0) # 敵の角度差分もランダムに決める self.add = random.uniform(-3.0, 3.0) # 弾丸を撃つタイミングもランダムに決める self.shoot = random.uniform(3.0, 7.0) # 弾丸を撃つタイミングをカウントする self.count = 0.0 # 当たり判定のサイズを設定する self.size = (16, 16) # 撃破時のスコアを設定する self.score = 100 # 更新関数 def update(self, events, time): # 敵機を回転させる self.angle += self.add # オブジェクトを動かす self.move() # 時間が来たら弾丸を発射する self.count += time if self.count >= self.shoot: ObjectBase.objects.append(EnemyBullet((self.x, self.y), 10.0, self.angle)) self.count = 0.0 # 位置を調整する self.adjust() # 弾丸を管理するクラス class BulletBase(ObjectBase): # コンストラクタ def __init__(self, pos, speed, angle): # 親クラスのコンストラクタを呼び出す super().__init__(pos, speed) # 弾の角度を設定する self.angle = angle # 当たり判定のサイズを設定する self.size = (8, 16) # 更新関数 def update(self, events, time): # オブジェクトを動かす self.move() # 画面から出たものは削除する self.adjust(True) # 自弾を管理するクラス class PlayerBullet(BulletBase): # コンストラクタ def __init__(self, pos, speed, angle): # 親クラスのコンストラクタを呼び出す super().__init__(pos, speed, angle) # 更新関数 def update(self, events, time): # 親クラスのupdate関数を呼び出す super().update(events, time) # 敵弾を管理するクラス class EnemyBullet(BulletBase): # コンストラクタ def __init__(self, pos, speed, angle): # 親クラスのコンストラクタを呼び出す super().__init__(pos, speed, angle) # 更新関数 def update(self, events, time): # 親クラスのupdate関数を呼び出す super().update(events, time) # オブジェクトを出現させる def spawn(): # 自機を出現させる ObjectBase.objects.append(PlayerFighter((ObjectBase.windowX / 2.0, ObjectBase.windowY), 2.5)) # 敵機を出現させる ObjectBase.objects.append(EnemyFighter((0.0, 0.0), 1.0)) ObjectBase.objects.append(EnemyFighter((ObjectBase.windowX * (1.0 - 0.25), 100.0), 1.0)) ObjectBase.objects.append(EnemyFighter((ObjectBase.windowX * (1.0 - 0.50), 100.0), 1.0)) ObjectBase.objects.append(EnemyFighter((ObjectBase.windowX * (1.0 - 0.75), 100.0), 1.0)) ObjectBase.objects.append(EnemyFighter((ObjectBase.windowX, 0.0), 1.0)) # main関数を定義する def main(): # pygameを初期化する pygame.init() # ウィンドウサイズを指定する(フルスクリーンモードで実行) screen = pygame.display.set_mode((480, 640), FULLSCREEN) # フレームレート制御のためのClockオブジェクトを生成する clock = pygame.time.Clock() # ウィンドウのタイトルを指定する pygame.display.set_caption("Game Programming") # フォントを設定する font = pygame.font.Font(None, 55) # Backgroundクラスのインスタンスを生成する background = Background() # Fighterクラスのインスタンスを生成する fighter = Fighter() # Bulletクラスのインスタンスを生成する bullet = Bullet() # ObjectBaseクラスにウィンドウサイズを設定する ObjectBase.windowX = screen.get_width() ObjectBase.windowY = screen.get_height() # オブジェクトの出現処理を行う spawn(); # ゲーム本体の処理は「無限ループ」で行う while(True): # 秒間60フレーム描画を実現するために、「clock.tick_busy_loop関数」を利用する time = clock.tick_busy_loop(60) / 1000.0 # Pygameのイベントを取得する events = pygame.event.get() # 画面を「青」で塗りつぶす screen.fill((0,32,128)) # fill関数の引数は「R(赤)」「G(緑)」「B(青)」 # 背景画像を表示する background.render(screen, time); # すべてのオブジェクトを更新する ObjectBase.update(events, time) # 全オブジェクトを描画する for obj in ObjectBase.get(): # 戦闘機を描画する if obj.type.endswith("Fighter"): fighter.render(obj, screen); # 弾丸を描画する if obj.type.endswith("Bullet"): bullet.render(obj, screen); # テキストを描画する text = font.render("SCORE:" + str(GameSystem.score), True, (255, 255, 255)) screen.blit(text, (0, 0)) # 画面を更新して描画する pygame.display.update() # pygameのイベントを処理する for event in events: # 「QUIT」メッセージが届いたらゲームを終了する if event.type == QUIT: pygame.quit() sys.exit() # 「ESCAPE」キーが押されたらゲームを終了する if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() if __name__ == "__main__": main() # main関数を実行する
//実行結果
長く複雑なプログラムですが、これは今後シューティングゲームの機能をさらに拡張するときや、アクションゲームなど他作品を作るときにも転用できます。プログラムに拡張性を持たせるコツは、クラスを細分化して「ポリフォーリズム」を実現することです。
弾丸を管理するために、まず「BulletBase」という基底クラスを作り、「PlayerBullet」と「EnemyBullet」で継承させています。全オブジェクトの基礎となる「ObjectBase」に、クラス変数として「objects配列」を保持しているため、どのクラスからでも容易にオブジェクトを追加・削除できることもポイントです。
当たり判定は「intersects関数」で行っていますが、ごく単純な「矩形」で判定しています。矩形同士の四隅の位置関係を判定するだけでOKです。オブジェクトの種類によって、衝突時にどのような処理を行うか分けています。
このあとさらに敵を増やしたり、友軍機を追加したりするときは、基底クラスを継承したクラスを作り、その型のオブジェクトを生成すればOKです。画像・オブジェクトともにポリフォーリズムを実現しているため、少しの変化を加えるだけで機能を追加していけます。ここまで理解することができれば、Pygameでのゲーム開発の基本はマスターできたといえるでしょう。
Pythonでゲーム開発するために必要なライブラリ・フレームワークや、Pygameの使い方などについてご紹介しました。Pythonは学習難易度が低く、効率的なプログラミングができる言語です。初心者の方でもすぐにゲームプログラミングができるようになります。まずはPygameなどで簡単な2Dゲームの開発にチャレンジし、機能を拡張していきましょう。
2024.06.17
子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介
#プログラミングスクール
2022.01.06
【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説
#プログラミングスクール
2024.01.26
【未経験でも転職可】30代におすすめプログラミングスクール8選!
#プログラミングスクール
2024.01.26
初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説
#JAVA
2024.01.26
忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説
#プログラミングスクール
2022.01.06
【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介
#プログラミングスクール
2024.01.26
【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説
#プログラミングスクール
2024.01.26
【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説
#プログラミングスクール