アヌティストではない堎合のゲヌムの䜜成方法







すべおのプログラマヌが面癜いゲヌムを䜜りたいず思った瞬間がありたした。 倚くのプログラマヌはこれらの倢を実珟し、さらには成功したすが、これは圌らに関するものではありたせん。 それは、ゲヌムをプレむするのが奜きな人、知識や経隓がなくおも䞀床ゲヌムを䜜成しようずしお、䞖界的な名声および倧きな利益を達成した孀独なヒヌロヌの䟋に觊発されたが、圌は䜙裕がないグルむグロストロダず競いたす。



しないでください...



小さな玹介



すぐに予玄したす。私たちの目暙はお金を皌ぐこずではありたせん-このトピックに関する倚くの蚘事がHabréにありたす。 いいえ、倢のゲヌムを䜜りたす。



倢のゲヌムに぀いおの叙情的な䜙談
独身の開発者や小さなスタゞオからこの蚀葉を䜕回聞いたこずがありたす。 どこにいおも、初心者のむグロデロフは圌らの倢ず「完璧なビゞョン」を急いで䞖界に公開し、圌らの英雄的な努力、仕事のプロセス、避けられない経枈的困難、出版瀟ず䞀般的な「プレヌダヌ-恩恵のない犬-むム-グラフずコむンずすべおを無料で支払い、ゲヌムをしないでください-海賊-そしお、私たちは-圌らのおかげで倱われた利益-それだけです。



人々、だたされおはいけたせん。 あなたは倢のゲヌムを䜜っおいるのではなく、よく売れるゲヌムを䜜っおいたす-これらは2぀の異なるものです。 プレむダヌ特に掗緎されたプレむダヌはあなたの倢を気にしたせん。 利益が必芁な堎合、トレンドを研究し、珟圚人気のあるものを確認し、ナニヌクなこずをし、他の人よりも優れた、より珍しいこずを行い、蚘事を読み倚くありたす、出版瀟ずコミュニケヌションを取りたす-䞀般に、あなたではなく゚ンドナヌザヌの倢を実珟したす。



ただ逃げおいないのに倢のゲヌムを実珟したい堎合は、事前に利益を攟棄しおください。 倢を売るのではなく、無料で共有したしょう。 人々にあなたの倢を䞎え、圌らにそれをもたらし、あなたの倢が䜕か䟡倀があるなら、あなたはお金ではなくおも、愛ず認識を受け取りたす。 これは時にははるかに䟡倀がありたす。



倚くの人は、ゲヌムは時間ず劎力の無駄であり、真面目な人はこのトピックに぀いおたったく話すべきではないず考えおいたす。 しかし、ここに集たった人々は深刻ではないので、私たちは郚分的に同意したす-あなたがそれらをプレむする堎合、ゲヌムは本圓に倚くの時間がかかりたす。 ただし、ゲヌムの開発には䜕倍も時間がかかりたすが、倚くのメリットがありたす。 たずえば、ゲヌム以倖のアプリケヌションの開発には芋られない原則、アプロヌチ、アルゎリズムに慣れるこずができたす。 たたは、ツヌルたずえば、プログラミング蚀語を所有するスキルを深め、異垞で刺激的なこずを行いたす。 自分でゲヌム開発成功しなくおもは垞に特別で比類のない䜓隓であるず付け加えたすそしお倚くの人が同意したす。それを恐怖ず愛で思い出したす。



私たちは、最新のゲヌム゚ンゞン、フレヌムワヌク、ラむブラリを䜿甚したせん。ゲヌムプレむの本質を芋お、内郚から感じたす。 柔軟な開発方法論は攟棄したす1人の䜜業を敎理する必芁があるため、タスクは簡玠化されたす。 サりンドのデザむナヌ、アヌティスト、䜜曲家、スペシャリストを探すために時間ず゚ネルギヌを費やすこずはありたせん-私たちはできる限りのこずをすべおしたす完成したフレヌム䞊のグラフィック。 最終的には、ツヌルを実際に研究しお適切なツヌルを遞択するこずすらしたせん。䜿甚方法がわかっおいるもので実行したす。 たずえば、Javaの堎合、埌で必芁に応じおAndroidたたはコヌヒヌメヌカヌに転送したす。



「ああ!!! ホラヌ 悪倢 どうしおそんなナンセンスに時間を費やすこずができたすか ここから出お、もっず面癜いものを読んでいきたしょう」



これはなぜですか ぀たり、車茪を再発明したすか 既補のゲヌム゚ンゞンを䜿甚しおみたせんか 答えは簡単です。圌に぀いおは䜕も知りたせんが、今はゲヌムが欲しいです。 平均的なプログラマヌの考え方を想像しおください。「私はゲヌムを䜜りたい 肉、爆発、ポンプがあり、 コロバニヌを奪うこずができ 、プロットは爆撃され、これは他のどこでも起こったこずはありたせん 私は今すぐ曞き始めたす 今、私たちに人気のあるものを芋おみたしょう...ええ、X、Y、Z。Zを取り、誰もが今曞いおいたす...」。 そしお、゚ンゞンの研究を開始したす。 そしお、圌はそのアむデアを投げたす。なぜなら、それにはすでに十分な時間がないからです。 フィン。 たたは、倧䞈倫、それはあきらめたせんが、実際に゚ンゞンを孊習するこずなく、ゲヌムに取り入れられたす。 たあ、その埌、圌は誰にも圌の最初の「工芞品」を芋せない良心を持っおいたす。 通垞はそうではありたせんアプリケヌションストアに行っお、自分で確認しおください-たあ、たあ、私は利益を望み、耐える力はありたせん。 か぀おゲヌムの䜜成は熱狂的な創造的な人々でした。 悲しいかな、この時間は取り返しの぀かないほど過ぎたした-ゲヌムの䞻なものは魂ではなく、ビゞネスモデルです少なくずもそれに぀いおの䌚話はもっずたくさんありたす。 私たちの目暙はシンプルです。魂でゲヌムを䜜りたす。 したがっお、ツヌルから抜象化し誰でも実行できたす、タスクに集䞭したす。



それでは、続けたしょう。

私自身の苊い経隓の詳现には觊れたせんが、ゲヌム開発におけるプログラマヌの䞻な問題の1぀はグラフィックスだず蚀いたす。 プログラマヌは通垞、描画の方法を知らず䟋倖はありたす、アヌティストは通垞​​、プログラミングの方法を知りたせん䟋倖はありたす。 そしお、グラフィックがなければ、あなたは認めなければなりたせん、たれなゲヌムはバむパスされたす。 どうする



オプションがありたす



1.シンプルなグラフィカル゚ディタですべおを自分で描く


2003幎のゲヌム「Kill​​ Him All」のスクリヌンショット



2.すべおを自分でベクトルに描く


ゲヌム「Raven」、2001幎のスクリヌンショット





ゲヌム「Inferno」、2002幎のスクリヌンショット



3.描き方もわからない兄匟に尋ねるしかし、少し良くする


ゲヌム「Fucking」、2004幎のスクリヌンショット



4. 3Dモデリング甚のプログラムをダりンロヌドしお、そこからアセットを取埗したす


2006幎のゲヌム「Fucking 2. Demo」のスクリヌンショット



5.絶望的に、頭の毛を匕き裂く




ゲヌム「Fucking」、2004幎のスクリヌンショット



6.自分ですべおを擬䌌グラフィックスASCIIで描画したす


ゲヌム「Fifa」のスクリヌンショット、2000





1998幎のゲヌム「盞撲」のスクリヌンショット



埌者に぀いお詳しく芋おいきたしょう䞀郚は他の人ほど気のめいるように芋えないからです。 経隓の浅いゲヌマヌの倚くは、クヌルでモダンなグラフィックのないゲヌムはプレむダヌの心を぀かむこずができないず考えおいたす。ゲヌムの名前でさえゲヌムに倉えるこずすらできたせん。 ADOM 、 NetHack 、 Dwarf Fortressなどの傑䜜の開発者は、そのような議論に暗黙的に反察しおいたす。 倖芳は垞に決定的な芁因ではありたせん。ASCIIを䜿甚するず、いく぀かの興味深い利点が埗られたす。





䞊蚘の長い玹介は、初心者のむグロデロフが恐怖や偏芋を克服し、心配をやめ、それでもそのようなこずをしようずするのを助けるこずを意図したした。 準備はいい それでは始めたしょう。



最初のステップ。 アむデア



どうやっお ただわからない



コンピュヌタヌの電源を切り、食べに行き、散歩し、運動したす。 たたは最悪の堎合、眠りたす。 ゲヌムを思い付くずいうこずは、窓を掗うこずではありたせん-プロセスの掞察は埗られたせん。 通垞、ゲヌムのアむデアは、たったく考えないずきに突然、予期せずに生たれたす。 これが突然起こった堎合は、鉛筆をより速く぀かみ、アむデアが消えるたで曞き留めおください。 クリ゚むティブプロセスはすべおこの方法で実装されたす。



そしお、他の人のゲヌムをコピヌできたす。 たあ、コピヌ。 もちろん、あなたがどれほど頭がいいかを隅々たで蚀っお䞍敬に戊うのではなく、あなたの補品で他の人の開発を䜿っおください。 倚くの堎合、ゲヌマヌはこれを持っおいるので、これがあなたの倢から具䜓的にどのくらい残っおいるのでしょうか圌らはいく぀かの迷惑なこずを陀いおゲヌムのすべおを奜きですが、それが異なっお行われた堎合...誰が知っおいたす誰かの良いアむデアを思い浮かべるのはあなたの倢でしょう。



しかし、私たちは簡単な方法で進みたす-私たちはすでにアむデアを持っおいるず仮定し、私たちは長い間それに぀いお考えおいたせんでした。 最初の壮倧なプロゞェクトずしお、Obsidian- Pathfinder Adventuresの優れたゲヌムのクロヌンを䜜成したす。



「これは䞀䜓䜕だ テヌブルはありたすか」



圌らが蚀うように、 プヌルコむパス 私たちはすでに偏芋を攟棄しおいるように芋えるので、私たちは倧胆にアむデアを掗緎し始めたす。 圓然、ゲヌムを1察1でクロヌンするこずはありたせんが、基本的なメカニズムは借甚したす。 さらに、タヌンベヌスの協調型ボヌドゲヌムの実装には次の利点がありたす。





ルヌルに慣れおいない人のために、簡単な玹介
Pathfinder Adventuresは、ボヌドロヌルプレむングゲヌムたたはロヌルプレむングシステム党䜓Pathfinderに基づいお䜜成されたボヌドカヌドゲヌムのデゞタルバヌゞョンです。 プレむダヌ1〜6人は自分でキャラクタヌを遞択し、圌ず䞀緒にいく぀かのシナリオに分かれお冒険に出たす。 各キャラクタヌは、さたざたなタむプのカヌド歊噚、鎧、呪文、味方、アむテムなどを自由に䜿甚できたす。各シナリオで、圌はその助けを借りお、悪党-特別な特性を持぀特別なカヌドを芋぀けお酷く眰しなければなりたせん。



各シナリオは、プレむダヌが蚪問しお探玢する必芁がある堎所たたは堎所の数それらの数はプレむダヌの数によっお異なりたすを提䟛したす。 各堎所には裏向きになっおいるカヌドのデッキが含たれおおり、キャラクタヌは順番に探玢したす。぀たり、トップカヌドを開き、関連するルヌルに埓っおそれを克服しようずしたす。 プレむダヌのデッキに補充する無害なカヌドに加えお、これらのデッキには邪悪な敵や障害物も含たれおいたす。さらに前進するには、それらを倒す必芁がありたす。 Scoundrelカヌドもデッキの1぀にありたすが、プレむダヌはどのカヌドかを知りたせん-それを芋぀ける必芁がありたす。



カヌドを打ち負かすおよび新しいカヌドを獲埗するために、キャラクタヌは、サむズが察応する特性の倀d4からd12によっお決定されるダむスを投げお、特性RPGの匷さ、噚甚さ、知恵などの暙準のテストに合栌する必芁がありたすルヌルずキャラクタヌ開発のレベル、そしお手からの適切なカヌドの効果を高めるためにプレむしたす。 勝利するず、出䌚ったカヌドはゲヌムから取り陀かれるか敵の堎合、プレむダヌの手札を補充しアむテムの堎合、移動は別のプレむダヌに移動したす。 負けるず、キャラクタヌはしばしばダメヌゞを受け、手からカヌドを捚おさせたす。 興味深いメカニズムは、デッキのカヌドの数によっおキャラクタヌの䜓力が決たるずいうこずです。プレむダヌがデッキからカヌドを匕く必芁があるずすぐに、キャラクタヌがそこにいなければ、キャラクタヌは死にたす。



目暙は、ロケヌションマップを通り抜けお、Scoundrelを芋぀けお倒し、退华ぞの道を以前ブロックしおいたこずですこれに぀いおは、ルヌルを読むこずでさらに孊ぶこずができたす。 ただし、これをしばらく行う必芁があり、これがゲヌムの䞻な難点です。 手の数は厳密に制限されおおり、利甚可胜なすべおのカヌドの単玔な列挙では目暙に達したせん。 さたざたなトリックずスマヌトなテクニックを適甚する必芁があるからです。



シナリオが満たされるず、キャラクタヌは成長しお発達し、キャラクタヌの特性が向䞊し、新しい有甚なスキルを獲埗したす。 デッキの管理もゲヌムの非垞に重芁な芁玠です。シナリオの結果特に埌の段階は、通垞、適切なカヌドに䟝存したすそしお、幞運に倧きく䟝存したすが、サむコロを䜿ったゲヌムには䜕が必芁ですか。


䞀般的に、ゲヌムは興味深く、䟡倀があり、泚目に倀したす。そしお、私たちにずっお重芁なこずは、クロヌンを実装するこずを面癜くするために非垞に耇雑です「難しい」ずは「難しい」ずいう意味ではありたせん。



このケヌスでは、1぀のグロヌバルな抂念倉曎を行いたす-カヌドを攟棄したす。 むしろ、私たちはたったく拒吊したせんが、サむズず色が異なる立方䜓にカヌドを眮き換えたす技術的には、正しい六角圢以倖の圢状があるため、「立方䜓」を䜿甚するこずはたったく正しくありたせんが、「骚」ず呌ぶのは珍しいですそれは䞍快ですが、アメリカのデむゞヌを䜿甚するこずは味のしるしの兆候なので、そのたたにしおおきたしょう。 今、デッキの代わりに、プレむダヌはバッグを持っおいたす。 たた、堎所にはバッグがあり、そこから研究プロセスのプレむダヌが任意のキュヌブを匕き出したす。 キュヌブの色によっお、キュヌブのタむプが決たり、それに応じお、テストに合栌するためのルヌルが決たりたす。 その結果、キャラクタヌの個人的な特性匷さ、噚甚さなどはなくなりたすが、新しい興味深いメカニズムが衚瀺されたす詳现は埌ほど。



遊ぶのは楜しいですか 私には芋圓が぀かず、実際のプロトタむプが完成するたで誰もこれを理解できたせん。 しかし、ゲヌムを楜しむのではなく、開発を楜しみたすか したがっお、成功の疑いはないはずです。



ステップ2 蚭蚈



アむデアを持぀こずは、物語の3分の1にすぎたせん。 今、この考えを発展させるこずが重芁です。 ぀たり、公園を散歩したり、スチヌムバスを济びたりするのではなく、テヌブルに座っおペンで玙を取りたたはお気に入りのテキスト゚ディタヌを開き、デザむンドキュメントを慎重に䜜成し、ゲヌムメカニクスのあらゆる偎面を入念に䜜成したす。 これには時間がかかりたすので、䞀気に文章を完成させるこずを期埅しないでください。 そしお、すべおを䞀床にすべお怜蚎するこずさえ望んでいたせん-実装するに぀れお、倚くの倉曎ず倉曎を行う必芁があるこずがわかりたすそしお、時にはグロヌバルに䜕かをやり盎したすが、開発プロセスが始たる前に䜕らかの基瀎が存圚しなければなりたせん。



最初は、蚭蚈ドキュメントは次のようになりたす








そしお、壮倧なアむデアの最初の波に察凊した埌に初めお、頭を取り、ドキュメントの構造を決定し、それをコンテンツで敎然ず満たし始めたす䞍芁な繰り返しや特に矛盟を避けるために、すでに曞かれた内容で毎秒をチェックしたす。 次第に、次のように意味のある簡朔なものが埗られたす。



蚭蚈を説明するずきは、特に䞀人で䜜業する堎合は、考えを衚珟しやすい蚀語を遞択しおください。 プロゞェクトにサヌドパヌティの開発者を参加させる必芁がある堎合は、あなたの頭の䞭で起こっおいるすべおの創造的なナンセンスを圌らが理解しおいるこずを確認しおください。



続行するには、匕甚されたドキュメントを少なくずも斜めに読むこずを匷くお勧めしたす。将来的には、その解釈に぀いお詳しく説明するこずなく、そこに提瀺されおいる甚語ず抂念を参照するからです。



「著者、壁に向かっお自殺しおください。 文字が倚すぎたす。」



ステップ3 モデリング



぀たり、すべお同じデザむンで、より詳现なものです。

倚くの人がIDEを開いおコヌディングを始めたいず思っおいたすが、もう少し忍耐匷く埅っおください。 アむデアが頭を圧倒するず、キヌボヌドに觊れるだけで手が空の高さの距離に突進するように思えたす-ストヌブでコヌヒヌが沞隰する前に、アプリケヌションの䜜業バヌゞョンはゎミ箱に行く準備ができおいたす... 同じこずを䜕床も曞き盎さないように特に、3時間の開発埌にレむアりトが機胜せず、新たに開始する必芁があるこずを確認しないように、最初にアプリケヌションの䞻芁構造を怜蚎および文曞化するこずをお勧めしたす。



開発者ずしお、オブゞェクト指向プログラミングOOPに粟通しおいるので、プロゞェクトでその原則を䜿甚したす。 しかし、OOPの堎合、退屈なUMLダむアグラムの束から開発を開始するこずほど期埅されおいたせん。  UMLが䜕なのかわかりたせんか私も忘れそうになりたしたが、喜んで思い出したす-私が勀勉なプログラマヌであるこずを瀺すためです。



ナヌスケヌス図から始めたしょう。 ナヌザヌプレヌダヌが将来のシステムずやり取りする方法を説明したす。



ナヌスケヌス




「ええず...それは䜕のこずですか」



冗談です、冗談です...そしお、おそらく、私はそれに぀いお冗談を蚀うのをやめたす-これは深刻な問題です結局、倢です。 ナヌスケヌスの図では、システムがナヌザヌに提䟛する可胜性を衚瀺する必芁がありたす。 詳现に。 しかし、この特定のタむプのダむアグラムが私にずっお最悪であるこずが刀明したのは歎史的に起こりたした-明らかに十分な忍耐がありたせん。 そしお、あなたはそのように私を芋る必芁はありたせん-私たちは倧孊で卒業蚌曞を保護しおいたせんが、私たちは仕事のプロセスを楜しんでいたす。 このプロセスでは、ナヌスケヌスはそれほど重芁ではありたせん。 アプリケヌションを独立したモゞュヌルに正しく分割するこず、぀たり、ビゞュアルむンタヌフェむスの機胜がゲヌムメカニクスに圱響を䞎えないようにゲヌムを実装し、必芁に応じおグラフィックコンポヌネントを簡単に倉曎できるようにするこずがはるかに重芁です。



この点は、次のコンポヌネント図で詳しく説明できたす。



システムコンポヌネント




ここで、アプリケヌションの䞀郚である特定のサブシステムをすでに特定したした。埌で瀺すように、それらはすべお互いに独立しお開発されたす。



たた、同じ段階で、ゲヌムのメむンサむクルがどのように芋えるかを掚定したすたたは、最も興味深い郚分は、スクリプト内のキャラクタヌを実装する郚分です。 これには、アクティビティ図が適しおいたす。



立ったら座っお




そしお最埌に、入出力システムを介した゚ンドナヌザヌずゲヌム゚ンゞンずの盞互䜜甚のシヌケンスを䞀般的な甚語で瀺すずよいでしょう。



゜ヌセヌゞ




倜は長く、倜明けのずっず前です。 テヌブルに座った埌、他の20個の図を萜ち着いお描きたす。将来的には、それらが存圚するこずで、遞択した道を歩み、自尊心を高め、郚屋のむンテリアを曎新し、色あせた壁玙で色あせた壁玙を吊るし、簡単にあなたのビゞョンを䌝えるこずができたすすぐにあなたの新しいスタゞオのドアに倧急ぎで駆け぀ける仲間の開発者私たちは成功を目指しおいたせん、芚えおいたすか



これたでのずころ、誰もが愛するクラス図クラスを匕甚する぀もりはありたせん-倚くのクラスが突砎するこずが期埅されおおり、最初は3぀の画面の画像は远加されたせん。 適切なサブシステムの開発に進むに぀れお、断片に分割しお埐々に配眮する方が適切です。



ステップ4 ツヌル遞択



すでに合意したように、さたざたなオペレヌティングシステムを実行するデスクトップずモバむルデバむスの䞡方で実行されるクロスプラットフォヌムアプリケヌションを開発したす。プログラミング蚀語ずしおJavaを遞択したす。埌者はより新しく新鮮なため、Kotlinはさらに優れおおり、前任者を圧倒するinりの波で泳ぐ時間はただありたせん同時に、誰かがそれを所有しおいない堎合はトレヌニングしたす。JVMはご存じのずおり、どこでも利甚できたす30億台のデバむスで、WindowsずUNIXの䞡方をサポヌトし、SSH接続を介したリモヌトサヌバヌ䞊でも再生できたす必芁な堎合は䞍明ですが、そのような機䌚を提䟛したす。たた、金持ちになっおアヌティストを雇うずきにAndroidに転送したすが、それに぀いおは埌で詳しく説明したす。



ラむブラリラむブラリなしではどこにもアクセスできたせんクロスプラットフォヌムの芁件に応じお遞択したす。Mavenをビルドシステムずしお䜿甚したす。たたはGradle。それずも、Maven、それから始めたしょう。すぐに、バヌゞョン管理システム奜きなものをセットアップするこずをお勧めしたす。そうするこずで、䜕幎も経った埌、それがか぀おどれほど玠晎らしかったかを懐かしい気持ちで思い出しやすくなりたす。IDEは、䜿い慣れた、お気に入りの、䟿利なものも遞択したす。



実際、他に䜕も必芁ありたせん。開発を開始できたす。



ステップ5 プロゞェクトの䜜成ず蚭定



IDEを䜿甚する堎合、プロゞェクトの䜜成は簡単です。 私たちの将来の傑䜜のためにいく぀かの邪悪な名前たずえば、 Dice を遞択する必芁があり、蚭定でMavenサポヌトを有効にするこずを忘れずに、 pom.xml



ファむルに必芁な識別子を曞き蟌みたす。



 <modelVersion>4.0.0</modelVersion> <groupId>my.company</groupId> <artifactId>dice</artifactId> <version>1.0</version> <packaging>jar</packaging>
      
      





たた、デフォルトでは存圚しないKotlinサポヌトを远加したす。



 <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> </dependency>
      
      





そしお、私たちが詳しく説明しないいく぀かの蚭定



 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <kotlin.version>1.3.20</kotlin.version> <kotlin.compiler.incremental>true</kotlin.compiler.incremental> </properties>
      
      





ハむブリッドプロゞェクトに関する少しの情報
プロゞェクトでJavaずKotlinの䞡方を䜿甚する堎合は、 src/main/kotlin



に加えお、 src/main/java



フォルダヌもありたす。 Kotlin開発者は、最初のフォルダヌ *.kt



の゜ヌスファむルは2番目のフォルダヌ *.java



の゜ヌスファむルよりも早くコンパむルする必芁があるず䞻匵しおいるため、暙準のMavenタヌゲットの蚭定を倉曎するこずを匷くお勧めしたす。



 <build> <plugins> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> <executions> <execution> <id>compile</id> <phase>process-sources</phase> <goals> <goal>compile</goal> </goals> <configuration> <sourceDirs> <sourceDir>${project.basedir}/src/main/kotlin</sourceDir> <sourceDir>${project.basedir}/src/main/java</sourceDir> </sourceDirs> </configuration> </execution> <execution> <id>test-compile</id> <goals> <goal>test-compile</goal> </goals> <configuration> <sourceDirs> <sourceDir>${project.basedir}/src/test/kotlin</sourceDir> <sourceDir>${project.basedir}/src/test/java</sourceDir> </sourceDirs> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <executions> <!-- Replacing default-compile --> <execution> <id>default-compile</id> <phase>none</phase> </execution> <!-- Replacing default-testCompile --> <execution> <id>default-testCompile</id> <phase>none</phase> </execution> <execution> <id>java-compile</id> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> <execution> <id>java-test-compile</id> <phase>test-compile</phase> <goals> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
      
      





これがどれほど重芁かは蚀えたせん。このシヌトがなければプロゞェクトは順調に進んでいたす。 ただし、念のため、譊告が衚瀺されたす。



䞀床に3぀のパッケヌゞを䜜成しおみたしょうなぜ䜕か詊しおみたせんか





埌者にはむンタヌフェヌスのみが含たれ、そのメ゜ッドはデヌタの入出力に䜿甚したす。 䞀般に特定の実装を別のプロゞェクトに保存したすが、それに぀いおは埌で詳しく説明したす。 それたでは、スプレヌしすぎないように、これらのクラスを䞊べお远加したす。



すぐに完璧にしようずしないでください。パッケヌゞ名、むンタヌフェヌス、クラス、メ゜ッドの詳现を熟考しおください。 オブゞェクト同士の盞互䜜甚を培底的に芏定したす-これはすべお、数十回以䞊倉化したす。 プロゞェクトが発展するに぀れお、倚くのものがugくおかさばり、あなたにずっおも効果的でないように芋えたす-珟代のIDEでのリファクタリングは非垞に安䟡な操䜜なので、気軜に倉曎しおください。



たた、 main



機胜を備えたクラスを䜜成したす。これで、すばらしい成果に備えるこずができたす。 IDE自䜓を起動に䜿甚できたすが、埌で芋るように、この方法は私たちの目的には適しおいたせん暙準のIDEコン゜ヌルは、必芁に応じおグラフィカルな結果を衚瀺できたせん。したがっお、バッチたたはUNIXシステムのシェルを䜿甚しお倖郚からの起動を構成したすファむル。 ただし、その前に、いく぀かの远加蚭定を行いたす。



mvn package



操䜜が完了した埌、コンパむルされたすべおのクラスを含むJARアヌカむブの出力を取埗したす。 たず、デフォルトでは、このアヌカむブには、プロゞェクトが機胜するために必芁な䟝存関係が含たれおいたせんこれたでのずころ、それらはありたせんが、将来的には確実に衚瀺されたす。 次に、 main



メ゜ッドを含むメむンクラスぞのパスがアヌカむブマニフェストファむルで指定されおいないため、 java -jar dice-1.0.jar



プロゞェクトを開始java -jar dice-1.0.jar



たせん。 これを修正するには、 pom.xml



远加の蚭定を远加したす。



 <build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>my.company.dice.MainKt</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build>
      
      





メむンクラスの名前に泚意しおください。 クラスの倖郚に含たれるKotlin関数 main



関数などの堎合、クラスはコンパむル䞭に䜜成されたすJVMは䜕も知らず、知りたくないため。 クラスの名前は、 Kt



远加したファむルの名前です。 ぀たり、メむンクラスにMain



ずいう名前を付けた堎合、 MainKt.class



ファむルにコンパむルされたす。 jarファむルのマニフェストで瀺す必芁があるのは、この最埌の1぀です。



これで、プロゞェクトをビルドするず、出力に2぀のjarファむルdice-1.0.jar



およびdice-1.0-jar-with-dependencies.jar



が取埗されたす。 第二に興味がありたす。 スタヌトアップスクリプトを䜜成したす。



dice.bat Windows甚



 @ECHO OFF rem Compiling call "path_to_maven\mvn.bat" -f "path_to_project\Dice\pom.xml" package if errorlevel 1 echo Project compilation failed! & pause & goto :EOF rem Running java -jar path_to_project\Dice\target\dice-1.0-jar-with-dependencies.jar pause
      
      





dice.sh UNIXの堎合



 #!/bin/sh # Compiling mvn -f "path_to_project/Dice/pom.xml" package if [[ "$?" -ne 0 ]] ; then echo 'Project compilation failed!'; exit $rc fi # Running java -jar path_to_project/Dice/target/dice-1.0-jar-with-dependencies.jar
      
      





コンパむルが倱敗するず、スクリプトの䞭断が匷制されるこずに泚意しおください。 それ以倖の堎合、最埌のハヌプではなく、前回の正垞なアセンブリから残っおいるファむルが起動されたす違いが芋぀からないこずもありたす。 倚くの堎合、開発者はmvn clean package



コマンドを䜿甚しお以前にコンパむルされたすべおのファむルを削陀したすが、この堎合、コンパむルプロセス党䜓は垞に゜ヌスコヌドが倉曎されおいなくおも最初から開始され、倚くの時間がかかりたす。 しかし、埅぀こずはできたせん-ゲヌムを䜜成する必芁がありたす。



そのため、プロゞェクトは正垞に起動したすが、これたでのずころ䜕もしたせん。 心配しないでください、すぐに修正したす。



ステップ6 䞻なオブゞェクト



埐々に、ゲヌムプレむに必芁なクラスをmodel



パッケヌゞに远加し始めたす。



クラス図




キュヌブがすべおです。最初に远加したす。 各ダむ Die



クラスのむンスタンスは、そのタむプ色ずサむズによっお特城付けられたす。 キュヌブのタむプに぀いおは、個別の列挙 Die.Type



を䜜成し、サむズを4から12の敎数でマヌクしたす。たた、 roll()



メ゜ッドを実装しroll()



。これは、キュヌブで䜿甚可胜な範囲1からサむズ倀たでから任意の均䞀に分散した数を生成したす。



クラスは、キュヌブを盞互に比范できるComparable



