見出し画像

Pythonでマイクラを作る ⑲サウンドを実装する

Pythonでマイクラを作る 第19回です。今回はサウンドを実装します。

今まで無音のマイクラクローン(Pynecrafter)で遊んでいましたが、BGM(BackGround Music)を鳴らすだけで、ゲームの面白さが倍増したように感じました。プレイヤーの歩行音やブロックの設置、破壊音を鳴らすと、ユーザーは自分の操作を確認できて、満足感が増します。
今回サウンドを実装してみて、ゲームにおけるサウンドの効果は高いことが実感できました。では始めましょう。

サウンド素材の準備

サウンドを実装するためには、サウンド素材(mp3ファイル)を準備しなくてはなりません。無料で公開されているサウンド素材をお借りして、マイクラクローンで使わせていただきます。

効果音ラボ

国内のフリー音源サイトの中で最大手の一つです。このサイトだけで全てが揃ってしまうほど、効果音が充実しています。マイクラクローンでは、次の5つの効果音を使わせていただきました。ダンロードしたファイルはリネームして、musicディレクトリに保存してください。

効果音ラボ「生活」
[土の上を歩く] → walk_on_land.mp
[ジャンプ] → jump.mp3
[ジャンプの着地] → landing_on_jump.mp3
[ドアを開ける1] → open_the_door1.mp3

効果音ラボ「戦闘」
[重いパンチ2] → heavy_punch2.mp3


自作のBGM

BGMについては、インターネット検索でご自分の好きな曲を見つけてください。とりあえず音を鳴らしたい方のために、私の娘が作ってくれた MP3 をフリーで公開しますので、ダウンロードしてお使いください。
BGM も同様にmusicディレクトリに保存してください。

ディレクトリ構造


# ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ fonts/
  │  ├ images/
  │  ├ music/
  │  │  ├ heavy_punch2.mp3  # 追加
  │  │  ├ jump.mp3  # 追加
  │  │  ├ landing_of_jump.mp3  # 追加
  │  │  ├ open_the_door1.mp3  # 追加
  │  │  ├ pleasant_stroll.mp3  # 追加
  │  │  ├ walk_on_land.mp3  # 追加
  │  │  
  │  ├ textures/
  │  ├ models/
  │  ├ src/
  │  │  ├ __init__.py
  │  │  ├ block.py  # ブロック関連
  │  │  ├ player.py  # プレイヤー関連
  │  │  ├ player_model.py  # プレイヤーモデル関連
  │  │  ├ camera.py  # カメラ関連
  │  │  ├ target.py  # ターゲットブロック関連
  │  │  ├ user_interface.py  # インターフェース関連
  │  │  ├ utils.py  # ユーティリティー
  │  │  ├ inventory.py  # インベントリ
  │  │  ├ menu.py  # メニュー関連
  │  │  ├ architecture.py  # 建築MOD
  │  │  ├ connect_to_mcpi.py  # マイクラとの連携
  │  │  ├ sound.py  # サウンド関連  # 追加
  │  │  ├ mc.py  # 統合クラス
  │  │  
  │  ├ 01_01_showbase.py
  │  ├ 01_02_showbase.py
  │  ├ 01_03_showbase.py
  │  ├ xxx.py
  │  ├ 05_01_main.py
  │  ├ 06_01_main.py
  │  ├ 07_01_egg_model_maker_1_2_3_4_5_6.py  # ブロックを作成
  │  ├ 07_02_main.py  # 統合クラスをインポートしてゲームを起動する
  │  ├ 08_01_main.py  # 統合クラスをインポートしてゲームを起動する
  │  ├ 17_01_main.py  # 統合クラスをインポートしてゲームを起動する

上図が今回のディレクトリ構造です。
musicディレクトリに効果音、BGMのサウンドファイルを保存します。
srcディレクトリに「sound.py」ファイルを作成し、Soundクラスを作成します。Soundクラスは、サウンド関連の機能を管理します。

ブロックの設置、破壊音を鳴らす

今回は修正するファイルが多くあります。一つずつ説明していきます。

"""src/__init__.py"""
from .block import Block
from .player import Player
from .user_interface import UserInterface
from .inventory import Inventory
from .menu import Menu
from .architecture import Architecture
from .connect_to_mcpi import ConnectToMCPI
from .sound import Sound  # 追記
from .mc import MC

