日経ソフトウェア2022年5月号の「特集4Pythonで3Dゲームを作ろう」を試してみた その3

前回ステップ3まで実行したので、ステップ4以降を実行していきます。

 

ステップ4:ボールを転がす

迷路の上を転がるボールを作成します。

ball.egg.pzを使用します。

必要なモジュールのインポートと変数の初期値設定を行います。

# 衝突判定
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerQueue, CollisionRay
# ボールの速度計算
from panda3d.core import LVector3, LRotationf
ACCEL = 70
MAX_SPEED = 5
MAX_SPEED_SQ = MAX_SPEED ** 2

 

 

ボールオブジェクトを設定します。

        # ボールオブジェクトの設定
        self.ballRoot = render.attachNewNode("ballRoot")
        self.ball = loader.loadModel("models/ball")
        self.ball.reparentTo(self.ballRoot)
        # ボールの衝突検出用のマスク
        self.ballSphere = self.ball.find("**/ball")
        self.ballSphere.node().setFromCollideMask(BitMask32.bit(0))
        self.ballSphere.node().setIntoCollideMask(BitMask32.allOff())
        # ボールが床と衝突したときの位置を知る光線
        self.ballGroundRay = CollisionRay()
        self.ballGroundRay.setOrigin(0, 0, 10)
        self.ballGroundRay.setDirection(0, 0, -1)
        # 光線の衝突検出用のマスク
        self.ballGroundCol = CollisionNode('groundRay')
        self.ballGroundCol.addSolid(self.ballGroundRay)
        self.ballGroundCol.setFromCollideMask(BitMask32.bit(1))
        self.ballGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.ballGroundColNp = self.ballRoot.attachNewNode(self.ballGroundCol)
        # 衝突ハンドラーにボールと光線を追加
        self.cTrav = CollisionTraverser()
        self.cHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.ballSphere, self.cHandler)
        self.cTrav.addCollider(self.ballGroundColNp, self.cHandler)

 

 

startメソッドの中でボールの初期値を設定します。

    def start(self):
        # ボールの初期位置の設定
        startPos = self.maze.find("**/start").getPos()
        self.ballRoot.setPos(startPos)
        self.ballV = LVector3(0, 0, 0)
        self.accelV = LVector3(0, 0, 0)

 

ボールが衝突したときの処理の分岐、ボールの速度や向きを設定します。

        # ボールが衝突したときの処理の分岐
        for i in range(self.cHandler.getNumEntries()):
            entry = self.cHandler.getEntry(i)
            name = entry.getIntoNode().getName()
            if name == "wall_collide":
                self.wallCollideHandler(entry)
            elif name == "ground_collide":
                self.groundCollideHandler(entry)
            elif name == "loseTrigger":
                self.loseGame(entry)
            elif name =="goalCol":
                self.winGame(entry)

        # ボールの速度や向きの計算
        self.ballV += self.accelV * dt * ACCEL
        if self.ballV.lengthSquared() > MAX_SPEED_SQ:
            self.ballV.normalize()
            self.ballV *= MAX_SPEED
        self.ballRoot.setPos(self.ballRoot.getPos() + (self.ballV * dt))
        prevRot = LRotationf(self.ball.getQuat())
        axis = LVector3.up().cross(self.ballV)
        newRot = LRotationf(axis, 45.5 * dt * self.ballV.length())
        self.ball.setQuat(prevRot * newRot)

 

groundCollideHandlerメソッドと wallCollideHandlerメソッドを定義します。

それぞれ、ボールが床にぶつかっているときの速度や向きを計算します。

