検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
当サイトのドメイン名は " getwebtips.net " です。
トップレベルドメインは .net であり、他の .com / .shop といったトップレベルドメインのサイトとは一切関係ありません。
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Prototype Pattern - Part. 1 「ちょっとゲームのことを考えてみよう」の巻 投稿一覧へ戻る

Published 2022年5月19日20:16 by T.Tsuyoshi

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

Practical Python Design Patterns - The Prototype Pattern 編

Chapter 3: The Prototype Pattern - Part 1
- Base for an Actual Game (ちょっとゲームのことを考えてみよう) -

StarCraft のような RTS (Real-Time Strategy game) を作成するとしましょう。あなたは様々なキャラクターの一団を指揮するプレイヤーとなって、建物を建設し、ユニットを生成し、最終的にある種の戦略目標を達成する必要があります。例えば、騎士 (Knight) ユニットを考えてみましょう。Knight は Barracks (兵舎) と呼ばれる建物で生産されます。あなたは Knight ユニットをより迅速に生産するために、1つのシナリオ内でそういった建物を複数所有することが可能です。
さて、Knight と Barracks のこの説明から、かなり明快な実装形態が思い描けますね。Barracks クラスが必要です。このクラスでは、Knight クラスのオブジェクトである Knight オブジェクトを返す generate_knight() メソッドを定義することになるでしょう。そして、Knight クラスでは以下のような属性を定義する必要があります:
Life
Speed
Attack power
Attack range
Weapon
Knight クラスにこれらのパラメータ値を渡して Knight ユニット (オブジェクト) を作成します:
rts_simple.py
class Knight:
def __init__(
self,
life,
speed,
attack_power,
attack_range,
weapon
):
self.life = life
self.speed = speed
self.attack_power = attack_power
self.attack_range = attack_range
self.weapon = weapon

def __str__(self):
return (f'Life: {self.life}\n'
f'Speed: {self.speed}\n'
f'Attack Power: {self.attack_power}\n'
f'Attack Range: {self.attack_range}\n'
f'Weapon : {self.weapon}')


class Barracks:
def generate_knight(self):
return Knight(400, 5, 3, 1, 'short sword')

if __name__ == '__main__':
barracks = Barracks()
knight1 = barracks.generate_knight()
print(f'[knight 1]\n{knight1}')
Barrack を建設し Knight ユニットを1つ生産しました。出力は次の通りです:
[knight 1]
Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon : short sword
ゲーム? なのに画面に描画するコードもプレーヤーを操作するコードもありませんが、デフォルト値を利用して新しい Knight ユニットを生成するのは非常に簡単です。この章の残りの部分ではこの「生成」コードを念頭に置いて、「ほぼ異なる点のない類似したオブジェクトを複数作成する」ということについて考えていきます。
NOTE
もしゲームプログラミングに興味があるのであれば PyGame パッケージを学習してみてください。
また、今まで RTS をプレイしたことがない方のために付け加えておくと、この種類のゲームの醍醐味の1つは、それぞれ異なる長所/短所、強味/弱点を持つ多種類のユニットが登場することです。そして、これらのユニットの組み合わせ方/使い方次第であなたが取るべき戦略が変化します。それぞれの短所・弱点を打ち消し合えるようなユニット構成を取れれば、戦略はより上手く機能するようになります。
ここでもう1種類 Barracks で生産される異なるユニットを定義したいと思います。私は Archer (弓兵) にしようと思いますが、独自の強味/弱味をもったご自分の好きなユニットで構いません。ご自分の「ドリームキャスト」を構成しましょう。また、ユニットが持っていたらよりゲームの戦略性が深まるであろう属性も自由に考えてみてください。そしてこの章を終えたら、その特徴を活かすコードを是非追加してみてください。そうすることでこの RTS がより楽しいものになるだけではなく、この章を通して取り上げる話題についての理解が一段と深まるはずです。
Archer クラスを追加したコードは以下の通りです:
rts_simple.py
class Knight:
def __init__(
self,
life,
speed,
attack_power,
attack_range,
weapon
):
self.unit_type = 'Knight'
self.life = life
self.speed = speed
self.attack_power = attack_power
self.attack_range = attack_range
self.weapon = weapon

