PythonのPanda3Dではじめる3Dゲーム開発初心者向け

## はじめに

Panda3Dは、Pythonで3Dゲームや視覚化アプリケーションを作成するための強力なオープンソースエンジンです。このエンジンは、初心者にも使いやすく、同時に高度な機能も備えています。この記事では、Panda3Dの基本から応用まで、15の章に分けて詳しく解説します。各章では、概念の説明とともに、実際のコード例を提供します。

## 第1章: Panda3Dの基本

Panda3Dは、3D空間を扱うためのシーングラフという概念を使用します。シーングラフは、3D世界の要素を階層的に管理する木構造です。

```python
from direct.showbase.ShowBase import ShowBase

class MyGame(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # 3Dモデルをロード
        self.model = self.loader.loadModel("models/panda")
        # シーンに追加
        self.model.reparentTo(self.render)

game = MyGame()
game.run()
```

このコードは、Panda3Dの基本的な構造を示しています。ShowBaseクラスを継承してゲームを作成し、3Dモデルをロードしてシーンに追加しています。

## 第2章: カメラの制御

3D空間を見るためには、カメラの制御が重要です。Panda3Dでは、カメラの位置や向きを簡単に変更できます。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Point3

class CameraControl(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        self.model = self.loader.loadModel("models/panda")
        self.model.reparentTo(self.render)
        
        # カメラの位置を設定
        self.camera.setPos(0, -10, 5)
        # カメラの向きを設定
        self.camera.lookAt(Point3(0, 0, 0))

game = CameraControl()
game.run()
```

このコードでは、カメラの位置を(0, -10, 5)に設定し、原点(0, 0, 0)を見るように向きを調整しています。

## 第3章: ライティング

3D空間の雰囲気を作り出すには、適切なライティングが欠かせません。Panda3Dでは、様々な種類のライトを使用できます。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, DirectionalLight, Vec4

class Lighting(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        self.model = self.loader.loadModel("models/panda")
        self.model.reparentTo(self.render)
        
        # 環境光を追加
        ambientLight = AmbientLight("ambient light")
        ambientLight.setColor(Vec4(0.2, 0.2, 0.2, 1))
        self.render.setLight(self.render.attachNewNode(ambientLight))
        
        # 指向性光源を追加
        directionalLight = DirectionalLight("directional light")
        directionalLight.setDirection(Vec4(-1, -1, -1, 0))
        directionalLight.setColor(Vec4(0.8, 0.8, 0.8, 1))
        self.render.setLight(self.render.attachNewNode(directionalLight))

game = Lighting()
game.run()
```

このコードでは、環境光と指向性光源を追加しています。環境光は全体的な明るさを、指向性光源は影の付き方を制御します。

## 第4章: テクスチャとマテリアル

3Dモデルの見た目を向上させるには、テクスチャとマテリアルの適用が重要です。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Texture, TextureStage

class TextureAndMaterial(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        self.model = self.loader.loadModel("models/panda")
        self.model.reparentTo(self.render)
        
        # テクスチャをロード
        texture = self.loader.loadTexture("textures/panda_texture.png")
        
        # テクスチャをモデルに適用
        self.model.setTexture(texture)
        
        # マテリアルを設定
        self.model.setMaterial(self.loader.loadMaterial("materials/panda_material.mat"))

game = TextureAndMaterial()
game.run()
```

このコードでは、テクスチャをロードしてモデルに適用し、さらにマテリアルを設定しています。

## 第5章: アニメーション

3Dモデルにアニメーションを適用することで、シーンに動きを加えることができます。

```python
from direct.showbase.ShowBase import ShowBase
from direct.actor.Actor import Actor

class Animation(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # アニメーション付きモデルをロード
        self.actor = Actor("models/panda-model", {"walk": "models/panda-walk"})
        self.actor.reparentTo(self.render)
        
        # アニメーションを再生
        self.actor.loop("walk")

game = Animation()
game.run()
```

このコードでは、アニメーション付きのモデルをロードし、「歩く」アニメーションをループ再生しています。

## 第6章: 物理シミュレーション

Panda3Dには物理エンジンが組み込まれており、リアルな物理シミュレーションを実現できます。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Vec3
from panda3d.bullet import BulletWorld, BulletPlaneShape, BulletRigidBodyNode, BulletBoxShape

class Physics(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # 物理世界を作成
        self.world = BulletWorld()
        self.world.setGravity(Vec3(0, 0, -9.81))
        
        # 地面を作成
        shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
        node = BulletRigidBodyNode('Ground')
        node.addShape(shape)
        np = self.render.attachNewNode(node)
        np.setPos(0, 0, 0)
        self.world.attachRigidBody(node)
        
        # 箱を作成
        shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))
        node = BulletRigidBodyNode('Box')
        node.setMass(1.0)
        node.addShape(shape)
        np = self.render.attachNewNode(node)
        np.setPos(0, 0, 2)
        self.world.attachRigidBody(node)
        
        # 更新タスクを追加
        self.taskMgr.add(self.update, 'update')
    
    def update(self, task):
        dt = globalClock.getDt()
        self.world.doPhysics(dt)
        return task.cont

game = Physics()
game.run()
```

このコードでは、物理世界を作成し、地面と箱を配置しています。箱は重力の影響を受けて落下します。

## 第7章: 衝突検出

ゲーム内のオブジェクト間の衝突を検出することは、多くのゲームメカニクスの基礎となります。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode, CollisionSphere
from panda3d.core import CollisionHandlerQueue

class Collision(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # 衝突検出システムを設定
        self.cTrav = CollisionTraverser()
        self.cHandler = CollisionHandlerQueue()
        
        # プレイヤーオブジェクトを作成
        self.player = self.loader.loadModel("models/panda")
        self.player.reparentTo(self.render)
        
        # プレイヤーに衝突球を追加
        cNode = CollisionNode('player')
        cNode.addSolid(CollisionSphere(0, 0, 0, 1))
        cNodePath = self.player.attachNewNode(cNode)
        self.cTrav.addCollider(cNodePath, self.cHandler)
        
        # 障害物を作成
        self.obstacle = self.loader.loadModel("models/box")
        self.obstacle.reparentTo(self.render)
        self.obstacle.setPos(5, 0, 0)
        
        # 障害物に衝突ノードを追加
        cNode = CollisionNode('obstacle')
        cNode.addSolid(CollisionSphere(0, 0, 0, 1))
        self.obstacle.attachNewNode(cNode)
        
        # 更新タスクを追加
        self.taskMgr.add(self.update, 'update')
    
    def update(self, task):
        self.cTrav.traverse(self.render)
        
        for i in range(self.cHandler.getNumEntries()):
            entry = self.cHandler.getEntry(i)
            print(f"Collision detected: {entry.getFromNodePath()} hit {entry.getIntoNodePath()}")
        
        return task.cont

game = Collision()
game.run()
```

このコードでは、プレイヤーと障害物に衝突球を設定し、衝突が発生したときにコンソールに出力します。

## 第8章: ユーザー入力の処理

ゲームを対話的にするには、キーボードやマウスからの入力を処理する必要があります。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Vec3

class UserInput(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        self.player = self.loader.loadModel("models/panda")
        self.player.reparentTo(self.render)
        
        # キー入力のイベントを設定
        self.accept("arrow_left", self.moveLeft)
        self.accept("arrow_right", self.moveRight)
        self.accept("arrow_up", self.moveForward)
        self.accept("arrow_down", self.moveBackward)
        
        # マウス操作を有効化
        self.disableMouse()
        self.taskMgr.add(self.spinCameraTask, "SpinCameraTask")
    
    def moveLeft(self):
        self.player.setPos(self.player.getPos() + Vec3(-1, 0, 0))
    
    def moveRight(self):
        self.player.setPos(self.player.getPos() + Vec3(1, 0, 0))
    
    def moveForward(self):
        self.player.setPos(self.player.getPos() + Vec3(0, 1, 0))
    
    def moveBackward(self):
        self.player.setPos(self.player.getPos() + Vec3(0, -1, 0))
    
    def spinCameraTask(self, task):
        angleDegrees = task.time * 6.0
        angleRadians = angleDegrees * (pi / 180.0)
        self.camera.setPos(20 * sin(angleRadians), -20 * cos(angleRadians), 3)
        self.camera.lookAt(0, 0, 0)
        return task.cont

game = UserInput()
game.run()
```

このコードでは、矢印キーでプレイヤーを移動させ、マウスでカメラを回転させることができます。

## 第9章: サウンドとBGM

ゲームに音楽や効果音を追加することで、プレイヤーの没入感を高めることができます。

```python
from direct.showbase.ShowBase import ShowBase
from direct.showbase import Audio3DManager

class Sound(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # 3Dオーディオマネージャーを作成
        self.audio3d = Audio3DManager.Audio3DManager(self.sfxManagerList[0], camera)
        
        # BGMを再生
        self.bgm = self.loader.loadMusic("music/background.ogg")
        self.bgm.setLoop(True)
        self.bgm.play()
        
        # 効果音をロード
        self.effect = self.audio3d.loadSfx("sounds/effect.wav")
        
        # 効果音を再生するオブジェクトを作成
        self.soundObject = self.loader.loadModel("models/panda")
        self.soundObject.reparentTo(self.render)
        self.soundObject.setPos(5, 0, 0)
        
        # 効果音を3D空間に配置
        self.audio3d.attachSoundToObject(self.effect, self.soundObject)
        
        # スペースキーで効果音を再生
        self.accept("space", self.playEffect)
    
    def playEffect(self):
        if self.effect.status() != self.effect.PLAYING:
            self.effect.play()

game = Sound()
game.run()
```

このコードでは、BGMをループ再生し、スペースキーを押すと3D効果音が再生されます。

## 第10章: パーティクルシステム

パーティクルシステムを使用することで、火や煙、雨などの複雑な視覚効果を作成できます。


```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Filename
from direct.particles.ParticleEffect import ParticleEffect

class Particles(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # パーティクル効果を作成
        self.effect = ParticleEffect()
        self.effect.loadConfig(Filename("particles/fire.ptf"))
        
        # パーティクルをシーンに追加
        self.effect.start(parent=self.render, renderParent=self.render)
        self.effect.setPos(0, 0, 0)
        
        # カメラの位置を調整
        self.camera.setPos(0, -10, 5)
        self.camera.lookAt(0, 0, 0)

game = Particles()
game.run()
```

このコードでは、火のパーティクル効果をロードし、シーンに追加しています。パーティクルファイル（fire.ptf）は事前に作成しておく必要があります。

## 第11章: シェーダーの使用

シェーダーを使用することで、高度なグラフィック効果を実現できます。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Shader

class ShaderExample(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # モデルをロード
        self.model = self.loader.loadModel("models/panda")
        self.model.reparentTo(self.render)
        
        # シェーダーをロード
        shader = Shader.load(Shader.SL_GLSL, 
                             vertex="shaders/vertex.glsl",
                             fragment="shaders/fragment.glsl")
        
        # シェーダーをモデルに適用
        self.model.setShader(shader)
        
        # シェーダー入力を設定
        self.model.setShaderInput("time", 0)
        
        # 更新タスクを追加
        self.taskMgr.add(self.update, "update")
    
    def update(self, task):
        # 時間を更新
        self.model.setShaderInput("time", task.time)
        return task.cont

game = ShaderExample()
game.run()
```

このコードでは、カスタムシェーダーをモデルに適用し、時間に基づいて変化するエフェクトを作成しています。vertex.glslとfragment.glslファイルは別途作成する必要があります。

## 第12章: GUIの作成

ゲーム内のメニューやHUD（ヘッドアップディスプレイ）を作成するには、GUIシステムを使用します。

```python
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import DirectButton, DirectFrame, DirectLabel
from panda3d.core import TextNode

class GUIExample(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # フレームを作成
        self.frame = DirectFrame(frameColor=(0, 0, 0, 0.5),
                                 frameSize=(-0.5, 0.5, -0.5, 0.5),
                                 pos=(0, 0, 0))
        
        # ラベルを作成
        self.label = DirectLabel(text="スコア: 0",
                                 parent=self.frame,
                                 scale=0.07,
                                 pos=(-0.4, 0, 0.4),
                                 text_align=TextNode.ALeft)
        
        # ボタンを作成
        self.button = DirectButton(text="クリック",
                                   parent=self.frame,
                                   scale=0.1,
                                   pos=(0, 0, -0.4),
                                   command=self.onButtonClick)
        
        self.score = 0
    
    def onButtonClick(self):
        self.score += 1
        self.label["text"] = f"スコア: {self.score}"

game = GUIExample()
game.run()
```

このコードでは、フレーム、ラベル、ボタンを含む簡単なGUIを作成しています。ボタンをクリックするとスコアが増加します。

## 第13章: マルチプレイヤーネットワーキング

ネットワークを介して複数のプレイヤーが参加できるゲームを作成するには、ネットワーキング機能を実装する必要があります。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import QueuedConnectionManager, QueuedConnectionListener
from panda3d.core import QueuedConnectionReader, ConnectionWriter
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator

class NetworkingExample(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # ネットワークコンポーネントを初期化
        self.cManager = QueuedConnectionManager()
        self.cListener = QueuedConnectionListener(self.cManager, 0)
        self.cReader = QueuedConnectionReader(self.cManager, 0)
        self.cWriter = ConnectionWriter(self.cManager, 0)
        
        # サーバーを開始
        self.port = 9099
        self.socket = self.cManager.openTCPServerRendezvous(self.port, 1000)
        self.cListener.addConnection(self.socket)
        
        # タスクを追加
        self.taskMgr.add(self.tskListenerPolling, "Poll the connection listener", -39)
        self.taskMgr.add(self.tskReaderPolling, "Poll the connection reader", -40)
    
    def tskListenerPolling(self, task):
        if self.cListener.newConnectionAvailable():
            rendezvous = PointerToConnection()
            netAddress = NetAddress()
            newConnection = PointerToConnection()
            
            if self.cListener.getNewConnection(rendezvous, netAddress, newConnection):
                newConnection = newConnection.p()
                self.cReader.addConnection(newConnection)
                print(f"New connection from {netAddress.getIpString()}")
        
        return task.cont
    
    def tskReaderPolling(self, task):
        if self.cReader.dataAvailable():
            datagram = NetDatagram()
            if self.cReader.getData(datagram):
                self.processData(datagram)
        
        return task.cont
    
    def processData(self, datagram):
        iterator = PyDatagramIterator(datagram)
        message = iterator.getString()
        print(f"Received message: {message}")

game = NetworkingExample()
game.run()
```

このコードは、基本的なTCPサーバーを実装しています。クライアントからの接続を受け付け、メッセージを処理します。実際のゲームでは、このコードをさらに拡張して、ゲーム状態の同期などを行う必要があります。

## 第14章: AIと経路探索

ゲーム内のNPC（ノンプレイヤーキャラクター）にインテリジェントな行動をさせるには、AIと経路探索アルゴリズムを実装する必要があります。

```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import NodePath, Vec3
from direct.task import Task
import random

class AIExample(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # NPCを作成
        self.npc = self.loader.loadModel("models/panda")
        self.npc.reparentTo(self.render)
        self.npc.setScale(0.5)
        
        # 目標地点のリスト
        self.waypoints = [
            Vec3(5, 5, 0),
            Vec3(-5, 5, 0),
            Vec3(-5, -5, 0),
            Vec3(5, -5, 0)
        ]
        
        self.current_waypoint = 0
        
        # 更新タスクを追加
        self.taskMgr.add(self.updateNPC, "UpdateNPC")
    
    def updateNPC(self, task):
        # 現在の目標地点
        target = self.waypoints[self.current_waypoint]
        
        # NPCの現在位置
        current_pos = self.npc.getPos()
        
        # 目標への方向ベクトル
        direction = target - current_pos
        
        # 一定の速度で移動
        speed = 0.1
        if direction.length() > speed:
            direction.normalize()
            new_pos = current_pos + direction * speed
            self.npc.setPos(new_pos)
        else:
            # 目標に到達したら次の目標へ
            self.current_waypoint = (self.current_waypoint + 1) % len(self.waypoints)
        
        # NPCの向きを進行方向に合わせる
        self.npc.lookAt(target)
        
        return Task.cont

game = AIExample()
game.run()
```

このコードでは、NPCが定義されたウェイポイント（目標地点）を順番に巡回します。実際のゲームでは、より複雑な経路探索アルゴリズム（例：A*アルゴリズム）を実装することで、障害物を避けながら目的地に到達するようなAIを作成できます。

## 第15章: ゲームステートとシーン管理

大規模なゲームでは、異なるゲーム状態（メニュー、ゲームプレイ、ゲームオーバーなど）を管理し、シーンを切り替える必要があります。

```python
from direct.showbase.ShowBase import ShowBase
from direct.fsm.FSM import FSM
from direct.gui.DirectGui import DirectButton

class GameState(FSM):
    def __init__(self, game):
        FSM.__init__(self, "GameState")
        self.game = game
    
    def enterMenu(self):
        self.game.menu_scene.show()
        self.game.play_scene.hide()
        self.game.gameover_scene.hide()
    
    def exitMenu(self):
        self.game.menu_scene.hide()
    
    def enterPlay(self):
        self.game.play_scene.show()
    
    def exitPlay(self):
        self.game.play_scene.hide()
    
    def enterGameOver(self):
        self.game.gameover_scene.show()
    
    def exitGameOver(self):
        self.game.gameover_scene.hide()

class GameExample(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        
        # シーンを作成
        self.menu_scene = self.loader.loadModel("models/menu")
        self.play_scene = self.loader.loadModel("models/level")
        self.gameover_scene = self.loader.loadModel("models/gameover")
        
        self.menu_scene.reparentTo(self.render)
        self.play_scene.reparentTo(self.render)
        self.gameover_scene.reparentTo(self.render)
        
        # ゲームステートを初期化
        self.game_state = GameState(self)
        
        # メニューボタンを作成
        self.start_button = DirectButton(text="ゲーム開始", scale=0.1, pos=(0, 0, 0.2),
                                         command=self.startGame)
        self.quit_button = DirectButton(text="終了", scale=0.1, pos=(0, 0, -0.2),
                                        command=self.quit)
        
        # 初期状態をメニューに設定
        self.game_state.request("Menu")
    
    def startGame(self):
        self.game_state.request("Play")
    
    def gameOver(self):
        self.game_state.request("GameOver")
    
    def quit(self):
        self.userExit()

game = GameExample()
game.run()
```

このコードでは、FSM（有限状態機械）を使用してゲームの状態を管理しています。メニュー、プレイ、ゲームオーバーの3つの状態があり、それぞれの状態に応じて適切なシーンを表示します。

以上で、Panda3Dを使用したPythonゲーム開発の15章にわたる詳細な解説が完了しました。これらの章を通じて、3Dゲーム開発の基本から応用まで幅広くカバーしています。実際のゲーム開発では、これらの要素を組み合わせて使用することになります。Panda3Dの豊富な機能を活用し、創造的なゲームを作成してください。