ボールが壁にぶつかっているときの速度や向きを計算します。

 

    # groundCollideHandler関数の定義
    def groundCollideHandler(self, colEntry):
        newZ = colEntry.getSurfacePoint(render).getZ()
        self.ballRoot.setZ(newZ + .4)

        norm = colEntry.getSurfaceNormal(render)
        accelSide = norm.cross(LVector3.up())
        self.accelV = norm.cross(accelSide)

    # wallCollideHandler関数の定義
    def wallCollideHandler(self, colEntry):
        norm = colEntry.getSurfaceNormal(render) * -1 # 壁の法線
        curSpeed = self.ballV.length() # 現在の速度
        inVec = self.ballV / curSpeed # 進行方向
        velAngle = norm.dot(inVec) # 角度
        hitDir = colEntry.getSurfacePoint(render) - self.ballRoot.getPos()
        hitDir.normalize()

        hitAngle = norm.dot(hitDir)
       
        if velAngle > 0 and hitAngle > .995:
            reflectVec = (norm * norm.dot(inVec * -1) * 2) + inVec
            self.ballV = reflectVec * (curSpeed * (((1 - velAngle) * .5) + .5))
            disp = (colEntry.getSurfacePoint(render) -
                    colEntry.getInteriorPoint(render))
            newPos = self.ballRoot.getPos() + disp
            self.ballRoot.setPos(newPos)

 

実行するとボールをマウスで操作するウインドウが表示されます。

穴に落ちると、loseGameメソッドを定義していないので、穴に落ちるとエラーでウインドウが落ちます。

main4.py

 

 

ステップ5:穴に落ちたら開始地点に戻る

ボールが穴に落ちるとスタート地点に戻るようにプログラムを追加します。

 

ステップ5に必要なモジュールのインストール

# インターバル
from direct.interval.MetaInterval import Sequence, Parallel
from direct.interval.LerpInterval import LerpFunc
from direct.interval.FunctionInterval import Func, Wait

 

loseGameの定義

    # loseGame関数の定義
    def loseGame(self, entry):
        toPos = entry.getInteriorPoint(render)
        taskMgr.remove('rollTask')

        # ボールを穴に落とした後最初の位置に移動させて再度スタート
        Sequence(
            Parallel(
                LerpFunc(self.ballRoot.setX, fromData=self.ballRoot.getX(),
                         toData=toPos.getX(), duration=.1),
                LerpFunc(self.ballRoot.setY, fromData=self.ballRoot.getY(),
                         toData=toPos.getY(), duration=.1),
                LerpFunc(self.ballRoot.setZ, fromData=self.ballRoot.getZ(),
                         toData=self.ballRoot.getZ() - .9, duration=.2)),
            Wait(1),
            Func(self.start)).start()

 

 

 

 

 

ステップ6:ゴールしたらテキストを表示する

ゴールしたら実行するwinGameメソッドを定義します。

# ゴールの衝突判定
from panda3d.core import CollisionBox,Point3
    # winGame関数の定義
    def winGame(self, entry):
        self.title.setText("ゴール!!")
        toPos = entry.getInteriorPoint(render)
        self.ballRoot.hide()
        taskMgr.remove('rollTask')

ボールがゴールすると、右下にゴールと表示されます。

main6.py

 

ステップ7:ボールに光を反射させる

最後にボールに光を追加します。

# ライトと素材
from panda3d.core import AmbientLight, DirectionalLight
from panda3d.core import Material

 

 

    # ライトの設定
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor*1
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(LVector3(0, 0, -1))
        directionalLight.setColor*2
        directionalLight.setSpecularColor*3
        self.ballRoot.setLight(render.attachNewNode(ambientLight))
        self.ballRoot.setLight(render.attachNewNode(directionalLight))
        # 光沢のある素材の設定
        m = Material()
        m.setSpecular*4
        m.setShininess(96)
        self.ball.setMaterial(m, 1)

 

ボールに光が入りました。

main7.py

 

光る前のボールはこんな感じでした。

main6.py

 

 

*1:.55, .55, .55, 1

*2:0.375, 0.375, 0.375, 1

*3:1, 1, 1, 1

*4:1, 1, 1, 1

/* -----codeの行番号----- */