def __str__(self):
return (f'Life: {self.life}\n'
f'Speed: {self.speed}\n'
f'Attack Power: {self.attack_power}\n'
f'Attack Range: {self.attack_range}\n'
f'Weapon : {self.weapon}')


class Archer:
def __init__(
self,
life,
speed,
attack_power,
attack_range,
weapon
):
self.unit_type = 'Archer'
self.life = life
self.speed = speed
self.attack_power = attack_power
self.attack_range = attack_range
self.weapon = weapon

def __str__(self):
return (f'Type: {self.unit_type}\n'
f'Life: {self.life}\n'
f'Speed: {self.speed}\n'
f'Attack Power: {self.attack_power}\n'
f'Attack Range: {self.attack_range}\n'
f'Weapon : {self.weapon}')


class Barracks:
def generate_knight(self):
return Knight(400, 5, 3, 1, 'short sword')

def generate_archer(self):
return Archer(200, 7, 1, 5, 'short bow')

if __name__ == '__main__':
barracks = Barracks()
knight1 = barracks.generate_knight()
archer1 = barracks.generate_archer()
print(f'[knight 1]\n{knight1}')
print(f'[archer 1]\n{archer1}')
実行結果は以下の通りです。これで Knight ユニットと Archer ユニットを1つずつ生産しました。それぞれの属性値もなかなか興味深いでしょ?
[knight 1]
Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon : short sword
[archer 1]
Type: Archer
Life: 200
Speed: 7
Attack Power: 1
Attack Range: 5
Weapon : short bow
現時点でのユニットは2種類だけですが、ほかにどのようなユニットを追加したいか、というプランは既に頭の中にあるはずです。そうであれば、ある建物で生成する予定であるユニット1つ1つに対してそれぞれ別個の関数を用意する方法はあまり賢いものではない、というのは明白でしょう。この点をもう少し深く考えてみます。例えば、Barracks で生産できるユニットをアップグレードしたい場合どうなるか、を想像してみてください。もし Archer ユニットが携帯する武器を short bow から long bow に格上げするとしたら、そしてそれと同時に、攻撃可能範囲 (attack range) は 5 から 10 に、攻撃力 (attack power) は 1 から 3 にそれぞれアップするとしたら。その顛末は、Barracks クラスで実装するメソッドの数がいきなり倍になる、ということです。加えて、この Barracks ではどういったレベルのどんなユニットを生産可能なのか、といったような「記録」も保持しておく必要が出てきます。
さあ、頭の中で「警報音」が鳴っていますね!
きっともっと良い実装方法があるはずです。1つの方法は、生産したいユニットのタイプだけではなく同時にレベルも考慮することでしょう。製造施設では generate_knight() や generate_archer() といったユニットタイプ毎に依頼を受けるのではなく、例えば、build_unit() といった総合窓口を設け、そこに生産したいユニット名と同時にレベルも指定するようにします。製造施設ではこの注文を元に「生産ライン」を振り分けます。そして個別の「生産ライン」でも、指定されたレベルごとに異なる属性値を持つユニットを生産できるようにします。
このような考え方で変更を加えた「ユニット生産」コードは次のようになるでしょう:
rts_multi_unit.py
class Knight:
def __init__(self, level):
self.unit_type = 'Knight'
if level == 1:
self.life = 400
self.speed = 5
self.attack_power = 3
self.attack_range = 1
self.weapon = 'short sword'
elif level == 2:
self.life = 400
self.speed = 5
self.attack_power = 6
self.attack_range = 2
self.weapon = 'long sword'

def __str__(self):
return (f'Type: {self.unit_type}\n'
f'Life: {self.life}\n'
f'Speed: {self.speed}\n'
f'Attack Power: {self.attack_power}\n'
f'Attack Range: {self.attack_range}\n'
f'Weapon : {self.weapon}')


class Archer:
def __init__(self, level):
self.unit_type = 'Archer'
if level == 1:
self.life = 200
self.speed = 7
self.attack_power = 1
self.attack_range = 5
self.weapon = 'short sword'
elif level == 2:
self.life = 200
self.speed = 7
self.attack_power = 3
self.attack_range = 10
self.weapon = 'long sword'

def __str__(self):
return (f'Type: {self.unit_type}\n'
f'Life: {self.life}\n'
f'Speed: {self.speed}\n'
f'Attack Power: {self.attack_power}\n'
f'Attack Range: {self.attack_range}\n'
f'Weapon : {self.weapon}')