むンタヌフェむスを実装したす埌で耇数のキュヌブを順序付けられた行に衚瀺するずきに䟿利です。 より倧きなキュヌブはより早く配眮されたす。



 class Die(val type: Type, val size: Int) : Comparable<Die> { enum class Type { PHYSICAL, //Blue SOMATIC, //Green MENTAL, //Purple VERBAL, //Yellow DIVINE, //Cyan WOUND, //Gray ENEMY, //Red VILLAIN, //Orange OBSTACLE, //Brown ALLY //White } fun roll() = (1.. size).random() override fun toString() = "d$size" override fun compareTo(other: Die): Int { return compareValuesBy(this, other, Die::type, { -it.size }) } }
      
      





ほこりを集めないために、キュヌブはハンドバッグ Bag



クラスのコピヌに保管されたす。 バッグ内で䜕が起こっおいるかを掚枬するこずしかできないため、順序付けられたコレクションを䜿甚する意味はありたせん。 のようです。 セットセットは必芁なアむデアをうたく​​実装したすが、2぀の理由で適合したせん。 たず、それらを䜿甚するずきは、 equals()



およびhashCode()



メ゜ッドを実装する必芁がありたすが、キュヌブのタむプずサむズを比范するのは間違っおいるため、どのようにすればよいか明確ではありたせん-任意の数の同じキュヌブをセットに栌玍できたす。 第二に、キュヌブを袋から取り出すず、毎回異なる非決定的なものだけでなく、ランダムなものが埗られるこずが期埅されたす。 したがっお、順序付けられたコレクションリストを䜿甚し、新しい芁玠を远加するたびput()



メ゜ッドでたたは発行する盎前 draw()



メ゜ッドでシャッフルするこずをお勧めしたす。



examine()



メ゜ッドは、䞍確実性に飜き飜きしおいるプレヌダヌがハヌトのテヌブルのバッグの䞭身を振る堎合゜ヌトに泚意を払うず、 clear()



メ゜ッド-振られたキュヌブがバッグに戻らない堎合に適しおいたす。



 open class Bag { protected val dice = LinkedList<Die>() val size get() = dice.size fun put(vararg dice: Die) { dice.forEach(this.dice::addLast) this.dice.shuffle() } fun draw(): Die = dice.pollFirst() fun clear() = dice.clear() fun examine() = dice.sorted().toList() }
      
      





キュヌブ付きバッグに加えお、キュヌブ付きヒヌプ Pile



クラスのむンスタンスも必芁です。 1぀目ず2぀目は、コンテンツがプレヌダヌに衚瀺される点で異なりたす。したがっお、必芁に応じおヒヌプからキュヌブを削陀し、プレヌダヌは特定のむンスタンスを遞択できたす。 removeDie()



メ゜ッドを䜿甚しおこのアむデアを実装したす。



 class Pile : Bag() { fun removeDie(die: Die) = dice.remove(die) }
      
      





次に、䞻人公であるヒヌロヌに目を向けたす。 ぀たり、今埌キャラクタヌをヒヌロヌず呌びたすJavaでCharacter



ずいう名前でクラスを呌び出さない理由はありたす。 キャラクタヌにはさたざたな皮類がありたすがクラスに入れるには単語class



を䜿甚しないほうが良いです、䜜業䞭のプロトタむプでは、 Brawler ぀たり、匷さず匷さに重点を眮いたFighterずHunter 別名Ranger / Thief、に重点を眮きたす噚甚さずステルス。 䞻人公のクラスは圌の特性、スキル、キュヌブの初期セットを決定したすが、埌で芋られるように、䞻人公はクラスに厳密に結び付けられないため、個人蚭定は1か所で簡単に倉曎できたす。



デザむンドキュメントに埓っお、ヒヌロヌに必芁なプロパティを远加したしょう。名前、お気に入りのキュヌブの皮類、サむコロの制限、習埗枈みのスキル、未熟なスキル、リセット甚の手、バッグ、パむル。 コレクションプロパティの実装の機胜に泚意しおください。 文明䞖界党䜓で、オブゞェクト内に栌玍されおいるコレクションぞの倖郚アクセスゲッタヌの助けを借りおを提䟛するのは悪い圢ず考えられおいたす-悪埳プログラマヌは、クラスの知識がなくおもこれらのコレクションの内容を倉曎できたす。 これに察凊する1぀の方法は、芁玠を远加および削陀し、その番号を取埗し、むンデックスでアクセスするための個別のメ゜ッドを実装するこずです。 ゲッタヌを実装できたすが、同時にコレクション自䜓ではなく、その䞍倉のコピヌを返したす-少数の芁玠の堎合、それだけを実行するこずは特に怖くありたせん。



 data class Hero(val type: Type) { enum class Type { BRAWLER HUNTER } var name = "" var isAlive = true var favoredDieType: Die.Type = Die.Type.ALLY val hand = Hand(0) val bag: Bag = Bag() val discardPile: Pile = Pile() private val diceLimits = mutableListOf<DiceLimit>() private val skills = mutableListOf<Skill>() private val dormantSkills = mutableListOf<Skill>() fun addDiceLimit(limit: DiceLimit) = diceLimits.add(limit) fun getDiceLimits(): List<DiceLimit> = Collections.unmodifiableList(diceLimits) fun addSkill(skill: Skill) = skills.add(skill) fun getSkills(): List<Skill> = Collections.unmodifiableList(skills) fun addDormantSkill(skill: Skill) = dormantSkills.add(skill) fun getDormantSkills(): List<Skill> = Collections.unmodifiableList(dormantSkills) fun increaseDiceLimit(type: Die.Type) { diceLimits.find { it.type == type }?.let { when { it.current < it.maximal -> it.current++ else -> throw IllegalArgumentException("Already at maximum") } } ?: throw IllegalArgumentException("Incorrect type specified") } fun hideDieFromHand(die: Die) { bag.put(die) hand.removeDie(die) } fun discardDieFromHand(die: Die) { discardPile.put(die) hand.removeDie(die) } fun hasSkill(type: Skill.Type) = skills.any { it.type == type } fun improveSkill(type: Skill.Type) { dormantSkills .find { it.type == type } ?.let { skills.add(it) dormantSkills.remove(it) } skills .find { it.type == type } ?.let { when { it.level < it.maxLevel -> it.level += 1 else -> throw IllegalStateException("Skill already maxed out") } } ?: throw IllegalArgumentException("Skill not found") } }
      
      





ヒヌロヌの手圌が珟圚持っおいる立方䜓は、別のオブゞェクト Hand



クラスによっお蚘述されたす。 連合キュヌブをメむンアヌムから分離しおおくずいう蚭蚈䞊の決定は、最初に思い぀いたものの1぀でした。 最初は非垞にクヌルな機胜のように芋えたしたが、埌に膚倧な数の問題ず䞍䟿を生み出したした。 それでも、簡単な方法を探しおいるわけではないため、 dice



ずallies



リストはサヌビスにあり、远加、受信、削陀する必芁があるすべおのメ゜ッドがありたすそのうちのいく぀かは、2぀のリストのどちらにアクセスするかを巧劙に決定したす。 手からキュヌブを削陀するず、埌続のすべおのキュヌブがリストの最䞊郚に移動し、空癜を埋めたす-将来的には怜玢が倧幅に容易になりたす null



状況を凊理する必芁はありたせん。



 class Hand(var capacity: Int) { private val dice = LinkedList<Die>() private val allies = LinkedList<Die>() val dieCount get() = dice.size val allyDieCount get() = allies.size fun dieAt(index: Int) = when { (index in 0 until dieCount) -> dice[index] else -> null } fun allyDieAt(index: Int) = when { (index in 0 until allyDieCount) -> allies[index] else -> null } fun addDie(die: Die) = when { die.type == Die.Type.ALLY -> allies.addLast(die) else -> dice.addLast(die) } fun removeDie(die: Die) = when { die.type == Die.Type.ALLY -> allies.remove(die) else -> dice.remove(die) } fun findDieOfType(type: Die.Type): Die? = when (type) { Die.Type.ALLY -> if (allies.isNotEmpty()) allies.first else null else -> dice.firstOrNull { it.type == type } } fun examine(): List<Die> = (dice + allies).sorted() }
      
      





DiceLimit



クラスのオブゞェクトのコレクションは、ヒヌロヌがスクリプトの最初に持぀こずができる各タむプのキュヌブの数に制限を蚭定したす。 特別なこずはありたせん。最初に、各タむプの最倧倀ず珟圚の倀を決定したす。



 class DiceLimit(val type: Die.Type, val initial: Int, val maximal: Int, var current: Int)
      
      





しかし、スキルがあればもっず面癜いです。 それぞれを個別に実装する必芁がありたす詳现は埌ほど説明したすが、 HitずShoot クラスごずに1぀の2぀だけを怜蚎したす。 スキルは、初期レベルから最倧レベルたで開発「ポンプ」できたす。これは、サむコロのロヌルに远加される修正に圱響を䞎えるこずがよくありたす。 これは、プロパティlevel



、 maxLevel



、 modifier1



およびmodifier2



maxLevel



れたす。



 class Skill(val type: Type) { enum class Type { //Brawler HIT, //Hunter SHOOT, } var level = 1 var maxLevel = 3 var isActive = true var modifier1 = 0 var modifier2 = 0 }
      
      





Hero



クラスの補助メ゜ッドに泚意しおください。これにより、手からダむスを隠したりロヌルしたり、ヒヌロヌに特定のスキルがあるかどうかを確認したり、孊習したスキルのレベルを䞊げたり、新しいスキルを習埗したりできたす。 それらはすべお遅かれ早かれ必芁になりたすが、今はそれらに぀いお詳しくは説明したせん。



䜜成しなければならないクラスの数を恐れないでください。 この耇雑なプロゞェクトでは、数癟が䞀般的なものです。 ここでは、あらゆる深刻な職業ず同様に、私たちは小さなこずから始め、埐々にペヌスを䞊げおいきたす。1か月で、私たちはその範囲に恐ろしくなりたす。 忘れないでください、私たちはただ䞀人の小さなスタゞオです-私たちは圧倒的な仕事に盎面しおいたせん。



「䜕かがうんざりした。 私はタバコを吞うか、䜕か...」



そしお、私たちは続けたす。

䞻人公ずその胜力が説明されおいたすが、今床は敵の力-倧きくお恐ろしいゲヌムメカニクスに行きたしょう。 むしろ、ヒヌロヌが察話しなければならないオブゞェクト。



別のクラス図




私たちの勇敢な䞻人公は、3皮類のキュヌブずカヌドに盎面したす悪圹 Villain



クラス、敵 Enemy



クラス、障害 Obstacle



クラス、「脅嚁」ずいう䞀般甚語の䞋で団結したす限定。 各脅嚁には、そのような脅嚁に盎面したずきの行動の特別なルヌルを蚘述し、ゲヌムプレむに倚様性を远加する䞀連の特城的な機胜 Trait



がありたす。



 sealed class Threat { var name: String = "" var description: String = "" private val traits = mutableListOf<Trait>() fun addTrait(trait: Trait) = traits.add(trait) fun getTraits(): List<Trait> = traits } class Obstacle(val tier: Int, vararg val dieTypes: Die.Type) : Threat() class Villain : Threat() class Enemy : Threat() enum class Trait { MODIFIER_PLUS_ONE, //Add +1 modifier MODIFIER_PLUS_TWO, //Add +2 modifier }
      
      





Trait



クラスのオブゞェクトのリストは可倉 MutableList



ずしお定矩されおいたすが、䞍倉のList



むンタヌフェヌスずしお提䟛されおいるこずに泚意しおください。 これはKotlinで機胜したすが、結果のリストが倉曎可胜なむンタヌフェむスに倉換されたり、さたざたな倉曎を加えたりするこずを劚げるものがないため、このアプロヌチは安党ではありたせん-Javaコヌドからクラスにアクセスする堎合 List



むンタヌフェむスが倉曎可胜な堎合、これは特に簡単です。 コレクションを保護する最も劄想的な方法は、次のようなこずです。



 fun getTraits(): List<Trait> = Collections.unmodifiableList(traits)
      
      





しかし、私たちは問題に近づいおいくのにそれほど慎重ではありたせんただし、譊告されおいたす。



ゲヌムメカニクスの特性により、 Obstacle



クラスは远加フィヌルドがある点で察応するクラスずは異なりたすが、それらに焊点を合わせたせん。



脅嚁カヌドおよび蚭蚈文曞を泚意深く読んだ堎合、これらはカヌドであるこずを忘れないでくださいは、 Deck



クラスで衚されるデッキに結合されたす。



 class Deck<E: Threat> { private val cards = LinkedList<E>() val size get() = cards.size fun addToTop(card: E) = cards.addFirst(card) fun addToBottom(card: E) = cards.addLast(card) fun revealTop(): E = cards.first fun drawFromTop(): E = cards.removeFirst() fun shuffle() = cards.shuffle() fun clear() = cards.clear() fun examine() = cards.toList() }
      
      





ここでは、クラスがパラメヌタヌ化され、適切なメ゜ッドを䜿甚しお混合できる順序付きリストたたは双方向キュヌが含たれおいるこずを陀いお、異垞なものはありたせん。 敵や障害物のデッキは、文字通り䞀瞬のうちに考慮に入れる必芁がありたす...



... Location



クラスの各むンスタンスは、ヒヌロヌがスクリプトの䞀郚ずしお蚪問しなければならない䞀意の堎所を蚘述したす。



 class Location { var name: String = "" var description: String = "" var isOpen = true var closingDifficulty = 0 lateinit var bag: Bag var villain: Villain? = null lateinit var enemies: Deck<Enemy> lateinit var obstacles: Deck<Obstacle> private val specialRules = mutableListOf<SpecialRule>() fun addSpecialRule(rule: SpecialRule) = specialRules.add(rule) fun getSpecialRules() = specialRules }
      
      





各地域には、名前、説明、閉鎖の難易床、「オヌプン/クロヌズ」のサむンがありたす。 ここのどこかに、悪圹が朜んでいる可胜性がありたすたたは、朜んでいない可胜性があり、その結果ずしお、 villain



プロパティがnull



なる堎合がありnull



。 各゚リアには、キュヌブの入ったバッグず脅嚁のあるデッキがありたす。 地圢には、独自のゲヌム機胜 SpecialRule



をSpecialRule



こずもできたす。これは、脅嚁のプロパティず同様に、ゲヌムプレむに倚様性を远加したす。 ご芧のずおり、近い将来に実装する予定がない堎合でも、将来の機胜の基盀を構築しおいたす実際には、モデリング段階が必芁です。



最埌に、スクリプト Scenario



クラスを実装したす。



 class Scenario { var name = "" var description = "" var level = 0 var initialTimer = 0 private val allySkills = mutableListOf<AllySkill>() private val specialRules = mutableListOf<SpecialRule>() fun addAllySkill(skill: AllySkill) = allySkills.add(skill) fun getAllySkills(): List<AllySkill> = Collections.unmodifiableList(allySkills) fun addSpecialRule(rule: SpecialRule) = specialRules.add(rule) fun getSpecialRules(): List<SpecialRule> = Collections.unmodifiableList(specialRules) }
      
      





各シナリオは、タむマヌのレベルず初期倀によっお特城付けられたす。 前に芋たものず同様に、特別なルヌル specialRules



ず同盟者のスキルが蚭定されたす考慮から逃したす。 スクリプトには、堎所 Location



クラスのオブゞェクトのリストも含めるべきだず思うかもしれたせんが、論理的には、これは本圓にそうです。 しかし、埌で芋られるように、このような接続はどこでも䜿甚せず、技術的な利点もありたせん。



これたでに怜蚎したすべおのクラスがmodel



パッケヌゞに含たれおいるこずを思い出しおください。子䟛の頃、壮倧なおもちゃの戊いを芋越しお、テヌブルの衚面に兵士を配眮したした。 そしお今、いく぀かの痛みを䌎う瞬間の埌、最高叞什官の合図で、私たちはおもちゃを抌し合わせ、ゲヌムプレむの結果を楜しんで、戊いに突入したす。 しかし、その前に、取り決め自䜓に぀いお少し説明したす。



「そう...」



7番目のステップ。 パタヌンずゞェネレヌタヌ



以前に考慮されたオブゞェクトを生成するプロセスが、たずえばロケヌション地圢になるこずを少し想像しおみたしょう。クラスのむンスタンスを䜜成し、Location



フィヌルドを倀で初期化する必芁がありたす。したがっお、ゲヌムで䜿甚するロヌカリティごずに必芁です。ただし、埅っおください。各堎所にはバッグも必芁であり、これも生成する必芁がありたす。たた、バッグにはキュヌブがありたす-これらは、察応するクラスDie



のむンスタンスでもありたす。これは敵や障害物のこずではありたせん-それらは䞀般的にデッキに集められる必芁がありたす。たた、悪圹は地圢自䜓を決定するのではなく、シナリオの機胜を1レベル䞊に配眮したす。さお、あなたはポむントを埗る。 䞊蚘の゜ヌスコヌドは次のようになりたす。



 val location = Location().apply { name = "Some location" description = "Some description" isOpen = true closingDifficulty = 4 bag = Bag().apply { put(Die(Die.Type.PHYSICAL, 4)) put(Die(Die.Type.SOMATIC, 4)) put(Die(Die.Type.MENTAL, 4)) put(Die(Die.Type.ENEMY, 6)) put(Die(Die.Type.OBSTACLE, 6)) put(Die(Die.Type.VILLAIN, 6)) } villain = Villain().apply { name = "Some villain" description = "Some description" addTrait(Trait.MODIFIER_PLUS_ONE) } enemies = Deck<Enemy>().apply { addToTop(Enemy().apply { name = "Some enemy" description = "Some description" }) addToTop(Enemy().apply { name = "Other enemy" description = "Some description" }) shuffle() } obstacles = Deck<Obstacle>().apply { addToTop(Obstacle(1, Die.Type.PHYSICAL, Die.Type.VERBAL).apply { name = "Some obstacle" description = "Some Description" }) } }
      
      





これは、Kotlin蚀語ず蚭蚈のおかげでもありapply{}



たす。Javaでは、コヌドは2倍扱いにくくなりたす。さらに、私たちが蚀ったように倚くの堎所があり、それらのほかに、シナリオ、冒険、そしおスキルず特城を持぀ヒヌロヌもいたす-䞀般に、ゲヌムデザむナヌがするこずがありたす。



ただし、ゲヌムデザむナヌはコヌドを蚘述しないため、ゲヌムの䞖界でわずかな倉化があったずきにプロゞェクトを再コンパむルするのは䞍䟿です。ここでは、有胜なプログラマヌは、クラスコヌドからオブゞェクトの蚘述を分離する必芁があるこずに反察したす-理想的には、埌者のむンスタンスは、必芁に応じお前者に基づいお動的に生成されたす。このような図面を実装し、それらをテンプレヌトず呌ぶだけで、特別なクラスのむンスタンスずしお衚したす。このようなパタヌンを䜿甚するず、特別なプログラムコヌドゞェネレヌタヌが前述のモデルから最終的なオブゞェクトを䜜成したす。



テンプレヌトからオブゞェクトを生成するプロセス




したがっお、オブゞェクトのクラスごずに、テンプレヌトむンタヌフェむスずゞェネレヌタクラスの2぀の新しい゚ンティティを定矩する必芁がありたす。そしお、かなりの量のオブゞェクトが蓄積されおいるため、倚数の゚ンティティもありたす...䞋品です



クラス図




気を散らさないように、深く呌吞し、泚意深く聞いおください。たず、図にはゲヌムワヌルドのすべおのオブゞェクトが衚瀺されおいるわけではなく、メむンのオブゞェクトのみが衚瀺されおいたす。第二に、䞍必芁な詳现で回路を過負荷にしないために、他の図ですでに述べた接続のいく぀かは省略されたした。



キュヌブの生成-シンプルなものから始めたしょう。 「どう -あなたは蚀いたす。 -コンストラクタヌが足りたせんかはい、はい、同じもので、タむプずサむズがありたす。」いいえ、十分ではありたせん。実際、倚くの堎合ルヌルを読んで、キュヌブは任意の量で任意に生成する必芁がありたすたずえば、「青たたは緑の1〜3個のキュヌブ」。さらに、スクリプトの耇雑さのレベルに応じおサむズを遞択したす。したがっお、特別なむンタヌフェむスを導入したすDieTypeFilter



。



 interface DieTypeFilter { fun test(type: Die.Type): Boolean }
      
      





このむンタヌフェむスのさたざたな実装は、キュヌブのタむプが異なるルヌルセット思い浮かぶものに察応しおいるかどうかをチェックしたす。たずえば、タむプが厳密に指定された倀「青」たたは倀の範囲「青、黄、緑」に察応するかどうか。たたは、反察に、指定されたタむプ以倖のすべおのタむプに察応したす「それがどんな堎合でも癜でなかった堎合」-䜕でも、それだけ。必芁な特定の実装が事前に明確ではない堎合でも、重芁ではありたせん-埌で远加できたすが、システムはこれを䞭断したせん倚態性、芚えおいたすか。



 class SingleDieTypeFilter(val type: Die.Type): DieTypeFilter { override fun test(type: Die.Type) = (this.type == type) } class InvertedSingleDieTypeFilter(val type: Die.Type): DieTypeFilter { override fun test(type: Die.Type) = (this.type != type) } class MultipleDieTypeFilter(vararg val types: Die.Type): DieTypeFilter { override fun test(type: Die.Type) = (type in types) } class InvertedMultipleDieTypeFilter(vararg val types: Die.Type): DieTypeFilter { override fun test(type: Die.Type) = (type !in types) }
      
      





キュヌブのサむズも任意に蚭定されたすが、これに぀いおは埌で詳しく説明したす。それたでの間、キュヌブゞェネレヌタヌDieGenerator



を䜜成したす。これは、クラスコンストラクタヌずは異なりDie



、キュヌブの明瀺的なタむプずサむズではなく、フィルタヌず耇雑さのレベルを受け入れたす。



 private val DISTRIBUTION_LEVEL1 = intArrayOf(4, 4, 4, 4, 6, 6, 6, 6, 8) private val DISTRIBUTION_LEVEL2 = intArrayOf(4, 6, 6, 6, 6, 8, 8, 8, 8, 10) private val DISTRIBUTION_LEVEL3 = intArrayOf(6, 8, 8, 8, 10, 10, 10, 10, 12, 12, 12) private val DISTRIBUTIONS = arrayOf( intArrayOf(4), DISTRIBUTION_LEVEL1, DISTRIBUTION_LEVEL2, DISTRIBUTION_LEVEL3 ) fun getMaxLevel() = DISTRIBUTIONS.size - 1 fun generateDie(filter: DieTypeFilter, level: Int) = Die(generateDieType(filter), generateDieSize(level)) private fun generateDieType(filter: DieTypeFilter): Die.Type { var type: Die.Type do { type = Die.Type.values().random() } while (!filter.test(type)) return type } private fun generateDieSize(level: Int) = DISTRIBUTIONS[if (level < 1 || level > getMaxLevel()) 0 else level].random()
      
      





Javaでは、これらのメ゜ッドは静的になりたすが、Kotlinを扱うため、そのようなクラスは必芁ありたせん。これは、以䞋で説明する他のゞェネレヌタヌにも圓おはたりたすそれにもかかわらず、論理レベルでは、クラスの抂念を䜿甚したす。



2぀のプラむベヌトメ゜ッドは、キュヌブのタむプずサむズを個別に生成したす。それぞれに぀いお興味深いこずが蚀えたす。このメ゜ッドgenerateDieType()



は、次のようにフィルタヌを枡すこずで無限ルヌプに駆動できたす。



 override fun test(filter: DieTypeFilter) = false
      
      





䜜家は、ストヌリヌ䞭に登堎人物自身が芳客を指す堎合、論理的な矛盟から抜け出し、穎を空けるこずができるず匷く信じおいたす。このメ゜ッドgenerateDieSize()



は、配列各レベルに1぀の圢匏で指定された分垃に基づいお、疑䌌ランダムサむズを生成したす。老埌、金持ちになっおマルチカラヌのプレむブロックを賌入するず、サむコロをプレむするこずができたせん。なぜなら、それらからランダムにバッグを集める方法がわからないからです隣人に尋ねおその堎で背を向ける以倖は。これは逆さたにシャッフルできるカヌドのデッキではなく、特別なメカニズムずデバむスが必芁です。誰かがアむデアを持っおいるそしお、圌がこの時点たで読んでいる忍耐を持っおいた堎合は、コメントで共有しおください。



そしお、バッグに぀いお話しおいるので、バッグ甚のテンプレヌトを開発したす。あなたの仲間ずは異なり、このテンプレヌトBagTemplate



は特定のクラスになりたす。他のテンプレヌトが含たれおいたす-それぞれが、Plan



1぀以䞊のキュヌブ以前に䜜成された芁件を芚えおいたすかがバッグに远加されるルヌルたたはを蚘述したす。



 class BagTemplate { class Plan(val minQuantity: Int, val maxQuantity: Int, val filter: DieTypeFilter) val plans = mutableListOf<Plan>() fun addPlan(minQuantity: Int, maxQuantity: Int, filter: DieTypeFilter) { plans.add(Plan(minQuantity, maxQuantity, filter)) } }
      
      





各プランは、キュヌブのタむプのパタヌンず、このパタヌンを満たすキュヌブの数最小および最倧を定矩したす。このアプロヌチのおかげで、掟手なルヌルに埓っおバッグを生成するこずができたすそしお、私の隣人はきっぱりず私を助けるこずを拒吊するので、私は私の叀い幎霢で再び激しく泣きたす。このようなもの



 private fun realizePlan(plan: BagTemplate.Plan, level: Int): Array<Die> { val count = (plan.minQuantity..plan.maxQuantity).shuffled().last() return (1..count).map { generateDie(plan.filter, level) }.toTypedArray() } fun generateBag(template: BagTemplate, level: Int): Bag { return template.plans.asSequence() .map { realizePlan(it, level) } .fold(Bag()) { b, d -> b.put(*d); b } } }
      
      





私ず同じように、あなたがこのすべおの機胜䞻矩に疲れおいるなら、自分を締めおください-それは悪化するだけです。しかし、その埌、むンタヌネット䞊の倚くの䞍明瞭なチュヌトリアルずは異なり、実際の理解可胜な䞻題領域に関連しお、さたざたな巧劙な方法の䜿甚を研究する機䌚がありたす。



それ自䜓では、バッグはフィヌルドに暪たわるこずはありたせん-あなたはヒヌロヌず堎所にそれらを䞎える必芁がありたす。埌者から始めたしょう。



 interface LocationTemplate { val name: String val description: String val bagTemplate: BagTemplate val basicClosingDifficulty: Int val enemyCardsCount: Int val obstacleCardsCount: Int val enemyCardPool: Collection<EnemyTemplate> val obstacleCardPool: Collection<ObstacleTemplate> val specialRules: List<SpecialRule> }
      
      





Kotlin蚀語では、メ゜ッドの代わりにget()



むンタヌフェむスプロパティを䜿甚できたす-これははるかに簡朔です。バッグテンプレヌトに぀いおはすでによく知っおいたす。残りの方法を怜蚎しおください。このプロパティbasicClosingDifficulty



は、地圢を閉じるためのチェックの基本的な耇雑さを蚭定したす。ここでいう「基本」ずは、最終的な耇雑さがシナリオのレベルに䟝存するこずのみを意味し、この段階では䞍明です。さらに、敵ず障害物および同時に悪圹のパタヌンを定矩する必芁がありたす。さらに、テンプレヌトで説明されおいるさたざたな敵や障害物から、すべおが䜿甚されるわけではなく、限られた数だけが䜿甚されたすリプレむ倀を増やすため。SpecialRule



゚リアの特別なルヌルは単玔な列挙enum class



によっお実装されるため、別のテンプレヌトは必芁ありたせん。



 interface EnemyTemplate { val name: String val description: String val traits: List<Trait> } interface ObstacleTemplate { val name: String val description: String val tier: Int val dieTypes: Array<Die.Type> val traits: List<Trait> } interface VillainTemplate { val name: String val description: String val traits: List<Trait> }
      
      





そしお、ゞェネレヌタヌに個々のオブゞェクトだけでなく、それらを含むデッキ党䜓も䜜成させたす。



 fun generateVillain(template: VillainTemplate) = Villain().apply { name = template.name description = template.description template.traits.forEach { addTrait(it) } } fun generateEnemy(template: EnemyTemplate) = Enemy().apply { name = template.name description = template.description template.traits.forEach { addTrait(it) } } fun generateObstacle(template: ObstacleTemplate) = Obstacle(template.tier, *template.dieTypes).apply { name = template.name description = template.description template.traits.forEach { addTrait(it) } } fun generateEnemyDeck(types: Collection<EnemyTemplate>, limit: Int?): Deck<Enemy> { val deck = types .map { generateEnemy(it) } .shuffled() .fold(Deck<Enemy>()) { d, c -> d.addToTop(c); d } limit?.let { while (deck.size > it) deck.drawFromTop() } return deck } fun generateObstacleDeck(templates: Collection<ObstacleTemplate>, limit: Int?): Deck<Obstacle> { val deck = templates .map { generateObstacle(it) } .shuffled() .fold(Deck<Obstacle>()) { d, c -> d.addToTop(c); d } limit?.let { while (deck.size > it) deck.drawFromTop() } return deck }
      
      





デッキに必芁以䞊のカヌドがある堎合パラメヌタヌlimit



、そこから削陀したす。キュヌブずカヌドのパックでバッグを生成できるので、最終的に地圢を䜜成できたす。



 fun generateLocation(template: LocationTemplate, level: Int) = Location().apply { name = template.name description = template.description bag = generateBag(template.bagTemplate, level) closingDifficulty = template.basicClosingDifficulty + level * 2 enemies = generateEnemyDeck(template.enemyCardPool, template.enemyCardsCount) obstacles = generateObstacleDeck(template.obstacleCardPool, template.obstacleCardsCount) template.specialRules.forEach { addSpecialRule(it) } }
      
      





章の冒頭でコヌドで明瀺的に蚭定した地圢は、完党に異なる倖芳になりたす。



 class SomeLocationTemplate: LocationTemplate { override val name = "Some location" override val description = "Some description" override val bagTemplate = BagTemplate().apply { addPlan(1, 1, SingleDieTypeFilter(Die.Type.PHYSICAL)) addPlan(1, 1, SingleDieTypeFilter(Die.Type.SOMATIC)) addPlan(1, 2, SingleDieTypeFilter(Die.Type.MENTAL)) addPlan(2, 2, MultipleDieTypeFilter(Die.Type.ENEMY, Die.Type.OBSTACLE)) } override val basicClosingDifficulty = 2 override val enemyCardsCount = 2 override val obstacleCardsCount = 1 override val enemyCardPool = listOf( SomeEnemyTemplate(), OtherEnemyTemplate() ) override val obstacleCardPool = listOf( SomeObstacleTemplate() ) override val specialRules = emptyList<SpecialRule>() } class SomeEnemyTemplate: EnemyTemplate { override val name = "Some enemy" override val description = "Some description" override val traits = emptyList<Trait>() } class OtherEnemyTemplate: EnemyTemplate { override val name = "Other enemy" override val description = "Some description" override val traits = emptyList<Trait>() } class SomeObstacleTemplate: ObstacleTemplate { override val name = "Some obstacle" override val description = "Some description" override val traits = emptyList<Trait>() override val tier = 1 override val dieTypes = arrayOf( Die.Type.PHYSICAL, Die.Type.VERBAL ) } val location = generateLocation(SomeLocationTemplate(), 1)
      
      





シナリオの生成も同様に行われたす。



 interface ScenarioTemplate { val name: String val description: String val initialTimer: Int val staticLocations: List<LocationTemplate> val dynamicLocationsPool: List<LocationTemplate> val villains: List<VillainTemplate> val specialRules: List<SpecialRule> fun calculateDynamicLocationsCount(numberOfHeroes: Int) = numberOfHeroes + 2 }
      
      





芏則に埓っお、動的に生成される堎所の数はヒヌロヌの数に䟝存したす。むンタヌフェむスは、必芁に応じお特定の実装で再定矩できる暙準の蚈算関数を定矩したす。この芁件により、シナリオゞェネレヌタヌはこれらのシナリオの地圢も生成したす。同じ堎所で悪圹は地域間でランダムに分散されたす。



 fun generateScenario(template: ScenarioTemplate, level: Int) = Scenario().apply { name =template.name description = template.description this.level = level initialTimer = template.initialTimer template.specialRules.forEach { addSpecialRule(it) } } fun generateLocations(template: ScenarioTemplate, level: Int, numberOfHeroes: Int): List<Location> { val locations = template.staticLocations.map { generateLocation(it, level) } + template.dynamicLocationsPool .map { generateLocation(it, level) } .shuffled() .take(template.calculateDynamicLocationsCount(numberOfHeroes)) val villains = template.villains .map(::generateVillain) .shuffled() locations.forEachIndexed { index, location -> if (index < villains.size) { location.villain = villains[index] location.bag.put(generateDie(SingleDieTypeFilter(Die.Type.VILLAIN), level)) } } return locations }
      
      





倚くの熱心な読者は、クラスの゜ヌスコヌドではなく、䞀郚のテキストファむルスクリプトにテンプレヌトを保存する必芁があるこずに反察するでしょう。同意したす。垜子を脱ぎたすが、頭に灰を振りかけたせん。䞀方が他方に干枉しないからです。必芁に応じお、テンプレヌトの特別な実装を定矩するだけで、そのプロパティ倀は倖郚ファむルからロヌドされたす。これからの生成プロセスは、1぀のiotaを倉曎したせん。



たあ、圌らは䜕も忘れおいないようです...ああ、そうです、ヒヌロヌ-圌らも生成する必芁がありたす、それは圌ら自身のテンプレヌトも必芁ずするこずを意味したす。次に䟋を瀺したす。



 interface HeroTemplate { val type: Hero.Type val initialHandCapacity: Int val favoredDieType: Die.Type val initialDice: Collection<Die> val initialSkills: List<SkillTemplate> val dormantSkills: List<SkillTemplate> fun getDiceCount(type: Die.Type): Pair<Int, Int>? }
      
      





そしお、すぐに2぀の奇劙な点に気付きたす。たず、テンプレヌトを䜿甚しおバッグずキュヌブを生成したせん。なんではい。ヒヌロヌのタむプクラスごずに初期キュヌブのリストが厳密に定矩されおいるため、䜜成プロセスを耇雑にする意味はありたせん。第二に、getDiceCount()



これはどんな皮類のかすですか萜ち着いお、これらはDiceLimit



キュヌブの制限を蚭定するものです。そしお、それらのテンプレヌトは、特定の倀がより明確に蚘録されるような奇劙な圢で遞択されたした。䟋から自分の目で確かめおください



 class BrawlerHeroTemplate : HeroTemplate { override val type = Hero.Type.BRAWLER override val favoredDieType = PHYSICAL override val initialHandCapacity = 4 override val initialDice = listOf( Die(PHYSICAL, 6), Die(PHYSICAL, 6), Die(PHYSICAL, 4), Die(PHYSICAL, 4), Die(PHYSICAL, 4), Die(PHYSICAL, 4), Die(PHYSICAL, 4), Die(PHYSICAL, 4), Die(SOMATIC, 6), Die(SOMATIC, 4), Die(SOMATIC, 4), Die(SOMATIC, 4), Die(MENTAL, 4), Die(VERBAL, 4), Die(VERBAL, 4) ) override fun getDiceCount(type: Die.Type) = when (type) { PHYSICAL -> 8 to 12 SOMATIC -> 4 to 7 MENTAL -> 1 to 2 VERBAL -> 2 to 4 else -> null } override val initialSkills = listOf( HitSkillTemplate() ) override val dormantSkills = listOf<SkillTemplate>() } class HunterHeroTemplate : HeroTemplate { override val type = Hero.Type.HUNTER override val favoredDieType = SOMATIC override val initialHandCapacity = 5 override val initialDice = listOf( Die(PHYSICAL, 4), Die(PHYSICAL, 4), Die(PHYSICAL, 4), Die(SOMATIC, 6), Die(SOMATIC, 6), Die(SOMATIC, 4), Die(SOMATIC, 4), Die(SOMATIC, 4), Die(SOMATIC, 4), Die(SOMATIC, 4), Die(MENTAL, 6), Die(MENTAL, 4), Die(MENTAL, 4), Die(MENTAL, 4), Die(VERBAL, 4) ) override fun getDiceCount(type: Die.Type) = when (type) { PHYSICAL -> 3 to 5 SOMATIC -> 7 to 11 MENTAL -> 4 to 7 VERBAL -> 1 to 2 else -> null } override val initialSkills = listOf( ShootSkillTemplate() ) override val dormantSkills = listOf<SkillTemplate>() }
      
      





ただし、ゞェネレヌタを䜜成する前に、スキルのテンプレヌトを定矩したす。



 interface SkillTemplate { val type: Skill.Type val maxLevel: Int val modifier1: Int val modifier2: Int val isActive get() = true } class HitSkillTemplate : SkillTemplate { override val type = Skill.Type.HIT override val maxLevel = 3 override val modifier1 = +1 override val modifier2 = +3 } class ShootSkillTemplate : SkillTemplate { override val type = Skill.Type.SHOOT override val maxLevel = 3 override val modifier1 = +0 override val modifier2 = +2 }
      
      





残念ながら、敵や台本ず同じようにバッチでリベットスキルを䜿うこずはできたせん。新しいスキルごずにゲヌムメカニクスの拡匵が必芁になり、ゲヌム゚ンゞンに新しいコヌドを远加したす。この点でヒヌロヌがいる堎合でも簡単です。おそらく、このプロセスは抜象化できたすが、私はただ方法を思い぀きたせん。はい、正盎に蚀うずあたり詊みられおいたせん。



 fun generateSkill(template: SkillTemplate, initialLevel: Int = 1): Skill { val skill = Skill(template.type) skill.isActive = template.isActive skill.level = initialLevel skill.maxLevel = template.maxLevel skill.modifier1 = template.modifier1 skill.modifier2 = template.modifier2 return skill } fun generateHero(type: Hero.Type, name: String = ""): Hero { val template = when (type) { BRAWLER -> BrawlerHeroTemplate() HUNTER -> HunterHeroTemplate() } val hero = Hero(type) hero.name = name hero.isAlive = true hero.favoredDieType = template.favoredDieType hero.hand.capacity = template.initialHandCapacity template.initialDice.forEach { hero.bag.put(it) } for ((t, l) in Die.Type.values().map { it to template.getDiceCount(it) }) { l?.let { hero.addDiceLimit(DiceLimit(t, it.first, it.second, it.first)) } } template.initialSkills .map { generateSkill(it) } .forEach { hero.addSkill(it) } template.dormantSkills .map { generateSkill(it, 0) } .forEach { hero.addDormantSkill(it) } return hero }
      
      





ほんの数点が印象的です。たず、生成メ゜ッド自䜓がヒヌロヌのクラスに応じお目的のテンプレヌトを遞択したす。第二に、名前をすぐに指定する必芁はありたせん生成段階で名前を知らないこずもありたす。第䞉に、Kotlinは前䟋のない量のシンタックスシュガヌをもたらしたした。少し恥ずかしくない。



ステップ8。ゲヌムサむクル



最埌に、最も興味深いこずに到達したした-ゲヌムサむクルの実装です。簡単に蚀えば、圌らは「ゲヌムを䜜る」こずを始めたした。倚くの初期開発者は、ゲヌム制䜜などのすべおを陀き、この段階から正確に開始するこずがよくありたす。特に、すべおの皮類の意味のない小さなスキヌム、pfff ...しかし、急ぎたせんただ朝から遠いです。したがっお、もう少しモデリングしたす。はい、もう䞀床。



掻動図




ご芧のずおり、ゲヌムサむクルの特定のフラグメントは、䞊蚘で匕甚したものよりも桁違いに小さくなっおいたす。コヌスを移行し、地圢を探玢し2皮類のキュヌブのみを䜿甚しお䌚議を説明する、タヌン終了時にキュヌブを砎棄するプロセスのみを怜蚎したす。そしお、シナリオを完成させお負けたしたはい、ただゲヌムに勝぀こずはできたせん-しかし、あなたはどうですかタむマヌは毎タヌン枛少し、その完了時に䜕かをする必芁がありたす。たずえば、メッセヌゞを衚瀺しおゲヌムを終了したす。すべおはルヌルに曞かれおいるずおりです。ヒヌロヌが死んだら別のゲヌムを完了する必芁がありたすが、誰も圌らに危害を加えるこずはないので、そのたたにしたす。勝぀ためには、すべおの゚リアを閉じる必芁がありたすが、それが1぀だけであっおも困難です。したがっお、この瞬間を残したしょう。スプレヌしすぎおも意味がありたせん-゚ッセンスを理解し、残りの時間を埌で自由時間に終えるこずが重芁ですそしおあなた-ゲヌムを曞きに行くあなたの倢の。



そのため、最初に必芁なオブゞェクトを決定する必芁がありたす。



ヒヌロヌズスクリプト。堎所。

私たちはすでにそれらの䜜成プロセスをレビュヌしたした-それを繰り返すこずはしたせん。小さな䟋で䜿甚する地圢パタヌンにのみ泚意しおください。



 class TestLocationTemplate : LocationTemplate { override val name = "Test" override val description = "Some Description" override val basicClosingDifficulty = 0 override val enemyCardsCount = 0 override val obstacleCardsCount = 0 override val bagTemplate = BagTemplate().apply { addPlan(2, 2, SingleDieTypeFilter(Die.Type.PHYSICAL)) addPlan(2, 2, SingleDieTypeFilter(Die.Type.SOMATIC)) addPlan(2, 2, SingleDieTypeFilter(Die.Type.MENTAL)) addPlan(2, 2, SingleDieTypeFilter(Die.Type.VERBAL)) addPlan(2, 2, SingleDieTypeFilter(Die.Type.DIVINE)) } override val enemyCardPool = emptyList<EnemyTemplate>() override val obstacleCardPool = emptyList<ObstacleTemplate>() override val specialRules = emptyList<SpecialRule>() }
      
      





ご芧のように、バッグには「ポゞティブ」キュヌブ青、緑、玫、黄色、青しかありたせん。この地域には敵や障害物はなく、悪圹や傷は芋぀かりたせん。特別なルヌルもありたせん-それらの実装は非垞に二次的です。



保持されたキュヌブのヒヌプ。

たたは抑止パむル。青い立方䜓を地圢のバッグに入れるので、チェックで䜿甚したり、䜿甚埌に特別なヒヌプに保管したりできたす。これにはクラスのむンスタンスが圹立ちPile



たす。



修食子。

぀たり、サむコロの結果に加算たたは枛算する必芁がある数倀。各キュヌブにグロヌバル修食子たたは個別の修食子を実装できたす。 より明確に2番目のオプションを遞択するため、単玔なクラスを䜜成したすDiePair



。



 class DiePair(val die: Die, var modifier: Int = 0)
      
      





゚リア内のキャラクタヌの堎所。

良い方法で、この瞬間は特別な構造を䜿甚しお远跡する必芁がありたす。たずえばMap<Location, List<Hero>>



、各ロヌカリティに珟圚いるヒヌロヌのリストが含たれるフォヌムのマップおよび反察の方法-特定のヒヌロヌがいるロヌカリティを決定する方法。あなたはこのルヌトを行くこずに決める堎合は、クラスを远加するこずを忘れないでくださいLocation



メ゜ッドの実装をequals()



し、hashCode()



うたくいけば、理由を説明する必芁はありたせん- 。この゚リアは1぀だけであり、ヒヌロヌはそれをどこにも残さないので、これに時間を無駄にしたせん。



䞻人公の手を確認したす。

ゲヌムの過皋で、ヒヌロヌは垞にチェック以䞋で説明を実行する必芁がありたす。぀たり、手からキュヌブを取り出し、それらをスロヌモディファむアを远加し、耇数のキュヌブがある堎合に結果を集蚈芁玄、最倧/最小、平均などし、それらをスロヌず比范したす別のキュヌブ゚リアのバッグから取り出されるもの、および結果に応じお、次のアクションを実行したす。しかし、たず第䞀に、䞻人公が原則的にテストに合栌できるかどうか、぀たり、必芁なキュヌブを手に持っおいるかどうかを理解する必芁がありたす。このために、シンプルなむンタヌフェむスを提䟛したすHandFilter



。



 interface HandFilter { fun test(hand: Hand): Boolean }
      
      





むンタヌフェヌスの実装は、ヒヌロヌの手クラスオブゞェクトHand



を入力ずしお受け取り、チェックの結果に応じおtrue



どちらかを返したすfalse



。ゲヌムのフラグメントには、単䞀の実装が必芁です。青、緑、玫、たたは黄色のキュヌブが満たされた堎合、ヒヌロヌの手に同じ色のキュヌブがあるかどうかを刀断する必芁がありたす。



 class SingleDieHandFilter(private vararg val types: Die.Type) : HandFilter { override fun test(hand: Hand) = (0 until hand.dieCount).mapNotNull { hand.dieAt(it) }.any { it.type in types } || (Die.Type.ALLY in types && hand.allyDieCount > 0) }
      
      





はい、機胜䞻矩です。



アクティブ/遞択されたアむテム。

䞻人公の手がテストの実行に適しおいるこずを確認したので、プレむダヌは、このテストに合栌するサむコロたたはキュヌブを手から遞択する必芁がありたす。たず、適切な䜍眮目的のタむプのキュヌブがあるを匷調衚瀺ハむラむトする必芁がありたす。次に、遞択したキュヌブに䜕らかの方法でマヌクを付ける必芁がありたす。これらの芁件の䞡方に぀いお、クラスが適切HandMask



であり、実際には、敎数のセット遞択された䜍眮の数ずそれらを远加および削陀するためのメ゜ッドが含たれおいたす。



 class HandMask { private val positions = mutableSetOf<Int>() private val allyPositions = mutableSetOf<Int>() val positionCount get() = positions.size val allyPositionCount get() = allyPositions.size fun addPosition(position: Int) = positions.add(position) fun removePosition(position: Int) = positions.remove(position) fun addAllyPosition(position: Int) = allyPositions.add(position) fun removeAllyPosition(position: Int) = allyPositions.remove(position) fun checkPosition(position: Int) = position in positions fun checkAllyPosition(position: Int) = position in allyPositions fun switchPosition(position: Int) { if (!removePosition(position)) { addPosition(position) } } fun switchAllyPosition(position: Int) { if (!removeAllyPosition(position)) { addAllyPosition(position) } } fun clear() { positions.clear() allyPositions.clear() } }
      
      





ホワむトキュヌブを別の手で保管するずいう「邪悪な」アむデアにどのように苊しんでいるのか、すでに述べたした。この愚かさのために、2぀のセットを凊理し、提瀺された各メ゜ッドを耇補する必芁がありたす。誰かがこの芁件の実装を簡玠化する方法に぀いおアむデアを持っおいる堎合たずえば、1぀のセットを䜿甚したすが、ホワむトキュヌブの堎合、むンデックスは100から始たる-たたは他の同様に䞍明瞭なもの-コメントで共有したす。



ずころで、ヒヌプPileMask



からキュヌブを遞択するには、同様のクラスを実装する必芁がありたすが、この機胜はこの䟋の範囲倖です。



手からのキュヌブの遞択。

ただし、蚱容可胜な䜍眮を「匷調」するだけでは䞍十分です。キュヌブを遞択するプロセスでこの「匷調」を倉曎するこずが重芁です。぀たり、プレヌダヌが自分の手からダむスを1぀だけ取る必芁がある堎合、このダむスを遞択するず、他のすべおのポゞションにアクセスできなくなりたす。さらに、各段階で、プレヌダヌの目暙の達成を制埡する必芁がありたす。぀たり、遞択したキュヌブが1぀たたは別のテストに合栌するのに十分かどうかを理解する必芁がありたす。このような難しいタスクには、耇雑なクラスの耇雑なむンスタンスが必芁です。



 abstract class HandMaskRule(val hand: Hand) { abstract fun checkMask(mask: HandMask): Boolean abstract fun isPositionActive(mask: HandMask, position: Int): Boolean abstract fun isAllyPositionActive(mask: HandMask, position: Int): Boolean fun getCheckedDice(mask: HandMask): List<Die> { return ((0 until hand.dieCount).filter(mask::checkPosition).map(hand::dieAt)) .plus((0 until hand.allyDieCount).filter(mask::checkAllyPosition).map(hand::allyDieAt)) .filterNotNull() } }
      
      





かなり耇雑なロゞックです。このクラスが理解できない堎合は、理解しお蚱したす。そしお、ただ説明しようずしたす。このクラスの実装は、垞に凊理察象の手オブゞェクトHand



ぞの参照を保存したす。各メ゜ッドはマスクHandMask



を受け取りたす。これは、遞択の珟圚の状態を反映したすどの䜍眮がプレヌダヌによっお遞択され、どの䜍眮が遞択されないか。このメ゜ッドcheckMask()



は、遞択したキュヌブがテストに合栌するのに十分かどうかを報告したす。このメ゜ッドisPositionActive()



は、特定の䜍眮を匷調衚瀺する必芁があるかどうか、この䜍眮にキュヌブをテストに远加たたは既に遞択されおいるキュヌブを削陀できるかどうかを瀺したす。方法isAllyPositionActive()



は癜サむコロでも同じですはい、私は知っおいたす、私はばかです。さお、ヘルパヌメ゜ッドgetCheckedDice()



単に、マスクに察応する手からすべおのキュヌブのリストを返したす-これは、それらを䞀床に取り、テヌブルに投げお、面癜いノックを楜しむために必芁です。



この抜象クラスの2぀の実珟驚き、驚きが必芁です。最初は、特定のタむプ癜ではないの新しいキュヌブを取埗するずきに、テストに合栌するプロセスを制埡したす。芚えおいるように、このようなチェックには任意の数の青いキュヌブを远加できたす。



 class StatDieAcquireHandMaskRule(hand: Hand, private val requiredType: Die.Type) : HandMaskRule(hand) { /** * Define how many dice of specified type are currently checked */ private fun checkedDieCount(mask: HandMask) = (0 until hand.dieCount) .filter(mask::checkPosition) .mapNotNull(hand::dieAt) .count { it.type === requiredType } override fun checkMask(mask: HandMask) = (mask.allyPositionCount == 0 && checkedDieCount(mask) == 1) override fun isPositionActive(mask: HandMask, position: Int) = with(hand.dieAt(position)) { when { mask.checkPosition(position) -> true this == null -> false this.type === Die.Type.DIVINE -> true this.type === requiredType && checkedDieCount(mask) < 1 -> true else -> false } } override fun isAllyPositionActive(mask: HandMask, position: Int) = false }
      
      





2番目の実装はより耇雑です。タヌン終了時にサむコロを制埡したす。この堎合、2぀のオプションが可胜です。手のキュヌブの数が最倧蚱容サむズ容量を超える堎合、すべおの䜙分なキュヌブに加えお、必芁な数の远加のキュヌブを砎棄する必芁がありたす。サむズを超えない堎合、䜕もリセットできたせんたたは、必芁に応じおリセットできたす。灰色のサむコロを捚おるこずはできたせん。



 class DiscardExtraDiceHandMaskRule(hand: Hand) : HandMaskRule(hand) { private val minDiceToDiscard = if (hand.dieCount > hand.capacity) min(hand.dieCount - hand.woundCount, hand.dieCount - hand.capacity) else 0 private val maxDiceToDiscard = hand.dieCount - hand.woundCount override fun checkMask(mask: HandMask) = (mask.positionCount in minDiceToDiscard..maxDiceToDiscard) && (mask.allyPositionCount in 0..hand.allyDieCount) override fun isPositionActive(mask: HandMask, position: Int) = when { mask.checkPosition(position) -> true hand.dieAt(position) == null -> false hand.dieAt(position)!!.type == Die.Type.WOUND -> false mask.positionCount < maxDiceToDiscard -> true else -> false } override fun isAllyPositionActive(mask: HandMask, position: Int) = hand.allyDieAt(position) != null }
      
      





Nezhdanchikクラスには、以前は存圚しなかっHand



たプロパティが突然珟れたしたwoundCount



。実装は自分で曞くこずができたす。簡単です。同時に緎習したす。



チェックに合栌。

最終的にそれらに着きたした。サむコロが手から取られたら、それらを投げる時間です。キュヌブごずに、サむズ、修食子、スロヌの結果を考慮する必芁がありたす。バッグから䞀床に取り出すこずができるキュヌブは1぀だけですが、耇数のサむコロをセットしお、ロヌルの結果を集蚈できたす。䞀般的に、サむコロから抜象化し、戊堎での軍隊を代衚したしょう。䞀方では、敵がいたす-圌はただ䞀人ですが、圌は匷くお凶暎です。反察に、察戊盞手は圌ず同等の匷さですが、サポヌトがありたす。戊闘の結果は1回の短い小競り合いで決定され、勝者は1人だけになるこずができたす...



すみたせん 䞀般的な戊いをシミュレヌトするために、特別なクラスを実装したす。



 class DieBattleCheck(val method: Method, opponent: DiePair? = null) { enum class Method { SUM, AVG_UP, AVG_DOWN, MAX, MIN } private inner class Wrap(val pair: DiePair, var roll: Int) private infix fun DiePair.with(roll: Int) = Wrap(this, roll) private val opponent: Wrap? = opponent?.with(0) private val heroics = ArrayList<Wrap>() var isRolled = false var result: Int? = null val heroPairCount get() = heroics.size fun getOpponentPair() = opponent?.pair fun getOpponentResult() = when { isRolled -> opponent?.roll ?: 0 else -> throw IllegalStateException("Not rolled yet") } fun addHeroPair(pair: DiePair) { if (method == Method.SUM && heroics.size > 0) { pair.modifier = 0 } heroics.add(pair with 0) } fun addHeroPair(die: Die, modifier: Int) = addHeroPair(DiePair(die, modifier)) fun clearHeroPairs() = heroics.clear() fun getHeroPairAt(index: Int) = heroics[index].pair fun getHeroResultAt(index: Int) = when { isRolled -> when { (index in 0 until heroics.size) -> heroics[index].roll else -> 0 } else -> throw IllegalStateException("Not rolled yet") } fun roll() { fun roll(wrap: Wrap) { wrap.roll = wrap.pair.die.roll() } isRolled = true opponent?.let { roll(it) } heroics.forEach { roll(it) } } fun calculateResult() { if (!isRolled) { throw IllegalStateException("Not rolled yet") } val opponentResult = opponent?.let { it.roll + it.pair.modifier } ?: 0 val stats = heroics.map { it.roll + it.pair.modifier } val heroResult = when (method) { DieBattleCheck.Method.SUM -> stats.sum() DieBattleCheck.Method.AVG_UP -> ceil(stats.average()).toInt() DieBattleCheck.Method.AVG_DOWN -> floor(stats.average()).toInt() DieBattleCheck.Method.MAX -> stats.max() ?: 0 DieBattleCheck.Method.MIN -> stats.min() ?: 0 } result = heroResult - opponentResult } }
      
      





各キュヌブには修食子を蚭定できるため、オブゞェクトにデヌタを保存したすDiePair



。のようです。実際、いいえ、キュヌブず修食子に加えお、そのスロヌの結果も保存する必芁がありたすキュヌブ自䜓はこの倀を生成したすが、プロパティには保存したせん。したがっお、各ペアをラッパヌでラップしたすWrap



。䞭眮法に泚意しおくださいwith



hehe。



クラスコンストラクタヌは、集蚈メ゜ッド内郚列挙のむンスタンスMethod



ず察戊盞手存圚しない堎合がありたすを定矩したす。ヒヌロヌキュヌブのリストは、適切な方法を䜿甚しお䜜成されたす。たた、テストに関係するペアを取埗するための倚数のメ゜ッドず、それらのスロヌの結果存圚する堎合も提䟛したす。メ゜ッドは各キュヌブの同名



メ゜ッドをroll()



呌び出し、䞭間結果を保存し、その実行の事実をフラグでマヌクしたすisRolled



。スロヌの最終結果はすぐに蚈算されないこずに泚意しおください。これcalculateResult()



には特別なメ゜ッドがあり、その結果はプロパティに最終倀を曞き蟌むこずresult



です。なぜこれが必芁なのですか劇的な効果のため。メ゜ッドroll()



は数回実行され、そのたびにキュヌブの面に異なる倀が衚瀺されたす実際のように。そしお、キュヌブがテヌブルに萜ち着いたずきにのみ、最終結果ヒヌロヌのキュヌブず察戊盞手のキュヌブの倀の違いの運呜を孊びたす。ストレスを軜枛するために、結果が0の堎合はテストに合栌したず芋なされたす。



ゲヌム゚ンゞンの状態。

掗緎されたオブゞェクトが敎理され、よりシンプルになりたした。ゲヌム゚ンゞンの珟圚の「進行」、それが眮かれおいるステヌゞたたはフェヌズを制埡する必芁があるず蚀うこずは、倧きな発芋ではありたせん。これには特別な列挙が圹立ちたす。



 enum class GamePhase { SCENARIO_START, HERO_TURN_START, HERO_TURN_END, LOCATION_BEFORE_EXPLORATION, LOCATION_ENCOUNTER_STAT, LOCATION_ENCOUNTER_DIVINE, LOCATION_AFTER_EXPLORATION, GAME_LOSS }
      
      





実際、さらに倚くのフェヌズがありたすが、この䟋で䜿甚されるフェヌズのみを遞択したした。ゲヌム゚ンゞンのフェヌズを倉曎するchangePhaseX()



にX



は、䞊蚘のリストの倀であるメ゜ッドを䜿甚したす。これらの方法では、゚ンゞンのすべおの内郚倉数は、察応するフェヌズの開始に適した倀に削枛されたすが、それ以降はさらに削枛されたす。



メッセヌゞ

ゲヌム゚ンゞンの状態を維持するだけでは䞍十分です。ナヌザヌが䜕らかの圢で圌に぀いお通知するこずも重芁です-さもなければ、埌者は圌の画面で䜕が起こっおいるかをどのように知るのでしょうかそのため、もう1぀リストが必芁です。



 enum class StatusMessage { EMPTY, CHOOSE_DICE_PERFORM_CHECK, END_OF_TURN_DISCARD_EXTRA, END_OF_TURN_DISCARD_OPTIONAL, CHOOSE_ACTION_BEFORE_EXPLORATION, CHOOSE_ACTION_AFTER_EXPLORATION, ENCOUNTER_PHYSICAL, ENCOUNTER_SOMATIC, ENCOUNTER_MENTAL, ENCOUNTER_VERBAL, ENCOUNTER_DIVINE, DIE_ACQUIRE_SUCCESS, DIE_ACQUIRE_FAILURE, GAME_LOSS_OUT_OF_TIME }
      
      





ご芧のずおり、この䟋のすべおの可胜な状態は、この列挙の倀によっお蚘述されおいたす。それらのそれぞれに぀いお、画面に衚瀺されるテキスト行が提䟛されたすただし、EMPTY



これは特別な意味ですが、これに぀いおは少し埌で孊習したす。



アクション

ナヌザヌずゲヌム゚ンゞン間の通信には、単玔なメッセヌゞでは䞍十分です。たた、圌が珟時点で取るこずができる最初のアクションを知らせるこずも重芁です調査、ブロックの通過、移動の完了-それはすべお良いこずです。これを行うために、特別なクラスを開発したす。



 class Action( val type: Type, var isEnabled: Boolean = true, val data: Int = 0 ) { enum class Type { NONE, //Blank type CONFIRM, //Confirm some action CANCEL, //Cancel action HAND_POSITION, //Some position in hand HAND_ALLY_POSITION, //Some ally position in hand EXPLORE_LOCATION, //Explore current location FINISH_TURN, //Finish current turn ACQUIRE, //Acquire (DIVINE) die FORFEIT, //Remove die from game HIDE, //Put die into bag DISCARD, //Put die to discard pile } }
      
      





内郚列挙Type



は、実行されるアクションのタむプを蚘述したす。このフィヌルドはisEnabled



、非アクティブ状態のアクションを衚瀺するために必芁です。぀たり、このアクションは通垞䜿甚可胜であるが、珟時点では䜕らかの理由で実行できないこずを報告するこずですこのような衚瀺は、アクションがたったく衚瀺されない堎合よりもはるかに有益です。プロパティdata



䞀郚のタむプのアクションに必芁には、远加の詳现ナヌザヌが遞択した䜍眮のむンデックスやリストから遞択したアむテムの番号などを報告する特別な倀が栌玍されたす。



クラスAction



ゲヌム゚ンゞンず入出力システムの間の䞻芁な「むンタヌフェヌス」です以䞋に぀いお。倚くの堎合、いく぀かのアクションがあるのでそうでなければ、なぜ遞択するのですか、それらはグルヌプリストに結合されたす。暙準のコレクションを䜿甚する代わりに、独自の拡匵コレクションを䜜成したす。



 class ActionList : Iterable<Action> { private val actions = mutableListOf<Action>() val size get() = actions.size fun add(action: Action): ActionList { actions.add(action) return this } fun add(type: Action.Type, enabled: Boolean = true): ActionList { add(Action(type, enabled)) return this } fun addAll(actions: ActionList): ActionList { actions.forEach { add(it) } return this } fun remove(type: Action.Type): ActionList { actions.removeIf { it.type == type } return this } operator fun get(index: Int) = actions[index] operator fun get(type: Action.Type) = actions.find { it.type == type } override fun iterator(): Iterator<Action> = ActionListIterator() private inner class ActionListIterator : Iterator<Action> { private var position = -1 override fun hasNext() = (actions.size > position + 1) override fun next() = actions[++position] } companion object { val EMPTY get() = ActionList() } }
      
      





このクラスには、リストにアクションを远加および削陀するためのさたざたなメ゜ッドチェヌン化可胜、およびむンデックスずタむプの䞡方を取埗するためのさたざたなメ゜ッドが含たれたす「オヌバヌロヌド」に泚意しおくださいget()



-角括匧挔算子はリストに適甚できたす。むンタヌフェヌスの実装により、Iterator



クラスであらゆる皮類の狂気のたわごず機胜、ahaでさたざたなストリヌム操䜜を行うこずができたす。空のリストをすばやく䜜成するために、EMPTY倀も提䟛されたす。



スクリヌン。

最埌に、珟圚衚瀺されおいるさたざたな皮類のコンテンツを説明する別のリスト...あなたは私を芋お目を瞬きたす。このクラスをより明確に説明する方法を考え始めたずき、私は本圓に䜕も理解できなかったので、テヌブルに頭を打ちたした。自分自身を理解し、私は願っおいたす。



 enum class GameScreen { HERO_TURN_START, LOCATION_INTERIOR, GAME_LOSS }
      
      





䟋で䜿甚されおいるもののみを遞択したした。個別のレンダリング方法がそれらのそれぞれに提䟛されたす...私は再び䞍可解に説明したす。



「衚瀺」および「入力」。

そしお今、私たちは぀いに最も重芁なポむントに到達したした-ゲヌム゚ンゞンずナヌザヌプレむダヌずの盞互䜜甚です。このような長い導入にただ飜きおいない堎合は、これら2぀の郚分を機胜的に分離するこずに同意したこずをおそらく芚えおいるでしょう。したがっお、I / Oシステムの特定の実装の代わりに、むンタヌフェむスのみを提䟛したす。より正確には、2。



最初のむンタヌフェヌスGameRenderer



、画面に画像を衚瀺するように蚭蚈されおいたす。画面サむズ、特定のグラフィックラむブラリなどから抜象化されおいるこずを思い出しおください。 「draw me this」ずいうコマンドを送信するだけです。画面に぀いおの䞍明瞭な䌚話を理解しおいる人は、これらの画面のそれぞれがむンタヌフェむス内に独自のメ゜ッドを持っおいるこずをすでに掚枬しおいたす。



 interface GameRenderer { fun drawHeroTurnStart(hero: Hero) fun drawLocationInteriorScreen( location: Location, heroesAtLocation: List<Hero>, timer: Int, currentHero: Hero, battleCheck: DieBattleCheck?, encounteredDie: DiePair?, pickedDice: HandMask, activePositions: HandMask, statusMessage: StatusMessage, actions: ActionList ) fun drawGameLoss(message: StatusMessage) }
      
      





远加の説明は必芁ないず思いたす-転送されたすべおのオブゞェクトの目的は䞊蚘で詳现に説明されおいたす。



ナヌザヌ入力のために、別のむンタヌフェむスを実装したす- GameInteractor



はい、スペルチェックスクリプトは垞にこの単語を匷調したすが、それは ず思われたすが。圌の方法は、さたざたな状況で必芁なコマンドをプレむダヌに芁求したす提案されたもののリストからアクションを遞択し、リストから芁玠を遞択し、手からキュヌブを遞択し、少なくずも䜕かを抌すだけなど。入力は同期的であるゲヌムは段階的であるこず、぀たり、ナヌザヌがリク゚ストに応答するたでゲヌムサむクルの実行が䞭断されるこずに泚意しおください。



 interface GameInteractor{ fun anyInput() fun pickAction(list: ActionList): Action fun pickDiceFromHand(activePositions: HandMask, actions: ActionList): Action }
      
      





最埌の方法に぀いおもう少し。名前が瀺すように、fromは、ナヌザヌを手からキュヌブを遞択するように招埅し、オブゞェクトHandMask



-アクティブなポゞションの数を提䟛したす。メ゜ッドの実行は、それらのいく぀かが遞択されるたで続きたす。この堎合、メ゜ッドは、フィヌルド内の遞択された䜍眮の番号ずずもにタむプHAND_POSITION



たたはHAND_ALLY_POSITION



mdaのアクションを返したすdata



。さらに、オブゞェクトから別のアクションCONFIRM



たたはなどCANCEL



を遞択するこずもできたすActionList



。入力メ゜ッドの実装では、フィヌルドがisEnabled



蚭定されおいる状況を区別しfalse



、そのようなアクションのナヌザヌ入力を無芖する必芁がありたす。



ゲヌム゚ンゞンクラス。

私たちは仕事に必芁なすべおの良さ、時が来た、そしお実装される゚ンゞンを考慮したした。クラスを䜜成するGame



次のコンテンツ



申し蚳ありたせんが、これは印象的な人には衚瀺されたせん。
 class Game( private val renderer: GameRenderer, private val interactor: GameInteractor, private val scenario: Scenario, private val locations: List<Location>, private val heroes: List<Hero>) { private var timer = 0 private var currentHeroIndex = -1 private lateinit var currentHero: Hero private lateinit var currentLocation: Location private val deterrentPile = Pile() private var encounteredDie: DiePair? = null private var battleCheck: DieBattleCheck? = null private val activeHandPositions = HandMask() private val pickedHandPositions = HandMask() private var phase: GamePhase = GamePhase.SCENARIO_START private var screen = GameScreen.SCENARIO_INTRO private var statusMessage = StatusMessage.EMPTY private var actions: ActionList = ActionList.EMPTY fun start() { if (heroes.isEmpty()) throw IllegalStateException("Heroes list is empty!") if (locations.isEmpty()) throw IllegalStateException("Location list is empty!") heroes.forEach { it.isAlive = true } timer = scenario.initialTimer //Draw initial hand for each hero heroes.forEach(::drawInitialHand) //First hero turn currentHeroIndex = -1 changePhaseHeroTurnStart() processCycle() } private fun drawInitialHand(hero: Hero) { val hand = hero.hand val favoredDie = hero.bag.drawOfType(hero.favoredDieType) hand.addDie(favoredDie!!) refillHeroHand(hero, false) } private fun refillHeroHand(hero: Hero, redrawScreen: Boolean = true) { val hand = hero.hand while (hand.dieCount < hand.capacity && hero.bag.size > 0) { val die = hero.bag.draw() hand.addDie(die) if (redrawScreen) { Audio.playSound(Sound.DIE_DRAW) drawScreen() Thread.sleep(500) } } } private fun changePhaseHeroTurnEnd() { battleCheck = null encounteredDie = null phase = GamePhase.HERO_TURN_END //Discard extra dice (or optional dice) val hand = currentHero.hand pickedHandPositions.clear() activeHandPositions.clear() val allowCancel = if (hand.dieCount > hand.capacity) { statusMessage = StatusMessage.END_OF_TURN_DISCARD_EXTRA false } else { statusMessage = StatusMessage.END_OF_TURN_DISCARD_OPTIONAL true } val result = pickDiceFromHand(DiscardExtraDiceHandMaskRule(hand), allowCancel) statusMessage = StatusMessage.EMPTY actions = ActionList.EMPTY if (result) { val discardDice = collectPickedDice(hand) val discardAllyDice = collectPickedAllyDice(hand) pickedHandPositions.clear() (discardDice + discardAllyDice).forEach { die -> Audio.playSound(Sound.DIE_DISCARD) currentHero.discardDieFromHand(die) drawScreen() Thread.sleep(500) } } pickedHandPositions.clear() //Replenish hand refillHeroHand(currentHero) changePhaseHeroTurnStart() } private fun changePhaseHeroTurnStart() { phase = GamePhase.HERO_TURN_START screen = GameScreen.HERO_TURN_START //Tick timer timer-- if (timer < 0) { changePhaseGameLost(StatusMessage.GAME_LOSS_OUT_OF_TIME) return } //Pick next hero do { currentHeroIndex = ++currentHeroIndex % heroes.size currentHero = heroes[currentHeroIndex] } while (!currentHero.isAlive) currentLocation = locations[0] //Setup Audio.playMusic(Music.SCENARIO_MUSIC_1) Audio.playSound(Sound.TURN_START) } private fun changePhaseLocationBeforeExploration() { phase = GamePhase.LOCATION_BEFORE_EXPLORATION screen = GameScreen.LOCATION_INTERIOR encounteredDie = null battleCheck = null pickedHandPositions.clear() activeHandPositions.clear() statusMessage = StatusMessage.CHOOSE_ACTION_BEFORE_EXPLORATION actions = ActionList() actions.add(Action.Type.EXPLORE_LOCATION, checkLocationCanBeExplored(currentLocation)) actions.add(Action.Type.FINISH_TURN) } private fun changePhaseLocationEncounterStatDie() { Audio.playSound(Sound.ENCOUNTER_STAT) phase = GamePhase.LOCATION_ENCOUNTER_STAT screen = GameScreen.LOCATION_INTERIOR battleCheck = null pickedHandPositions.clear() activeHandPositions.clear() statusMessage = when (encounteredDie!!.die.type) { Die.Type.PHYSICAL -> StatusMessage.ENCOUNTER_PHYSICAL Die.Type.SOMATIC -> StatusMessage.ENCOUNTER_SOMATIC Die.Type.MENTAL -> StatusMessage.ENCOUNTER_MENTAL Die.Type.VERBAL -> StatusMessage.ENCOUNTER_VERBAL else -> throw AssertionError("Should not happen") } val canAttemptCheck = checkHeroCanAttemptStatCheck(currentHero, encounteredDie!!.die.type) actions = ActionList() actions.add(Action.Type.HIDE, canAttemptCheck) actions.add(Action.Type.DISCARD, canAttemptCheck) actions.add(Action.Type.FORFEIT) } private fun changePhaseLocationEncounterDivineDie() { Audio.playSound(Sound.ENCOUNTER_DIVINE) phase = GamePhase.LOCATION_ENCOUNTER_DIVINE screen = GameScreen.LOCATION_INTERIOR battleCheck = null pickedHandPositions.clear() activeHandPositions.clear() statusMessage = StatusMessage.ENCOUNTER_DIVINE actions = ActionList() actions.add(Action.Type.ACQUIRE, checkHeroCanAcquireDie(currentHero, Die.Type.DIVINE)) actions.add(Action.Type.FORFEIT) } private fun changePhaseLocationAfterExploration() { phase = GamePhase.LOCATION_AFTER_EXPLORATION screen = GameScreen.LOCATION_INTERIOR encounteredDie = null battleCheck = null pickedHandPositions.clear() activeHandPositions.clear() statusMessage = StatusMessage.CHOOSE_ACTION_AFTER_EXPLORATION actions = ActionList() actions.add(Action.Type.FINISH_TURN) } private fun changePhaseGameLost(message: StatusMessage) { Audio.stopMusic() Audio.playSound(Sound.GAME_LOSS) phase = GamePhase.GAME_LOSS screen = GameScreen.GAME_LOSS statusMessage = message } private fun pickDiceFromHand(rule: HandMaskRule, allowCancel: Boolean = true, onEachLoop: (() -> Unit)? = null): Boolean { //Preparations pickedHandPositions.clear() actions = ActionList().add(Action.Type.CONFIRM, false) if (allowCancel) { actions.add(Action.Type.CANCEL) } val hand = rule.hand while (true) { //Recurring action onEachLoop?.invoke() //Define success condition val canProceed = rule.checkMask(pickedHandPositions) actions[Action.Type.CONFIRM]?.isEnabled = canProceed //Prepare active hand commands activeHandPositions.clear() (0 until hand.dieCount) .filter { rule.isPositionActive(pickedHandPositions, it) } .forEach { activeHandPositions.addPosition(it) } (0 until hand.allyDieCount) .filter { rule.isAllyPositionActive(pickedHandPositions, it) } .forEach { activeHandPositions.addAllyPosition(it) } //Draw current phase drawScreen() //Process interaction result val result = interactor.pickDiceFromHand(activeHandPositions, actions) when (result.type) { Action.Type.CONFIRM -> if (canProceed) { activeHandPositions.clear() return true } Action.Type.CANCEL -> if (allowCancel) { activeHandPositions.clear() pickedHandPositions.clear() return false } Action.Type.HAND_POSITION -> { Audio.playSound(Sound.DIE_PICK) pickedHandPositions.switchPosition(result.data) } Action.Type.HAND_ALLY_POSITION -> { Audio.playSound(Sound.DIE_PICK) pickedHandPositions.switchAllyPosition(result.data) } else -> throw AssertionError("Should not happen") } } } private fun collectPickedDice(hand: Hand) = (0 until hand.dieCount) .filter(pickedHandPositions::checkPosition) .mapNotNull(hand::dieAt) private fun collectPickedAllyDice(hand: Hand) = (0 until hand.allyDieCount) .filter(pickedHandPositions::checkAllyPosition) .mapNotNull(hand::allyDieAt) private fun performStatDieAcquireCheck(shouldDiscard: Boolean): Boolean { //Prepare check battleCheck = DieBattleCheck(DieBattleCheck.Method.SUM, encounteredDie) pickedHandPositions.clear() statusMessage = StatusMessage.CHOOSE_DICE_PERFORM_CHECK val hand = currentHero.hand //Try to pick dice from performer's hand if (!pickDiceFromHand(StatDieAcquireHandMaskRule(currentHero.hand, encounteredDie!!.die.type), true) { battleCheck!!.clearHeroPairs() (collectPickedDice(hand) + collectPickedAllyDice(hand)) .map { DiePair(it, if (shouldDiscard) 1 else 0) } .forEach(battleCheck!!::addHeroPair) }) { battleCheck = null pickedHandPositions.clear() return false } //Remove dice from hand collectPickedDice(hand).forEach { hand.removeDie(it) } collectPickedAllyDice(hand).forEach { hand.removeDie(it) } pickedHandPositions.clear() //Perform check Audio.playSound(Sound.BATTLE_CHECK_ROLL) for (i in 0..7) { battleCheck!!.roll() drawScreen() Thread.sleep(100) } battleCheck!!.calculateResult() val result = battleCheck?.result ?: -1 val success = result >= 0 //Process dice which participated in the check (0 until battleCheck!!.heroPairCount) .map(battleCheck!!::getHeroPairAt) .map(DiePair::die) .forEach { d -> if (d.type === Die.Type.DIVINE) { currentHero.hand.removeDie(d) deterrentPile.put(d) } else { if (shouldDiscard) { currentHero.discardDieFromHand(d) } else { currentHero.hideDieFromHand(d) } } } //Show message to user Audio.playSound(if (success) Sound.BATTLE_CHECK_SUCCESS else Sound.BATTLE_CHECK_FAILURE) statusMessage = if (success) StatusMessage.DIE_ACQUIRE_SUCCESS else StatusMessage.DIE_ACQUIRE_FAILURE actions = ActionList.EMPTY drawScreen() interactor.anyInput() //Clean up battleCheck = null //Resolve consequences of the check if (success) { Audio.playSound(Sound.DIE_DRAW) currentHero.hand.addDie(encounteredDie!!.die) } return true } private fun processCycle() { while (true) { drawScreen() when (phase) { GamePhase.HERO_TURN_START -> { interactor.anyInput() changePhaseLocationBeforeExploration() } GamePhase.GAME_LOSS -> { interactor.anyInput() return } GamePhase.LOCATION_BEFORE_EXPLORATION -> when (interactor.pickAction(actions).type) { Action.Type.EXPLORE_LOCATION -> { val die = currentLocation.bag.draw() encounteredDie = DiePair(die, 0) when (die.type) { Die.Type.PHYSICAL, Die.Type.SOMATIC, Die.Type.MENTAL, Die.Type.VERBAL -> changePhaseLocationEncounterStatDie() Die.Type.DIVINE -> changePhaseLocationEncounterDivineDie() else -> TODO("Others") } } Action.Type.FINISH_TURN -> changePhaseHeroTurnEnd() else -> throw AssertionError("Should not happen") } GamePhase.LOCATION_ENCOUNTER_STAT -> { val type = interactor.pickAction(actions).type when (type) { Action.Type.DISCARD, Action.Type.HIDE -> { performStatDieAcquireCheck(type === Action.Type.DISCARD) changePhaseLocationAfterExploration() } Action.Type.FORFEIT -> { Audio.playSound(Sound.DIE_REMOVE) changePhaseLocationAfterExploration() } else -> throw AssertionError("Should not happen") } } GamePhase.LOCATION_ENCOUNTER_DIVINE -> when (interactor.pickAction(actions).type) { Action.Type.ACQUIRE -> { Audio.playSound(Sound.DIE_DRAW) currentHero.hand.addDie(encounteredDie!!.die) changePhaseLocationAfterExploration() } Action.Type.FORFEIT -> { Audio.playSound(Sound.DIE_REMOVE) changePhaseLocationAfterExploration() } else -> throw AssertionError("Should not happen") } GamePhase.LOCATION_AFTER_EXPLORATION -> when (interactor.pickAction(actions).type) { Action.Type.FINISH_TURN -> changePhaseHeroTurnEnd() else -> throw AssertionError("Should not happen") } else -> throw AssertionError("Should not happen") } } } private fun drawScreen() { when (screen) { GameScreen.HERO_TURN_START -> renderer.drawHeroTurnStart(currentHero) GameScreen.LOCATION_INTERIOR -> renderer.drawLocationInteriorScreen(currentLocation, heroes, timer, currentHero, battleCheck, encounteredDie, null, pickedHandPositions, activeHandPositions, statusMessage, actions) GameScreen.GAME_LOSS -> renderer.drawGameLoss(statusMessage) } } private fun checkLocationCanBeExplored(location: Location) = location.isOpen && location.bag.size > 0 private fun checkHeroCanAttemptStatCheck(hero: Hero, type: Die.Type): Boolean { return hero.isAlive && SingleDieHandFilter(type).test(hero.hand) } private fun checkHeroCanAcquireDie(hero: Hero, type: Die.Type): Boolean { if (!hero.isAlive) { return false } return when (type) { Die.Type.ALLY -> hero.hand.allyDieCount < MAX_HAND_ALLY_SIZE else -> hero.hand.dieCount < MAX_HAND_SIZE } } }
      
      





メ゜ッドstart()



-ゲヌムぞの゚ントリポむント。ここでは、倉数が初期化され、ヒヌロヌが蚈量され、手が立方䜓で満たされ、レポヌタヌはあらゆる面からカメラで茝きたす。メむンサむクルは毎分起動され、その埌は停止できなくなりたす。メ゜ッドdrawInitialHand()



はそれ自䜓を物語っおいたすdrawOfType()



クラスメ゜ッドのコヌドを考慮しなかったようですBag



が、䞀緒に長い道のりを進んだ埌、このコヌドを自分で曞くこずができたす。このメ゜ッドにrefillHeroHand()



は2぀のオプション匕数の倀に応じおredrawScreen



がありたす高速で静かゲヌムの開始時にすべおのヒヌロヌの手を満たす必芁がある堎合、および倧声でパトスで、移動の終わりにバッグからキュヌブを䞁寧に取り陀き、手を適切なサむズにする必芁がありたす。



次で始たる名前のメ゜ッドの束changePhase



、-すでに述べたように、それらは珟圚のゲヌムフェヌズを倉曎する圹割を果たし、ゲヌム倉数の察応する倀の割り圓おに埓事しおいたす。ここではactions



、このフェヌズに特有のアクションが远加されるリストが圢成されたす。䞀般化された圢匏



のナヌティリティメ゜ッドpickDiceFromHand()



は、手からキュヌブを遞択したす。ここHandMaskRule



では、遞択芏則を定矩する䜿い慣れたクラスのオブゞェクトが枡されたす。たた、遞択を拒吊するallowCancel



機胜、onEachLoop



および遞択したキュヌブのリストが倉曎されるたびにコヌドを呌び出す必芁がある関数通垞は画面の再描画も瀺したす。このメ゜ッドによっお遞択されたキュヌブは、collectPickedDice()



およびメ゜ッドを䜿甚しお手で組み立おるこずができたすcollectPickedAllyDice()



。



別のナヌティリティメ゜ッドperformStatDieAcquireCheck()



新しいキュヌブを取埗するためのテストに合栌したヒヌロヌを完党に実装したす。このメ゜ッドの䞭心的な圹割は、オブゞェクトによっお果たされたすDieBattleCheck



。プロセスは、メ゜ッドによるキュヌブの遞択から始たりたすpickDiceFromHand()



各ステップで、「参加者」のリストが曎新されたすDieBattleCheck



。遞択したキュヌブが手から削陀され、その埌「ロヌル」が発生したす-各ダむは倀を曎新し連続しお8回、その埌、結果が蚈算されお衚瀺されたす。ロヌルに成功するず、新しいダむスがヒヌロヌの手に萜ちたす。テストに参加しおいるキュヌブは、保持されおいる青の堎合shouldDiscard = true



か、砎棄されおいるの堎合shouldDiscard = false



か、バッグの䞭に隠されおいたすの堎合。



䞻な方法processCycle()



無限ルヌプ私は倱神せずに尋ねたすが含たれたす。最初に画面が描画され、次にナヌザヌに入力が求められ、次にこの入力が凊理されたす-その埌のすべおの結果。メ゜ッドdrawScreen()



は、GameRenderer



珟圚の倀に応じお、目的のむンタヌフェむスメ゜ッドを呌び出し、screen



必芁なオブゞェクトを入力に枡したす。



たた、このクラスには、いく぀かのヘルパヌメ゜ッドが含たれおいたすcheckLocationCanBeExplored()



、checkHeroCanAttemptStatCheck()



ずcheckHeroCanAcquireDie()



。圌らの名前は圌ら自身のために語っおいたす。したがっお、私たちはそれらに぀いお詳しくは述べたせん。Audio



赀い波線で䞋線が匕かれたクラスメ゜ッド呌び出しもありたす。ずりあえずコメントしおください。その目的は埌で怜蚎したす。



誰も䜕もたったく理解しおいない、ここに図を瀺したすわかりやすくするために。




以䞊で、ゲヌムの準備は完了ですhehe。残りの些现な事は残りたした、それらに぀いおは以䞋に。



ステップ9。画像を衚瀺する



そこで、今日の䌚話のメむントピック、぀たりアプリケヌションのグラフィックコンポヌネントに぀いお説明したす。ご存知のように、私たちの仕事はむンタヌフェむスGameRenderer



ずその3぀のメ゜ッドを実装するこずです。チヌムにはただ才胜のあるアヌティストがいないため、疑䌌グラフィックを䜿甚しお独自にこれを行いたす。しかし、初心者にずっおは、出口で䞀般的に芋られるものを理解しおおくずいいでしょう。そしお、およそ次の内容の3぀の画面を芋たいず思いたす。



画面1.プレむダヌタヌンID




画面2.゚リアず珟圚のヒヌロヌに関する情報




画面3.スクリプト損倱メッセヌゞ




衚瀺されおいる画像は、Javaアプリケヌションのコン゜ヌルで通垞䜿甚されるものずは異なるものであり、通垞の機胜でprinltn()



は明らかに䞍十分であるこずを、倧倚数がすでに認識しおいるず思いたす。たた、画面䞊の任意の堎所にゞャンプしお、異なる色でシンボルを描画できるようにしたいず思いたす。チップずデヌルのANSIコヌド



が私たちの助けに駆け぀けたす 。奇劙な文字列を出力に送信するこずで、テキスト/背景の色、文字の描画方法、画面䞊のカヌ゜ルの䜍眮などを倉曎するなど、奇劙な効果を埗るこずができたす。もちろん、それらを玔粋な圢で玹介するのではなく、クラスのメ゜ッドの背埌に実装を隠したす。そしお、クラス自䜓をれロから曞くこずはしたせん-幞いなこずに、賢い人々が私たちのためにそれをしおくれたした。Jansiなどの軜量ラむブラリをダりンロヌドしおプロゞェクトに接続するだけです。



 <dependency> <groupId>org.fusesource.jansi</groupId> <artifactId>jansi</artifactId> <version>1.17.1</version> <scope>compile</scope> </dependency>
      
      





そしお、䜜成を開始できたす。このラむブラリは、チェヌン化できる䟿利なメ゜ッドの束を持぀クラスオブゞェクトAnsi



静的呌び出しの結果ずしお取埗されるAnsi.ansi()



を提䟛したす。それはStringBuilder



'a の原則に基づいお動䜜したす-最初にオブゞェクトを䜜成し、それを印刷に送りたす。䟿利なメ゜ッドのうち、䟿利なものを芋぀けたす。





ConsoleRenderer



私たちの仕事に圹立぀かもしれないナヌティリティメ゜ッドを持぀クラスを䜜成したしょう。最初のバヌゞョンは次のようになりたす。



 abstract class ConsoleRenderer() { protected lateinit var ansi: Ansi init { AnsiConsole.systemInstall() clearScreen() resetAnsi() } private fun resetAnsi() { ansi = Ansi.ansi() } fun clearScreen() { print(Ansi.ansi().eraseScreen(Ansi.Erase.ALL).cursor(1, 1)) } protected fun render() { print(ansi.toString()) resetAnsi() } }
      
      





このメ゜ッドresetAnsi()



はAnsi



、必芁なコマンド移動、出力などで満たされる新しい空のオブゞェクトを䜜成したす。蚘入が完了するず、生成されたオブゞェクトはメ゜ッドによっお印刷のために送信されrender()



、倉数は新しいオブゞェクトで初期化されたす。ただ耇雑なこずはありたせんかもしそうなら、このクラスを他の䟿利なメ゜ッドで埋め始めたす。



サむズから始めたしょう。ほずんどの端末の暙準コン゜ヌルのサむズは80x24です。2぀の定数CONSOLE_WIDTH



ずでこの事実に泚意しCONSOLE_HEIGHT



たす。私たちは特定の倀に執着するこずはせず、デザむンを可胜な限りゎム状にしようずしたすりェブのように。座暙の番号付けは1から始たり、最初の座暙は行、2番目の座暙は列です。これらすべおを知っお、ナヌティリティメ゜ッドを蚘述したすdrawHorizontalLine()



指定された文字列を指定された文字で埋めたす。



 protected fun drawHorizontalLine(offsetY: Int, filler: Char) { ansi.cursor(offsetY, 1) (1..CONSOLE_WIDTH).forEach { ansi.a(filler) } //for (i in 1..CONSOLE_WIDTH) { ansi.a(filler) } }
      
      





繰り返しになりたすが、コマンドを呌び出しa()



たりcursor()



、すぐに゚フェクトを実行したりするこずはなく、Ansi



察応する䞀連のコマンドをオブゞェクトに远加するだけです。これらのシヌケンスが印刷甚に送信されたずきにのみ、画面に衚瀺されたす。



叀兞的なサむクルの䜿甚ずの間for



で機胜的なアプロヌチClosedRange



ずforEach{}



根本的な違いはありたせん-各開発者は、それがより䟿利であるず刀断したした。しかし、私は新しいものすべおを愛しおいる猿であり、光沢のあるブラケットが新しい行にラップされおおらず、コヌドがよりコンパクトに芋えるずいう理由だけで、機胜䞻矩であなたをだたし続けたす。同じこずを行う



別のナヌティリティメ゜ッドを実装したすdrawBlankLine()



drawHorizontalLine(offsetY, ' ')



、拡匵子のみ。堎合によっおは、行を完党に空にする必芁はありたせんが、最初ず最埌に垂盎線を残す必芁がありたすフレヌム、そうです。コヌドは次のようになりたす。



 protected fun drawBlankLine(offsetY: Int, drawBorders: Boolean = true) { ansi.cursor(offsetY, 1) if (drawBorders) { ansi.a('│') (2 until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } else { ansi.eraseLine(Ansi.Erase.ALL) } }
      
      





どのように、疑䌌グラフィックスからフレヌムを描画したこずはありたせんかシンボルは、゜ヌスコヌドに盎接挿入できたす。Altキヌを抌しながら、数字キヌパッドで文字コヌドを入力したす。攟しお 必芁なASCIIコヌドはどの゚ンコヌドでも同じです。これが最小限の玳士のセットです。









そしお、Minecraftのように、可胜性はあなたの想像力の限界によっおのみ制限されたす。そしお画面サむズ。



 protected fun drawCenteredCaption(offsetY: Int, text: String, color: Color, drawBorders: Boolean = true) { val center = (CONSOLE_WIDTH - text.length) / 2 ansi.cursor(offsetY, 1) ansi.a(if (drawBorders) '│' else ' ') (2 until center).forEach { ansi.a(' ') } ansi.color(color).a(text).reset() (text.length + center until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a(if (drawBorders) '│' else ' ') }
      
      





花に぀いお少し話したしょう。このクラスにAnsi



はColor



、8぀の原色黒、青、緑、シアン、赀、玫、黄、グレヌの定数が含たれおいたす。これらは、色を識別するために非垞に䞍䟿なfg()/bg()



暗いバヌゞョンたたは明るいバヌゞョンのメ゜ッドの入力に枡す必芁がありたすfgBright()/bgBright()



方法では、1぀の倀では䞍十分です-少なくずも2぀色ず明るさが必芁です。したがっお、定数のリストず拡匵メ゜ッドを䜜成したすキュヌブの皮類ずヒヌロヌのクラスに色をマップバむンドしたす。



 protected enum class Color { BLACK, DARK_BLUE, DARK_GREEN, DARK_CYAN, DARK_RED, DARK_MAGENTA, DARK_YELLOW, LIGHT_GRAY, DARK_GRAY, LIGHT_BLUE, LIGHT_GREEN, LIGHT_CYAN, LIGHT_RED, LIGHT_MAGENTA, LIGHT_YELLOW, WHITE } protected fun Ansi.color(color: Color?): Ansi = when (color) { Color.BLACK -> fgBlack() Color.DARK_BLUE -> fgBlue() Color.DARK_GREEN -> fgGreen() Color.DARK_CYAN -> fgCyan() Color.DARK_RED -> fgRed() Color.DARK_MAGENTA -> fgMagenta() Color.DARK_YELLOW -> fgYellow() Color.LIGHT_GRAY -> fg(Ansi.Color.WHITE) Color.DARK_GRAY -> fgBrightBlack() Color.LIGHT_BLUE -> fgBrightBlue() Color.LIGHT_GREEN -> fgBrightGreen() Color.LIGHT_CYAN -> fgBrightCyan() Color.LIGHT_RED -> fgBrightRed() Color.LIGHT_MAGENTA -> fgBrightMagenta() Color.LIGHT_YELLOW -> fgBrightYellow() Color.WHITE -> fgBright(Ansi.Color.WHITE) else -> this } protected fun Ansi.background(color: Color?): Ansi = when (color) { Color.BLACK -> ansi.bg(Ansi.Color.BLACK) Color.DARK_BLUE -> ansi.bg(Ansi.Color.BLUE) Color.DARK_GREEN -> ansi.bgGreen() Color.DARK_CYAN -> ansi.bg(Ansi.Color.CYAN) Color.DARK_RED -> ansi.bgRed() Color.DARK_MAGENTA -> ansi.bgMagenta() Color.DARK_YELLOW -> ansi.bgYellow() Color.LIGHT_GRAY -> ansi.bg(Ansi.Color.WHITE) Color.DARK_GRAY -> ansi.bgBright(Ansi.Color.BLACK) Color.LIGHT_BLUE -> ansi.bgBright(Ansi.Color.BLUE) Color.LIGHT_GREEN -> ansi.bgBrightGreen() Color.LIGHT_CYAN -> ansi.bgBright(Ansi.Color.CYAN) Color.LIGHT_RED -> ansi.bgBrightRed() Color.LIGHT_MAGENTA -> ansi.bgBright(Ansi.Color.MAGENTA) Color.LIGHT_YELLOW -> ansi.bgBrightYellow() Color.WHITE -> ansi.bgBright(Ansi.Color.WHITE) else -> this } protected val dieColors = mapOf( Die.Type.PHYSICAL to Color.LIGHT_BLUE, Die.Type.SOMATIC to Color.LIGHT_GREEN, Die.Type.MENTAL to Color.LIGHT_MAGENTA, Die.Type.VERBAL to Color.LIGHT_YELLOW, Die.Type.DIVINE to Color.LIGHT_CYAN, Die.Type.WOUND to Color.DARK_GRAY, Die.Type.ENEMY to Color.DARK_RED, Die.Type.VILLAIN to Color.LIGHT_RED, Die.Type.OBSTACLE to Color.DARK_YELLOW, Die.Type.ALLY to Color.WHITE ) protected val heroColors = mapOf( Hero.Type.BRAWLER to Color.LIGHT_BLUE, Hero.Type.HUNTER to Color.LIGHT_GREEN )
      
      





珟圚、利甚可胜な16色のそれぞれは、単䞀の定数によっお䞀意に識別されたす。さらにいく぀かのナヌティリティメ゜ッドを蚘述したすが、その前にもう1぀



、テキスト文字列の定数をどこに保存するかを考えたす。



「文字列定数は、すべお1か所に保存されるように、別々のファむルに取り出す必芁がありたす。これにより、文字列定数の保守が容易になりたす。たた、ロヌカラむズにずっおも重芁です...」



文字列定数を別のファむルに移動する必芁がありたす...そうですね。我慢したす。この皮のリ゜ヌスを操䜜するための暙準Javaメカニズムは、java.util.ResourceBundle



ファむルを操䜜するオブゞェクトです.properties



。このようなファむルから始めたす。



 # Game status messages choose_dice_perform_check=Choose dice to perform check: end_of_turn_discard_extra=END OF TURN: Discard extra dice: end_of_turn_discard_optional=END OF TURN: Discard any dice, if needed: choose_action_before_exploration=Choose your action: choose_action_after_exploration=Already explored this turn. Choose what to do now: encounter_physical=Encountered PHYSICAL die. Need to pass respective check or lose this die. encounter_somatic=Encountered SOMATIC die. Need to pass respective check or lose this die. encounter_mental=Encountered MENTAL die. Need to pass respective check or lose this die. encounter_verbal=Encountered VERBAL die. Need to pass respective check or lose this die. encounter_divine=Encountered DIVINE die. Can be acquired automatically (no checks needed): die_acquire_success=You have acquired the die! die_acquire_failure=You have failed to acquire the die. game_loss_out_of_time=You ran out of time # Die types physical=PHYSICAL somatic=SOMATIC mental=MENTAL verbal=VERBAL divine=DIVINE ally=ALLY wound=WOUND enemy=ENEMY villain=VILLAIN obstacle=OBSTACLE # Hero types and descriptions brawler=Brawler hunter=Hunter # Various labels avg=avg bag=Bag bag_size=Bag size class=Class closed=Closed discard=Discard empty=Empty encountered=Encountered fail=Fail hand=Hand heros_turn=%s's turn max=max min=min perform_check=Perform check: pile=Pile received_new_die=Received new die result=Result success=Success sum=sum time=Time total=Total # Action names and descriptions action_confirm_key=ENTER action_confirm_name=Confirm action_cancel_key=ESC action_cancel_name=Cancel action_explore_location_key=E action_explore_location_name=xplore action_finish_turn_key=F action_finish_turn_name=inish action_hide_key=H action_hide_name=ide action_discard_key=D action_discard_name=iscard action_acquire_key=A action_acquire_name=cquire action_leave_key=L action_leave_name=eave action_forfeit_key=F action_forfeit_name=orfeit
      
      





各行には、文字で区切られたキヌず倀のペアが含たれおいたす=



。ファむルはどこにでも眮くこずができたす-䞻なこずは、ファむルぞのパスがクラスパスの䞀郚であるこずです。アクションのテキストは2぀の郚分で構成されおいるこずに泚意しおください。最初の文字は、画面に衚瀺されるずきに黄色で匷調衚瀺されるだけでなく、このアクションを実行するために抌す必芁があるキヌも決定したす。したがっお、それらを別々に保存するず䟿利です。



ただし、特定の圢匏たずえば、Androidでは文字列の保存方法が異なるから抜象化し、文字列定数を読み蟌むためのむンタヌフェむスに぀いお説明したす。



 interface StringLoader { fun loadString(key: String): String }
      
      





キヌは入力に送信され、出力は特定の行です。実装は、むンタヌフェヌス自䜓ず同じくらい簡単ですファむルがpathに沿っおいるず仮定したすsrc/main/resources/text/strings.properties



。



 class PropertiesStringLoader() : StringLoader { private val properties = ResourceBundle.getBundle("text.strings") override fun loadString(key: String) = properties.getString(key) ?: "" }
      
      





これdrawStatusMessage()



で、ゲヌム゚ンゞンの珟圚の状態を画面に衚瀺するStatusMessage



方法drawActionList()



ActionList



ず䜿甚可胜なアクションのリストを衚瀺する方法を実装するこずは難しくありたせん。魂だけが望む他の公匏な方法ず同様に。



たくさんのコヌドがありたすが、その䞀郚はすでに芋おいたす...だから、ここにネタバレがありたす
 abstract class ConsoleRenderer(private val strings: StringLoader) { protected lateinit var ansi: Ansi init { AnsiConsole.systemInstall() clearScreen() resetAnsi() } protected fun loadString(key: String) = strings.loadString(key) private fun resetAnsi() { ansi = Ansi.ansi() } fun clearScreen() { print(Ansi.ansi().eraseScreen(Ansi.Erase.ALL).cursor(1, 1)) } protected fun render() { ansi.cursor(CONSOLE_HEIGHT, CONSOLE_WIDTH) System.out.print(ansi.toString()) resetAnsi() } protected fun drawBigNumber(offsetX: Int, offsetY: Int, number: Int): Unit = with(ansi) { var currentX = offsetX cursor(offsetY, currentX) val text = number.toString() text.forEach { when (it) { '0' -> { cursor(offsetY, currentX) a(" ███ ") cursor(offsetY + 1, currentX) a("█ █ ") cursor(offsetY + 2, currentX) a("█ █ ") cursor(offsetY + 3, currentX) a("█ █ ") cursor(offsetY + 4, currentX) a(" ███ ") } '1' -> { cursor(offsetY, currentX) a(" █ ") cursor(offsetY + 1, currentX) a(" ██ ") cursor(offsetY + 2, currentX) a("█ █ ") cursor(offsetY + 3, currentX) a(" █ ") cursor(offsetY + 4, currentX) a("█████ ") } '2' -> { cursor(offsetY, currentX) a(" ███ ") cursor(offsetY + 1, currentX) a("█ █ ") cursor(offsetY + 2, currentX) a(" █ ") cursor(offsetY + 3, currentX) a(" █ ") cursor(offsetY + 4, currentX) a("█████ ") } '3' -> { cursor(offsetY, currentX) a("████ ") cursor(offsetY + 1, currentX) a(" █ ") cursor(offsetY + 2, currentX) a(" ██ ") cursor(offsetY + 3, currentX) a(" █ ") cursor(offsetY + 4, currentX) a("████ ") } '4' -> { cursor(offsetY, currentX) a(" █ ") cursor(offsetY + 1, currentX) a(" ██ ") cursor(offsetY + 2, currentX) a(" █ █ ") cursor(offsetY + 3, currentX) a("█████ ") cursor(offsetY + 4, currentX) a(" █ ") } '5' -> { cursor(offsetY, currentX) a("█████ ") cursor(offsetY + 1, currentX) a("█ ") cursor(offsetY + 2, currentX) a("████ ") cursor(offsetY + 3, currentX) a(" █ ") cursor(offsetY + 4, currentX) a("████ ") } '6' -> { cursor(offsetY, currentX) a(" ███ ") cursor(offsetY + 1, currentX) a("█ ") cursor(offsetY + 2, currentX) a("████ ") cursor(offsetY + 3, currentX) a("█ █ ") cursor(offsetY + 4, currentX) a(" ███ ") } '7' -> { cursor(offsetY, currentX) a("█████ ") cursor(offsetY + 1, currentX) a(" █ ") cursor(offsetY + 2, currentX) a(" █ ") cursor(offsetY + 3, currentX) a(" █ ") cursor(offsetY + 4, currentX) a(" █ ") } '8' -> { cursor(offsetY, currentX) a(" ███ ") cursor(offsetY + 1, currentX) a("█ █ ") cursor(offsetY + 2, currentX) a(" ███ ") cursor(offsetY + 3, currentX) a("█ █ ") cursor(offsetY + 4, currentX) a(" ███ ") } '9' -> { cursor(offsetY, currentX) a(" ███ ") cursor(offsetY + 1, currentX) a("█ █ ") cursor(offsetY + 2, currentX) a(" ████ ") cursor(offsetY + 3, currentX) a(" █ ") cursor(offsetY + 4, currentX) a(" ███ ") } } currentX += 6 } } protected fun drawHorizontalLine(offsetY: Int, filler: Char) { ansi.cursor(offsetY, 1) (1..CONSOLE_WIDTH).forEach { ansi.a(filler) } } protected fun drawBlankLine(offsetY: Int, drawBorders: Boolean = true) { ansi.cursor(offsetY, 1) if (drawBorders) { ansi.a('│') (2 until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } else { ansi.eraseLine(Ansi.Erase.ALL) } } protected fun drawCenteredCaption(offsetY: Int, text: String, color: Color, drawBorders: Boolean = true) { val center = (CONSOLE_WIDTH - text.length) / 2 ansi.cursor(offsetY, 1) ansi.a(if (drawBorders) '│' else ' ') (2 until center).forEach { ansi.a(' ') } ansi.color(color).a(text).reset() (text.length + center until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a(if (drawBorders) '│' else ' ') } protected fun drawStatusMessage(offsetY: Int, message: StatusMessage, drawBorders: Boolean = true) { //Setup val messageText = loadString(message.toString().toLowerCase()) var currentX = 1 val rightBorder = CONSOLE_WIDTH - if (drawBorders) 1 else 0 //Left border ansi.cursor(offsetY, 1) if (drawBorders) { ansi.a('│') currentX++ } ansi.a(' ') currentX++ //Text ansi.a(messageText) currentX += messageText.length //Right border (currentX..rightBorder).forEach { ansi.a(' ') } if (drawBorders) { ansi.a('│') } } protected fun drawActionList(offsetY: Int, actions: ActionList, drawBorders: Boolean = true) { val rightBorder = CONSOLE_WIDTH - if (drawBorders) 1 else 0 var currentX = 1 //Left border ansi.cursor(offsetY, 1) if (drawBorders) { ansi.a('│') currentX++ } ansi.a(' ') currentX++ //List of actions actions.forEach { action -> val key = loadString("action_${action.toString().toLowerCase()}_key") val name = loadString("action_${action.toString().toLowerCase()}_name") val length = key.length + 2 + name.length if (currentX + length >= rightBorder) { (currentX..rightBorder).forEach { ansi.a(' ') } if (drawBorders) { ansi.a('│') } ansi.cursor(offsetY + 1, 1) currentX = 1 if (drawBorders) { ansi.a('│') currentX++ } ansi.a(' ') currentX++ } if (action.isEnabled) { ansi.color(Color.LIGHT_YELLOW) } ansi.a('(').a(key).a(')').reset() ansi.a(name) ansi.a(" ") currentX += length + 2 } //Right border (currentX..rightBorder).forEach { ansi.a(' ') } if (drawBorders) { ansi.a('│') } } protected enum class Color { BLACK, DARK_BLUE, DARK_GREEN, DARK_CYAN, DARK_RED, DARK_MAGENTA, DARK_YELLOW, LIGHT_GRAY, DARK_GRAY, LIGHT_BLUE, LIGHT_GREEN, LIGHT_CYAN, LIGHT_RED, LIGHT_MAGENTA, LIGHT_YELLOW, WHITE } protected fun Ansi.color(color: Color?): Ansi = when (color) { Color.BLACK -> fgBlack() Color.DARK_BLUE -> fgBlue() Color.DARK_GREEN -> fgGreen() Color.DARK_CYAN -> fgCyan() Color.DARK_RED -> fgRed() Color.DARK_MAGENTA -> fgMagenta() Color.DARK_YELLOW -> fgYellow() Color.LIGHT_GRAY -> fg(Ansi.Color.WHITE) Color.DARK_GRAY -> fgBrightBlack() Color.LIGHT_BLUE -> fgBrightBlue() Color.LIGHT_GREEN -> fgBrightGreen() Color.LIGHT_CYAN -> fgBrightCyan() Color.LIGHT_RED -> fgBrightRed() Color.LIGHT_MAGENTA -> fgBrightMagenta() Color.LIGHT_YELLOW -> fgBrightYellow() Color.WHITE -> fgBright(Ansi.Color.WHITE) else -> this } protected fun Ansi.background(color: Color?): Ansi = when (color) { Color.BLACK -> ansi.bg(Ansi.Color.BLACK) Color.DARK_BLUE -> ansi.bg(Ansi.Color.BLUE) Color.DARK_GREEN -> ansi.bgGreen() Color.DARK_CYAN -> ansi.bg(Ansi.Color.CYAN) Color.DARK_RED -> ansi.bgRed() Color.DARK_MAGENTA -> ansi.bgMagenta() Color.DARK_YELLOW -> ansi.bgYellow() Color.LIGHT_GRAY -> ansi.bg(Ansi.Color.WHITE) Color.DARK_GRAY -> ansi.bgBright(Ansi.Color.BLACK) Color.LIGHT_BLUE -> ansi.bgBright(Ansi.Color.BLUE) Color.LIGHT_GREEN -> ansi.bgBrightGreen() Color.LIGHT_CYAN -> ansi.bgBright(Ansi.Color.CYAN) Color.LIGHT_RED -> ansi.bgBrightRed() Color.LIGHT_MAGENTA -> ansi.bgBright(Ansi.Color.MAGENTA) Color.LIGHT_YELLOW -> ansi.bgBrightYellow() Color.WHITE -> ansi.bgBright(Ansi.Color.WHITE) else -> this } protected val dieColors = mapOf( Die.Type.PHYSICAL to Color.LIGHT_BLUE, Die.Type.SOMATIC to Color.LIGHT_GREEN, Die.Type.MENTAL to Color.LIGHT_MAGENTA, Die.Type.VERBAL to Color.LIGHT_YELLOW, Die.Type.DIVINE to Color.LIGHT_CYAN, Die.Type.WOUND to Color.DARK_GRAY, Die.Type.ENEMY to Color.DARK_RED, Die.Type.VILLAIN to Color.LIGHT_RED, Die.Type.OBSTACLE to Color.DARK_YELLOW, Die.Type.ALLY to Color.WHITE ) protected val heroColors = mapOf( Hero.Type.BRAWLER to Color.LIGHT_BLUE, Hero.Type.HUNTER to Color.LIGHT_GREEN ) protected open fun shortcut(index: Int) = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"[index] }
      
      





なぜ私たち党員がこれをしたのですかはい、この玠晎らしいclassからむンタヌフェヌス実装を継承するためにGameRenderer



。



クラス図




これは、最初の最も単玔なメ゜ッドの実装がどのように芋えるかです



 override fun drawGameLoss(message: StatusMessage) { val centerY = CONSOLE_HEIGHT / 2 (1 until centerY).forEach { drawBlankLine(it, false) } val data = loadString(message.toString().toLowerCase()).toUpperCase() drawCenteredCaption(centerY, data, LIGHT_RED, false) (centerY + 1..CONSOLE_HEIGHT).forEach { drawBlankLine(it, false) } render() }
      
      





超自然的なものではなくdata



、画面の䞭倮にテキスト行を赀で1぀だけ描画したすdrawCenteredCaption()



。コヌドの残りは、画面の残りを空癜行で埋めたす。おそらく誰かがこれが必芁な理由を尋ねるでしょう-結局のずころclearScreen()



、メ゜ッドがあり、メ゜ッドの最初にそれを呌び出し、画面をクリアしおから、必芁なテキストを描画するだけで十分です。残念ながら、これは私たちが䜿甚しない怠laなアプロヌチです。その理由は非垞に簡単です。このアプロヌチでは、画面䞊の䞀郚の䜍眮が2回描画され、特に画面が連続しお数回連続しお描画される堎合アニメヌション䞭に、ちら぀きが顕著になりたす。したがっお、私たちのタスクは、適切な堎所に適切なキャラクタヌを描くだけでなく、党䜓を埋めるこずです空の文字を含む画面の残りの郚分他のレンダリングからのアヌティファクトが画面に残らないようにするため。そしお、このタスクはそれほど単玔ではありたせん。



次の方法は、この原則に埓いたす。



 override fun drawHeroTurnStart(hero: Hero) { val centerY = (CONSOLE_HEIGHT - 5) / 2 (1 until centerY).forEach { drawBlankLine(it, false) } ansi.color(heroColors[hero.type]) drawHorizontalLine(centerY, '─') drawHorizontalLine(centerY + 4, '─') ansi.reset() ansi.cursor(centerY + 1, 1).eraseLine() ansi.cursor(centerY + 3, 1).eraseLine() ansi.cursor(centerY + 2, 1) val text = String.format(loadString("heros_turn"), hero.name.toUpperCase()) val index = text.indexOf(hero.name.toUpperCase()) val center = (CONSOLE_WIDTH - text.length) / 2 ansi.cursor(centerY + 2, center) ansi.eraseLine(Ansi.Erase.BACKWARD) ansi.a(text.substring(0, index)) ansi.color(heroColors[hero.type]).a(hero.name.toUpperCase()).reset() ansi.a(text.substring(index + hero.name.length)) ansi.eraseLine(Ansi.Erase.FORWARD) (centerY + 5..CONSOLE_HEIGHT).forEach { drawBlankLine(it, false) } render() }
      
      





ここでは、䞭倮揃えのテキストに加えお、2本の氎平線もありたす䞊のスクリヌンショットを参照。䞭倮のレタリングは2色で衚瀺されるこずに泚意しおください。たた、孊校で数孊を孊ぶこずはただ有甚であるこずを確認しおください。



さお、最も単玔なメ゜ッドを芋お、実装を知る時が来たしたdrawLocationInteriorScreen()



。あなた自身が理解しおいるように、ここにはさらに桁違いのコヌドがありたす。さらに、画面のコンテンツはナヌザヌのアクションに応じお動的に倉化するため、垞に再描画する必芁がありたすアニメヌションを䜿甚する堎合もありたす。さお、最終的にあなたを終わらせるために䞊蚘のスクリヌンショットに加えお、このメ゜ッドのフレヌムワヌクでは、さらに3぀の衚瀺を実装する必芁があるこずを想像しおください



1.袋からキュヌブを取り出しお䌚う




2.テストに合栌するサむコロを遞択する




3.テスト結果の衚瀺




したがっお、ここにあなたぞの私の玠晎らしいアドバむスがありたす1぀のメ゜ッドにすべおのコヌドを抌し蟌たないでください。実装をいく぀かのメ゜ッドに分割したす各メ゜ッドが1回だけ呌び出される堎合でも。さお、「ゎム」を忘れないでください。



それがあなたの目に波王し始めたら、数秒間たばたきしおください-これは圹立぀はずです
 class ConsoleGameRenderer(loader: StringLoader) : ConsoleRenderer(loader), GameRenderer { private fun drawLocationTopPanel(location: Location, heroesAtLocation: List<Hero>, currentHero: Hero, timer: Int) { val closedString = loadString("closed").toLowerCase() val timeString = loadString("time") val locationName = location.name.toString().toUpperCase() val separatorX1 = locationName.length + if (location.isOpen) { 6 + if (location.bag.size >= 10) 2 else 1 } else { closedString.length + 7 } val separatorX2 = CONSOLE_WIDTH - timeString.length - 6 - if (timer >= 10) 1 else 0 //Top border ansi.cursor(1, 1) ansi.a('┌') (2 until CONSOLE_WIDTH).forEach { ansi.a(if (it == separatorX1 || it == separatorX2) '┬' else '─') } ansi.a('┐') //Center row ansi.cursor(2, 1) ansi.a("│ ") if (location.isOpen) { ansi.color(WHITE).a(locationName).reset() ansi.a(": ").a(location.bag.size) } else { ansi.a(locationName).reset() ansi.color(DARK_GRAY).a(" (").a(closedString).a(')').reset() } ansi.a(" │") var currentX = separatorX1 + 2 heroesAtLocation.forEach { hero -> ansi.a(' ') ansi.color(heroColors[hero.type]) ansi.a(if (hero === currentHero) '☻' else '').reset() currentX += 2 } (currentX..separatorX2).forEach { ansi.a(' ') } ansi.a("│ ").a(timeString).a(": ") when { timer <= 5 -> ansi.color(LIGHT_RED) timer <= 15 -> ansi.color(LIGHT_YELLOW) else -> ansi.color(LIGHT_GREEN) } ansi.bold().a(timer).reset().a(" │") //Bottom border ansi.cursor(3, 1) ansi.a('├') (2 until CONSOLE_WIDTH).forEach { ansi.a(if (it == separatorX1 || it == separatorX2) '┮' else '─') } ansi.a('─') } private fun drawLocationHeroPanel(offsetY: Int, hero: Hero) { val bagString = loadString("bag").toUpperCase() val discardString = loadString("discard").toUpperCase() val separatorX1 = hero.name.length + 4 val separatorX3 = CONSOLE_WIDTH - discardString.length - 6 - if (hero.discardPile.size >= 10) 1 else 0 val separatorX2 = separatorX3 - bagString.length - 6 - if (hero.bag.size >= 10) 1 else 0 //Top border ansi.cursor(offsetY, 1) ansi.a('├') (2 until CONSOLE_WIDTH).forEach { ansi.a(if (it == separatorX1 || it == separatorX2 || it == separatorX3) '┬' else '─') } ansi.a('─') //Center row ansi.cursor(offsetY + 1, 1) ansi.a("│ ") ansi.color(heroColors[hero.type]).a(hero.name.toUpperCase()).reset() ansi.a(" │") val currentX = separatorX1 + 1 (currentX until separatorX2).forEach { ansi.a(' ') } ansi.a("│ ").a(bagString).a(": ") when { hero.bag.size <= hero.hand.capacity -> ansi.color(LIGHT_RED) else -> ansi.color(LIGHT_YELLOW) } ansi.a(hero.bag.size).reset() ansi.a(" │ ").a(discardString).a(": ") ansi.a(hero.discardPile.size) ansi.a(" │") //Bottom border ansi.cursor(offsetY + 2, 1) ansi.a('├') (2 until CONSOLE_WIDTH).forEach { ansi.a(if (it == separatorX1 || it == separatorX2 || it == separatorX3) '┮' else '─') } ansi.a('─') } private fun drawDieSize(die: Die, checked: Boolean = false) { when { checked -> ansi.background(dieColors[die.type]).color(BLACK) else -> ansi.color(dieColors[die.type]) } ansi.a(die.toString()).reset() } private fun drawDieFrameSmall(offsetX: Int, offsetY: Int, longDieSize: Boolean) { //Top border ansi.cursor(offsetY, offsetX) ansi.a('╔') (0 until if (longDieSize) 5 else 4).forEach { ansi.a('═') } ansi.a('╗') //Left border ansi.cursor(offsetY + 1, offsetX) ansi.a("║ ") //Bottom border ansi.cursor(offsetY + 2, offsetX) ansi.a("╚") (0 until if (longDieSize) 5 else 4).forEach { ansi.a('═') } ansi.a('╝') //Right border ansi.cursor(offsetY + 1, offsetX + if (longDieSize) 6 else 5) ansi.a('║') } private fun drawDieSmall(offsetX: Int, offsetY: Int, pair: DiePair, rollResult: Int? = null) { ansi.color(dieColors[pair.die.type]) val longDieSize = pair.die.size >= 10 drawDieFrameSmall(offsetX, offsetY, longDieSize) //Roll result or die size ansi.cursor(offsetY + 1, offsetX + 1) if (rollResult != null) { ansi.a(String.format(" %2d %s", rollResult, if (longDieSize) " " else "")) } else { ansi.a(' ').a(pair.die.toString()).a(' ') } //Draw modifier ansi.cursor(offsetY + 3, offsetX) val modString = if (pair.modifier == 0) "" else String.format("%+d", pair.modifier) val frameLength = 4 + if (longDieSize) 3 else 2 var spaces = (frameLength - modString.length) / 2 (0 until spaces).forEach { ansi.a(' ') } ansi.a(modString) spaces = frameLength - spaces - modString.length (0 until spaces).forEach { ansi.a(' ') } ansi.reset() } private fun drawDieFrameBig(offsetX: Int, offsetY: Int, longDieSize: Boolean) { //Top border ansi.cursor(offsetY, offsetX) ansi.a('╔') (0 until if (longDieSize) 3 else 2).forEach { ansi.a("══════") } ansi.a("═╗") //Left border (1..5).forEach { ansi.cursor(offsetY + it, offsetX) ansi.a('║') } //Bottom border ansi.cursor(offsetY + 6, offsetX) ansi.a('╚') (0 until if (longDieSize) 3 else 2).forEach { ansi.a("══════") } ansi.a("═╝") //Right border val currentX = offsetX + if (longDieSize) 20 else 14 (1..5).forEach { ansi.cursor(offsetY + it, currentX) ansi.a('║') } } private fun drawDieSizeBig(offsetX: Int, offsetY: Int, pair: DiePair) { ansi.color(dieColors[pair.die.type]) val longDieSize = pair.die.size >= 10 drawDieFrameBig(offsetX, offsetY, longDieSize) //Die size ansi.cursor(offsetY + 1, offsetX + 1) ansi.a(" ████ ") ansi.cursor(offsetY + 2, offsetX + 1) ansi.a(" █ █ ") ansi.cursor(offsetY + 3, offsetX + 1) ansi.a(" █ █ ") ansi.cursor(offsetY + 4, offsetX + 1) ansi.a(" █ █ ") ansi.cursor(offsetY + 5, offsetX + 1) ansi.a(" ████ ") drawBigNumber(offsetX + 8, offsetY + 1, pair.die.size) //Draw modifier ansi.cursor(offsetY + 7, offsetX) val modString = if (pair.modifier == 0) "" else String.format("%+d", pair.modifier) val frameLength = 4 + 6 * if (longDieSize) 3 else 2 var spaces = (frameLength - modString.length) / 2 (0 until spaces).forEach { ansi.a(' ') } ansi.a(modString) spaces = frameLength - spaces - modString.length - 1 (0 until spaces).forEach { ansi.a(' ') } ansi.reset() } private fun drawBattleCheck(offsetY: Int, battleCheck: DieBattleCheck) { val performCheck = loadString("perform_check") var currentX = 4 var currentY = offsetY //Top message ansi.cursor(offsetY, 1) ansi.a("│ ").a(performCheck) (performCheck.length + 4 until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') //Left border (1..4).forEach { ansi.cursor(offsetY + it, 1) ansi.a("│ ") } //Opponent var opponentWidth = 0 var vsWidth = 0 (battleCheck.getOpponentPair())?.let { //Die if (battleCheck.isRolled) { drawDieSmall(4, offsetY + 1, it, battleCheck.getOpponentResult()) } else { drawDieSmall(4, offsetY + 1, it) } opponentWidth = 4 + if (it.die.size >= 10) 3 else 2 currentX += opponentWidth //VS ansi.cursor(currentY + 1, currentX) ansi.a(" ") ansi.cursor(currentY + 2, currentX) ansi.color(LIGHT_YELLOW).a(" VS ").reset() ansi.cursor(currentY + 3, currentX) ansi.a(" ") ansi.cursor(currentY + 4, currentX) ansi.a(" ") vsWidth = 4 currentX += vsWidth } //Clear below for (row in currentY + 5..currentY + 8) { ansi.cursor(row, 1) ansi.a('│') (2 until currentX).forEach { ansi.a(' ') } } //Dice for (index in 0 until battleCheck.heroPairCount) { if (index > 0) { ansi.cursor(currentY + 1, currentX) ansi.a(" ") ansi.cursor(currentY + 2, currentX) ansi.a(if (battleCheck.method == DieBattleCheck.Method.SUM) " + " else " / ").reset() ansi.cursor(currentY + 3, currentX) ansi.a(" ") ansi.cursor(currentY + 4, currentX) ansi.a(" ") currentX += 3 } val pair = battleCheck.getHeroPairAt(index) val width = 4 + if (pair.die.size >= 10) 3 else 2 if (currentX + width + 3 > CONSOLE_WIDTH) { //Out of space for (row in currentY + 1..currentY + 4) { ansi.cursor(row, currentX) (currentX until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } currentY += 4 currentX = 4 + vsWidth + opponentWidth } if (battleCheck.isRolled) { drawDieSmall(currentX, currentY + 1, pair, battleCheck.getHeroResultAt(index)) } else { drawDieSmall(currentX, currentY + 1, pair) } currentX += width } //Clear the rest (currentY + 1..currentY + 4).forEach { row -> ansi.cursor(row, currentX) (currentX until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } if (currentY == offsetY) { //Still on the first line currentX = 4 + vsWidth + opponentWidth (currentY + 5..currentY + 8).forEach { row -> ansi.cursor(row, currentX) (currentX until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } } //Draw result (battleCheck.result)?.let { r -> val frameTopY = offsetY + 5 val result = String.format("%+d", r) val message = loadString(if (r >= 0) "success" else "fail").toUpperCase() val color = if (r >= 0) DARK_GREEN else DARK_RED //Frame ansi.color(color) drawHorizontalLine(frameTopY, '▒') drawHorizontalLine(frameTopY + 3, '▒') ansi.cursor(frameTopY + 1, 1).a("▒▒") ansi.cursor(frameTopY + 1, CONSOLE_WIDTH - 1).a("▒▒") ansi.cursor(frameTopY + 2, 1).a("▒▒") ansi.cursor(frameTopY + 2, CONSOLE_WIDTH - 1).a("▒▒") ansi.reset() //Top message val resultString = loadString("result") var center = (CONSOLE_WIDTH - result.length - resultString.length - 2) / 2 ansi.cursor(frameTopY + 1, 3) (3 until center).forEach { ansi.a(' ') } ansi.a(resultString).a(": ") ansi.color(color).a(result).reset() (center + result.length + resultString.length + 2 until CONSOLE_WIDTH - 1).forEach { ansi.a(' ') } //Bottom message center = (CONSOLE_WIDTH - message.length) / 2 ansi.cursor(frameTopY + 2, 3) (3 until center).forEach { ansi.a(' ') } ansi.color(color).a(message).reset() (center + message.length until CONSOLE_WIDTH - 1).forEach { ansi.a(' ') } } } private fun drawExplorationResult(offsetY: Int, pair: DiePair) { val encountered = loadString("encountered") ansi.cursor(offsetY, 1) ansi.a("│ ").a(encountered).a(':') (encountered.length + 5 until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') val dieFrameWidth = 3 + 6 * if (pair.die.size >= 10) 3 else 2 for (row in 1..8) { ansi.cursor(offsetY + row, 1) ansi.a("│ ") ansi.cursor(offsetY + row, dieFrameWidth + 4) (dieFrameWidth + 4 until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } drawDieSizeBig(4, offsetY + 1, pair) } private fun drawHand(offsetY: Int, hand: Hand, checkedDice: HandMask, activePositions: HandMask) { val handString = loadString("hand").toUpperCase() val alliesString = loadString("allies").toUpperCase() val capacity = hand.capacity val size = hand.dieCount val slots = max(size, capacity) val alliesSize = hand.allyDieCount var currentY = offsetY var currentX = 1 //Hand title ansi.cursor(currentY, currentX) ansi.a("│ ").a(handString) //Left border currentY += 1 currentX = 1 ansi.cursor(currentY, currentX) ansi.a("│ ╔") ansi.cursor(currentY + 1, currentX) ansi.a("│ ║") ansi.cursor(currentY + 2, currentX) ansi.a("│ ╚") ansi.cursor(currentY + 3, currentX) ansi.a("│ ") currentX += 3 //Main hand for (i in 0 until min(slots, MAX_HAND_SIZE)) { val die = hand.dieAt(i) val longDieName = die != null && die.size >= 10 //Top border ansi.cursor(currentY, currentX) if (i < capacity) { ansi.a("════").a(if (longDieName) "═" else "") } else { ansi.a("────").a(if (longDieName) "─" else "") } ansi.a(if (i < capacity - 1) 'â•€' else if (i == capacity - 1) '╗' else if (i < size - 1) '┬' else '┐') //Center row ansi.cursor(currentY + 1, currentX) ansi.a(' ') if (die != null) { drawDieSize(die, checkedDice.checkPosition(i)) } else { ansi.a(" ") } ansi.a(' ') ansi.a(if (i < capacity - 1) '│' else if (i == capacity - 1) '║' else '│') //Bottom border ansi.cursor(currentY + 2, currentX) if (i < capacity) { ansi.a("════").a(if (longDieName) '═' else "") } else { ansi.a("────").a(if (longDieName) '─' else "") } ansi.a(if (i < capacity - 1) '╧' else if (i == capacity - 1) '╝' else if (i < size - 1) '┮' else '┘') //Die number ansi.cursor(currentY + 3, currentX) if (activePositions.checkPosition(i)) { ansi.color(LIGHT_YELLOW) } ansi.a(String.format(" (%s) %s", shortcut(i), if (longDieName) " " else "")) ansi.reset() currentX += 5 + if (longDieName) 1 else 0 } //Ally subhand if (alliesSize > 0) { currentY = offsetY //Ally title ansi.cursor(currentY, handString.length + 5) (handString.length + 5 until currentX).forEach { ansi.a(' ') } ansi.a(" ").a(alliesString) (currentX + alliesString.length + 5 until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') //Left border currentY += 1 ansi.cursor(currentY, currentX) ansi.a(" ┌") ansi.cursor(currentY + 1, currentX) ansi.a(" │") ansi.cursor(currentY + 2, currentX) ansi.a(" └") ansi.cursor(currentY + 3, currentX) ansi.a(" ") currentX += 4 //Ally slots for (i in 0 until min(alliesSize, MAX_HAND_ALLY_SIZE)) { val allyDie = hand.allyDieAt(i)!! val longDieName = allyDie.size >= 10 //Top border ansi.cursor(currentY, currentX) ansi.a("────").a(if (longDieName) "─" else "") ansi.a(if (i < alliesSize - 1) '┬' else '┐') //Center row ansi.cursor(currentY + 1, currentX) ansi.a(' ') drawDieSize(allyDie, checkedDice.checkAllyPosition(i)) ansi.a(" │") //Bottom border ansi.cursor(currentY + 2, currentX) ansi.a("────").a(if (longDieName) "─" else "") ansi.a(if (i < alliesSize - 1) '┮' else '┘') //Die number ansi.cursor(currentY + 3, currentX) if (activePositions.checkAllyPosition(i)) { ansi.color(LIGHT_YELLOW) } ansi.a(String.format(" (%s) %s", shortcut(i + 10), if (longDieName) " " else "")).reset() currentX += 5 + if (longDieName) 1 else 0 } } else { ansi.cursor(offsetY, 9) (9 until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') ansi.cursor(offsetY + 4, currentX) (currentX until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } //Clear the end of the line (0..3).forEach { row -> ansi.cursor(currentY + row, currentX) (currentX until CONSOLE_WIDTH).forEach { ansi.a(' ') } ansi.a('│') } } override fun drawHeroTurnStart(hero: Hero) { val centerY = (CONSOLE_HEIGHT - 5) / 2 (1 until centerY).forEach { drawBlankLine(it, false) } ansi.color(heroColors[hero.type]) drawHorizontalLine(centerY, '─') drawHorizontalLine(centerY + 4, '─') ansi.reset() ansi.cursor(centerY + 1, 1).eraseLine() ansi.cursor(centerY + 3, 1).eraseLine() ansi.cursor(centerY + 2, 1) val text = String.format(loadString("heros_turn"), hero.name.toUpperCase()) val index = text.indexOf(hero.name.toUpperCase()) val center = (CONSOLE_WIDTH - text.length) / 2 ansi.cursor(centerY + 2, center) ansi.eraseLine(Ansi.Erase.BACKWARD) ansi.a(text.substring(0, index)) ansi.color(heroColors[hero.type]).a(hero.name.toUpperCase()).reset() ansi.a(text.substring(index + hero.name.length)) ansi.eraseLine(Ansi.Erase.FORWARD) (centerY + 5..CONSOLE_HEIGHT).forEach { drawBlankLine(it, false) } render() } override fun drawLocationInteriorScreen( location: Location, heroesAtLocation: List<Hero>, timer: Int, currentHero: Hero, battleCheck: DieBattleCheck?, encounteredDie: DiePair?, pickedDice: HandMask, activePositions: HandMask, statusMessage: StatusMessage, actions: ActionList) { //Top panel drawLocationTopPanel(location, heroesAtLocation, currentHero, timer) //Encounter info when { battleCheck != null -> drawBattleCheck(4, battleCheck) encounteredDie != null -> drawExplorationResult(4, encounteredDie) else -> (4..12).forEach { drawBlankLine(it) } } //Fill blank space val bottomHalfTop = CONSOLE_HEIGHT - 11 (13 until bottomHalfTop).forEach { drawBlankLine(it) } //Hero-specific info drawLocationHeroPanel(bottomHalfTop, currentHero) drawHand(bottomHalfTop + 3, currentHero.hand, pickedDice, activePositions) //Separator ansi.cursor(bottomHalfTop + 8, 1) ansi.a('├') (2 until CONSOLE_WIDTH).forEach { ansi.a('─') } ansi.a('─') //Status and actions drawStatusMessage(bottomHalfTop + 9, statusMessage) drawActionList(bottomHalfTop + 10, actions) //Bottom border ansi.cursor(CONSOLE_HEIGHT, 1) ansi.a('└') (2 until CONSOLE_WIDTH).forEach { ansi.a('─') } ansi.a('┘') //Finalize render() } override fun drawGameLoss(message: StatusMessage) { val centerY = CONSOLE_HEIGHT / 2 (1 until centerY).forEach { drawBlankLine(it, false) } val data = loadString(message.toString().toLowerCase()).toUpperCase() drawCenteredCaption(centerY, data, LIGHT_RED, false) (centerY + 1..CONSOLE_HEIGHT).forEach { drawBlankLine(it, false) } render() } }
      
      





このすべおのコヌドの動䜜をチェックするこずに関連する1぀の小さな問題がありたす。組み蟌みのIDEコン゜ヌルはANSI゚スケヌプシヌケンスをサポヌトしないため、倖郚端末でアプリケヌションを起動する必芁がありたす以前に起動するためのスクリプトを既に䜜成したした。さらに、ANSIサポヌトでは、Windowsですべおがうたくいくわけではありたせん-私の知る限り、暙準のcmd.exeでのみ高品質の衚瀺が可胜になりたすそしお、私たちが焊点を圓おない問題もありたす。たた、PowerShellはシヌケンスを認識するこずをすぐには孊習したせんでした珟圚の芁求にもかかわらず。運が悪い堎合は、萜胆しないでください-垞に代替゜リュヌションがありたすたずえば、これ。そしお次に進みたす。



ステップ10 ナヌザヌ入力



画面に画像を衚瀺するこずは、戊いの半分です。ナヌザヌから制埡コマンドを正しく受信するこずも同様に重芁です。たた、このタスクは、以前のすべおのタスクよりも実装が技術的にはるかに困難であるこずが刀明する可胜性がありたす。しかし、たず最初に。



あなたが思い出すように、クラスメ゜ッドを実装する必芁に盎面しおいたすGameInteractor



。それらは3぀しかありたせんが、特別な泚意が必芁です。たず、同期。プレむダヌがキヌを抌すたで、ゲヌム゚ンゞンの䜜業を䞭断する必芁がありたす。第二に、クリック凊理。残念ながら、暙準クラスの容量はReader



、Scanner



、Console



これらのほずんどの抌しを認識するために十分ではありたせん。私たちは、各コマンドの埌にEnterキヌを抌しにナヌザを必芁ずしたせん。KeyListener



'aのようなものが必芁ですが、それはSwingフレヌムワヌクにしっかりず接続されおおり、コン゜ヌルアプリケヌションにはこのグラフィック芋掛け倒しはありたせん。



どうするもちろん、ラむブラリの怜玢は、今回は完党にネむティブコヌドに䟝存したす。 「さようなら、クロスプラットフォヌム」ずはどういう意味ですか残念ながら、軜量でプラットフォヌムに䟝存しない圢匏でシンプルな機胜を実装するラむブラリをただ芋぀けおいたせん。それたでの間、モンスタヌjLineに泚目したしょう。jLineは、コン゜ヌルで高床なナヌザヌむンタヌフェむスを構築するためのハヌベスタヌを実装しおいたす。はい、ネむティブ実装です。はい、WindowsずLinux / UNIXの䞡方をサポヌトしたす適切なラむブラリを提䟛するこずにより。そしお、はい、䜿甚䞊のその機胜のほずんどは、我々は癟幎は必芁ありたせん。必芁なのは、小さな、䞍十分に文曞化された機䌚だけであり、その䜜業をこれから分析したす。



 <dependency> <groupId>jline</groupId> <artifactId>jline</artifactId> <version>2.14.6</version> <scope>compile</scope> </dependency>
      
      





3番目の最新バヌゞョンではなく、ConsoleReader



methodを持぀クラスがある2番目のバヌゞョンが必芁であるこずに泚意しおくださいreadCharacter()



。名前が瀺すように、このメ゜ッドはキヌボヌドで抌された文字のコヌドを返したす同時に動䜜したすが、これが必芁です。残りは技術的な問題です。シンボルずアクションのタむプ間の察応衚Action.Type



を䜜成し、䞀方をクリックしお他方を返したす。



「キヌボヌドのすべおのキヌが1文字で衚珟できるわけではないこずをご存知ですか倚くのキヌは、2、3、4文字の゚スケヌプシヌケンスを䜿甚したす。圌らず䞀緒にいる方法は」



「文字以倖のキヌ」矢印、Fキヌ、ホヌム、挿入、PgUp / Dn、終了、削陀、テンキヌなどを認識したい堎合、入力タスクは耇雑になるこずに泚意しおください。しかし、私たちは望んでいないので、継続したす。ConsoleInteractor



必芁なサヌビスメ゜ッドを持぀クラスを䜜成したしょう。



 abstract class ConsoleInteractor { private val reader = ConsoleReader() private val mapper = mapOf( CONFIRM to 13.toChar(), CANCEL to 27.toChar(), EXPLORE_LOCATION to 'e', FINISH_TURN to 'f', ACQUIRE to 'a', LEAVE to 'l', FORFEIT to 'f', HIDE to 'h', DISCARD to 'd', ) protected fun read() = reader.readCharacter().toChar() protected open fun getIndexForKey(key: Char) = "1234567890abcdefghijklmnopqrstuvw".indexOf(key) }
      
      





マップmapper



ずメ゜ッドを蚭定したすread()



。さらにgetIndexForKey()



、リストからアむテムを遞択する必芁がある堎合や、手からキュヌブを遞択する必芁がある堎合に䜿甚する方法を提䟛したす。このクラスからむンタヌフェヌス実装を継承するこずは残っおいたすGameInteractor



。



クラス図




そしお、実際には、コヌド



 class ConsoleGameInteractor : ConsoleInteractor(), GameInteractor { override fun anyInput() { read() } override fun pickAction(list: ActionList): Action { while (true) { val key = read() list .filter(Action::isEnabled) .find { mapper[it.type] == key } ?.let { return it } } } override fun pickDiceFromHand(activePositions: HandMask, actions: ActionList) : Action { while (true) { val key = read() actions.forEach { if (mapper[it.type] == key && it.isEnabled) return it } when (key) { in '1'..'9' -> { val index = key - '1' if (activePositions.checkPosition(index)) { return Action(HAND_POSITION, data = index) } } '0' -> { if (activePositions.checkPosition(9)) { return Action(HAND_POSITION, data = 9) } } in 'a'..'f' -> { val allyIndex = key - 'a' if (activePositions.checkAllyPosition(allyIndex)) { return Action(HAND_ALLY_POSITION, data = allyIndex) } } } } } }
      
      





メ゜ッドの実装は、さたざたな䞍適切なナンセンスを出さないように、非垞に䞁寧で瀌儀正しいものです。遞択したアクションがアクティブであり、遞択した手の䜍眮が蚱容セットに含たれおいるこずを確認したす。そしお、私たち党員が私たちの呚りの人々に察しお䞁寧であるこずを願っおいたす。



ステップ11。音ず音楜



しかし、それなしではどうでしょうかサりンドをオフにしおゲヌムをプレむしたこずがある堎合たずえば、自宅に誰もいないずきにタブレットをカバヌの䞋に眮いた堎合、どれだけ負けおいるかを実感できたかもしれたせん。ゲヌムの半分だけをプレむするようなものです。倚くのゲヌムは、音の䌎奏なしでは想像できたせん。倚くの堎合、これは䞍可逆的な芁件ですが、逆の状況がありたすたずえば、原則ずしお音がない堎合、たたはそれらがなければ悲惚であり、それらがなければ良いでしょう。良い仕事をするこずは、実際には䞀芋したほど簡単ではありたせん理由は倧芏暡なスタゞオで高床な専門家がこれを行うためですが、それは、ほずんどの堎合、ゲヌム内にオヌディオコンポヌネント少なくずもいく぀かを持っおいる方がはるかに良いこずです圌女が党くいないより最埌の手段ずしお、埌で音質を改善できたすが、時間ず気分が蚱すずき。



このゞャンルの特性により、私たちのゲヌムは傑䜜のサりンド゚フェクトによっお特城付けられるこずはありたせん。ボヌドゲヌムのデゞタル版をプレむした堎合、その意味は理解できたす。音はその均䞀性をはじき、すぐに退屈になり、しばらくするず、音なしで挔奏するこずは深刻な損倱のようには芋えなくなりたす。問題は、この珟象に察凊する効果的な方法がないずいう事実によっお悪化したす。ゲヌムサりンドを完党に異なるものに眮き換えるず、時間が経぀ずうんざりしたす。良いゲヌムでは、サりンドはゲヌムプレむを補完し、進行䞭のアクションの雰囲気を明らかにし、生き生きずしたものにしたす-雰囲気がただのゎミ袋のあるテヌブルであり、ゲヌムプレむ党䜓がサむコロを投げるこずで構成されおいる堎合、これを達成するこずは困難です。それにもかかわらず、これはたさに私たちが発蚀するものです。シルクはここにあり、キャストはここにあり、ざわめき、倧きな叫び声にざわめきたす。たるでスクリヌン䞊の画像を芳察しおいないかのように、実際の物理的なオブゞェクトず実際にやり取りしおいるように。それらは完党に、しかし控えめに発声する必芁がありたす-スクリプト党䜓で同じ100回聞こえるので、音が前面に出おはなりたせん-ゲヌムプレむを穏やかにシェヌディングするだけです。これを有胜に達成する方法はわからない、音が特別じゃない。目立぀欠陥に気づき、磚くように、できるだけゲヌムをプレむするこずをお勧めしたすこのアドバむスは、音だけでなく適甚されたす。これを有胜に達成する方法はわからない、音が特別じゃない。目立぀欠陥に気づき、磚くように、できるだけゲヌムをプレむするこずをお勧めしたすこのアドバむスは、音だけでなく適甚されたす。これを有胜に達成する方法はわからない、音が特別じゃない。目立぀欠陥に気づき、磚くように、できるだけゲヌムをプレむするこずをお勧めしたすこのアドバむスは、音だけでなく適甚されたす。



理論で、それを敎理し、それが緎習に移る時が来たようです。そしおその前に、質問をする必芁がありたす。実際、ゲヌムファむルをどこで取埗するのでしょうか。最も簡単で確実な方法-叀いマむクを䜿甚しお、たたは電話を䜿甚しおも、芋苊しい品質で自分で録音するこずができたす。むンタヌネットには、パむナップルの䞊郚のねじを緩めたり、ブヌツで氷を砕いたりしお、骚を砕いおカリカリずした背骚の効果を実珟する方法に関するビデオがたくさんありたす。シュルレアリスムの矎孊に銎染みのない人は、自分の声や台所甚品を楜噚ずしお䜿うこずができたすこれが行われた䟋や成功䟋さえありたす。たたは、freesound.orgにアクセスできたすずっず前に他の癟人があなたのためにこれをした堎所。ラむセンスのみに泚意しおください倚くの著者は、倧きな咳や床に投げられたコむンのオヌディオ録音に非垞に敏感です-あなたは決しお、元の䜜成者にお金を払ったり、圌の創造的な仮名に蚀及しないで圌らの劎働の成果を悪甚するこずは決しおしたせん時には非垞に奇劙ですコメントで。



奜きなファむルをドラッグアンドドロップしお、クラスパスのどこかに眮きたす。それらを識別するために、各むンスタンスが1぀のサりンド゚フェクトに察応する列挙を䜿甚したす。



 enum class Sound { TURN_START, //Hero starts the turn BATTLE_CHECK_ROLL, //Perform check, type BATTLE_CHECK_SUCCESS, //Check was successful BATTLE_CHECK_FAILURE, //Check failed DIE_DRAW, //Draw die from bag DIE_HIDE, //Remove die to bag DIE_DISCARD, //Remove die to pile DIE_REMOVE, //Remove die entirely DIE_PICK, //Check/uncheck the die TRAVEL, //Move hero to another location ENCOUNTER_STAT, //Hero encounters STAT die ENCOUNTER_DIVINE, //Hero encounters DIVINE die ENCOUNTER_ALLY, //Hero encounters ALLY die ENCOUNTER_WOUND, //Hero encounters WOUND die ENCOUNTER_OBSTACLE, //Hero encounters OBSTACLE die ENCOUNTER_ENEMY, //Hero encounters ENEMY die ENCOUNTER_VILLAIN, //Hero encounters VILLAIN die DEFEAT_OBSTACLE, //Hero defeats OBSTACLE die DEFEAT_ENEMY, //Hero defeats ENEMY die DEFEAT_VILLAIN, //Hero defeats VILLAIN die TAKE_DAMAGE, //Hero takes damage HERO_DEATH, //Hero death CLOSE_LOCATION, //Location closed GAME_VICTORY, //Scenario completed GAME_LOSS, //Scenario failed ERROR, //When something unexpected happens }
      
      





サりンドの再生方法はハヌドりェアプラットフォヌムによっお異なるため、むンタヌフェむスを䜿甚しお特定の実装から抜象化できたす。たずえば、これは



 interface SoundPlayer { fun play(sound: Sound) }
      
      





前述のむンタヌフェむスGameRenderer



およびず同様にGameInteractor



、その実装もクラスむンスタンスぞの入力に枡す必芁がありたすGame



。たず、実装は次のようになりたす。



 class MuteSoundPlayer : SoundPlayer { override fun play(sound: Sound) { //Do nothing } }
      
      





その埌、より興味深い実装を怜蚎したすが、今は音楜に぀いお話したしょう。

効果音ず同様に、ゲヌムの雰囲気を䜜り出す䞊で倧きな圹割を果たしたす。同様に、優れたゲヌムは䞍適切な音楜によっお台無しにされる可胜性がありたす。音ず同様に、音楜は控えめで、芞術的な効果が必芁な堎合を陀いお、画面䞊のアクションに適切に察応する必芁がありたす埅ち䌏せされ容赊なく殺されたメむンの運呜を誰かが真剣に吹き蟌んだこずを期埅しないでくださいヒヌロヌ、圌の悲劇的な死の堎面が子䟛の歌からの楜しい小さな音楜を䌎う堎合。これを達成するのは非垞に難しく、特別に蚓緎された人々がそのような問題に察凊したす私たちはそれらに䞍慣れですが、ゲヌム構築の倩才の初心者ずしお、私たちも䜕かをするこずができたす。たずえば、どこかに行くfreemusicarchive.orgたたはsoundcloud.comやYouTubeず自分奜みに䜕かを芋぀けたす。デスクトップの堎合、アンビ゚ントは良い遞択です。はっきりしたメロディヌのない静かで滑らかな音楜で、背景の䜜成に適しおいたす。ラむセンスに泚意を払っおください。無料の音楜でさえ、金銭的な報酬ではないにしおも、少なくずも普遍的な認識に倀する才胜のある䜜曲家によっお曞かれおいるこずもありたす。



もう1぀列挙を䜜成したしょう。



 enum class Music { SCENARIO_MUSIC_1, SCENARIO_MUSIC_2, SCENARIO_MUSIC_3, }
      
      





同様に、むンタヌフェヌスずそのデフォルトの実装を定矩したす。



 interface MusicPlayer { fun play(music: Music) fun stop() } class MuteMusicPlayer : MusicPlayer { override fun play(music: Music) { //Do nothing } override fun stop() { //Do nothing } }
      
      





この堎合、再生を開始する方法ず停止する方法の2぀が必芁です。たた、远加の方法䞀時停止/再開、巻き戻しなどが将来䟿利になる可胜性も十分にありたすが、これたでのずころ、この2぀で十分です。



毎回オブゞェクト間でプレヌダヌクラスぞの参照を枡すこずは、非垞に䟿利な解決策ではないようです。私は別のオブゞェクトにサりンドや音楜メ゜ッドを挔奏し、それにするために必芁なすべおの䜜るこずを提案するベンチャヌ䌁業ですので、1時間で、我々は、䞀぀だけekzepmlyarプレヌダヌが必芁䞀匹狌シングルトン。したがっお、同じむンスタンスぞのリンクを絶えず送信せずに、アプリケヌション内のどこからでも責任あるオヌディオサブシステムを垞に利甚できたす。次のようになりたす。



オヌディオ再生システムのクラス図




クラスAudio



は私たちのシングルトンです。サブシステムに単䞀のファサヌドを提䟛したす...ずころで、ここにファサヌドファサヌドがありたす。これは、これらのむンタヌネット䞊で培底的に蚭蚈され、繰り返し䟋ずずもに蚘述された別のデザむンパタヌンです。したがっお、すでに埌列から䞍満の叫び声を聞いたので、私は長い間知られおいるこずの説明をやめお、先に進みたす。コヌドは次のずおりです。



 object Audio { private var soundPlayer: SoundPlayer = MuteSoundPlayer() private var musicPlayer: MusicPlayer = MuteMusicPlayer() fun init(soundPlayer: SoundPlayer, musicPlayer: MusicPlayer) { this.soundPlayer = soundPlayer this.musicPlayer = musicPlayer } fun playSound(sound: Sound) = this.soundPlayer.play(sound) fun playMusic(music: Music) = this.musicPlayer.play(music) fun stopMusic() = this.musicPlayer.stop() }
      
      





init()



必芁なオブゞェクトで初期化するこずにより、最初のどこかで䞀床だけ呌び出すだけで十分であり、将来的には実装の詳现を完党に忘れお䟿利なメ゜ッドを䜿甚したす。あなたがそうしなくおも、心配しないでください、システムは死ぬでしょう-オブゞェクトはデフォルトのクラスによっお初期化されたす。



以䞊です。実際の再生を凊理するために残りたす。サりンドたたは、賢い人が蚀うように、サンプルの再生に関しおは、Javaには䟿利なクラスAudioSystem



ずむンタヌフェヌスがありたすClip



。必芁なのは、オヌディオファむルぞのパスを正しく蚭定するこずですクラスパスにありたす、芚えおいたすか。



 import javax.sound.sampled.AudioSystem class BasicSoundPlayer : SoundPlayer { private fun pathToFile(sound: Sound) = "/sound/${sound.toString().toLowerCase()}.wav" override fun play(sound: Sound) { val url = javaClass.getResource(pathToFile(sound)) val audioIn = AudioSystem.getAudioInputStream(url) val clip = AudioSystem.getClip() clip.open(audioIn) clip.start() } }
      
      





メ゜ッドopen()



はそれを捚おるこずができたすIOException



特に、ファむル圢匏が気に入らなかった堎合-この堎合、オヌディオ゚ディタヌでファむルを開いお再保存するこずをお勧めしたす。そのため、ブロックにラップするこずをお勧めしたすtry-catch



が、最初は、音に問題があるたびにクラッシュしたした。



「䜕お蚀えばいいのかさえわからない...」



音楜では事態はさらに悪化したす。私の知る限り、Javaで音楜ファむルmp3圢匏などを再生する暙準的な方法はないため、いずれにしおもサヌドパヌティのラむブラリを䜿甚する必芁がありたすさたざたなラむブラリがありたす。かなり人気のあるJLayerなど、最小限の機胜を備えた軜量のものが適しおいたす。䟝存しお远加したす



 <dependencies> <dependency> <groupId>com.googlecode.soundlibs</groupId> <artifactId>jlayer</artifactId> <version>1.0.1.4</version> <scope>compile</scope> </dependency> </dependencies>
      
      





そしお、プレヌダヌの助けを借りお実装したす。



 class BasicMusicPlayer : MusicPlayer { private var currentMusic: Music? = null private var thread: PlayerThread? = null private fun pathToFile(music: Music) = "/music/${music.toString().toLowerCase()}.mp3" override fun play(music: Music) { if (currentMusic == music) { return } currentMusic = music thread?.finish() Thread.yield() thread = PlayerThread(pathToFile(music)) thread?.start() } override fun stop() { currentMusic = null thread?.finish() } // Thread responsible for playback private inner class PlayerThread(private val musicPath: String) : Thread() { private lateinit var player: Player private var isLoaded = false private var isFinished = false init { isDaemon = true } override fun run() { loop@ while (!isFinished) { try { player = Player(javaClass.getResource(musicPath).openConnection().apply { useCaches = false }.getInputStream()) isLoaded = true player.play() } catch (ex: Exception) { finish() break@loop } player.close() } } fun finish() { isFinished = true this.interrupt() if (isLoaded) { player.close() } } } }
      
      





たず、このラむブラリは同期的に再生を実行し、ファむルの終わりに達するたでメむンストリヌムをブロックしたす。したがっお、個別のスレッドPlayerThread



を実装し、それを「オプション」デヌモンにしなければなりたせん。そうするこずで、アプリケヌションが早期に終了するこずはありたせん。次に、珟圚再生䞭の音楜ファむルの識別子currentMusic



がプレヌダヌコヌドに保存されたす。 2番目のコマンドが突然再生される堎合、最初から再生を開始したせん。第䞉に、音楜ファむルの最埌に到達するず、その再生が再び開始されたす-ストリヌムがコマンドによっお明瀺的に停止されるたで続きたすfinish()



たたは既に述べたように、他のスレッドが完了するたで。第4に、䞊蚘のコヌドは䞀芋䞍必芁なフラグずコマンドでいっぱいですが、培底的にデバッグおよびテストされおいたす-プレむダヌは期埅どおりに動䜜し、システムを遅くせず、途䞭で突然䞭断せず、メモリリヌクを匕き起こさず、遺䌝子組み換えオブゞェクトを含たず、茝きたす鮮床ず玔床。それを取り、プロゞェクトで倧胆に䜿甚しおください。



ステップ12。ロヌカリれヌション



私たちのゲヌムはほずんど準備ができおいたすが、誰もそれをプレむしたせん。 なんで



「ロシア語はありたせん..ロシア語はありたせん..ロシア語を远加しおください..犬を開発し



おください」店舗のりェブサむトで面癜いストヌリヌゲヌム特にモバむルのペヌゞを開き、レビュヌを読んでください。圌らは玠晎らしい、手描きのグラフィックを賞賛し始めたすかたたは倧気のサりンドトラックに驚嘆したすかたたは、最初の1分間から䞭毒性があり、最埌たで手攟さない゚キサむティングなストヌリヌを話したすか



いや䞍満な「プレむダヌ」は倚くのナニットを指瀺し、通垞はゲヌムを削陀したす。そしお、圌らはたた返金を必芁ずしたす-そしお、これらすべおは䞀぀の簡単な理由のために。はい、あなたは傑䜜を95のすべおの蚀語に翻蚳するのを忘れおいたした。むしろ、キャリアが最も倧声で叫ぶ人。それだけです分かりたすか数ヶ月のハヌドワヌク、長い眠れぬ倜、絶え間ない神経衰匱-これはすべお、尻尟の䞋のハムスタヌです。膚倧な数のプレむダヌを倱いたしたが、これは修正できたせん。



だから先に考えおください。察象読者を決定し、いく぀かの䞻芁蚀語を遞択し、翻蚳サヌビスを泚文したす...䞀般的に、他の人がテヌマ蚘事で耇数回説明したこずをすべお行いたす私よりも賢い。問題の技術的な偎面に焊点を圓お、補品を安党にロヌカラむズする方法に぀いお説明したす。



たず、テンプレヌトに入りたす。名前ず説明が単玔なものずしお保存される前に芚えおいString



たすか今では機胜したせん。デフォルトの蚀語に加えお、サポヌトする予定のすべおの蚀語ぞの翻蚳も提䟛する必芁がありたす。たずえば、次のように



 class TestEnemyTemplate : EnemyTemplate { override val name = "Test enemy" override val description = "Some enemy standing in your way." override val nameLocalizations = mapOf( "ru" to " -", "ar" to "ؚعض العدو", "iw" to "איזה אויב", "zh" to "䞀些敵人", "ua" to "і " ) override val descriptionLocalizations = mapOf( "ru" to " - .", "ar" to "وصف العدو", "iw" to "תיאוך האויב", "zh" to "䞀些敵人的描述", "ua" to " ї і   ." ) override val traits = listOf<Trait>() }
      
      





テンプレヌトの堎合、このアプロヌチは非垞に適しおいたす。どの蚀語の翻蚳も指定したくない堎合は、その必芁はありたせん-垞にデフォルト倀がありたす。ただし、最終的なオブゞェクトでは、耇数の異なるフィヌルドに行を広げたくないでしょう。したがっお、1぀を残したすが、そのタむプは眮き換えたす。



 class LocalizedString(defaultValue: String, localizations: Map<String, String>) { private val default: String = defaultValue private val values: Map<String, String> = localizations.toMap() operator fun get(lang: String) = values.getOrDefault(lang, default) override fun equals(other: Any?) = when { this === other -> true other !is LocalizedString -> false else -> default == other.default } override fun hashCode(): Int { return default.hashCode() } }
      
      





それに応じおゞェネレヌタヌコヌドを修正したす。



 fun generateEnemy(template: EnemyTemplate) = Enemy().apply { name = LocalizedString(template.name, template.nameLocalizations) description = LocalizedString(template.description, template.descriptionLocalizations) template.traits.forEach { addTrait(it) } }
      
      





圓然、同じアプロヌチを残りのタむプのテンプレヌトに適甚する必芁がありたす。倉曎の準備ができたら、問題なく䜿甚できたす。



 val language = Locale.getDefault().language val enemyName = enemy.name[language]
      
      





この䟋では、蚀語のみが考慮されるロヌカラむズの簡易バヌゞョンを提䟛しおいたす。䞀般に、クラスオブゞェクトLocale



は囜ず地域も定矩したす。これがアプリケヌションで重芁な堎合は、LocalizedString



倖芳が少し異なりたすが、ずにかく満足しおいたす。



テンプレヌトを凊理したしたが、アプリケヌションで䜿甚されるサヌビスラむンをロヌカラむズする必芁がありたす。幞いなこずに、これにResourceBundle



は必芁なメカニズムがすべお含たれおいたす。翻蚳枈みのファむルを準備し、ダりンロヌド方法を倉曎するだけです。



 # Game status messages choose_dice_perform_check=    : end_of_turn_discard_extra= :   : end_of_turn_discard_optional= :    : choose_action_before_exploration=,  : choose_action_after_exploration= .   ? encounter_physical=  .   . encounter_somatic=  .   . encounter_mental=  .   . encounter_verbal=  .   . encounter_divine=  .    : die_acquire_success=   ! die_acquire_failure=    . game_loss_out_of_time=    # Die types physical= somatic= mental= verbal= divine= ally= wound= enemy= villain= obstacle= # Hero types and descriptions brawler= hunter= # Various labels avg= bag= bag_size=  class= closed= discard= empty= encountered=  fail= hand= heros_turn= %s max= min= perform_check= : pile= received_new_die=   result= success= sum= time= total= # Action names and descriptions action_confirm_key=ENTER action_confirm_name= action_cancel_key=ESC action_cancel_name= action_explore_location_key=E action_explore_location_name= action_finish_turn_key=F action_finish_turn_name=  action_hide_key=H action_bag_name= action_discard_key=D action_discard_name= action_acquire_key=A action_acquire_name= action_leave_key=L action_leave_name= action_forfeit_key=F action_forfeit_name=
      
      





蚘録のために私は蚀いたせんロシア語でフレヌズを曞くこずは英語よりはるかに耇雑です。決定的なケヌスで名詞を䜿甚するか、性別から離脱する必芁がある堎合およびそのような芁件は必ず成立したす、たず芁件を満たし、次にサむボヌグによっお行われた機械翻蚳のように芋えない結果を埗るために汗をかかなければなりたせん鶏の脳を持぀。たた、アクションキヌを倉曎しないこずに泚意しおください-以前ず同じように、埌者は英語ず同じ文字を䜿甚しお実行されたすちなみに、ラテン語以倖のキヌボヌドレむアりトでは機胜したせんが、これは私たちのビゞネスではありたせん-今のずころはそのたたにしおおきたす。



 class PropertiesStringLoader(locale: Locale) : StringLoader { private val properties = ResourceBundle.getBundle("text.strings", locale) override fun loadString(key: String) = properties.getString(key) ?: "" }
      
      



。

すでに述べたように、ResourceBundle



圌自身がロヌカラむズファむルの䞭から珟圚のロケヌルに最も近いものを芋぀ける責任を負いたす。そしお、芋぀からない堎合は、デフォルトのファむルstring.properties



を取埗したす。そしお、すべおがうたくいきたす...



うんあった
, Unicode .properties



Java 9. ISO-8859-1 — ResourceBundle



. , , — . Unicode- — , , : '\uXXXX'



. , , Java native2ascii , . :



 # Game status messages choose_dice_perform_check=\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u0443\u0431\u0438\u043a\u0438 \u0434\u043b\u044f \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438: end_of_turn_discard_extra=\u041a\u041e\u041d\u0415\u0426 \u0425\u041e\u0414\u0410: \u0421\u0431\u0440\u043e\u0441\u044c\u0442\u0435 \u043b\u0438\u0448\u043d\u0438\u0435 \u043a\u0443\u0431\u0438\u043a\u0438: end_of_turn_discard_optional=\u041a\u041e\u041d\u0415\u0426 \u0425\u041e\u0414\u0410: \u0421\u0431\u0440\u043e\u0441\u044c\u0442\u0435 \u043a\u0443\u0431\u0438\u043a\u0438 \u043f\u043e \u0436\u0435\u043b\u0430\u043d\u0438\u044e: choose_action_before_exploration=\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c: choose_action_after_exploration=\u0418\u0441\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u0427\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435? encounter_physical=\u0412\u0441\u0442\u0440\u0435\u0447\u0435\u043d \u0424\u0418\u0417\u0418\u0427\u0415\u0421\u041a\u0418\u0419 \u043a\u0443\u0431\u0438\u043a. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443. encounter_somatic=\u0412\u0441\u0442\u0440\u0435\u0447\u0435\u043d \u0421\u041e\u041c\u0410\u0422\u0418\u0427\u0415\u0421\u041a\u0418\u0419 \u043a\u0443\u0431\u0438\u043a. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443. encounter_mental=\u0412\u0441\u0442\u0440\u0435\u0447\u0435\u043d \u041c\u0415\u041d\u0422\u0410\u041b\u042c\u041d\u042b\u0419 \u043a\u0443\u0431\u0438\u043a. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443. encounter_verbal=\u0412\u0441\u0442\u0440\u0435\u0447\u0435\u043d \u0412\u0415\u0420\u0411\u0410\u041b\u042c\u041d\u042b\u0419 \u043a\u0443\u0431\u0438\u043a. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443. encounter_divine=\u0412\u0441\u0442\u0440\u0435\u0447\u0435\u043d \u0411\u041e\u0416\u0415\u0421\u0422\u0412\u0415\u041d\u041d\u042b\u0419 \u043a\u0443\u0431\u0438\u043a. \u041c\u043e\u0436\u043d\u043e \u0432\u0437\u044f\u0442\u044c \u0431\u0435\u0437 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438: die_acquire_success=\u0412\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u043d\u043e\u0432\u044b\u0439 \u043a\u0443\u0431\u0438\u043a! die_acquire_failure=\u0412\u0430\u043c \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u0443\u0431\u0438\u043a. game_loss_out_of_time=\u0423 \u0432\u0430\u0441 \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u043e\u0441\u044c \u0432\u0440\u0435\u043c\u044f
      
      





. — . — . , IDE ( ) « », — - ( ), IDE, .



, . getBundle()



, , , ResourceBundle.Control



— - .



 class PropertiesStringLoader(locale: Locale) : StringLoader { private val properties = ResourceBundle.getBundle( "text.strings", locale, Utf8ResourceBundleControl()) override fun loadString(key: String) = properties.getString(key) ?: "" }
      
      





, , :



 class Utf8ResourceBundleControl : ResourceBundle.Control() { @Throws(IllegalAccessException::class, InstantiationException::class, IOException::class) override fun newBundle(baseName: String, locale: Locale, format: String, loader: ClassLoader, reload: Boolean): ResourceBundle? { val bundleName = toBundleName(baseName, locale) return when (format) { "java.class" -> super.newBundle(baseName, locale, format, loader, reload) "java.properties" -> with((if ("://" in bundleName) null else toResourceName(bundleName, "properties")) ?: return null) { when { reload -> reload(this, loader) else -> loader.getResourceAsStream(this) }?.let { stream -> InputStreamReader(stream, "UTF-8").use { r -> PropertyResourceBundle(r) } } } else -> throw IllegalArgumentException("Unknown format: $format") } } @Throws(IOException::class) private fun reload(resourceName: String, classLoader: ClassLoader): InputStream { classLoader.getResource(resourceName)?.let { url -> url.openConnection().let { connection -> connection.useCaches = false return connection.getInputStream() } } throw IOException("Unable to load data!") } }
      
      





, 
 , ( ) — ( Kotlin ). — , .properties



UTF-8 - .



さたざたな蚀語でアプリケヌションの動䜜をテストするために、オペレヌティングシステムの蚭定を倉曎する必芁はありたせん。JREの起動時に必芁な蚀語を指定するだけです。



 java -Duser.language=ru -jar path_to_project\Dice\target\dice-1.0-jar-with-dependencies.jar
      
      





ただWindowsで䜜業しおいる堎合は、問題を予期しおください
, Windows (cmd.exe) 437 ( DOSLatinUS), — . , UTF-8 , :



 chcp 65001
      
      





Java , , . :



 java -Dfile.encoding=UTF-8 -Duser.language=ru -jar path_to_project\Dice\target\dice-1.0-jar-with-dependencies.jar
      
      





, , Unicode- (, Lucida Console)



すべおの゚キサむティングな冒険の埌、結果は誇らしげに䞀般倧衆に瀺され、倧声で宣蚀されたす「私たちは犬ではありたせん」



人皮的忠実なオプション




そしおそれは良いこずです。



ステップ13 すべおをたずめる



気配りのある読者は、特定のパッケヌゞの名前に䞀床だけ蚀及し、決しお戻っおこなかったこずに気付いたに違いありたせん。たず、各開発者は、どのクラスをどのパッケヌゞに配眮するかに関しお、独自の考慮事項を持っおいたす。第二に、プロゞェクトに取り組むに぀れお、より倚くの新しいクラスが远加されるず、考えが倉わりたす。第䞉に、アプリケヌションの構造の倉曎は簡単で安䟡ですそしお、最新のバヌゞョン管理システムが移行を怜出するため、履歎を倱うこずはありたせん。クラス、パッケヌゞ、メ゜ッド、倉数の名前を倧胆に倉曎したす-ドキュメントを曎新するこずを忘れないでください、そうですか。



そしお私たちに残っおいるのは、プロゞェクトをたずめお立ち䞊げるこずだけです。芚えおいるように、メ゜ッドmain()



をすでに䜜成したした。今床はその内容を埋めたす。必芁なもの





行こう



 fun main(args: Array<String>) { Audio.init(BasicSoundPlayer(), BasicMusicPlayer()) val loader = PropertiesStringLoader(Locale.getDefault()) val renderer = ConsoleGameRenderer(loader) val interactor = ConsoleGameInteractor() val template = TestScenarioTemplate() val scenario = generateScenario(template, 1) val locations = generateLocations(template, 1, heroes.size) val heroes = listOf( generateHero(Hero.Type.BRAWLER, "Brawler"), generateHero(Hero.Type.HUNTER, "Hunter") ) val game = Game(renderer, interactor, scenario, locations, heroes) game.start() }
      
      





私たちは最初の実甚プロトタむプを立ち䞊げお楜しみたす。行くぞ



ステップ14。ゲヌムバランス



うヌん...



ステップ15。テスト



今すぐこずを曞かれた最初の䜜業プロトタむプのコヌドの䞻芁な郚分は、ナニットテストのカップルを远加しおいいだろう...



「どのようにたった今はい、テストは最初に蚘述し、次にコヌドを蚘述しなければなりたせんでした」



倚くの読者は、単䜓テストの蚘述が䜜業コヌドTDDその他のファッショナブルな方法論。他の人は激怒したす少なくずも䜕かを開発し始めたずしおも、人々がテストで頭をだたすこずは䜕もありたせん。別のカップルがベヌスボヌドの隙間からofい出しお、「これらのテストが必芁な理由がわからない-すべおが私にずっおうたくいく」ず”病に蚀うでしょう...そしお圌らはブヌツで顔に抌し蟌たれ、すぐに抌し戻されたす。私はむデオロギヌ的察立を開始し始めたせん圌らはすでにむンタヌネット䞊でそれらに満ちおいたす。したがっお、私はすべおの人に郚分的に同意したす。はい、テストは時々圹に立ちたす特に、頻繁に倉曎される、たたは耇雑な蚈算に関連するコヌドで、はい、ナニットテストはすべおのコヌドに適しおいたせんたずえば、ナヌザヌたたは倖郚システムずの盞互䜜甚をカバヌしたせん、はい、ナニットテスト以倖にもありたす他の皮の倚くたあ、少なくずも5぀が呜名された、そしお、はい、私たちはテストを曞くこずに集䞭したせん-私たちの蚘事は䜕か他のものに぀いおです。



倚くのプログラマヌ特に初心者はテストを怠りたす。倚くの人は、アプリケヌションの機胜がテストで十分にカバヌされおいないず蚀うこずで正圓化しおいたす。たずえば、ナヌザヌむンタヌフェヌスをテストするための特殊なフレヌムワヌクを䜿甚しお耇雑な構造をフェンスするよりも、アプリケヌションを起動しお、すべおが倖芳ず盞互䜜甚に問題がないかどうかを確認する方がはるかに簡単です。そしお、むンタヌフェむスを実装しおいたずきにお知らせしたすRenderer



-それだけでした。ただし、コヌドの䞭には、単䜓テストの抂念が優れおいるメ゜ッドがありたす。



たずえば、ゞェネレヌタヌ。そしおそれだけです。これは理想的なブラックボックスです。テンプレヌトは入力に送信され、ゲヌムワヌルドのオブゞェクトは出力で取埗されたす。内郚では䜕が起こりたすが、テストする必芁があるのはたさに圌です。たずえば、次のように



 public class DieGeneratorTest { @Test public void testGetMaxLevel() { assertEquals("Max level should be 3", 3, DieGeneratorKt.getMaxLevel()); } @Test public void testDieGenerationSize() { DieTypeFilter filter = new SingleDieTypeFilter(Die.Type.ALLY); List<? extends List<Integer>> allowedSizes = Arrays.asList( null, Arrays.asList(4, 6, 8), Arrays.asList(4, 6, 8, 10), Arrays.asList(6, 8, 10, 12) ); IntStream.rangeClosed(1, 3).forEach(level -> { for (int i = 0; i < 10; i++) { int size = DieGeneratorKt.generateDie(filter, level).getSize(); assertTrue("Incorrect level of die generated: " + size, allowedSizes.get(level).contains(size)); assertTrue("Incorrect die size: " + size, size >= 4); assertTrue("Incorrect die size: " + size, size <= 12); assertTrue("Incorrect die size: " + size, size % 2 == 0); } }); } @Test public void testDieGenerationType() { List<Die.Type> allowedTypes1 = Arrays.asList(Die.Type.PHYSICAL); List<Die.Type> allowedTypes2 = Arrays.asList(Die.Type.PHYSICAL, Die.Type.SOMATIC, Die.Type.MENTAL, Die.Type.VERBAL); List<Die.Type> allowedTypes3 = Arrays.asList(Die.Type.ALLY, Die.Type.VILLAIN, Die.Type.ENEMY); for (int i = 0; i < 10; i++) { Die.Type type1 = DieGeneratorKt.generateDie(new SingleDieTypeFilter(Die.Type.PHYSICAL), 1).getType(); assertTrue("Incorrect die type: " + type1, allowedTypes1.contains(type1)); Die.Type type2 = DieGeneratorKt.generateDie(new StatsDieTypeFilter(), 1).getType(); assertTrue("Incorrect die type: " + type2, allowedTypes2.contains(type2)); Die.Type type3 = DieGeneratorKt.generateDie(new MultipleDieTypeFilter(Die.Type.ALLY, Die.Type.VILLAIN, Die.Type.ENEMY), 1).getType(); assertTrue("Incorrect die type: " + type3, allowedTypes3.contains(type3)); } } }
      
      





たたは



 public class BagGeneratorTest { @Test public void testGenerateBag() { BagTemplate template1 = new BagTemplate(); template1.addPlan(0, 10, new SingleDieTypeFilter(Die.Type.PHYSICAL)); template1.addPlan(5, 5, new SingleDieTypeFilter(Die.Type.SOMATIC)); template1.setFixedDieCount(null); BagTemplate template2 = new BagTemplate(); template2.addPlan(10, 10, new SingleDieTypeFilter(Die.Type.DIVINE)); template2.setFixedDieCount(5); BagTemplate template3 = new BagTemplate(); template3.addPlan(10, 10, new SingleDieTypeFilter(Die.Type.ALLY)); template3.setFixedDieCount(50); for (int i = 0; i < 10; i++) { Bag bag1 = BagGeneratorKt.generateBag(template1, 1); assertTrue("Incorrect bag size: " + bag1.getSize(), bag1.getSize() >= 5 && bag1.getSize() <= 15); assertEquals("Incorrect number of SOMATIC dice", 5, bag1.examine().stream().filter(d -> d.getType() == Die.Type.SOMATIC).count()); Bag bag2 = BagGeneratorKt.generateBag(template2, 1); assertEquals("Incorrect bag size", 5, bag2.getSize()); Bag bag3 = BagGeneratorKt.generateBag(template3, 1); assertEquals("Incorrect bag size", 50, bag3.getSize()); List<Die.Type> dieTypes3 = bag3.examine().stream().map(Die::getType).distinct().collect(Collectors.toList()); assertEquals("Incorrect die types", 1, dieTypes3.size()); assertEquals("Incorrect die types", Die.Type.ALLY, dieTypes3.get(0)); } } }
      
      





たたはこのように



 public class LocationGeneratorTest { private void testLocationGeneration(String name, LocationTemplate template) { System.out.println("Template: " + template.getName()); assertEquals("Incorrect template type", name, template.getName()); IntStream.rangeClosed(1, 3).forEach(level -> { Location location = LocationGeneratorKt.generateLocation(template, level); assertEquals("Incorrect location type", name, location.getName().get("")); assertTrue("Location not open by default", location.isOpen()); int closingDifficulty = location.getClosingDifficulty(); assertTrue("Closing difficulty too small", closingDifficulty > 0); assertEquals("Incorrect closing difficulty", closingDifficulty, template.getBasicClosingDifficulty() + level * 2); Bag bag = location.getBag(); assertNotNull("Bag is null", bag); assertTrue("Bag is empty", location.getBag().getSize() > 0); Deck<Enemy> enemies = location.getEnemies(); assertNotNull("Enemies are null", enemies); assertEquals("Incorrect enemy threat count", enemies.getSize(), template.getEnemyCardsCount()); if (bag.drawOfType(Die.Type.ENEMY) != null) { assertTrue("Enemy cards not specified", enemies.getSize() > 0); } Deck<Obstacle> obstacles = location.getObstacles(); assertNotNull("Obstacles are null", obstacles); assertEquals("Incorrect obstacle threat count", obstacles.getSize(), template.getObstacleCardsCount()); List<SpecialRule> specialRules = location.getSpecialRules(); assertNotNull("SpecialRules are null", specialRules); }); } @Test public void testGenerateLocation() { testLocationGeneration("Test Location", new TestLocationTemplate()); testLocationGeneration("Test Location 2", new TestLocationTemplate2()); } }
      
      





「ストップ、ストップ、ストップ」これは䜕 Java ???」



あなたは理解しおいたす。さらに、ゞェネレヌタヌ自䜓の実装を開始する前に、最初にこのようなテストを蚘述するこずをお勧めしたす。もちろん、テスト察象のコヌドは非垞に単玔であり、メ゜ッドはテストなしで初めお動䜜する可胜性が高いですが、テストを䜜成するず、それを氞久に忘れ、将来起こりうる問題から保護したすその解決には倚くの時間がかかりたす。特に開発の開始時から 5幎が経過し、メ゜ッド内のすべおがそこでどのように機胜するかをすでに忘れおいたした。たた、テストが倱敗したために突然プロゞェクトの収集が停止した堎合、システム芁件が倉曎され、叀いテストでそれらが満たされなくなった理由がわかりたすどう思いたしたか。



その他。クラスHandMaskRule



ずその盞続人を芚えおいたすかある時点で、スキルを䜿甚するためにヒヌロヌが3぀のサむコロを手から取る必芁があり、これらのサむコロの皮類には厳しい制限があるこずを想像しおくださいたずえば、「最初のサむコロは青、緑、癜、2番目は黄色、癜、青、そしお3番目-青たたは玫 "-あなたは困難を感じたすか。クラスの実装にアプロヌチする方法はたあ...たず、入力パラメヌタず出力パラメヌタを決めるこずができたす。明らかに、3぀の配列たたはセットを受け入れるクラスが必芁です。各配列には、それぞれ、1番目、2番目、3番目のキュヌブの有効な型が含たれおいたす。それから䜕぀ぶしたすか再垰䜕かを芋逃したらどうなりたすか深い入り口を䜜りたす。クラスメ゜ッドの実装を延期し、テストを䜜成したす。芁件はシンプルで理解しやすく、正匏に定匏化されおいるためです。そしお、いく぀かのテストを䜜成する方が良いでしょう...しかし、ここではそのようなものを怜蚎したす



 public class TripleDieHandMaskRuleTest { private Hand hand; @Before public void init() { hand = new Hand(10); hand.addDie(new Die(Die.Type.PHYSICAL, 4)); //0 hand.addDie(new Die(Die.Type.PHYSICAL, 4)); //1 hand.addDie(new Die(Die.Type.SOMATIC, 4)); //2 hand.addDie(new Die(Die.Type.SOMATIC, 4)); //3 hand.addDie(new Die(Die.Type.MENTAL, 4)); //4 hand.addDie(new Die(Die.Type.MENTAL, 4)); //5 hand.addDie(new Die(Die.Type.VERBAL, 4)); //6 hand.addDie(new Die(Die.Type.VERBAL, 4)); //7 hand.addDie(new Die(Die.Type.DIVINE, 4)); //8 hand.addDie(new Die(Die.Type.DIVINE, 4)); //9 hand.addDie(new Die(Die.Type.ALLY, 4)); //A (0) hand.addDie(new Die(Die.Type.ALLY, 4)); //B (1) } @Test public void testRule1() { HandMaskRule rule = new TripleDieHandMaskRule( hand, new Die.Type[]{Die.Type.PHYSICAL, Die.Type.SOMATIC}, new Die.Type[]{Die.Type.MENTAL, Die.Type.VERBAL}, new Die.Type[]{Die.Type.PHYSICAL, Die.Type.ALLY} ); HandMask mask = new HandMask(); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 0)); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 0)); assertTrue("Should be on", rule.isPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 2)); assertTrue("Should be on", rule.isPositionActive(mask, 3)); assertTrue("Should be on", rule.isPositionActive(mask, 4)); assertTrue("Should be on", rule.isPositionActive(mask, 5)); assertTrue("Should be on", rule.isPositionActive(mask, 6)); assertTrue("Should be on", rule.isPositionActive(mask, 7)); assertFalse("Should be off", rule.isPositionActive(mask, 8)); assertFalse("Should be off", rule.isPositionActive(mask, 9)); assertFalse("Rule should not be met yet", rule.checkMask(mask)); mask.addPosition(0); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 0)); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 0)); assertTrue("Should be on", rule.isPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 2)); assertTrue("Should be on", rule.isPositionActive(mask, 3)); assertTrue("Should be on", rule.isPositionActive(mask, 4)); assertTrue("Should be on", rule.isPositionActive(mask, 5)); assertTrue("Should be on", rule.isPositionActive(mask, 6)); assertTrue("Should be on", rule.isPositionActive(mask, 7)); assertFalse("Should be off", rule.isPositionActive(mask, 8)); assertFalse("Should be off", rule.isPositionActive(mask, 9)); assertFalse("Rule should not be met yet", rule.checkMask(mask)); mask.addPosition(4); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 0)); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 0)); assertTrue("Should be on", rule.isPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 2)); assertTrue("Should be on", rule.isPositionActive(mask, 3)); assertTrue("Should be on", rule.isPositionActive(mask, 4)); assertFalse("Should be off", rule.isPositionActive(mask, 5)); assertFalse("Should be off", rule.isPositionActive(mask, 6)); assertFalse("Should be off", rule.isPositionActive(mask, 7)); assertFalse("Should be off", rule.isPositionActive(mask, 8)); assertFalse("Should be off", rule.isPositionActive(mask, 9)); assertFalse("Rule should not be met yet", rule.checkMask(mask)); mask.addAllyPosition(0); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 0)); assertFalse("Ally should be off", rule.isAllyPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 0)); assertFalse("Should be off", rule.isPositionActive(mask, 1)); assertFalse("Should be off", rule.isPositionActive(mask, 2)); assertFalse("Should be off", rule.isPositionActive(mask, 3)); assertTrue("Should be on", rule.isPositionActive(mask, 4)); assertFalse("Should be off", rule.isPositionActive(mask, 5)); assertFalse("Should be off", rule.isPositionActive(mask, 6)); assertFalse("Should be off", rule.isPositionActive(mask, 7)); assertFalse("Should be off", rule.isPositionActive(mask, 8)); assertFalse("Should be off", rule.isPositionActive(mask, 9)); assertTrue("Rule should be met", rule.checkMask(mask)); mask.removePosition(0); assertTrue("Ally should be on", rule.isAllyPositionActive(mask, 0)); assertFalse("Ally should be off", rule.isAllyPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 0)); assertTrue("Should be on", rule.isPositionActive(mask, 1)); assertTrue("Should be on", rule.isPositionActive(mask, 2)); assertTrue("Should be on", rule.isPositionActive(mask, 3)); assertTrue("Should be on", rule.isPositionActive(mask, 4)); assertFalse("Should be off", rule.isPositionActive(mask, 5)); assertFalse("Should be off", rule.isPositionActive(mask, 6)); assertFalse("Should be off", rule.isPositionActive(mask, 7)); assertFalse("Should be off", rule.isPositionActive(mask, 8)); assertFalse("Should be off", rule.isPositionActive(mask, 9)); assertFalse("Rule should not be met again", rule.checkMask(mask)); } }
      
      





これは骚が折れたすが、開始するたでは芋かけほどではありたせんある時点で、さらに楜しくなりたす。しかし、そのようなテストおよび別の機䌚のためのいく぀かのテストを曞くず、あなたは突然萜ち着きず自信を感じたす。これで、小さなタむプミスでメ゜ッドが台無しになり、䞍愉快な驚きに぀ながり、手䜜業でテストするのがはるかに難しくなりたす。少しず぀、クラスの必芁なメ゜ッドを実装し始めたす。そしお最埌にテストを実行しお、どこかで間違いを犯したこずを確認したす。問題箇所を芋぀けお曞き盎しおください。準備が敎うたで繰り返したす。



 class TripleDieHandMaskRule( hand: Hand, types1: Array<Die.Type>, types2: Array<Die.Type>, types3: Array<Die.Type>) : HandMaskRule(hand) { private val types1 = types1.toSet() private val types2 = types2.toSet() private val types3 = types3.toSet() override fun checkMask(mask: HandMask): Boolean { if (mask.positionCount + mask.allyPositionCount != 3) { return false } return getCheckedDice(mask).asSequence() .filter { it.type in types1 } .any { d1 -> getCheckedDice(mask) .filter { d2 -> d2 !== d1 } .filter { it.type in types2 } .any { d2 -> getCheckedDice(mask) .filter { d3 -> d3 !== d1 } .filter { d3 -> d3 !== d2 } .any { it.type in types3 } } } } override fun isPositionActive(mask: HandMask, position: Int): Boolean { if (mask.checkPosition(position)) { return true } val die = hand.dieAt(position) ?: return false return when (mask.positionCount + mask.allyPositionCount) { 0 -> die.type in types1 || die.type in types2 || die.type in types3 1 -> with(getCheckedDice(mask).first()) { (this.type in types1 && (die.type in types2 || die.type in types3)) || (this.type in types2 && (die.type in types1 || die.type in types3)) || (this.type in types3 && (die.type in types1 || die.type in types2)) } 2-> with(getCheckedDice(mask)) { val d1 = this[0] val d2 = this[1] (d1.type in types1 && d2.type in types2 && die.type in types3) || (d2.type in types1 && d1.type in types2 && die.type in types3) || (d1.type in types1 && d2.type in types3 && die.type in types2) || (d2.type in types1 && d1.type in types3 && die.type in types2) || (d1.type in types2 && d2.type in types3 && die.type in types1) || (d2.type in types2 && d1.type in types3 && die.type in types1) } 3 -> false else -> false } } override fun isAllyPositionActive(mask: HandMask, position: Int): Boolean { if (mask.checkAllyPosition(position)) { return true } if (hand.allyDieAt(position) == null) { return false } return when (mask.positionCount + mask.allyPositionCount) { 0 -> ALLY in types1 || ALLY in types2 || ALLY in types3 1 -> with(getCheckedDice(mask).first()) { (this.type in types1 && (ALLY in types2 || ALLY in types3)) || (this.type in types2 && (ALLY in types1 || ALLY in types3)) || (this.type in types3 && (ALLY in types1 || ALLY in types2)) } 2-> with(getCheckedDice(mask)) { val d1 = this[0] val d2 = this[1] (d1.type in types1 && d2.type in types2 && ALLY in types3) || (d2.type in types1 && d1.type in types2 && ALLY in types3) || (d1.type in types1 && d2.type in types3 && ALLY in types2) || (d2.type in types1 && d1.type in types3 && ALLY in types2) || (d1.type in types2 && d2.type in types3 && ALLY in types1) || (d2.type in types2 && d1.type in types3 && ALLY in types1) } 3 -> false else -> false } } }
      
      





このような機胜をより簡単に実装する方法に぀いおアむデアがある堎合は、コメントを歓迎したす。そしお、テストを曞くこずでこのクラスの実装を始めるのに十分なほど頭が良かったず信じられないほど嬉しいです。



「そしお、私は<...>も<...>非垞に<...>うれしい<...>です。入ろう<...>戻る<...>ギャップに」



ステップ16。モゞュヌル性



予想通り、成熟した子䟛は䞀生芪の保護䞋にいるこずはできたせん。遅かれ早かれ、自分の道を遞んで倧胆に道を進み、困難や混乱を克服しなければなりたせん。そのため、私たちが開発したコンポヌネントは非垞に成熟しおいたため、1぀の屋根の䞋でcr屈になりたした。それらをいく぀かの郚分に分ける時が来たした。



かなり些现な䜜業に盎面しおいたす。これたでに䜜成されたすべおのクラスを3぀のグルヌプに分ける必芁がありたす。





この分離の結果は、最終的に次の図のようになりたす。



ショヌの最埌の俳優のように、私たちの今日のヒヌロヌは再び党力で舞台に登堎したす




远加のプロゞェクトを䜜成し、察応するクラスを転送したす。そしお、プロゞェクト間の盞互䜜甚を正しく構成する必芁がありたす。



コア

プロゞェクトこのプロゞェクトは玔粋な゚ンゞンです。特定のクラスはすべお他のプロゞェクトに転送されたした-基本的な機胜であるコアのみが残りたした。必芁に応じおラむブラリ。起動クラスはもうありたせん。パッケヌゞをビルドする必芁もありたせん。このプロゞェクトのアセンブリは、ロヌカルのMavenリポゞトリでホストされ詳现は埌述、他のプロゞェクトで䟝存関係ずしお䜿甚されたす。



ファむルpom.xml



は次のずおりです。



 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>my.company</groupId> <artifactId>dice-core</artifactId> <version>1.0</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit-dep</artifactId> <version>4.8.2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.jetbrains.kotlin</groupId> <!-- other Kotlin setup --> </plugin> </plugins> </build> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <kotlin.version>1.3.20</kotlin.version> <kotlin.compiler.incremental>true</kotlin.compiler.incremental> </properties> </project>
      
      





これから、次のように収集したす。



 mvn -f "path_to_project/DiceCore/pom.xml" install
      
      





Cliプロゞェクト

これは、アプリケヌションぞの゚ントリポむントです-゚ンドナヌザヌが察話するのはこのプロゞェクトです。カヌネルは䟝存関係ずしお䜿甚されたす。この䟋ではコン゜ヌルで䜜業しおいるため、プロゞェクトにはそれで䜜業するために必芁なクラスが含たれたす突然コヌヒヌメヌカヌでゲヌムを開始したい堎合は、単にこのプロゞェクトを察応する実装の同様のプロゞェクトに眮き換えたす。すぐにリ゜ヌスラむン、オヌディオファむルなどを远加したす。倖郚ラむブラリぞの䟝存関係は



ファむルにpom.xml



転送されたす



 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>my.company</groupId> <artifactId>dice-cli</artifactId> <version>1.0</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>my.company</groupId> <artifactId>dice-core</artifactId> <version>1.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.fusesource.jansi</groupId> <artifactId>jansi</artifactId> <version>1.17.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>jline</groupId> <artifactId>jline</artifactId> <version>2.14.6</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.googlecode.soundlibs</groupId> <artifactId>jlayer</artifactId> <version>1.0.1.4</version> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.jetbrains.kotlin</groupId> <!-- other Kotlin setup --> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>my.company.dice.MainKt</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <kotlin.version>1.3.20</kotlin.version> <kotlin.compiler.incremental>true</kotlin.compiler.incremental> </properties> </project>
      
      





既に芋たこのプロゞェクトをビルドしお実行するスクリプト-繰り返したせん。



冒険

最埌に、別のプロゞェクトでプロットを取り出したす。぀たり、あなたの䌚瀟のシナリオ郚門のスタッフが想像できる、ゲヌム䞖界のすべおのシナリオ、地圢、敵、およびその他のナニヌクなオブゞェクトたあ、これたでのずころ、私たち自身の病気の想像力だけです-私たちはただこの地域で唯䞀のゲヌムデザむナヌです。アむデアは、スクリプトをセットにグルヌプ化しアドベンチャヌ、そのようなセットを個別のプロゞェクトずしお配垃するこずですボヌドゲヌムやビデオゲヌムの䞖界で行われる方法ず同様。぀たり、jarアヌカむブを収集し、それらを別のフォルダヌに配眮しお、ゲヌム゚ンゞンがこのフォルダヌをスキャンし、そこに含たれるすべおのアドベンチャヌを自動的に接続するようにしたす。ただし、このアプロヌチの技術的な実装には非垞に倚くの困難が䌎いたす。



どこから始めたすかたあ、たず、特定のJavaクラスの圢匏でテンプレヌトを配垃しおいるずいう事実からええ、私をbeatり、oldる-私はこれを予芋しおいたした。その堎合、これらのクラスは起動時にアプリケヌションのクラスパスに存圚する必芁がありたす。この芁件を匷制するこずは難しくありたせん。適切な環境倉数にjarファむルを明瀺的に登録したすJava 6以降、*- wildcardsも䜿甚できたす。



 java -classpath "path_to_project/DiceCli/target/adventures/*" -jar path_to_project/DiceCli/target/dice-1.0-jar-with-dependencies.jar
      
      





「ばか、たたは䜕 -jarスむッチを䜿甚するず、-classpathスむッチは無芖されたす」



ただし、これは機胜したせん。実行可胜jarアヌカむブのクラスパスは、内郚ファむルに明瀺的に蚘述する必芁がありたすMETA-INF/MANIFEST.MF



セクションの名前は- Claspath:



。このため、特別なプラグむンも利甚可胜ですmaven-compiler-pluginたたは、最悪の堎合、maven-assembly-plugin。ただし、マニフェスト内のワむルドカヌドは機胜したせん。䟝存するjarファむルの名前を明瀺的に指定する必芁がありたす。぀たり、それらを事前に知るこずです。これは、私たちの堎合は問題です。



ずにかく、私はそれを望んでいたせんでした。プロゞェクトを再コンパむルする必芁がないようにしたかった。フォルダヌぞadventures/



任意の数のアドベンチャヌを投げるこずができたため、実行䞭にすべおのアドベンチャヌがゲヌム゚ンゞンに衚瀺されたした。残念ながら、䞀芋明らかな機胜は、Javaの䞖界の暙準的な衚珟を超えおいたす。したがっお、それは歓迎されたせん。独立した冒険を広めるには、異なるアプロヌチを実装する必芁がありたす。どっち私は知りたせん、コメントに曞いおください-確かに誰かがスマヌトなアむデアを持っおいたす。



それたではわかりたせん。名前を知らなくおも、プロゞェクトを再コンパむルしなくおも、クラスパスに䟝存関係を動的に远加できる小さなたたは芋た目によっおは倧きなトリックがありたす



。Windowsの堎合



 @ECHO OFF call "path_to_maven\mvn.bat" -f "path_to_project\DiceCore\pom.xml" install call "path_to_maven\mvn.bat" -f "path_to_project\DiceCli\pom.xml" package call "path_to_maven\mvn.bat" -f "path_to_project\TestAdventure\pom.xml" package mkdir path_to_project\DiceCli\target\adventures copy "path_to_project\TestAdventure\target\test-adventure-1.0.jar" path_to_project\DiceCli\target\adventures\ chcp 65001 cd path_to_project\DiceCli\target\ java -Dfile.encoding=UTF-8 -cp "dice-cli-1.0-jar-with-dependencies.jar;adventures\*" my.company.dice.MainKt pause
      
      





Unixの堎合



 #!/bin/sh mvn -f "path_to_project/DiceCore/pom.xml" install mvn -f "path_to_project/DiceCli/pom.xml" package mvn -f "path_to_project/TestAdventure/pom.xml" package mkdir path_to_project/DiceCli/target/adventures cp path_to_project/TestAdventure/target/test-adventure-1.0.jar path_to_project/DiceCli/target/adventures/ cd path_to_project/DiceCli/target/ java -cp "dice-cli-1.0-jar-with-dependencies.jar:adventures/*" my.company.dice.MainKt
      
      





そしお、ここにトリックがありたす。キヌを䜿甚する代わりに-jar



、Cliプロゞェクトをクラスパスに远加し、その䞭に含たれるクラスを゚ントリポむントずしお明瀺的に指定したすMainKt



。さらに、ここでフォルダからすべおのアヌカむブを接続したすadventures/



。



この曲がった決定がどれほど倧きいかをもう䞀床瀺す必芁はありたせん-私自身、感謝したす。コメントであなたのアむデアを提案しおください。お願いしたす。 ಥ﹏ಥ



ステップ17。 プロット



少し歌詞。

私たちの蚘事はワヌクフロヌの技術的な偎面に関するものですが、ゲヌムは単なる゜フトりェアコヌドではありたせん。これらは面癜いむベントず掻気のあるキャラクタヌを備えた゚キサむティングな䞖界であり、あなたは頭で思いっきり飛び蟌み、珟実の䞖界を攟棄したす。そのような䞖界はそれぞれ独自の方法で珍しく、独自の方法で興味深いものであり、その倚くは䜕幎も経った今でも芚えおいたす。あなたの䞖界も枩かい気持ちで思い出されたいなら、それを珍しくお面癜くしおください。



ここで私たちはスクリプトラむタヌではなくプログラマヌであるこずは知っおいたすが、ゲヌムのゞャンルの物語の芁玠経隓のあるゲヌマヌ、そうですねに぀いおの基本的なアむデアはありたす。どんな本でもそうですが、ストヌリヌには目䞻人公が盎面しおいる問題を埐々に説明したす、発展、2、3回の興味深いタヌン、クラむマックス読者が興奮しお凍り぀いお息をするのを忘れるずきのプロットの最も急性の瞬間ず吊認どのむベントが埐々に論理的な結論に達するか。控えめな衚珟、論理的な根拠のないこず、プロットの穎を避けおください。すべおの開始行は適切な結論に達するはずです。



さお、私たちの話を他の人に読んでみたしょう-偎からの公平な倖芳は、䜜られた欠陥を理解し、それらを時間内に修正するのに非垞によく圹立ちたす。



ゲヌムのプロット
, , . , : ( ) ( ), . , .



— , . , , .



, , - . , , , , . .



幞運なこずに、私はTolkienではありたせん。ゲヌムの䞖界をあたり詳しく説明しおいたせんでしたが、それを十分に興味深いものにしようずしたした。同時に、圌はいく぀かのあいたいさを導入するこずを蚱可したした。各あいたいさは自分のやり方で自由に解釈できたす。たずえば、封建制床ず珟代の民䞻的制床、邪悪な暎君、組織的な犯眪グルヌプ、最高の目暙ず圓たり前の生存、居酒屋でのバス乗りず戊い-キャラクタヌも䜕らかの理由で撮圱したす匓/クロスボり、たたはアサルトラむフルから。䞖界には魔法のようなものがありその存圚が戊術的な胜力にゲヌムプレむを远加したす、神秘䞻矩の芁玠あるべき姿がありたす。



私は陰謀の決たり文句や幻想的な消費財から離れたいず思っおいたした-これらすべおの゚ルフ、ノヌム、ドラゎン、黒、そしお絶察的な䞖界の悪ず同様に遞択されたヒヌロヌ、叀代の予蚀、スヌパヌアヌティファクト、壮倧な戊い...たた、私は実際に䞖界を生き生きずさせたいず思ったので、各キャラクタヌがマむナヌなものでも出䌚っお、ゲヌムの仕組みの芁玠が䞖界の法則に適合し、ヒヌロヌの成長が自然に発生し、堎所の敵や障害物の存圚が堎所自䜓の特城によっお論理的に正圓化されるように、自分の物語ず動機付けを持ちたした...など。残念なこずに、この願望は残酷な冗談を挔じ、開発プロセスを非垞に遅くし、ゲヌムの慣習から離れるこずは垞に可胜ではありたせんでした。それにもかかわらず、最終補品からの満足床は桁違いに倧きいこずが刀明したした。



このすべおで私は䜕を蚀いたいですかよく考え抜かれた興味深いプロットはそれほど必芁ではないかもしれたせんが、あなたのゲヌムはその存圚に苊しむこずはありたせんせいぜい、プレむダヌはそれを楜しむでしょう、最悪の堎合、圌らは単にそれを無芖したす。そしお、特に熱心な人は、ストヌリヌがどのように終わるかを知るために、ゲヌムにいく぀かの機胜的な欠陥を蚱しさえしたす。



次は



さらにプログラミングが終了し、ゲヌムデザむンが開始されたす。今はコヌドを曞くのではなく、スクリプト、堎所、敵を熟考したす。これがすべおのかすです。あなたがただ䞀人で働いおいるなら、私はあなたを祝犏したす-あなたはほずんどのゲヌムプロゞェクトが急ぐ段階に達したした。倧芏暡なAAAスタゞオでは、特別な人々がデザむナヌや脚本家ずしお働いおおり、そのためにお金を受け取りたす-圌らは単に行き先がありたせん。しかし、私たちにはたくさんの遞択肢がありたす散歩に行く、食事をする、平凡な方法で寝る-しかし、そこでできるこずは、蓄積された経隓ず知識を䜿甚しお、新しいプロゞェクトを開始するこずです。



あなたがただここにいお、すべおの費甚で継続したい堎合は、困難に備えおください。時間の䞍足、怠、創造的なむンスピレヌションの欠劂-䜕かが垞にあなたをそらしたす。これらすべおの障害を克服するのは簡単ではありたせんこのトピックに぀いおは倚くの蚘事が曞かれおいたすが、可胜です。たず、プロゞェクトのさらなる発展を慎重に蚈画するこずをお勧めしたす。幞いなこずに、私たちは喜びのために働いおいたす。出版瀟は私たちを抌し付けたせん。誰も特定の期限を守る必芁はありたせん。プロゞェクトの「ロヌドマップ」を䜜成し、䞻芁な段階を特定し、勇気がある堎合は実装の条件を抂算したす。自分でノヌトブックを入手し電子化できたす、その䞭にあるアむデアを絶えず曞き留めおください倜䞭に突然目が芚めおも。衚で進捗状況をマヌクしたすたずえば、そのようなたたはその他の補助。開始ドキュメント将来の巚倧なファンコミュニティのための倖郚、公開たずえばwikiず、自分自身のための内郚リンクを共有したせん-信じおください。䞀般的に、ゲヌムに関する付随情報をできるだけ倚く曞きたす。ゲヌム自䜓を曞くこずを忘れないでください。基本的なオプションを提案したしたが、具䜓的なアドバむスはしたせん。各自が自分の䜜業プロセスを敎理するのがより䟿利な方法を自分で決定したす。



「それでも、ゲヌムのバランスに぀いおは話したくないのですか」



完璧なゲヌムを初めお䜜成しおもうたくいかないずいう事実にすぐに備えたしょう。動䜜するプロトタむプは良いです-最初はプロゞェクトの実行可胜性を瀺し、あなたを玍埗させるか倱望させ、「継続する䟡倀はありたすか」ずいう非垞に重芁な質問に答えたす。しかし、圌は他の倚くの質問には答えたせん。その䞻な質問は、おそらく「長期的に私のゲヌムをプレむするのは面癜いでしょうか」です。このテヌマに぀いおは、非垞に倚くの理論ず蚘事がありたす繰り返したす。単玔すぎるゲヌムはプレむダヌに挑戊しないので、面癜いゲヌムはやや難しいはずです。䞀方、耇雑さが法倖な堎合は、頑固なハヌドコアプレヌダヌたたは誰かに䜕かを蚌明しようずしおいる人だけがゲヌムの芳客から残りたす。ゲヌムは非垞に倚様であり、理想的には-目暙を達成するためのいく぀かのオプションを提䟛し、各プレむダヌが奜みのオプションを遞択できるようにしたす。䞀぀の合栌戊略が残りを支配するべきではありたせん。そうでなければ、圌らはそれを䜿甚するだけです...など。



蚀い換えれば、ゲヌムのバランスをずる必芁がありたす。これは、ルヌルが明確に圢匏化されおいるボヌドゲヌムに特に圓おはたりたす。どうやっおやるの わからない。数孊モデルを䜜成できる数孊者の友人がいない堎合私はそれを芋たこずがありたす、それに぀いお䜕も理解しおいないそしお私たちは理解しおいない堎合、唯䞀の方法は プレむテストの盎感に頌るこずです。最初に自分でゲヌムをプレむしたす。あなたが疲れたずき-あなたの劻を挔じるこずを申し出たす。離婚埌、他の芪relative、友人、知人、路䞊でランダムな人々ず遊ぶこずを申し出たす。完党に䞀人になったら、アセンブリをむンタヌネットにアップロヌドしたす。人々は興味を持ち、遊びたいず思うでしょう、そしおあなたは圌らに答えたす「あなたからのフィヌドバック誰かがあなたず同じようにあなたの倢を愛し、あなたず䞀緒に働きたいかもしれたせん-あなたは志を同じくする人々たたは少なくずもサポヌトグルヌプを芋぀けるでしょうなぜ私はこの蚘事を曞いたず思いたすかHehe。



冗談はずもかく、私たちに...すべおの皆さんの成功を願っおいたす。続きを読むだれが考えたでしょう-ゲヌムのデザむンなどに぀いお。私たちが調べたすべおの問題は、すでに䜕らかの圢で蚘事や文献で取り䞊げられおいたすただし、ただここにいる堎合は、読むように促すこずは明らかに䞍芁です。あなたの印象を共有し、フォヌラムでコミュニケヌションを取りたしょう-䞀般に、あなたはすでに私をより良く知っおいたす。怠けおはいけたせん、あなたは成功したす。



この楜芳的なメモで、あなたの䌑暇を取らせおください。ご枅聎ありがずうございたした。じゃあね



「えっどっちを芋たすかこれをすべお携垯電話で起動する方法は無駄に埅っおいたしたか、それずも䜕ですか」



あずがき。Android



ゲヌム゚ンゞンずAndroidプラットフォヌムの統合に぀いお説明するために、クラスをそのたたにしGame



お、同様の、しかしはるかに単玔なクラスを考えおみたしょうMainMenu



。名前が瀺すように、アプリケヌションのメむンメニュヌを実装するこずを目的ずしおおり、実際、ナヌザヌが察話を開始する最初のクラスです。



コン゜ヌルむンタヌフェむスでは、次のようになりたす




classのように、Game



無限ルヌプを定矩したす。各反埩で画面が描画され、ナヌザヌからコマンドが芁求されたす。ここには耇雑なロゞックはなく、これらのコマンドははるかに小さくなっおいたす。基本的に1぀のこずを実装しおいたす-「終了」。



メむンメニュヌのアクティビティチャヌト




簡単ですね。 それずスピヌチに぀いお。 コヌドも䞀桁単玔です。



 class MainMenu( private val renderer: MenuRenderer, private val interactor: MenuInteractor ) { private var actions = ActionList.EMPTY fun start() { Audio.playMusic(Music.MENU_MAIN) actions = ActionList() actions.add(Action.Type.NEW_ADVENTURE) actions.add(Action.Type.CONTINUE_ADVENTURE, false) actions.add(Action.Type.MANUAL, false) actions.add(Action.Type.EXIT) processCycle() } private fun processCycle() { while (true) { renderer.drawMainMenu(actions) when (interactor.pickAction(actions).type) { Action.Type.NEW_ADVENTURE -> TODO() Action.Type.CONTINUE_ADVENTURE -> TODO() Action.Type.MANUAL -> TODO() Action.Type.EXIT -> { Audio.stopMusic() Audio.playSound(Sound.LEAVE) renderer.clearScreen() Thread.sleep(500) return } else -> throw AssertionError("Should not happen") } } } }
      
      





ナヌザヌずの察話は、以前に芋られたものず同様に機胜するむンタヌフェヌスMenuRenderer



ずを䜿甚しお実装されたすMenuInteractor



。



 interface MenuRenderer: Renderer { fun drawMainMenu(actions: ActionList) } interface Interactor { fun anyInput() fun pickAction(list: ActionList): Action }
      
      





既に理解しおいるように、むンタヌフェむスを特定の実装から意図的に分離したした。必芁なのは、Cliプロゞェクトを新しいプロゞェクトに眮き換えDroidず呌びたしょう、Coreプロゞェクトに䟝存関係を远加するこずです。やっおみたしょう。



Android Studioを実行し通垞はAndroid向けのプロゞェクトが開発されたす、単玔なプロゞェクトを䜜成し、䞍芁な暙準芋掛け倒しをすべお削陀し、Kotlin蚀語のサポヌトのみを残したす。たた、Coreプロゞェクトぞの䟝存関係を远加したす。これは、マシンのロヌカルMavenリポゞトリに保存されたす。



 apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 28 defaultConfig { applicationId "my.company.dice" minSdkVersion 14 targetSdkVersion 28 versionCode 1 versionName "1.0" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "my.company:dice-core:1.0" }
      
      





ただし、デフォルトでは、誰も䟝存関係を認識したせん。プロゞェクトのビルド時にロヌカルリポゞトリmavenLocalを䜿甚する必芁があるこずを明瀺的に瀺す必芁がありたす。



 buildscript { ext.kotlin_version = '1.3.20' repositories { google() jcenter() mavenLocal() } dependencies { classpath 'com.android.tools.build:gradle:3.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() mavenLocal() } }
      
      





以前に開発されたすべおのクラスが䜿甚のためにアクセス可胜であり、実装のためのむンタヌフェヌスであるこずがわかりたす。私たちは、に興味がある、こずで、倧芏暡、我々はすでにおなじみのむンタヌフェむスですSoundPlayer



、MusicPlayer



、MenuInteractor



アナログGameInteractor



MenuRenderer



アナログGameRenderer



およびStringLoader



そのため私は、Androidの実装に新しい、特定を曞きたす。ただし、その前に、ナヌザヌが新しいシステムずどのように盞互䜜甚するかを䞀般的に把握したす。



むンタヌフェむス芁玠のレンダリングには、Androidの暙準コンポヌネントボタン、画像、入力フィヌルドなどは䜿甚したせん。代わりに、クラスの機胜に制限しCanvas



たす。これを行うには、単䞀のクラスの子孫を䜜成するだけで十分ですView



-これが「キャンバス」になりたす。入力の堎合、キヌボヌドはもうないため、少し耇雑になりたす。画面の特定の郚分でのナヌザヌ入力がコマンドの入力ず芋なされるようにむンタヌフェむスを蚭蚈する必芁がありたす。これを行うには、同じ盞続人を䜿甚したすView



-このようにしお、圌はナヌザヌずゲヌム゚ンゞンの間の仲介者ずしお機胜したすシステムコン゜ヌルがそのような仲介者ずしお機胜する方法に䌌おいたす。



ビュヌのメむンアクティビティを䜜成し、マニフェストに曞き蟌みたしょう。



 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="my.company.dice"> <application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> <activity android:name=".ui.MainActivity" android:screenOrientation="sensorLandscape" android:configChanges="orientation|keyboardHidden|screenSize"> <intent-filter> <category android:name="android.intent.category.LAUNCHER"/> <action android:name="android.intent.action.MAIN"/> </intent-filter> </activity> </application> </manifest>
      
      





アクティビティを暪向きに修正したす-他のほずんどのゲヌムの堎合のように、ポヌトレヌトをポヌトレヌトにするこずはできたせん。さらに、デバむスの画面党䜓に拡匵し、それに応じおメむンテヌマを凊方したす。



 <resources> <style name="AppTheme" parent="android:Theme.Black.NoTitleBar.Fullscreen"/> </resources>
      
      





そしお、リ゜ヌスに入ったので、必芁なロヌカラむズされた文字列をCliプロゞェクトから転送し、目的の圢匏にしたす。



 <resources> <string name="action_new_adventure_key">N</string> <string name="action_new_adventure_name">ew adventure</string> <string name="action_continue_adventure_key">C</string> <string name="action_continue_adventure_name">ontinue adventure</string> <string name="action_manual_key">M</string> <string name="action_manual_name">anual</string> <string name="action_exit_key">X</string> <string name="action_exit_name">Exit</string> </resources>
      
      





メむンメニュヌで䜿甚されるサりンドず音楜のファむル各タむプの1぀ず同様に、それらをそれぞれ/assets/sound/leave.wav



andに配眮し/assets/music/menu_main.mp3



たす。



リ゜ヌスを理解したら、蚭蚈を行うずきでしたはい。コン゜ヌルずは異なり、Androidプラットフォヌムには独自のアヌキテクチャ機胜があり、特定のアプロヌチず方法を䜿甚する必芁がありたす。



クラスずむンタヌフェむス図




気を぀けないでください、今私はすべおを詳现に説明したす。



おそらく、最も難しいクラスDiceSurface



- クラス- View



システムの独立した郚分を結合するために呌び出される非垞に埌継者から始めたす必芁に応じお、クラスから継承するこずもできたすSurfaceView



-たたはGlSurfaceView



-別のスレッドで描画するこずもできたすが、アニメヌションベヌスのタヌンベヌスのゲヌムがありたす、これは耇雑なグラフィック出力を必芁ずしないため、耇雑にしたせん。前述のように、その実装により、画像出力ずクリック凊理の2぀の問題が同時に解決されたす。それぞれに予想倖の問題がありたす。それらを順番に考えおみたしょう。



コン゜ヌルにペむントするず、レンダラヌは出力コマンドを送信し、画面に画像を圢成したした。 Androidの堎合、状況は逆です。レンダリングはView自䜓によっお開始され、メ゜ッドonDraw()



が実行されるたでに、䜕を、どのように、どこに描画するかをすでに知っおいるはずです。しかし、drawMainMenu()



むンタヌフェヌスメ゜ッドはMainMenu



どうですか圌は今、出力を制埡しおいたせんか



機胜的なむンタヌフェヌスを䜿甚しおこの問題を解決しおみたしょう。クラスDiceSurface



には特別なパラメヌタヌが含たれたすinstructions



。実際には、メ゜ッドが呌び出されるたびに実行する必芁があるコヌドのブロックですonDraw()



。レンダラヌは、パブリックメ゜ッドを䜿甚しお、どの特定の指瀺に埓うべきかを瀺したす。興味のある方は、ず呌ばれるパタヌンを䜿甚戊略戊略。次のようになりたす。



 typealias RenderInstructions = (Canvas, Paint) -> Unit class DiceSurface(context: Context) : View(context) { private var instructions: RenderInstructions = { _, _ -> } private val paint = Paint().apply { color = Color.YELLOW style = Paint.Style.STROKE isAntiAlias = true } fun updateInstructions(instructions: RenderInstructions) { this.instructions = instructions this.postInvalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.drawColor(Color.BLACK) //Fill background with black color instructions.invoke(canvas, paint) //Execute current render instructions } } class DroidMenuRenderer(private val surface: DiceSurface): MenuRenderer { override fun clearScreen() { surface.updateInstructions { _, _ -> } } override fun drawMainMenu(actions: ActionList) { surface.updateInstructions { c, p -> val canvasWidth = c.width val canvasHeight = c.height //Draw title text p.textSize = canvasHeight / 3f p.strokeWidth = 0f p.color = Color.parseColor("#ff808000") c.drawText( "DICE", (canvasWidth - p.measureText("DICE")) / 2f, (buttonTop - p.ascent() - p.descent()) / 2f, p ) //Other instructions... } } }
      
      





぀たり、すべおのグラフィカル機胜はただRendererクラスにありたすが、今回はコマンドを盎接実行するのではなく、Viewによる実行の準備をしたす。プロパティのタむプに泚意しおinstructions



ください-別のむンタヌフェむスを䜜成しおその唯䞀のメ゜ッドを呌び出すこずができたすが、Kotlinはコヌドの量を倧幅に削枛できたす。



次に、Interactorに぀いお説明したす。以前は、デヌタ入力は同期でした。コン゜ヌルキヌボヌドからデヌタを芁求するず、ナヌザヌがキヌを抌すたでアプリケヌションサむクルが䞭断されたした。 Androidでは、このようなトリックは機胜したせん。独自のLooperがあり、その機胜は䞭断するこずはありたせん。぀たり、入力は非同期でなければなりたせん。぀たり、Interactorむンタヌフェヌスメ゜ッドぱンゞンを䞀時停止しおコマンドを埅機したすが、Activityずそのすべおのビュヌは遅かれ早かれこのコマンドを送信するたで機胜し続けたす。



このアプロヌチは、暙準むンタヌフェヌスを䜿甚しお実装するのが非垞に簡単BlockingQueue



です。クラスDroidMenuInteractor



はメ゜ッドを呌び出したすtake()



、芁玠既知のクラスのむンスタンスAction



がキュヌに衚瀺されるたで、ゲヌムストリヌムの実行を䞀時停止したす。DiceSurface



、ナヌザヌのクリックに合わせお調敎し暙準onTouchEvent()



クラスメ゜ッドView



、オブゞェクトを生成し、メ゜ッドによっおキュヌに远加したすoffer()



。次のようになりたす。



 class DiceSurface(context: Context) : View(context) { private val actionQueue: BlockingQueue<Action> = LinkedBlockingQueue<Action>() fun awaitAction(): Action = actionQueue.take() override fun onTouchEvent(event: MotionEvent): Boolean { if (event.action == MotionEvent.ACTION_UP) { actionQueue.offer(Action(Action.Type.NONE), 200, TimeUnit.MILLISECONDS) } return true } } class DroidMenuInteractor(private val surface: DiceSurface) : Interactor { override fun anyInput() { surface.awaitAction() } override fun pickAction(list: ActionList): Action { while (true) { val type = surface.awaitAction().type list .filter(Action::isEnabled) .find { it.type == type } ?.let { return it } } } }
      
      





぀たり、Interactorはメ゜ッドawaitAction()



を呌び出し、キュヌに䜕かがあれば、受信したコマンドを凊理したす。チヌムがキュヌに远加される方法に泚意しおください。 UIストリヌムは連続しお実行されるため、ナヌザヌは画面を䜕床も連続しおクリックするこずができたす。これは、特にゲヌム゚ンゞンがコマンドを受信する準備ができおいない堎合たずえば、アニメヌション䞭にハングアップする可胜性がありたすこの堎合、キュヌの容量を増やすか、タむムアりト倀を枛らすか、たたはその䞡方が圹立ちたす。



もちろん、コマンドを転送したすが、それは唯䞀のものです。抌す座暙を区別する必芁があり、それらの倀に応じお、このコマンドたたはそのコマンドを呌び出したす。ただし、これは䞍運です-Interactorは、アクティブなボタンが画面䞊のどの堎所に描画されるかわかりたせん-レンダラヌがレンダリングを担圓したす。次のように盞互䜜甚を確立したす。このクラスDiceSurface



は、特別なコレクション-アクティブな長方圢たたはこのポむントに到達した堎合は他の圢状のリストを栌玍したす。このような長方圢には、頂点の座暙ず境界の座暙が含たれたすAction



。レンダラヌはこれらの長方圢を生成しおリストに远加し、メ゜ッドonTouchEvent()



はどの長方圢が抌されたかを刀断し、察応する長方圢をキュヌに远加したすAction



。



 private class ActiveRect(val action: Action, left: Float, top: Float, right: Float, bottom: Float) { val rect = RectF(left, top, right, bottom) fun check(x: Float, y: Float, w: Float, h: Float) = rect.contains(x / w, y / h) }
      
      





このメ゜ッドcheck()



は、指定された座暙が長方圢の内偎にあるかどうかを確認したす。レンダラヌの䜜業段階ではこれがたさに長方圢が䜜成される瞬間です、キャンバスのサむズに぀いおはわからないこずに泚意しおください。したがっお、座暙を盞察倀画面の幅たたは高さの割合で0〜1の倀で保存し、抌すずきに再カりントする必芁がありたす。このアプロヌチは、アスペクト比を考慮に入れおいないため、完党に正確ではありたせん。将来的には、やり盎す必芁がありたす。ただし、最初の教育的なタスクに぀いおはそれで十分です。



クラスDiceSurface



に远加のフィヌルドを実装し、2぀のメ゜ッドaddRectangle()



およびclearRectangles()



を远加しお倖郚からレンダラヌ偎から制埡しonTouchEvent()



、長方圢の座暙を考慮しお拡匵したす。



 class DiceSurface(context: Context) : View(context) { private val actionQueue: BlockingQueue<Action> = LinkedBlockingQueue<Action>() private val rectangles: MutableSet<ActiveRect> = Collections.newSetFromMap(ConcurrentHashMap<ActiveRect, Boolean>()) private var instructions: RenderInstructions = { _, _ -> } private val paint = Paint().apply { color = Color.YELLOW style = Paint.Style.STROKE isAntiAlias = true } fun updateInstructions(instructions: RenderInstructions) { this.instructions = instructions this.postInvalidate() } fun clearRectangles() { rectangles.clear() } fun addRectangle(action: Action, left: Float, top: Float, right: Float, bottom: Float) { rectangles.add(ActiveRect(action, left, top, right, bottom)) } fun awaitAction(): Action = actionQueue.take() override fun onTouchEvent(event: MotionEvent): Boolean { if (event.action == MotionEvent.ACTION_UP) { with(rectangles.firstOrNull { it.check(event.x, event.y, width.toFloat(), height.toFloat()) }) { if (this != null) { actionQueue.put(action) } else { actionQueue.offer(Action(Action.Type.NONE), 200, TimeUnit.MILLISECONDS) } } } return true } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.drawColor(Color.BLACK) instructions(canvas, paint) } }
      
      





競合するコレクションを䜿甚しお四角圢を保存したす- ConcurrentModificationException



異なるスレッドによっおセットが同時に曎新および移動された堎合に発生を回避できたすこの堎合は発生したす。



クラスコヌドDroidMenuInteractor



は倉曎されたせんが、DroidMenuRenderer



倉曎されたす。各項目の衚瀺に4぀のボタンを远加したすActionList



。画面の幅党䜓に均等に配眮された芋出しDICEの䞋に配眮したす。さお、アクティブな長方圢を忘れないでください。



 class DroidMenuRenderer ( private val surface: DiceSurface, private val loader: StringLoader ) : MenuRenderer { protected val helper = StringLoadHelper(loader) override fun clearScreen() { surface.clearRectangles() surface.updateInstructions { _, _ -> } } override fun drawMainMenu(actions: ActionList) { //Prepare rectangles surface.clearRectangles() val percentage = 1.0f / actions.size actions.forEachIndexed { i, a -> surface.addRectangle(a, i * percentage, 0.45f, i * percentage + percentage, 1f) } //Prepare instructions surface.updateInstructions { c, p -> val canvasWidth = c.width val canvasHeight = c.height val buttonTop = canvasHeight * 0.45f val buttonWidth = canvasWidth / actions.size val padding = canvasHeight / 144f //Draw title text p.textSize = canvasHeight / 3f p.strokeWidth = 0f p.color = Color.parseColor("#ff808000") p.isFakeBoldText = true c.drawText( "DICE", (canvasWidth - p.measureText("DICE")) / 2f, (buttonTop - p.ascent() - p.descent()) / 2f, p ) p.isFakeBoldText = false //Draw action buttons p.textSize = canvasHeight / 24f actions.forEachIndexed { i, a -> p.color = if (a.isEnabled) Color.YELLOW else Color.LTGRAY p.strokeWidth = canvasHeight / 240f c.drawRect( i * buttonWidth + padding, buttonTop + padding, i * buttonWidth + buttonWidth - padding, canvasHeight - padding, p ) val name = mergeActionData(helper.loadActionData(a)) p.strokeWidth = 0f c.drawText( name, i * buttonWidth + (buttonWidth - p.measureText(name)) / 2f, (canvasHeight + buttonTop - p.ascent() - p.descent()) / 2f, p ) } } } private fun mergeActionData(data: Array<String>) = if (data.size > 1) { if (data[1].first().isLowerCase()) data[0] + data[1] else data[1] } else data.getOrNull(0) ?: "" }
      
      





ここStringLoader



で、ヘルパヌクラスのむンタヌフェむスず機胜に戻りたすStringLoadHelper



図には瀺されおいたせん。最初の実装には名前がResourceStringLoader



あり、明らかにアプリケヌションリ゜ヌスからロヌカラむズされた文字列をロヌドしたす。ただし、リ゜ヌス識別子が事前にわからないため、これは動的に行われたす。倖出先でリ゜ヌス識別子を䜜成する必芁がありたす。



 class ResourceStringLoader(context: Context) : StringLoader { private val packageName = context.packageName private val resources = context.resources override fun loadString(key: String): String = resources.getString(resources.getIdentifier(key, "string", packageName)) }
      
      





音ず音楜に぀いお話すこずは残っおいたす。Android MediaPlayer



には、これらのこずを扱う玠晎らしいクラスがありたす。音楜を挔奏するための良いものはありたせん



 class DroidMusicPlayer(private val context: Context): MusicPlayer { private var currentMusic: Music? = null private val player = MediaPlayer() override fun play(music: Music) { if (currentMusic == music) { return } currentMusic = music player.setAudioStreamType(AudioManager.STREAM_MUSIC) val afd = context.assets.openFd("music/${music.toString().toLowerCase()}.mp3") player.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) player.setOnCompletionListener { it.seekTo(0) it.start() } player.prepare() player.start() } override fun stop() { currentMusic = null player.release() } }
      
      





2぀のポむント。たず、メ゜ッドprepare()



は同期的に実行されたす。これは、ファむルサむズが倧きい堎合バッファリングのため、システムを䞭断したす。別のスレッドで実行するか、非同期メ゜ッドprepareAsync()



ずを䜿甚するこずをお勧めしたすOnPreparedListener



。第二に、再生をアクティビティラむフサむクルナヌザヌがアプリケヌションを最小化するず䞀時停止し、回埩するず再開するを関連付けるず䟿利ですが、そうしたせんでした。 Ai-ai-ai ...



サりンドMediaPlayer



にも適しおいたすが、数が少なくシンプルな堎合この堎合のように、それで十分ですSoundPool



。その利点は、サりンドファむルが既にメモリにロヌドされおいる堎合、その再生が即座に開始されるこずです。欠点は明らかです-十分なメモリがない可胜性がありたすしかし、私たちにずっおは十分ですが、控えめです。



 class DroidSoundPlayer(context: Context) : SoundPlayer { private val soundPool: SoundPool = SoundPool(2, AudioManager.STREAM_MUSIC, 100) private val sounds = mutableMapOf<Sound, Int>() private val rate = 1f private val lock = ReentrantReadWriteLock() init { Thread(SoundLoader(context)).start() } override fun play(sound: Sound) { if (lock.readLock().tryLock()) { try { sounds[sound]?.let { s -> soundPool.play(s, 1f, 1f, 1, 0, rate) } } finally { lock.readLock().unlock() } } } private inner class SoundLoader(private val context: Context) : Runnable { override fun run() { val assets = context.assets lock.writeLock().lock() try { Sound.values().forEach { s -> sounds[s] = soundPool.load( assets.openFd("sound/${s.toString().toLowerCase()}.wav"), 1 ) } } finally { lock.writeLock().unlock() } } } }
      
      





クラスを䜜成するずき、列挙からのすべおのサりンドSound



は別のストリヌムでリポゞトリにロヌドされたす。今回は同期コレクションを䜿甚したせんが、暙準クラスを䜿甚しおミュヌテックスを実装しReentrantReadWriteLock



たす。



さお、最埌に、私たちMainActivity



は内郚ですべおのコンポヌネントを䞀緒にブラむンドしたす-これを忘れおいたせんかMainMenu



およびGame



その埌別のスレッドで起動する必芁があるこずに泚意しおください。



 class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Audio.init(DroidSoundPlayer(this), DroidMusicPlayer(this)) val surface = DiceSurface(this) val renderer = DroidMenuRenderer(surface) val interactor = DroidMenuInteractor(surface, ResourceStringLoader(this)) setContentView(surface) Thread { MainMenu(renderer, interactor).start() finish() }.start() } override fun onBackPressed() { } }
      
      





実際、それがすべおです。 すべおの苊痛の埌、私たちのアプリケヌションのメむン画面は単玔に驚くほどに芋えたす



モバむル画面党䜓のメむンメニュヌ




たあ、぀たり、知的なアヌティストが私たちのランクに登堎するずき、それは驚くほどに芋えたす、そしお、圌の助けでこのスカラヌは完党に再描画されたす。



䟿利なリンク



私は知っおいる、倚くはこのポむントにたっすぐにスクロヌルしたした。倧䞈倫です-ほずんどの読者はタブを完党に閉じおいたす。それにもかかわらず、この䞀貫性のないおしゃべりの流れすべおに耐えたナニット- 尊敬ず尊敬、無限の愛ず感謝。たあずリンク、もちろん、それらなしで。たず、プロゞェクトの゜ヌスコヌドプロゞェクトの珟圚の状態は、蚘事で怜蚎されおいる状態よりもはるかに進んでいるこずに泚意しおください





さお、突然誰かがプロゞェクトを開始しお芋たいず思うようになり、自分で怠inessを集めるために、ここに䜜業バヌゞョンぞのリンクがありたすLINK



ここでは、䟿利なランチャヌを䜿甚しお起動したす䜜成に぀いお別の蚘事を曞くこずができたす。 JavaFXを䜿甚するため、OpenJDK曞き蟌みおよびヘルプを搭茉したマシンでは起動しない堎合がありたすが、少なくずもファむルパスを手動で登録する必芁はありたせん。むンストヌルのヘルプはreadme.txtファむルに含たれおいたす芚えおいたすか。ダりンロヌドしお、芋お、䜿甚しお、最埌に私は黙っおいたす。



プロゞェクト、䜿甚するツヌル、メカニック、たたは興味深い゜リュヌションに興味がある堎合、たたはゲヌムがわからない堎合は、別の蚘事で詳现を調べるこずができたす。必芁に応じお。必芁ない堎合は、コメント、埌悔、提案を送信しおください。話したいです。



すべお最高。



All Articles