パッケージの初期化モジュール __init__.pyは、「from .sound import Sound」の1行を追記します。soundモジュールから、Soundクラスをインポートします。

"""src/mc.py"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from . import *


class MC(ShowBase, UserInterface, Inventory, Menu, Architecture, ConnectToMCPI, Sound):  # 追記
    def __init__(self, ground_size=128, mode='normal'):
        self.mode = mode
        self.ground_size = ground_size
        self.enable_sound_effect = False  # 追記
        # ShowBaseを継承する
        ShowBase.__init__(self)
        self.font = self.loader.loadFont('fonts/PixelMplus12-Regular.ttf')
        UserInterface.__init__(self)
        Inventory.__init__(self)
        Menu.__init__(self)
        if self.mode == 'mcpi':
            ConnectToMCPI.__init__(self)
        Sound.__init__(self)  # 追記

以下略

ゲームを起動するクラスである MCクラスは、Soundクラスを継承するように変更します。クラス定義文の引数に Sound を追記します。次に、「Sound.__init__(self)」により、Soundクラスの初期化メソッドを実行します。
インスタンス変数 enable_sound_effect を追加して、値を False にしておきます。この変数は、スプラッシュ画面(ゲームの起動画面)が終了してから、効果音を鳴らすために使用します。
以上の3行を変更してください。

"""src/user_interface.py"""
from time import time
from math import *
from panda3d.core import *
from .utils import *


class UserInterface:name}:

略

    def close_splash_screen(self, task):
        self.splash_screen_node.detachNode()
        wellcome_text = 'ようこそ Pynecrafter!'
        self.console_window.setText(wellcome_text)
        self.console_window.start_time = time()  # 実行した時間を start_time に記録
        self.enable_sound_effect = True  # 追記
        return task.done

UserInterfaceクラスは、ユーザーインターフェースを管理します。
close_splash_screenメソッドに、「self.enable_sound_effect = True」を追記して、スプラッシュスクリーンが終了したら、効果音が有効になるようにします。変更はこの1行のみです。

"""src/block.py"""
import re
from math import *
from panda3d.core import *


class Block:def add_block_model(self, x, y, z, block_id):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        self.base.set(key, self.base.block_node.attachNewNode(PandaNode(key)))
        placeholder = self.base.get(key)
        placeholder.setPos(floor(x), floor(y), floor(z))
        # block = self.base.loader.loadModel(f'models/{block_id}')
        # block.reparentTo(placeholder)
        if block_id in self.block_models:
            block_model = self.block_models[block_id]
        else:
            block_model = self.base.loader.loadModel(f'models/{block_id}')
            self.block_models[block_id] = block_model
        block_model.instanceTo(placeholder)
        if self.base.enable_sound_effect:  # 追記
            self.base.set_block_sound.play()  # 追記def remove_block(self, x, y, z):
        self.remove_block_dictionary(x, y, z)
        self.remove_block_model(x, y, z)
        self.hide_invisible_blocks(Point3(x, y, z))
        if self.base.enable_sound_effect:  # 追記
            self.base.break_block_sound.play()  # 追記

Blockクラスは、ブロックを管理します。訂正は2箇所です。
add_block_modelメソッドに「self.base.set_block_sound.play()」を、remove_block_modelメソッドに「self.base.break_block_sound.play()」を追記します。2箇所とも、条件式「if self.base.enable_sound_effect:」により、スプラッシュ画面が終わってから効果音を鳴らすようにしておきます。

"""src/sound.py"""


class Sound:
    BGM = 'music/pleasant_stroll.mp3'
    WALK_SOUND = 'music/walk_on_land.mp3'
    JUMP_SOUND = 'music/jump.mp3'
    LANDING_OF_JUMP_SOUND = 'music/landing_of_jump.mp3'
    SET_BLOCK_SOUND = 'music/open_the_door1.mp3'
    BREAK_BLOCK_SOUND = 'music/heavy_punch2.mp3'

    def __init__(self):
        # Allow playing two music files at the same time.
        self.musicManager.setConcurrentSoundLimit(3)

        # bgm
        self.bgm = self.loader.loadMusic(Sound.BGM)
        self.bgm.setLoop(True)
        self.bgm.play()

        # sound effect
        self.walk_sound = self.loader.loadMusic(Sound.WALK_SOUND)
        self.walk_sound.setLoop(True)
        self.jump_sound = self.loader.loadMusic(Sound.JUMP_SOUND)
        self.landing_of_jump_sound = self.loader.loadMusic(Sound.LANDING_OF_JUMP_SOUND)
        self.set_block_sound = self.loader.loadMusic(Sound.SET_BLOCK_SOUND)
        self.break_sound = self.loader.loadMusic(Sound.BREAK_BLOCK_SOUND)

新規作成したsound.py にSoundクラスのプログラムを記述します。Soundクラスで、BGMと歩行音、ジャンプ音、ジャンプの着地音、ブロックの設置音、破壊音を定義します。
Soundクラスのクラス変数は、先ほど用意したサウンドファイルを指定していきます。クラス変数 BGM、WALK_SOUND、JUMP_SOUND、LANDING_OF_JUMP_SOUND、SET_BLOCK_SOUND、BREAK_BLOCK_SOUNDの6つに、それぞれサウンドファイルのパスを指定します。
次に、インタンス変数も同様に6つ定義します。インスタンス変数 bgm、walk_sound、jump_sound、landing_on_sound、set_block_sound、break_block_sound を、それぞれ loader.loadMusicメソッドでサウンドオブジェクトとして定義します。引数はサウンドファイルのパスにしてください。
BGM(bgm_sound)と歩行音(walk_sound)は、ループさせる必要があるので、「setLoop(True)」を指定します。
そして最後に「self.bgm.play()」で 、ゲーム開始と同時に BGM をスタートします。

ブロックの効果音を鳴らすテストをしましょう。
17_01_main.py を実行して、自動で村を作成するワールドを開始します。ゲーム開始と同時に、BGMが鳴り始めます。そしてスプラッシュ画面が終了し、プレイヤーが見えるようになったら、適当なところにブロックを設置、破壊をして、それぞれ設置音、破壊音が聞こえたら、テストは完了です。

次にプレイヤーの動作に効果音を追加します。

プレイヤーの動作に効果音を追加する

プレイヤーの歩行音、ジャンプ音、ジャンプから着地音を鳴らすようにします。Playerクラスを修正します。

"""src/player.py"""
from math import *
from panda3d.core import *
from direct.showbase.ShowBaseGlobal import globalClock
from .player_model import PlayerModel
from .camera import Camera
from .target import Target


class Player(PlayerModel, Camera, Target):
    heading_angular_velocity = 15000
    pitch_angular_velocity = 5000
    max_pitch_angle = 60
    speed = 10
    eye_height = 1.6
    gravity_force = 9.8
    jump_speed = 10

    # コンストラクタ
    def __init__(self, base):
        self.base = base
        PlayerModel.__init__(self)
        Camera.__init__(self)
        Target.__init__(self)

        self.position = Point3(0, 0, 0)
        self.direction = VBase3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)
        self.mouse_pos_x = 0
        self.mouse_pos_y = 0
        self.target_position = None
        self.is_on_ground = True  # 地面に接している
        self.is_flying = False  # 空中に浮かんでいる
        self.is_walking = False  # 歩いている  # 追加

以下略

インスタンス変数を追加して、プレイヤーの歩行状態を保存できるようにします。is_walking は歩いているときに True にするので、初めは False を代入しておきます。

# メソッドを修正

    def update_velocity(self):
        key_map = self.key_map
        walk_sound = self.base.walk_sound  # 追加

        if self.is_on_ground or self.is_flying:
            if key_map['w'] or key_map['a'] or key_map['s'] or key_map['d']:
                self.is_walking = True  # 追加
                if walk_sound.status() is not walk_sound.PLAYING:  # 追加
                    walk_sound.play()  # 追加
                heading = self.direction.x
                if key_map['w'] and key_map['a']:
                    angle = 135
                elif key_map['a'] and key_map['s']:
                    angle = 225
                elif key_map['s'] and key_map['d']:
                    angle = 315
                elif key_map['d'] and key_map['w']:
                    angle = 45
                elif key_map['w']:
                    angle = 90
                elif key_map['a']:
                    angle = 180
                elif key_map['s']:
                    angle = 270
                else:  # key_map['d']
                    angle = 0
                self.velocity = \
                    Vec3(
                        cos(radians(angle + heading)),
                        sin(radians(angle + heading)),
                        0
                    ) * Player.speed
            else:
                self.is_walking = False  # 追加
                if walk_sound.status() is walk_sound.PLAYING:  # 追加
                    walk_sound.stop()  # 追加
                self.velocity = Vec3(0, 0, 0)

            if key_map['space']:
                if self.is_on_ground:  # 追加
                    self.base.jump_sound.play()  # 追加
                self.is_on_ground = False
                self.is_flying = False
                self.velocity.setZ(Player.jump_speed)

update_velocityメソッドに、歩行音、ジャンプ音を実装します。
「walk_sound = self.base.walk_sound」で変数 walk_sound を定義します。self.base.walk_sound がメソッドの中にたくさん出てくるので、変数 walk_sound に代入しておくと、コードがきれいに書けます。

「WASD」キーが押されているとき、プレイヤーは歩いている状態なので「self.is_walking = True」を代入します。歩行音はループ設定になっているため、条件式「if walk_sound.status() is not walk_sound.PLAYING:」で歩行音が再生状態でないことを確認して、「walk_sound.play()」で歩行音をスタートします。

「WASD」キーが押されいないとき、「self.is_walking = False」を代入します。条件式「if walk_sound.status() is walk_sound.PLAYING:」で歩行音が再生状態であることを確認して、「walk_sound.stop()」で歩行音を停止します。

「スペース」キーが押されているとき、条件式「if self.is_on_ground:」でプレイヤーが着地していることを確認して、「self.jump_sound.play()」でジャンプ音を鳴らします。

# メソッドを修正

    def update_position(self):
        self.update_velocity()
        dt = globalClock.getDt()
        self.position = self.position + self.velocity * dt

        floor_height = self.base.block.get_floor_height() + 1

        # # ジャンプ中の位置情報を保存
        # self.record_jump_positions_with_time(dt, floor_height)

        key_map = self.key_map
        if key_map['arrow_up']:
            self.is_on_ground = False
            self.is_flying = True
            self.position.setZ(self.position.getZ() + Player.jump_speed * dt)
        elif key_map['arrow_down']:
            self.position.setZ(self.position.getZ() - Player.jump_speed * dt)
            if self.position.z <= floor_height:
                self.position.z = floor_height
                self.is_on_ground = True
                self.is_flying = False

        if not self.is_flying:
            if not self.is_on_ground:  # (1)
                if self.position.z <= floor_height:  # (2)
                    self.position.z = floor_height
                    self.is_on_ground = True
                    self.base.landing_of_jump_sound.play()  # 追加
                else:
                    self.velocity.setZ(self.velocity.getZ() - Player.gravity_force * dt)
            else:
                if floor_height < self.position.z:
                    self.is_on_ground = False
                    self.velocity.setZ(-Player.gravity_force * dt)

update_positionメソッドに、着地音を実装します。条件式 (1) はプレイヤーがジャンプ中であることを確認しています。条件式 (2) はプレイヤーの位置のZ座標が、着地点の高さ(floor_height)より下に落ちてきたことを判定します。そのとき、「self.base.landing_of_jump_sound.play()」により、着地音を鳴らします。

歩行音、ジャンプ音、着地音をテストします。17_01_main.py のワールドを実行します。プレイヤーを動かして、各効果音が再生できれば、テストは成功です。

今回は、地味なアップデートになりました。しかし、サウンドの実装はゲームユーザーにとって、ゲームの評価を左右する重要な要素となります。BGM や効果音はゲームの面白さを増し、満足度を上げることができるのです。

次回は、プレイヤーのモーション(動作)を実装します。そして、2段ジャンプと自動ジャンプを実装して、プレイヤーの操作性をアップさせます。お楽しみに。


前の記事
Pythonでマイクラを作る ⑱マインクラフトにワールドを転送する
次の記事
Pythonでマイクラを作る ⑳プレイヤーのモーションを実装する

その他のタイトルはこちら

いいなと思ったら応援しよう!