class Barracks:
def build_unit(self, unit_type, level):
if unit_type == 'knight':
return Knight(level)
elif unit_type == 'archer':
return Archer(level)


if __name__ == '__main__':
barracks = Barracks()
knight1 = barracks.build_unit('knight', 1)
archer1 = barracks.build_unit('archer', 2)
print(f'[knight 1]\n{knight1}')
print(f'[archer 1]\n{archer1}')
実行結果は次の通りです。レベル1の Knight ユニットが1つと、レベル2の Archer ユニットが1つ、それぞれ設定どおりの属性値で作成されているのが分かります。この際 Barracks クラスでは、作成すべきユニットの種類、レベル、属性値などの詳細を保持しておく必要がなくなっています:
[knight 1]
Type: Knight
Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon : short sword
[archer 1]
Type: Archer
Life: 200
Speed: 7
Attack Power: 3
Attack Range: 10
Weapon : long sword
前の実装よりは今回の方が望ましいでしょう。メソッドの数を減らしつつ、レベルの概念も取り入れられています。レベル自体に関しても、Barracks クラスが保持するよりは該当するユニットクラスに定義するのが理に適っています。
現代の RTS ゲームにおける非常に大きな要素は「バランス」です。そしてこの「バランス」を如何に取るか、でゲームデザイナーは頭を悩ませます。「ゲームバランス」の背後にある考え方は、プレイヤーが時にあるユニットの特性を利用することで、他のすべての戦略や状況を圧倒する方法を見つけてしまうことがある、というものです。「これこそが探し求めているものじゃないか」という方もいるかもしれませんが、このような状況の出現は間違いなくプレイヤーからゲームへの興味を奪ってしまうものなのです。また正反対の状況としては、ゲーム上全く役に立たない特性を備えてしまったキャラクターが存在する、というものもあります。どちらの場合にしろ、該当するユニット、大きく言ってしまえばゲームそのものが「バランスを欠いている (to be imbalanced)」ということになります。このようなバランス上の欠陥を補正するために、攻撃力などといったユニットの属性値を変更したい、と考えるデザイナーもいるでしょう。
デザイナーの意向を受けた開発者は、何千、何万行にも及ぶコードの中から該当ユニットクラスの該当パラメータが定義されている部分を探し出し値を変更します。もしかするとこのようなことが開発中に何百、何千回と発生するかもしれません。
Eve Online のような、ゲームロジックが Python ベースで実装されている超大規模なゲームであったら、こういった作業がどのような苦痛を伴うものか、想像するのはそれほど難しくはないはずです。
しかし、ユニットタイプかつレベル毎のパラメータ値がそれぞれ異なる JSON ファイル、もしくは、データベースのレコードに保存されていれば、値の変更はかなり容易になります。また、デザイナー自身に値の管理をしてもらうために GUI (Graphical User Interface) を提供することも可能でしょう。
このようなシステム構成を採用してユニットを「インスタンス化」する場合、該当するユニット and レベルの属性値を対応するファイル/DBエントリーから取り出し、それを利用することになります。例えば、各ファイルの内容は次のようになるかもしれません:
knight_1.dat (Knight ユニット、レベル 1 用)
400
5
3
1
short sword
archer_2.dat (Archer ユニット、レベル 2 用)
200
7
3
10
long bow
これを利用するユニット生産コードは次のようになるでしょう:
rts_file_based.py
from collections import namedtuple

UnitParam = namedtuple('UnitParam', ['life',
'speed',
'attack_power',
'attack_range',
'weapon'])


class Knight:
def __init__(self, level):
self.unit_type = 'Knight'

filename = f'{self.unit_type.lower()}_{level}.dat'
with open(filename, 'r', encoding='utf-8') as param_file:
self.unit_params = UnitParam._make([p.strip() for p in param_file])

def __str__(self):
return (f'Type: {self.unit_type}\n'
f'Life: {self.unit_params.life}\n'
f'Speed: {self.unit_params.speed}\n'
f'Attack Power: {self.unit_params.attack_power}\n'
f'Attack Range: {self.unit_params.attack_range}\n'
f'Weapon : {self.unit_params.weapon}')


class Archer:
def __init__(self, level):
self.unit_type = 'Archer'

filename = f'{self.unit_type.lower()}_{level}.dat'
with open(filename, 'r', encoding='utf-8') as param_file:
self.unit_params = UnitParam._make([p.strip() for p in param_file])

def __str__(self):
return (f'Type: {self.unit_type}\n'
f'Life: {self.unit_params.life}\n'
f'Speed: {self.unit_params.speed}\n'
f'Attack Power: {self.unit_params.attack_power}\n'
f'Attack Range: {self.unit_params.attack_range}\n'
f'Weapon : {self.unit_params.weapon}')


class Barracks:
def build_unit(self, unit_type, level):
if unit_type == 'knight':
return Knight(level)
elif unit_type == 'archer':
return Archer(level)


if __name__ == '__main__':
barracks = Barracks()
knight1 = barracks.build_unit('knight', 1)
archer1 = barracks.build_unit('archer', 2)
print(f'[Knight: level 1]\n{knight1}')
print(f'[Archer: level 2]\n{archer1}')
ファイル内の各行はユニットのそれぞれの属性と一致するようにあらかじめ順番を決めてありますから、ここでは namedtuple を利用することで非常に簡単に Python のオブジェクトとして取り込めるようになっています (ファイル名を決め、ファイルを開き、データを namedtuple にセットする、というたった3行のコードで実現しています)。
コードの実行結果に変化はありませんが、それぞれのユニット、それぞれのレベルの属性値をコード内からファイルへと移行したことで、属性値変更の必要性が頻繁に生じる場合でも容易に対応可能となり、ゲームのバランスを試行する環境は改善していると、いえるでしょう。
実行結果は以下のようになっているはずです:
[Knight: level 1]
Type: Knight
Life: 400
Speed: 5
Attack Power: 3
Attack Range: 1
Weapon : short sword
[Archer: level 2]
Type: Archer
Life: 200
Speed: 7
Attack Power: 3
Attack Range: 10
Weapon : long bow
更にゲームを拡張していきます。
ゲームを進めていく中で、プレイヤーが、同じ製造施設でもレベルの異なるものを建設できるようにしましょう。そして、その建物のレベルによって、製造可能なユニットのタイプとレベルが変化していくようにしたいと思います。
例えば、レベル1の Barracks ではレベル1の Knight しか生成できませんが、レベル2の Barracks ではレベル2の Knight に加えて Archer も生産できるようになります。
ある製造施設のレベルアップはその建物だけに適用され、プレイヤーが建設した他の同タイプの施設には適用されません。すなわち、今までのように Barracks ではこのユニットとあのユニットが生産可能、と割り切ることができず、建物ごとにどういった種類のユニットを生産可能なのか、といった情報を押さえておく必要がある、ということになります。
ユニットを製造しようとするたびに、その建物ではどの種類のユニットを生産可能なのか、を確認し、対象とするユニットクラスへ発注します。ユニットクラスではストレージシステムを検索して属性値情報を読み取り、それを基にオブジェクトを作成する、という流れになります。んー、あまり効率的とは言えません。
もしある製造施設で同じタイプのユニットを500体生成する必要があるとしたら、ストレージシステムに対して全く同じリクエストを499回繰り返すことになります。これをすべての製造施設で行うとしたら、同じ手間が製造施設の数だけかかり、なおかつ、各製造施設での「私のところではどのユニットが生産可能ですか」検索の手間もかかるわけです。
超巨大な Eve Online やその他の最新の RTS ゲームにおいて、もしユニットや建物を製造する度にこのようなプロセスを経る必要があるとしたら、あなたのゲーム環境はあっという間にダウンしてしまうでしょう。ゲームの中には、製造施設に特別な機能を追加することで、生産ユニットに対して一般的なユニットとは異なる「特技」を持たせることを可能にしているものもあります。こうした場合はより一層ゲーム機のリソース消費が厳しくなります。
ここまで私たちが見てきたのは、1,2点のほんのちょっとした違いしかないほぼ同じオブジェクトを大量に作成する、という現実でも十分に起こり得る問題でした。これに対して、1つ1つのオブジェクトをそれぞれ1から作り上げていく、という方法は、柔軟な解決策とはとても言えないものでした。
では我々の取るべき途は...

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)