LuaとLÖVEでゲームを作成する-4

画像






目次





13.スキルツリー



14.コンソール



15.最終



パート9:ディレクターとゲームプレイ



はじめに



このパートでは、最小限のコンテンツでゲーム全体の基本の実装を完了します。 ディレクター(ディレクター)-敵とリソースの作成を制御するコードを調べます。 次に、プレイヤーの死亡後にゲームを再開することを検討します。 その後、プレーヤーが自分のインジケーターについて知ることができるように、シンプルなポイントシステムと基本的なUIを扱います。



監督



ディレクター(監督)-ゲーム内の敵、攻撃、リソースの作成を制御するコード。 ゲームの目的は、できるだけ長く生き残り、できるだけ多くのポイントを獲得することです。 ゲームの難易度は、作成された敵の増え続ける数と複雑さによって決まります。 この複雑さは、これから記述するコードによって完全に制御されます。



ディレクターが従うルールは非常に簡単です:



  1. 22秒ごとに、難易度が増加します。
  2. 敵を作成する各難易度の持続時間は、ポイントシステムに基づきます。

    • 各難易度(またはラウンド)には、使用できる一定数のポイントがあります。
    • 敵は一定の一定量のポイントを消費します(敵が難しくなればなるほど、コストも高くなります)。
    • 難易度が高いほど、監督のポイントが多くなります。
    • 敵は、監督がポイントを使い果たすまで、ラウンド中にランダムに選択されて作成されます。
  3. 16秒ごとにリソースが作成されます(HP、SP、またはBoost)。
  4. 30秒ごとに攻撃が作成されます。


まず、 Director



オブジェクトを作成します。これは、通常のオブジェクト(GameObjectを継承するものではなく、Areaで使用されるもの)になります。 コードを入れます:



 Director = Object:extend() function Director:new(stage) self.stage = stage end function Director:update(dt) end
      
      





次のように、ステージルームでオブジェクトを作成し、そのインスタンスを作成できます。



 function Stage:new() ... self.director = Director(self) end function Stage:update(dt) self.director:update(dt) ... end
      
      





敵とリソースを作成する必要があるため、Directorオブジェクトにステージルームへの参照が必要stage.area



。これを行う唯一の方法はstage.area



を使用することstage.area



。 ディレクターも時間にアクセスする必要があるため、適切な更新が必要です。



ルール1から始め、単純な属性のdifficulty



と、この属性の増加時間を制御するためのいくつかの補助的なdifficulty



を定義します。 この一時的な変更コードは、アクセラレーションメカニズムまたはPlayerループで使用されるものと同じです。



 function Director:new(...) ... self.difficulty = 1 self.round_duration = 22 self.round_timer = 0 end function Director:update(dt) self.round_timer = self.round_timer + dt if self.round_timer > self.round_duration then self.round_timer = 0 self.difficulty = self.difficulty + 1 self:setEnemySpawnsForThisRound() end end
      
      





したがって、ルール1に従ってdifficulty



度は22秒ごとに増加しますsetEnemySpawnsForThisRound



関数を呼び出して、ルール2を実行することもできます。



ルール2の最初の部分は、各難易度に一定のポイントを使用できることです。 最初に必要なことは、ゲームでやりたい難易度のレベルと、これらのポイントをどのように設定するかを手動で決定することです。 2番目のオプションを選択して、ゲームが無限になり、プレイヤーが対処できなくなるまでますます難しくなるようにしました。 難易度は1024であると判断しました。これは、だれもほとんど到達できないかなり大きな数字だからです。



各難易度に割り当てられるポイントの数は、試行錯誤を繰り返して得た簡単な式によって決定されます。 繰り返しますが、このようなことはゲームの設計により関連しているので、決定を説明する時間を無駄にしないでしょう。 もっとうまくやれると思うなら、あなたはあなた自身のアイデアを試すことができます。



ポイントは、次の式に従って割り当てられます。





コードでは、次のようになります。



 function Director:new(...) ... self.difficulty_to_points = {} self.difficulty_to_points[1] = 16 for i = 2, 1024, 4 do self.difficulty_to_points[i] = self.difficulty_to_points[i-1] + 8 self.difficulty_to_points[i+1] = self.difficulty_to_points[i] self.difficulty_to_points[i+2] = math.floor(self.difficulty_to_points[i+1]/1.5) self.difficulty_to_points[i+3] = math.floor(self.difficulty_to_points[i+2]*2) end end
      
      





つまり、たとえば、最初の14の難易度には次のポイントがあります。



-

1 - 16

2 - 24

3 - 24

4 - 16

5 - 32

6 - 40

7 - 40

8 - 26

9 - 56

10 - 64

11 - 64

12 - 42

13 - 84








つまり、最初は3ラウンド続く特定のレベルのポイントがあり、その後1ラウンド減少し、次のラウンドで大幅に増加します。これは約3ラウンド続く新しいプラトーになり、次のラウンドで再びジャンプします。これは約3ラウンド続く新しいプラトーになり、このサイクルが無限に繰り返されます。 したがって、私たちはあなたが実験できる興味深いサイクル「正規化->緩和->強化」を作成します。



ポイント数を増やすことは非常に迅速で厳しいルールに従います。つまり、たとえば、40の難易度では、ラウンドは約400ポイントになります。 敵には一定のポイント数の価値があり、各ラウンドは与えられたすべてのポイントを費やさなければならないため、ゲームはすぐに過飽和状態になり、ある時点でプレイヤーは勝てなくなります。 しかし、これはゲームの設計だからです。 彼女の目標は、そのような条件で最大ポイントを獲得することです。



これに対処したら、ルール2の2番目の部分、つまり各敵のコストを決定することを試みることができます。 これまでのところ、2種類の敵を作成しただけなので、かなり簡単ですが、次のパートの1つでは、敵を作成した後にこれに戻ります。 これで、コードは次のようになります。



 function Director:new(...) ... self.enemy_to_points = { ['Rock'] = 1, ['Shooter'] = 2, } end
      
      





これは単純なテーブルで、敵の名前で作成するためのポイント数を取得できます。



ルール2の最後の部分は、 setEnemySpawnsForThisRound



関数の実装です。 しかし、それに取りかかる前に、チャンスと確率に関連する非常に重要な構成を紹介します。 ゲーム全体で使用します。



チャンスリスト



Xを25%の確率で発生させ、Yを25%の確率で発生させ、Zを50%の確率で発生させたいとします。 通常の方法で、これはlove.math.random



ような関数で実現できます-1から100までの値を生成し、値がどこになったかを確認します。 25未満の場合、イベントXが発生し、25から50の場合はイベントY、50を超える場合はイベントZが発生したと言います。



この実装の大きな問題は、 love.math.random



100回実行されたときに、Xが正確に25回発生することを保証できないことです。 10,000回行うと、おそらく確率は25%に近くなりますが、多くの場合、状況をさらに制御する必要があります。 したがって、簡単な解決策は、「変更リスト」( chanceList



)と呼ばれるものを作成することです。



chanceListリストは次のように機能します。1〜100の値を持つリストを生成します。このリストからランダムな値を取得する必要がある場合、 next



関数を呼び出します。 これは、イベントYが発生することを意味しますが、関数を呼び出すと、選択したランダム値もリストから削除されます。 本質的に、これは28が再び発生することはなく、Yイベントが他の2つのイベントよりもわずかに少ない可能性があることを意味します。 next



をより頻繁に呼び出すと、リストは空になり、完全に空になったら、100個すべての番号を再作成します。



したがって、イベントXが正確に25回、イベントYも正確に25回、イベントZが正確に50回発生することを保証できます。 関数で100個の数値を生成する代わりに20個生成することもできますこの場合、Xイベントは5回、Yイベントも5回、Zは10回発生します。



この原則のインターフェースは、かなり単純な方法で機能します。



 events = chanceList({'X', 25}, {'Y', 25}, {'Z', 50}) for i = 1, 100 do print(events:next()) --> will print X 25 times, Y 25 times and Z 50 times end
      
      





 events = chanceList({'X', 5}, {'Y', 5}, {'Z', 10}) for i = 1, 20 do print(events:next()) --> will print X 5 times, Y 5 times and Z 10 times end
      
      





 events = chanceList({'X', 5}, {'Y', 5}, {'Z', 10}) for i = 1, 40 do print(events:next()) --> will print X 10 times, Y 10 times and Z 20 times end
      
      





utils.lua



関数を作成し、このチュートリアルの第2部で説明したLua機能の一部を利用します。



まず、この関数がnext



関数を呼び出すことができるオブジェクトを返すことを認識する必要があります。 これを達成する最も簡単な方法は、このオブジェクトに次のような単純なテーブルを与えることです。



 function chanceList(...) return { next = function(self) end } end
      
      





ここで、値と確率のすべての可能な定義を...



として取得し...



これについては後で詳しく説明します。 次に、 next



関数を持つテーブルを返します。 :



関数を呼び出すと、それ自体に最初の引数として渡されることがわかっているため、この関数はself



を唯一の引数として受け取ります。 つまり、関数内ではnext



self



chanceList



が返すテーブルをchanceList



ます。



next



関数の内容を定義する前に、この関数が持ついくつかの属性を定義できます。 最初はchance_list



自体で、 next



関数によって返される値が含まれます。



 function chanceList(...) return { chance_list = {}, next = function(self) end } end
      
      





最初は、このテーブルは空であり、 next



機能で設定されます。 この例では:



 events = chanceList({'X', 3}, {'Y', 3}, {'Z', 4})
      
      





chance_list



属性は次のようになります。



 .chance_list = {'X', 'X', 'X', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z'}
      
      





chance_definitions



と呼ばれる別の属性が必要になります。これはchanceList



関数に渡されるすべての値と確率をchanceList



ます。



 function chanceList(...) return { chance_list = {}, chance_definitions = {...}, next = function(self) end } end
      
      





そして、それが私たちに必要なすべてです。 これで、 next



機能に進むことができます。 この関数には2つの動作が必要です。それは、 chance_definitions



で説明されている確率に従ってランダムな値を返し、ゼロ要素に達したときに内部のchance_list



復元する必要があります。 リストに要素が入力されていると仮定すると、次のように最初の動作を実装できます。



 next = function(self) return table.remove(self.chance_list, love.math.random(1, #self.chance_list)) end
      
      





chance_list



テーブル内のランダムなアイテムを選択し、それを返します。 要素の内部構造により、すべての制限が満たされます。



そして今、最も重要な部分chance_list



テーブルchance_list



を構築します。 同じコードを使用して、空にするために使用されるリストを作成できます。 次のようになります。



 next = function(self) if #self.chance_list == 0 then for _, chance_definition in ipairs(self.chance_definitions) do for i = 1, chance_definition[2] do table.insert(self.chance_list, chance_definition[1]) end end end return table.remove(self.chance_list, love.math.random(1, #self.chance_list)) end
      
      





ここでは、最初にchance_list



サイズchance_list



ゼロかどうかを判断します。 これは、 next



の最初の呼び出しと、多くの呼び出しの後にリストが空の場合に当てはまります。 これが当てはまる場合は、 chance_definitions



テーブルをバイパスします。このテーブルには、これらの値の値と確率でchance_definition



を呼び出すテーブルが含まれています。 つまり、 chanceList



関数を呼び出した場合:



 events = chanceList({'X', 3}, {'Y', 3}, {'Z', 4})
      
      





chance_definitions



テーブルは次のようになります。



 .chance_definitions = {{'X', 3}, {'Y', 3}, {'Z', 4}}
      
      





そして、このリストを見てchance_definitions[1]



chance_definitions[1]



は値を参照し、 chance_definitions[2]



chance_list



値が発生するchance_list



ます。 これを知って、リストを埋めるために、我々は単にchance_definition[1]



chance_list



chance_definition[2]



回挿入します。 したがって、すべてのchance_definitions



テーブルに対して行います。



これをテストすると、システムが機能することがわかります。



 events = chanceList({'X', 2}, {'Y', 2}, {'Z', 4}) for i = 1, 16 do print(events:next()) end
      
      





監督



ディレクターに戻りましょう: setEnemySpawnsForThisRound



の実装に関連するルール2の2番目の部分を実装したかったのsetEnemySpawnsForThisRound



。 最初に行うことは、敵が作成される確率を決定することです。 難易度レベルが異なると作成される確率も異なるため、少なくとも最初のいくつかの難易度を手動で設定する必要があります。 その後、難易度はランダムに設定されます。なぜなら、それらは非常に多くのポイントを持っているので、プレイヤーがとにかく過負荷になるからです。



したがって、最初のいくつかの難易度は次のようになります。



 function Director:new(...) ... self.enemy_spawn_chances = { [1] = chanceList({'Rock', 1}), [2] = chanceList({'Rock', 8}, {'Shooter', 4}), [3] = chanceList({'Rock', 8}, {'Shooter', 8}), [4] = chanceList({'Rock', 4}, {'Shooter', 8}), } end
      
      





これらは最終的な値ではなく、単なる例です。 最初の難易度では、石だけが作成されます。 第二に、射撃敵が追加されますが、石よりも少ないでしょう。 3番目の難易度では、両方の敵がほぼ同じ量で作成されます。 最後に、4番目は石よりも多くの射撃敵を作成します。



5〜1024の難易度の場合、各敵にランダムな確率を与えるだけです。



 function Director:new(...) ... for i = 5, 1024 do self.enemy_spawn_chances[i] = chanceList( {'Rock', love.math.random(2, 12)}, {'Shooter', love.math.random(2, 12)} ) end end
      
      





より多くの敵に気づいたら、最初の16の難易度を手動で作成し、複雑さ17の後、ランダムに作成します。 一般的に、スキルツリーが完全に設定されているプレイヤーは、ほとんどの場合、難易度レベル16を超えることはできません。



それでは、 setEnemySpawnsForThisRound



関数に移りましょう。 最初に行うことは、現在の難易度のポイントがなくなるまで、 enemy_spawn_chances



テーブルに従ってリストに敵を作成することです。 次のようになります。



 function Director:setEnemySpawnsForThisRound() local points = self.difficulty_to_points[self.difficulty] -- Find enemies local enemy_list = {} while points > 0 do local enemy = self.enemy_spawn_chances[self.difficulty]:next() points = points - self.enemy_to_points[enemy] table.insert(enemy_list, enemy) end end
      
      





したがって、ローカルのenemy_list



テーブルには、現在の複雑さの確率に従ってRock



Shooter



行が入力されます。 このコードをwhileループ内に配置し、残りのポイントの数がゼロになったときに実行を停止します。



その後、現在のラウンドの22秒の間隔で、 enemy_list



テーブル内の各敵がenemy_list



作成されるかを決定する必要があります。 次のようになります。



 function Director:setEnemySpawnsForThisRound() ... -- Find enemies spawn times local enemy_spawn_times = {} for i = 1, #enemy_list do enemy_spawn_times[i] = random(0, self.round_duration) end table.sort(enemy_spawn_times, function(a, b) return a < b end) end
      
      





ここでは、 enemy_list



各敵に0からround_duration



間の乱数enemy_list



割り当てられていることを確認します。このenemy_spawn_times



は、 enemy_spawn_times



テーブルに格納されてenemy_spawn_times



ます。 この表を並べ替えて、値が適切になるようにします。 つまり、 enemy_list



テーブルが次のようになっている場合です。



 .enemy_list = {'Rock', 'Shooter', 'Rock'}
      
      





enemy_spawn_times



テーブルは次のようになります。



 .enemy_spawn_times = {2.5, 8.4, 14.8}
      
      





これは、ラウンドの開始後、ロックが2.5秒で作成され、シューターが8.4秒で作成され、別のロックが14.8秒で作成されることを意味します。



最後に、 timer:after



を呼び出して敵の作成を​​セットアップする必要がありますtimer:after







 function Director:setEnemySpawnsForThisRound() ... -- Set spawn enemy timer for i = 1, #enemy_spawn_times do self.timer:after(enemy_spawn_times[i], function() self.stage.area:addGameObject(enemy_list[i]) end) end end
      
      





そして、ここではすべてが非常に簡単です。 enemy_spawn_times



のリストをenemy_spawn_times



て、最初のテーブルの番号に従ってenemy_list



からの敵の作成を​​設定します。 最後に行うことは、ゲームの開始時にこの関数を1回呼び出すことです。



 function Director:new(...) ... self:setEnemySpawnsForThisRound() end
      
      





そうしないと、わずか22秒で敵が作成され始めます。 起動時に攻撃リソースの作成を追加することもできます。これにより、プレイヤーは攻撃を置き換える機会を得ることができますが、これは必須ではありません。 場合によっては、今すぐコードを実行すると、すべてが意図したとおりに機能します。



この時点で、とりあえずディレクターを放っておきますが、ゲームにコンテンツを追加したら次の記事でディレクターに戻ります!



ディレクターエクササイズ



116.(CONTENT)ルール3を実装します。ルール1として機能しますが、複雑さを増す代わりに、リストに示されている3つのリソースの1つを作成する必要があります。 各リソースを作成する確率は、この定義に対応する必要があります。



 function Director:new(...) ... self.resource_spawn_chances = chanceList({'Boost', 28}, {'HP', 14}, {'SkillPoint', 58}) end
      
      





117.(コンテンツ)ルール4を実装します。ルール1として機能しますが、複雑さを増す代わりに、ランダム攻撃を作成する必要があります。



118.作成された敵の検索を処理するwhileループには、1つの大きな問題があります。無限ループで永久にスタックする可能性があります。 現在の難易度ではロックが作成されないため、1つのポイントのみが残っているが、1つのポイントに立っている敵(たとえば、ロック)を作成できない状況を想像してください。 敵の価格、難易度レベルのポイント数を変更せず、敵が確率を作成する確率に依存せずに、この問題の一般的な解決策を見つけます(たとえば、すべての難易度を常に低コストで敵を作成することにより)。



ゲームサイクル



それでは、ゲームループに移りましょう。 ここで、プレイヤーが何度もプレイできるようにします-プレイヤーが死んだとき、彼は新たにレベルを開始します。 終了したゲームでは、死後プレイヤーはコンソールルームに移動する必要があるため、サイクルは少し異なりますが、まだコンソールルームがないため、ステージルームを再起動するだけです。 ステージルームを何度も再起動するため、ここでメモリの問題を確認すると便利です。



コードの構造のおかげで、これは非常に簡単です。 gotoRoom



を使用して別のステージルームに切り替えるStageクラスでfinish



関数を定義します。 この関数は次のようになります。



 function Stage:finish() timer:after(1, function() gotoRoom('Stage') end) end
      
      





gotoRoom



は以前のStageインスタンスを破棄し、新しいインスタンスを作成するため、オブジェクトを手動で破棄する必要はありません。 注意する必要があるのは、destroy関数でStageクラスのplayer



属性をnil



に設定することだけです。そうしないと、Playerオブジェクトは正しく削除されません。



finish



関数は、プレーヤーが死んだときにPlayerオブジェクト自体から呼び出すことができます。



 function Player:die() ... current_room:finish() end
      
      





current_room



は現在のアクティブルームを含むグローバル変数であり、プレーヤーに対してdie



関数が呼び出されると、ステージが唯一のアクティブルームになるため、すべてが正常に機能します。 コードを実行すると、期待どおりに機能します。 プレイヤーが死亡した場合、1秒後に新しいステージルームが起動し、ゲームを新たに開始できます。



部屋とエリアの原則に従ってゲームを構築したため、すべてが非常にシンプルになったことは注目に値します。 すべてを異なって構造化すると、はるかに複雑になり、このため(多くの人は)LÖVEでゲームを作成するときに多くの人が混乱します。 必要に応じてシステムを構築できますが、ゲームの再起動などのいくつかの側面を実装するのがそれほど簡単ではないようにすることは簡単です。 アーキテクチャが果たす役割を理解することが重要です。



アカウント



ゲームの主な目標は、最大ポイント数を獲得することです。そのため、アカウントシステムを作成する必要があります。 これも、すでに行ったことに比べて非常に簡単です。 これを行うには、Stageクラスでscore



属性を作成するだけで、収集するポイントを追跡します。 ゲーム終了後、このスコアはどこかに保存され、以前の記録と比較できます。 ここでは、ポイントの比較を行う部分をスキップし、基本の分析にのみ焦点を当てます。



 function Stage:new() ... self.score = 0 end
      
      





これで、アクションを実行するときにスコアを増やすことができます。 スコアリングにはこのようなルールがありますが:



  1. 弾薬リソースの選択により、スコアに50ポイントが追加されます
  2. アクセラレーションリソースを選択すると、スコアに150ポイントが追加されます
  3. スキルポイントのリソースを選択すると、スコアに250ポイントが追加されます
  4. 攻撃リソースを選択すると、スコアに500ポイントが追加されます。
  5. ロックを破壊すると、スコアに100ポイントが加算されます。
  6. シューターを破壊すると、スコアに150ポイントが追加されます。


次のように実装するルール1:



 function Player:addAmmo(amount) self.ammo = math.min(self.ammo + amount, self.max_ammo) current_room.score = current_room.score + 50 end
      
      





最も明白な場所-イベントが発生する場所(この場合、これは関数addAmmo



)に移動し、スコアを変更するコードをここに追加します。function finish



これを行ったように、ここではステージルームにアクセスできますcurrent_room



。これは、この場合アクティブにできるのはステージルームだけだからです。



アカウント演習



119.(コンテンツ)ルール2〜6を実装します。これらは実装が非常に簡単で、例として示したものと非常に似ています。



UI



それでは、ユーザーインターフェイス(UI)に移りましょう。完成したゲームでは、次のようになります。









使用可能なスキルポイントの数は左上隅に示され、スコアは右上部分に表示され、プレイヤーの主な特徴は画面の上部と下部にあります。アカウントから始めましょう。ここで必要なのは、画面の右上隅に数字を表示することだけです。次のようになります。



 function Stage:draw() love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... love.graphics.setFont(self.font) -- Score love.graphics.setColor(default_color) love.graphics.print(self.score, gw - 20, 10, 0, 1, 1, math.floor(self.font:getWidth(self.score)/2), self.font:getHeight()/2) love.graphics.setColor(255, 255, 255) love.graphics.setCanvas() ... end
      
      





UIを他のすべての上に描画したいのですが、これは2つの方法で実装できます。 UIと呼ばれるオブジェクトを作成し、depth



すべての上に描画されるように属性を設定するmain_canvas



、ステージルームが使用するキャンバスのエリアの上に単純に描画することができます。 2番目の方法を選択することにしましたが、両方とも機能します。



上記のコードでは、フォントを設定するために使用しましたlove.graphics.setFont







 function Stage:new() ... self.font = fonts.m5x7_16 end
      
      





そして、画面の右上隅の対応する位置にスコアを描画します。テキストの幅を半分にシフトして、スコアが開始位置ではなくこの位置の中央にくるようにしました。そうしないと、数値が大きすぎる(> 10000)ときに、テキストが画面の境界を越えてしまうことがあります。



スキルポイントのテキストもほぼ同じ簡単な方法で作成されるため、演習用に残しておきます。






次に、UIの2番目の重要な部分、つまり中心的な要素に移りましょう。まずは健康(HP)から始めましょう。3つの要素を描画する必要があります。パラメーターを表す単語(この場合は「HP」)、パラメーターの完全性を示すストリップ、同じ情報をより正確な形式で示す数字です。



ストリップの描画から始めます。



 function Stage:draw() ... love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... -- HP local r, g, b = unpack(hp_color) local hp, max_hp = self.player.hp, self.player.max_hp love.graphics.setColor(r, g, b) love.graphics.rectangle('fill', gw/2 - 52, gh - 16, 48*(hp/max_hp), 4) love.graphics.setColor(r - 32, g - 32, b - 32) love.graphics.rectangle('line', gw/2 - 52, gh - 16, 48, 4) love.graphics.setCanvas() end
      
      





最初に、この長方形を位置に描画しgw/2 - 52, gh - 16



、その幅は等しくなり48



ます。つまり、両方のストライプは、8ピクセルの小さなギャップで画面の中心に対して相対的に描画されます。このことから、右側のストリップの位置がになることも理解できますgw/2 + 4, gh - 16







このストリップは色付きの塗りつぶされた長方形hp_color



になり、その輪郭は色付きの長方形になりますhp_color - 32



。テーブルから減算を実行できないため、テーブルhp_color



を個別のコンポーネントに分割し、各コンポーネントから減算する必要があります



何らかの方法で変更される唯一のストリップは、塗りつぶされた長方形で、その幅は比率に従って変化しhp/max_hp



ます。たとえば、hp/max_hp



1の場合、HPはいっぱいです。 0.5の場合、hp



サイズは半分max_hp



です。0.25の場合、サイズの1/4です。そして、この比率にストリップの幅を掛けると、プレーヤーのHP充填の美しい視覚化が得られます。これを実装すると、ゲームは次のようになります。









ここでは、プレイヤーがダメージを受けると、それに応じてレーンが反応することがわかります。



これに似ています。ポイントの数を描いたので、HPテキストを描画できます。



 function Stage:draw() ... love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... -- HP ... love.graphics.print('HP', gw/2 - 52 + 24, gh - 24, 0, 1, 1, math.floor(self.font:getWidth('HP')/2), math.floor(self.font:getHeight()/2)) love.graphics.setCanvas() end
      
      





ここでも、カウントに対して行ったのと同じ方法で、テキストを相対的にgw/2 - 52 + 24



中央に配置する必要があります。つまり、ストリップの中心に対して相対的に、つまり、このフォントで入力されたこのテキストの幅にシフトする必要があります(関数を使用してこれを行いますgetWidth



)。



最後に、HPの数字をバーの下に単純に描くこともできます。



 function Stage:draw() ... love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... -- HP ... love.graphics.print(hp .. '/' .. max_hp, gw/2 - 52 + 24, gh - 6, 0, 1, 1, math.floor(self.font:getWidth(hp .. '/' .. max_hp)/2), math.floor(self.font:getHeight()/2)) love.graphics.setCanvas() end
      
      





ここでも同じ原則が適用されます。テキストを中央に揃える必要があるため、テキストを幅にシフトする必要があります。これらの座標のほとんどは試行錯誤によって取得されているため、必要に応じて他の距離を試すことができます。



UI演習



120.(CONTENT) AmmoパラメーターのUIを実装します。ストリップの位置は等しいgw/2 - 52, 16



です。



121.(コンテンツ) BoostパラメーターのUIを実装します。ストリップの位置は等しいgw/2 + 4, 16



です。



122.(CONTENT) CycleパラメーターのUIを実装します。ストリップの位置は等しいgw/2 + 4, gh - 16



です。



終わり



これで、ゲームの最初の主要部分が完成しました。これは、最小限のコンテンツを持つゲーム全体の基本的なスケルトンです。後半(5部程度)は、ゲームにコンテンツを追加することに専念します。パーツの構造は、このパーツに似たものになります。このパーツでは、一度何かをしてから、演習で他の要素に同じアイデアを実装します。



ただし、次の部分は短い休憩で、コードの作成の実践に関する考えを共有し、選択したアーキテクチャの決定とコード構造について説明します。ゲームの作成にのみ興味がある場合は、それをスキップできます。なぜなら、それはよりカテゴリ的な部分であり、他のゲームほどゲームに関連していないからです。



パート10:コード作成のプラクティス



はじめに



このパートでは、この一連のチュートリアルで行うことに対して、推奨されるコーディング手法と、それらがどのように適用できるか、または適用できないかについて説明します。最初から読んで、ほとんどの演習(特に「コンテンツ」とラベル付けされているもの)を行った場合、おそらくプログラミングプラクティスの観点から質問を提起するソリューションに出くわしました。巨大なif / elseifチェーン、グローバル関数、巨大な関数、多数の操作を実行する巨大なクラス、適切に抽象化する代わりにコピーアンドペーストおよび繰り返しコードなど。



すでに別の分野でプログラミングの経験がある場合は、してはいけないことを知っているので、このパートでは、これらのソリューションのいくつかをさらに詳しく説明したいと思いました。これまでのすべての部分とは異なり、この部分は非常にカテゴリー的であり、おそらく誤りがあるため、問題なくスキップできます。ゲームに直接関係するものは考慮しません。私が話している内容のコンテキストのために、私たちが作成しているゲームの例を挙げます。このパートでは、2つの主要な側面、グローバル変数と抽象化について説明します。まず、グローバル変数をいつ、どこで使用できるかを説明し、次に、抽象化/一般化する方法としない場合、または行うべきではない方法について、より広く見ていきます。



さらに、チュートリアルを購入した場合は、この記事のコードベースに、演習で以前に「コンテンツ」としてマークされたコード、つまりすべてのプレイヤーの艦船、すべての攻撃、すべてのリソースのオブジェクトのグラフィックを追加しました。ここでは例として使用します。



グローバル変数



通常、グローバル変数の使用を避けることをお勧めします。このトピックについてさまざま 議論 があり、このアドバイスの根拠は非常に論理的です。一般的な場合、グローバル変数を使用する主な問題は、必要以上にすべてを予測不可能にすることです。最後のリンクに書かれているものは次のとおりです。



— , , . , , ( ), .



, , , . , , .


そして、これはすべて非常に正確で合理的です。しかし、そのような議論でコンテキストは常に忘れられます。上記のアドバイスは一般的なガイドとして理にかなっていますが、特定の状況を詳細に検討し始める場合、これがあなたのケースに当てはまるかどうかを明確に理解する必要があることがわかります。



そして、これはまさに私が深く信じているため、この記事全体で繰り返すアイデアです。数年/数十年にわたってサポートされるソフトウェアを開発する際に、複数の人々のチームに役立つアドバイスは、開発者にとってもうまく機能しません-1つのインディーズビデオゲーム。ほとんど自分でコードを書くときは、チームにとって許されない単純化を求めることができます。また、ゲームは通常短時間でサポートされるため、ビデオゲームを作成するときは、他の種類のソフトウェアよりもさらに単純化できます。



コンテキストのこの違いは、グローバル変数になると現れます。私の意見では、グローバル変数を使用する方法と理由を知っていれば、グローバル変数を使用できます。私たちはそれらの利点を最大限に活用し、同時にそれらの欠点を回避したいと考えています。そしてこの意味で、私たちは自分たちの利点も考慮する必要があります。まず、自分でコードを書き、次にビデオゲームを書きます。



グローバル変数の種類



私の意見では、グローバル変数には3つのタイプがあります。ほとんどが読み取り可能な変数、ほとんどが書き込まれる変数、および頻繁に読み書きされる変数です。



タイプ1



最初のタイプは、頻繁に読み取られるがめったに書き込まれないグローバル変数です。このタイプの変数は、実際にはプログラムの予測不可能性を実際には増加させないため、無害です。これらは、常にまたはほぼ常に一定の単純な既存の値です。また、定数と見なすこともできます。



ゲーム内のこのタイプの変数の例は、all_colors



すべての色のリストを含む変数ですこれらの色は決して変化せず、表は書き込まれません。同時に、たとえばランダムな色を取得する必要がある場合など、さまざまなオブジェクトから読み取られます。



タイプ2



2番目のタイプは、頻繁に書き込まれ、めったに読み取られないグローバル変数です。このような変数は、プログラムの予測不能性を増加させないため、ほとんど無害です。これは、非常に特定の制御された条件で使用される値の単なるリポジトリです。



これまでのところ、この定義に対応する変数はゲームにありませんが、例としては、プレーヤーのプレイ方法に関するデータを含むテーブルがあり、ゲームを終了すると、すべてのデータがサーバーに送信されます。このテーブルには、コードベースのさまざまな場所であらゆる種類の情報が常に書き込まれますが、サーバーに送信することを決定した場合にのみ読み取られ、場合によってはわずかに変更されます。



タイプ3



3番目のタイプは、アクティブな読み取りと書き込みを行うグローバル変数です。それらは本当の脅威を表し、実際に予測不可能性を高め、多くの点で作業を複雑にします。人々が「グローバル変数を使用しない」と言うとき、彼らはこのタイプを意味します。



ゲームにはそのような変数がいくつかありますが、最も顕著になると思いますcurrent_room



。彼女の名前は、現在の部屋がobject Stage



、object Console



、object SkillTree



、または他のタイプのRoomオブジェクトになる可能性があるため、特定の不確実性を暗示しています。私たちのゲームでは、これは完全に受け入れられる明瞭さの低下になると判断しました。






このようなグローバル変数の型への分離の主なポイントは、問題をもう少し深く理解し、いわば、穀物をもみ殻から分離することです。独断的すぎて、どうしてもグローバル変数を避けると、生産性に大きな影響が出ます。ソフトウェアを何年もサポートしてきたチームや人々がそれらを回避するのは努力する価値がありますがall_colors



、長期的には変数が私たち妨害する可能性は低いです。このような変数を追跡し、変数current_room



が多すぎたり混乱したりしないようにcurrent_room



する限り(たとえば、関数が呼び出されたときにのみ変更するgotoRoom



)、すべてを制御できます。



グローバル変数を見たり、使用したい場合は、最初にそれがどのようなタイプになるかを考えてください。タイプ1または2の場合、問題を引き起こすことはほとんどありません。タイプ3の場合は、いつ、どのくらいの頻度で読み書きされるかを考えることが重要です。コードベース全体のランダムオブジェクトから頻繁に書き込み、コードベース全体のランダムオブジェクトから読み取る場合は、おそらくグローバルにしないでください。ごくまれに非常に小さなオブジェクトのセットから書き込み、コードベース全体のランダムなオブジェクトから読み取る場合、これはまだあまり良くありませんが、受け入れられるかもしれません。重要なのは、このような問題について批判的に考えることであり、いくつかの独断的なルールに従うだけではありません。



抽象化とコピーアンドペースト



抽象化について話すとき、より限定的かつ明確な方法で再利用するために、複製または類似のコードから抽出されたコードの層を意味します。たとえば、このゲームには次のような行があります。



 local direction = table.random({-1, 1}) self.x = gw/2 + direction*(gw/2 + 48) self.y = random(16, gh - 16)
      
      





そして、y軸に沿ってランダムな位置で画面の左または右に作成されるすべてのオブジェクトで同じです。これらの3行は約6〜7個のオブジェクトの先頭にあるようです。ここでの抽象化の議論は、これらの行が複数のオブジェクトで繰り返される場合、オブジェクトが文字列のコードベースで繰り返すのではなく、この抽象化を使用できるように何らかの方法で抽象化する必要があるということです。この抽象化は、継承、コンポーネント、関数、またはその他のメカニズムを通じて実装できます。私たちの議論では、これらの異なる方法はすべて同じ問題を示しているため、1つのトピックと見なされます。



これで、私が話していることがわかったので、問題を詳しく見てみましょう。私の観点からは、これらの問題の主な議論は、既存の抽象化で新しいコードを追加するか、新しいコードを自由に追加するかです。つまり、何らかの形で私たちを助ける抽象化があると、他の方法で私たちを遅くする可能性のある(しばしば隠された)コストもあります。



抽象化



上記の例では、これらの3行をカプセル化する関数/コンポーネント/親クラスを作成できるため、それらをどこでも繰り返す必要はありません。コンポーネントは現在流行のピークにあるので、それらを取り上げてSpawnerComponentコンポーネントを実装しましょう(ただし、これは関数/継承/不純物、およびコードの抽象化/再利用の他の同様の方法に適用されることを忘れてはなりません)。として初期化できspawner_component = SpawnerComponent()



、オブジェクトを生成するすべてのロジックを魔法のように処理します。この例では、これらは3行ですが、同じロジックがより複雑な動作に適用されます。



このソリューションの利点は、オブジェクトのスポーンのロジックに関連するすべてが1つのインターフェイスの下の1つの場所にあることです。これは、スポーンの動作に何らかの変更を加える場合、1つの場所でのみ変更すれば十分であり、多くのファイルでコードを手動で変更する必要がないことを意味します。これの利点はよく理解されており、私はそれらを疑問視しません。



ただし、このようなソリューションには独自のコストがあり、ユーザーが何らかのソリューションを「販売」する場合はほとんどの場合無視されます。コストは、古いものに似ているが完全ではない新しい動作を追加するときに明らかになります。そして、ゲームではこれが頻繁に起こります。



したがって、たとえば、画面の真ん中に正確に生成されるオブジェクトを追加する必要があるとしましょう。2つのオプションがあります:SpawnerComponentを変更してこの新しい動作を受け入れるか、この新しい動作を実装する新しいコンポーネントを作成します。私たちの場合、当然の選択はSpawnerComponentを変更することですが、より複雑な例では、選択はそれほど明白ではないかもしれません。ここでのポイントは、既存のコード(この場合はSpawnerComponent)を考慮して新しいコードを追加する必要があるため、機能を追加する場所と場所を決定する必要があるという事実を考慮して、より精神的な努力が必要なことです静かに追加します。



コピーペースト



コードベースに現在適用されている代替ソリューションは、この動作が必要な場所にこれら3行が挿入されることです。このソリューションの欠点は、スポーンの動作を変更する必要がある場合、すべてのファイルを単調に調べてすべて変更する必要があることです。さらに、スポーン動作は別の環境に適切にカプセル化されていません。つまり、ゲームに新しい動作を追加する場合、他のすべてから分離することはより困難になります(ほとんどの場合、これらの3行だけであるとは限りません)。



ただし、このソリューションには利点があります。画面の真ん中に作成されたオブジェクトを追加する場合、前のオブジェクトからこれらの行をコピーして、最後の行を変更するだけです。



 local direction = table.random({-1, 1}) self.x = gw/2 + direction*(gw/2 + 48) self.y = gh/2
      
      





この場合、前の動作に似た新しい動作を追加することは完全に簡単であり、精神的な努力を必要としません(SpawnerComponentを使用したソリューションとは異なります)。



次に問題が発生します-両方の方法に長所と短所がある場合、デフォルトでどちらを使用する必要がありますか?通常、最初の方法はデフォルトで使用されるべきであり、このような重複したコードを悪臭がするので、長期間保持するべきではないと言われます。しかし、私の意見では、反対のことが必要です。デフォルトでは、重複するコードを使用し、絶対に必要な場合にのみ抽象化する必要があります。そしてその理由は...



変更の頻度と種類



コードの一部を抽象化する必要があるかどうかを確認する良い方法を見つけました。コードの変更頻度と変更方法を調べる必要があります。予測不可能な変更と予測可能な変更の2つの主なタイプの変更を発見しました。



予測できない変更



予測不可能な変更とは、単純な小さな変更よりもはるかに多く、動作を根本的に変更する変更です。スポーン動作の例では、予測できない変更は、たとえば、画面の左右に偶然敵を作成する代わりに、プロシージャジェネレーターアルゴリズムで指定された位置に基づいて敵を作成することです。このタイプの根本的な変化を予測することはできません。



このような変更は、開発のごく初期の段階で非常に頻繁に発生します。この段階では、ゲームについて漠然としたアイデアがありますが、詳細はありません。デフォルトのコピーと貼り付けの方法を選択することで、このような変更に対処できます。作成する抽象化が多いほど、これらの重複する変更をすべてコードベースに実装することが難しくなるためです。



予測可能な変更



予測可能な変更とは、動作をマイナーで特定の方法で変更する変更です。スポーンの動作に関する上記の例では、予測される変化は、y軸に沿って画面の真ん中にオブジェクトを正確に作成する必要がある例になります。この変更はスポーンの動作を変更しますが、それは非常に重要ではなく、スポーンの動作方法の基本を完全に破壊するものではありません。



そのような変更は、ゲームが成熟するときにより頻繁になります。それまでにはほとんどのシステムがすでにあり、同じ基本的な側面に小さなバリエーションや追加を加えるだけで十分だからです。このような変更は、対応するコードの変更の頻度を分析することで検討できます。頻繁に変更され、これらの変更が予測可能な場合は、抽象化を選択する価値があります。まれにしか変更されない場合は、デフォルトでコピーと貼り付けを選択する価値があります。






変更をこれらの2つのタイプに分ける主なポイントは、状況をより明確に分析し、より多くの情報に基づいた意思決定を行えるようにすることです。デフォルトですべてを独断的に抽象化し、すべてのコストでコードの重複を回避すると、生産性が低下します。どうしても回避は、長期にわたってサポートされるべきソフトウェアに取り組んでいるチームや人々には適していますが、単独で作成されたインディーズゲームには適していません。



何かを一般化する衝動を感じたら、それが必要かどうか一生懸命に考えてください。このコードが頻繁に変更されない場合は、心配する必要はありません。頻繁に変化する場合、どのように予測可能または予測不能ですか?予想外に変更された場合、カプセル化によりゲームに深刻な変更を加える必要がなくなるため、多くの作業とカプセル化の試みは時間の無駄になる可能性があります。予想どおりに変化する場合、抽象化が実際に役立つ可能性があります。ポイントは、これらの問題について批判的に考えることであり、盲目的にいくつかの独断的なルールに従うだけではありません。





これらの問題のより深い議論に使用できる他の例がゲームにあります。



左/右



この側面は、スポーンコード、つまり、直線で左または右に移動するすべてのエンティティの動作に非常に似ています。つまり、これは複数の敵とほとんどのリソースに適用されます。この動作を制御するコードは次のようになり、これらすべてのエンティティに対して繰り返されます。



 function Rock:new(area, x, y, opts) ... self.w, self.h = 8, 8 self.collider = self.area.world:newPolygonCollider(createIrregularPolygon(8)) self.collider:setPosition(self.x, self.y) self.collider:setObject(self) self.collider:setCollisionClass('Enemy') self.collider:setFixedRotation(false) self.v = -direction*random(20, 40) self.collider:setLinearVelocity(self.v, 0) self.collider:applyAngularImpulse(random(-100, 100)) ... end function Rock:update(dt) ... self.collider:setLinearVelocity(self.v, 0) end
      
      





性質によって、コライダーの設定はわずかに異なる場合がありますが、一般的にはほぼ同じです。スポーンコードの場合と同様に、このコードをLineMovementComponentなどの他の何かに抽象化することをお勧めします。



ここでの分析は以前と同じです。これらすべてのエンティティについて、この動作がどのくらいの頻度で変化するかを考慮する必要があります。正解はほとんどありません。振る舞いは、これらのエンティティの一部が左/右に移動する必要があり、方向が選択され、変更されないため、これを処理する意味がありません。つまり、コードベース全体でコードを繰り返すことができます。



プレイヤーの船のグラフィックとそれらの痕跡



ほとんどの演習を完了している場合、Playerクラスには次のようなコードがあります。



GIF






実際、これらは2つのif / elseifであり、1つはあらゆる種類の船のグラフィックを制御し、もう1つはこれらの船の航跡を制御します。このコードを見たときに最初に思うことは、それをきれいにする必要があるということです。しかし、再び、それは必要ですか?前の例とは異なり、このコードは多くの異なる場所で繰り返されません。つまり、単なる一連のコードです。



また、これらのさまざまな種類の船をすべて個別のファイルに抽象化し、これらのファイルの違いを判別し、Playerクラスでこれらのファイルを読み取るだけで、すべてのコードがきれいで美しくなるようにすることもできます。そして、これは実際に行うことができますが、私の意見では、これはオプションの抽象化の場合に適用されます。個人的には、明確に見えるシンプルなコードを持ち、いくつかの抽象化レベルに散らばらないようにしています。Playerクラスの先頭にあるこの巨大なコードに本当に悩まされている場合は、関数に入れてクラスの最後に配置することができます。または、エディターでサポートされる必要がある折りたたみを使用します。たとえば、エディターでは折りたたみは次のようになります。











プレイヤークラスサイズ



Playerクラスは現在、約500行で構成されています。受動的なスキルを追加する次のパートでは、約2,000行以上になります。これを見ると、あなたの自然な反応は、コードをより美しくきれいにすることです。そして、ここでも質問をする価値があります-これを行うことが本当に必要ですか?ほとんどのゲームでは、Playerクラスにはほとんどの機能が含まれており、多くの場合、人々がすべてが発生するような巨大なクラスにならないように多大な努力を払っています。



しかし、前の例から船のグラフィックとトレースを抽象化しないことにした同じ理由で、私の意見では、プレイヤーのクラスを構成するこれらの異なる論理部分をすべて抽象化することは意味がありません。そのため、プレーヤーの動き、プレーヤーの衝突、プレーヤーの攻撃などに個別のファイルを作成するのではなく、これらすべてを1つのファイルに入れて2000行のプレーヤークラスを取得することをお勧めします。要素間の抽象化のレイヤーなしで1つの場所にいることの利点とコストの比率は、要素の正しい抽象化による利点とコストの比率よりも高い(私の意見では!)。



エンティティーコンポーネントシステム



最後に、ECSは近年、単一の開発者にとって最も人気のあるミームでした。上記を考慮して、あなたはすでにこの問題に関する私の立場を理解していると思いますが、とにかく説明します。 ECSパターンの利点は明確に見えており、私には、誰もがそれらを理解しているようです。人々が理解していないのはその欠陥です。



そもそも、ECSはより複雑なシステムです。その意味は、ゲームに機能を追加すると、コンポーネントを再利用して、そこから新しいエンティティを作成できるということです。しかし、明らかなコスト(人々はしばしば無視します)は、開発の開始時に、再利用可能なコンポーネントの作成に必要以上に多くの時間を費やすことです。そして、抽象化/コピー&ペーストのセクションで述べたように、要素とデフォルトの振る舞いを作成して抽象化する場合、既存の抽象化と構造を考慮して追加する必要があるため、コードベースにコードを追加するのははるかに高価な作業になります。そして、これはコンポーネントに基づいたゲームで非常に顕著です。



さらに、ほとんどのインディーズゲームは、ECSアーキテクチャが成果を上げ始めているとは考えていません。私が描いたこの科学的なグラフを見てください。









ポイントは、最初は「ヨーロコーディング」(この記事のこの部分で推奨している)はECSよりも少ない労力で済むということです。プロジェクトが進むにつれて、ヨーロコーディングのコストは増加しますが、ECSのコストは減少します。次に、ECSがヨーロコーディングよりも効率的になる瞬間が来ます。ほとんどのインディーズゲームは(少なくとも私の意見では)非常にわずかな例外を除いて、この2行の交差点には決して到達しないと考えています。









そして、あなたの場合はそうですが、私の意見ではそうだとしたら、ECSのようなものを使う意味はありません。これは、多くの人が宣伝している他の多くのプログラミング手法や実践にも当てはまります。実際、チュートリアルのこの部分はこのアイデアに専念しています。長い目で見れば報われるものもありますが、インディーゲームの開発には適していません。なぜなら、彼らにとっては、長期的ではないからです。



終了



そうかもしれないが、これらの問題については十分に表明したと思う。この記事から何かを取り上げる場合、インターネットで見つかるプログラミングの推奨事項のほとんどは、長期的なサポートを必要とするソフトウェアに取り組んでいるチーム向けであることに留意してください。インディーズのビデオゲーム開発者としてのあなたのコンテキストは完全に異なるため、他の人から与えられたアドバイスがあなたに合っているかどうかを常に批判する必要があります。多くの場合、適切なのは、あらゆるコンテキストで役立つプログラミングの側面(変数の正しい名前など)があるためですが、時には適切ではないためです。そして、これに注意を払わないと、速度が低下して生産性が低下します。



同時に、大企業で働いており、長期間サポートされる必要があるソフトウェアを作成し、関連するプラクティスやプログラミングスタイルに慣れている場合、ゲームのハウスコードを別のスタイルで記述しようとする場合があります失敗します。したがって、あなたにとっての「自然な」コーディング環境と、私が説明するプログラミング環境がインディーズゲーム開発者にとってどれだけ自然であり、それらをどれだけうまく切り替えることができるかを考慮する必要があります。私は、あなたがあなたのプログラミング慣行、特定の文脈に対してどれほど適切であるか、そしてそれらを使用することがどれほど便利であるかについて批判的である必要があると言いたいです。



パート11:パッシブスキル



はじめに



このパートでは、すべてのパッシブゲームスキルの実装を検討します。合計で、ゲームには約120の異なる要素があり、これで非常に大きなスキルツリーになります(たとえば、約900個のノードを作成しました)。



記事のこの部分では、「コンテンツ」とラベル付けされた多くの演習があります。それらは次のように機能します。まず、どのように行われるかを示し、次に他のパラメーターについても同じことを行う必要があるエクササイズを多数提供します。たとえば、HP乗数、つまりプレイヤーのHPを特定の割合で乗算する特性を実装する方法を示し、演習では弾薬およびブースト係数を実装するように求めます。実際、すべてが少し複雑になりますが、一般的には次の意味です。



この部分のコード全体の実装が完了したら、ゲームのコンテンツの大部分を実装します。その後、実装した受動的なスキルから巨大なスキルツリーを構築するなど、細かい部分を完了するために残ります。



特性タイプ



すべての実装を進める前に、最初にゲームに含める受動的スキルのタイプを決定する必要があります。私は何をしたいのかをすでに決めているので、私の計画に従うだけですが、あなたはそれから安全に逸脱して、あなた自身のアイデアを思いつくことができます。



ゲームには、リソース、特性の乗数、および確率の3つの主要なタイプのパッシブ値があります。





また、ゲームには、追加のタイプのノードと追加のメカニズムがあります。重要なノードと一時的なボーナスです。








このすべてを学んだので、先に進むことができます。まず、コードベースでリソースパラメータがどのように見えるかを見てみましょう。



 function Player:new(...) ... -- Boost self.max_boost = 100 self.boost = self.max_boost ... -- HP self.max_hp = 100 self.hp = self.max_hp -- Ammo self.max_ammo = 100 self.ammo = self.max_ammo ... end
      
      





モーションコードの値は次のようになります。



 function Player:new(...) ... -- Movement self.r = -math.pi/2 self.rv = 1.66*math.pi self.v = 0 self.base_max_v = 100 self.max_v = self.base_max_v self.a = 100 ... end
      
      





そして、サイクルの値は次のようになります(整合性のために、以前のすべてのリンクの名前を「tick」から「cycle」に変更しました)。



 function Player:new(...) ... -- Cycle self.cycle_timer = 0 self.cycle_cooldown = 5 ... end
      
      





HP乗数



それでは、HP乗数から始めましょう。最も単純なケースではhp_multiplier



、最初に1の値を持つ変数を定義し、ツリーからのすべての増加をこの変数に適用し、ある時点でそれを掛けるだけで十分ですmax_hp



最初のものから始めましょう:



 function Player:new(...) ... -- Multipliers self.hp_multiplier = 1 end
      
      





第二に、HPがツリーから増加していると仮定する必要があります。これを行うには、これらの増加の送信方法と決定方法を決定する必要があります。ここで少しカンニングをしなければなりません(すでにこのゲームを書いているため)。ツリーのノードは次の形式で決定されると言います。



 tree[2] = {'HP', {'6% Increased HP', 'hp_multiplier', 0.06}}
      
      





これは、ノード2が呼び出されHP



、説明が6% Increased HP



ありhp_multiplier



、0.06(6%)の変数影響することを意味しますtreeToPlayer



これらの900個のノード定義をすべて受け取り、プレーヤーのオブジェクトに適用する関数があります。また、ノード定義で使用される変数名は、プレーヤーのオブジェクトで定義されている名前と同じでなければならないことに注意してください。そうしないと機能しません。これは非常に細かく結合され、エラーが発生しやすい方法ですが、前のパートで述べたように、すべてを単独で記述するため、そのようなことに耐えることができます。



最後の質問はこれです:私たちは、掛けたときhp_multiplier



max_hp



自然な選択は、コンストラクターで行うことです。なぜなら、新しいプレーヤーが作成され、新しいステージルームが作成されると新しいプレーヤーが作成されるからです。これは、新しいゲームの開始時にも発生します。ただし、すべてのリソース、要因、および確率が定義された後、コンストラクターの最後でこれを行います。



 function Player:new(...) ... -- treeToPlayer(self) self:setStats() end
      
      





したがって、関数でsetStats



は次のことができます。



 function Player:setStats() self.max_hp = self.max_hp*self.hp_multiplier self.hp = self.max_hp end
      
      





つまり、たとえば、hp_multiplier



値を1.5に設定してゲームを開始すると、プレーヤーのHPは100ではなく150



になりますここでは、関数の存在も想定しtreeToPlayer



、プレーヤーのオブジェクトをこの関数に渡す必要があることに注意してください後で、スキルツリーのコードを記述してこの関数を実装すると、ツリーからのボーナスに基づいてすべての要素の値が設定され、値を設定した後、setStats



それらを使用してプレーヤーのパラメーターを変更できます。



123.(CONTENT)変数を実装しますammo_multiplier







124.(CONTENT)変数を実装しますboost_multiplier







HPシンプル



次に、単純な機能について説明しましょう。単純な特性は、パーセンテージに基づくのではなく、いくつかの特性の直接的な増加です。(係数を乗算する前にflat_hp



)追加される変数定義することにより、HPにそれらを実装しますmax_hp







 function Player:new(...) ... -- Flats self.flat_hp = 0 end
      
      





 function Player:setStats() self.max_hp = (self.max_hp + self.flat_hp)*self.hp_multiplier self.hp = self.max_hp end
      
      





前と同様に、ツリーでノードを指定するとき、対応する変数にバインドしたいので、たとえば、単純なHPを追加するノードは次のようになります。



 tree[15] = {'Flat HP', {'+10 Max HP', 'flat_hp', 10}}
      
      





125.(CONTENT)変数を実装しますflat_ammo







126.(CONTENT)変数を実装しますflat_boost







127.(CONTENT)ammo_gain



プレイヤーがリソースを拾ったときに受け取る弾薬の量に追加される変数実装しますそれに応じて関数の計算を変更しますaddAmmo







ホーミングシェル



次に実装するパッシブスキルは、「弾薬の選択時にホーミング発射体が発砲する確率」になりますが、ここでは、ホーミング発射体のある部分に焦点を当てます。プレーヤーが受ける攻撃の1つはホーミング発射体であるため、現在、それを実装しています。



ホーミング機能は、属性attack



値が割り当てられたときに発射体に対してアクティブになります'Homing'



ホーミングコードは、弾薬リソースに使用されるコードと同じになります。



 function Projectile:update(dt) ... self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) -- Homing if self.attack == 'Homing' then -- Move towards target if self.target then local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized() local angle = math.atan2(self.target.y - self.y, self.target.x - self.x) local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized() local final_heading = (projectile_heading + 0.1*to_target_heading):normalized() self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y) end end end
      
      





異なる唯一のものは、変数の定義ですtarget



。弾薬オブジェクトでは、変数target



はプレイヤーのオブジェクトを示しますが、発射物の場合、最も近い敵を示します。最も近い敵を取得するにgetAllGameObjectsThat



は、Areaクラスで定義された関数を使用し、敵であり十分近くにあるオブジェクトのみを選択するフィルターを適用します。これを行うには、最初にどのオブジェクトが敵で、どのオブジェクトが敵ではないかを判断する必要があります。これを行う最も簡単な方法enemies



は、敵クラスの名前を持つ行のリストを含むグローバルテーブルを作成することです。つまりglobals.lua



、次の定義を追加できます。



 enemies = {'Rock', 'Shooter'}
      
      





新しい敵をゲームに追加する場合、このテーブルに適切に行を追加します。どのタイプのオブジェクトが敵であるかがわかったので、それらを簡単に選択できます。



 local targets = self.area:getAllGameObjectsThat(function(e) for _, enemy in ipairs(enemies) do if e:is(_G[enemy]) then return true end end end)
      
      





文字列を使用して、_G[enemy]



ループする現在の文字列のクラス定義にアクセスします。つまり_G['Rock']



、クラス定義を含むテーブルを返しますRock



これについては、チュートリアルのいくつかの部分で説明しているため、これが機能する理由を既に理解している必要があります。



別の条件では、発射物から特定の半径内にある敵のみを選択する必要があります。試行錯誤によって、半径400ユニットに到達しましたが、これは発射体が適切なターゲットを見つけることができないほど小さくはなく、発射体がスクリーンの後ろにいる敵を頻繁に攻撃しようとするほど大きくはありませんでした。



 local targets = self.area:getAllGameObjectsThat(function(e) for _, enemy in ipairs(enemies) do if e:is(_G[enemy]) and (distance(ex, ey, self.x, self.y) < 400) then return true end end end)
      
      





distance



で定義できる関数ですutils.lua



2つの位置間の距離を返します。



 function distance(x1, y1, x2, y2) return math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)) end
      
      





そしてその後、敵はリストに載らなければなりませんtargets



次に、必要なのは、それらの1つをランダムに選択target



し、発射体が向かっている方向を示すことです。



 self.target = table.remove(targets, love.math.random(1, #targets))
      
      





そして、それはすべてこのように見えるはずです:



 function Projectile:update(dt) ... self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) -- Homing if self.attack == 'Homing' then -- Acquire new target if not self.target then local targets = self.area:getAllGameObjectsThat(function(e) for _, enemy in ipairs(enemies) do if e:is(_G[enemy]) and (distance(ex, ey, self.x, self.y) < 400) then return true end end end) self.target = table.remove(targets, love.math.random(1, #targets)) end if self.target and self.target.dead then self.target = nil end -- Move towards target if self.target then local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized() local angle = math.atan2(self.target.y - self.y, self.target.x - self.x) local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized() local final_heading = (projectile_heading + 0.1*to_target_heading):normalized() self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y) end end end
      
      





新しいターゲットを取得するブロックの最後self.target



に、ターゲットが殺された場合にnil を割り当てる追加の行があります。このため、発射体のターゲットが存在しなくself.target



なると、条件が満たされるため、値nilが割り当てられ、新しいターゲットが取得さnot self.target



れます。その後、プロセス全体が繰り返されます。また、目標を受け取った後は計算を行わないので、関数の速度を心配する必要は特にありません。関数の速度はgetAllGameObjectsThat



、ゲーム内のすべての生きているオブジェクトを単純にサイクルでバイパスします。



次に変更する必要があるのは、ホーミングされていないとき、またはターゲットが欠落しているときの発射物の動作です。最初に使用するのが論理的ですsetLinearVelocity



発射体の速度を設定し、サイクル内で再び使用します。これif self.attack == 'Homing'



は、発射体が実際にホーミングし、ターゲットが存在する場合にのみ速度が変化するためです。しかし、何らかの理由でこれはあらゆる種類の問題につながるため、setLinearVelocity



一度だけ呼び出す必要があります。つまり、次のように記述します。



 -- Homing if self.attack == 'Homing' then ... -- Normal movement else self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end
      
      





これは前のスキームよりも少し混乱しますが、機能します。これをすべてテストし、属性にattack



値が割り当てられたシェルを作成すると'Homing'



、次のようになります。









128.(CONTENT)攻撃を実装しHoming



ます。攻撃テーブルでの定義は次のとおりです。



 attacks['Homing'] = {cooldown = 0.56, ammo = 4, abbreviation = 'H', color = skill_point_color}
      
      





そして、攻撃自体は次のようになります。









この攻撃(および作成する他の攻撃)の発射物はわずかに異なることに注意してください。これは菱形で、半分は白く塗られ、半分は攻撃の色(この場合skill_point_color



)になり、プレイヤーの色を持つ痕跡もあります。



弾薬の選択中にホーミング発射体を発射する確率



これで、実現したいもの、つまり、確率に関連付けられた受動的なスキルに進むことができます。このスキルは、弾薬リソースを選択するときに機能する可能性があります。この確率を変数に保存しlaunch_homing_projectile_on_ammo_pickup_chance



、Ammoリソースを選択するときに、このイベントの確率を「キューブでロール」する関数を呼び出します。



しかし、これを行う前に、これらの確率をどのように処理するかを示す必要があります。前のパートのいずれかで述べたように、ここでもリストの概念を適用しますchanceList



。イベントの発生確率が5%の場合、これらの5%がかなり公平に観察されるようにする必要があるため、chanceListを使用することは論理的です。



次のように実装します。関数を呼び出した後setStats



PlayerコンストラクターではgenerateChances



、ゲーム全体に適用されるすべてのchanceListリストを作成する関数も呼び出します。ゲームにはサイコロを振る必要のあるさまざまなイベントがあるので、すべてのチャンスリストリストをテーブルに入れて、chances



何かをする確率のためにサイコロを振る必要があるときに何かをすることができるようにすべてを整理しますのような:



 if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then -- launch homing projectile end
      
      





テーブルをchances



手動で設定できます。つまり_chance



、何らかのイベントの実行の確率が格納される型の新しい変数を追加するたびに、関数にgenerateChances



chanceListリストを追加して生成ます。しかし、ここでは少し賢くして、確率を処理する各変数がで終わることを決定し_chance



、これを有利に使用することができます。



 function Player:generateChances() self.chances = {} for k, v in pairs(self) do if k:find('_chance') and type(v) == 'number' then end end end
      
      





ここでは、プレーヤーのオブジェクト内のすべてのキーと値のペアを調べて、名前_chance



部分文字列を含む属性が見つかったときにtrueを返します。これらの条件の両方が真である場合、独自の決定に基づいて、これは特定のイベントの確率に関連する変数です。つまり、chanceListを作成してテーブルに追加するだけですchances







 function Player:generateChances() self.chances = {} for k, v in pairs(self) do if k:find('_chance') and type(v) == 'number' then self.chances[k] = chanceList({true, math.ceil(v)}, {false, 100-math.ceil(v)}) end end end
      
      





したがって、100の値のchanceListを作成します。この値のv



trueと100-v



false は等しくなります。つまり、プレーヤーのオブジェクトで定義されたタイプ「確率」の唯一の変数がで、そのlaunch_homing_projectile_on_ammo_pickup_chance



値が5(イベントの5パーセントの確率を意味する)である場合、chanceListには5つの真の値と95の偽の値があり、望ましい結果が得られます。



そしてgenerateChances



、プレーヤーのコンストラクターを呼び出す場合



 function Player:new(...) ... -- treeToPlayer(self) self:setStats() self:generateChances() end
      
      





それはうまくいきます。これで変数を定義できますlaunch_homing_projectile_on_ammo_pickup_chance







 function Player:new(...) ... -- Chances self.launch_homing_projectile_on_ammo_pickup_chance = 0 end
      
      





また、この「サイコロ」システムの動作をテストする場合は、値を50に設定し、数回呼び出して:next()



何が起こるを確認できます。



ショットはonAmmoPickup



、弾薬リソースを選択するときに呼び出される関数を通じて実装されます



 function Player:update(dt) ... if self.collider:enter('Collectable') then ... if object:is(Ammo) then object:die() self:addAmmo(5) self:onAmmoPickup() ... end end
      
      





この関数は次のように機能します。



 function Player:onAmmoPickup() if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then local d = 1.2*self.w self.area:addGameObject('Projectile', self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), {r = self.r, attack = 'Homing'}) self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'}) end end
      
      





その結果、これはすべて次のようになります。









129.(CONTENT)受動的スキルを実装しregain_hp_on_ammo_pickup_chance



ます。回復されるHPの量は25になります。addHP



指定された量のHPを値hp



追加する関数使用して追加する必要がありますmax_hp



。さらに、InfoText



テキスト'HP Regain!'



と色持つオブジェクト作成する必要がありますhp_color







130.(CONTENT)受動的スキルを実装しregain_hp_on_sp_pickup_chance



ます。回復されるHPの量は25になります。addHP



指定された量のHPを値hp



追加する関数使用して追加する必要がありますmax_hp



InfoText



テキスト'HP Regain!'



と色のオブジェクトも作成する必要がありますhp_color



。さらに、Playerクラスに関数を追加する必要があります。onSPPickup



、およびすべての作業をその中で実行する必要があります(関数の使用方法と同様onAmmoPickup



)。



加速エリア



次に実装するパッシブスキルは、「HPを選択したときに加速領域を作成する確率」と「SPを選択したときに加速領域を作成する確率」です。「リソースを選択するとき」の部分を実装する方法はすでにわかっているため、「加速の領域」に焦点を当てます。加速エリアは単純な円で、プレイヤーが攻撃中にいるときの攻撃速度を上げます。この攻撃速度の加速は乗数として使用されるため、攻撃速度の乗数を実装することから始めるのが論理的です。



ASPD乗算器



ASPDファクターを単純に変数として定義し、aspd_multiplier



この変数に発火間の「休息」時間を掛けることができます



 function Player:new(...) ... -- Multipliers self.aspd_multiplier = 1 end
      
      





 function Player:update(dt) ... -- Shoot self.shoot_timer = self.shoot_timer + dt if self.shoot_timer > self.shoot_cooldown*self.aspd_multiplier then self.shoot_timer = 0 self:shoot() end end
      
      





主な違いは、この乗数の場合、大きい値よりも小さい値のほうが優れていることです。通常、乗数値が0.5の場合、適用されるパラメーターが半分になります。つまり、HP、スピード、その他ほとんどすべてにとって、これは悪いことです。ただし、攻撃速度については、値が小さいほど優れており、これは上記のコードで簡単に説明できます。変数shoot_cooldown



係数を適用するため、値が低いほどポーズが短くなります。つまり、プレーヤーはより速くシュートします。この知識を使用してオブジェクトを作成しますHasteArea







加速エリア



ASPD乗数ができたので、エリアに戻ることができます。ここではaspd_multiplier



、プレイヤーがその中にいる間に一定量だけ減少する円形の領域を作成しますこれを実現するためにHasteArea



、プレーヤーが内部にいるかどうかをチェックし、適切な値を設定するロジックを制御する新しいオブジェクト作成します。このオブジェクトの基本構造は次のとおりです。



 function HasteArea:new(...) ... self.r = random(64, 96) self.timer:after(4, function() self.timer:tween(0.25, self, {r = 0}, 'in-out-cubic', function() self.dead = true end) end) end function HasteArea:update(dt) ... end function HasteArea:draw() love.graphics.setColor(ammo_color) love.graphics.circle('line', self.x, self.y, self.r + random(-2, 2)) love.graphics.setColor(default_color) end
      
      





エフェクトを適用するロジックを実装するには、プレイヤーのヒット/エリアからの退出を追跡し、aspd_multiplier



これが発生したときに値を変更する必要がありますこれはおおよそ次の方法で実行できます。



 function HasteArea:update(dt) ... local player = current_room.player if not player then return end local d = distance(self.x, self.y, player.x, player.y) if d < self.r and not player.inside_haste_area then -- Enter event player:enterHasteArea() elseif d >= self.r and player.inside_haste_area then -- Leave event player:exitHasteArea() end end
      
      





変数を使用してinside_haste_area



、プレーヤーがエリアにいるかどうかを追跡します。この変数は値true inside enterHasteArea



とfalse insideを取りますexitHasteArea



。つまり、これらの関数は、これらのイベントがオブジェクトで発生したときにのみ呼び出されますHasteArea



。 Playerクラスでは、両方の関数が必要な変更を適用するだけです:



 function Player:enterHasteArea() self.inside_haste_area = true self.pre_haste_aspd_multiplier = self.aspd_multiplier self.aspd_multiplier = self.aspd_multiplier/2 end function Player:exitHasteArea() self.inside_haste_area = false self.aspd_multiplier = self.pre_haste_aspd_multiplier self.pre_haste_aspd_multiplier = nil end
      
      





したがって、プレイヤーがエリアに進入すると、攻撃速度は2倍になり、エリアから退出すると、通常に戻ります。ここで見落としやすい重要な点の1つはHasteArea



、変数を介してプレーヤーにリンクするのではなく、オブジェクト内にすべてのロジックを配置する誘惑があるということですinside_haste_area



。これを行うことができない場合、プレーヤーが同時に複数のエリアに入るか、それらを離れるときに問題が発生するためです。現在の形式では、変数の存在はinside_haste_area



、プレイヤーが交差する3つのHasteAreaオブジェクトの上にいる場合でも、ボーナスを1回だけ適用することを意味します。



131.(CONTENT)受動的スキルを実装しspawn_haste_area_on_hp_pickup_chance



ます。オブジェクトInfoText



はテキストで作成する必要があります'Haste Area!'



さらに、Playerクラスに関数を追加する必要がありますonHPPickup







132.(CONTENT)受動的スキルを実装しspawn_haste_area_on_sp_pickup_chance



ます。オブジェクトInfoText



はテキストで作成する必要があります'Haste Area!'







ループ内でSPを作成する確率



次の効果はになりますspawn_sp_on_cycle_chance



実装方法は完全にわかっています。「ループ内」の部分は、「リソース内の選択」と非常によく似ています。唯一の違いはonCycle



、リソースを選択するときではなく、新しいサイクルを実行するときに関数を呼び出すことです。そして、「SPの作成」の部分は、新しいSPリソースを作成することであり、その実装は既知です。



したがって、最初の部分では、関数に入っcycle



て呼び出す必要がありますonCycle







 function Player:cycle() ... self:onCycle() end
      
      





次に、変数をPlayerに追加しますspawn_sp_on_cycle_chance







 function Player:new(...) ... -- Chances self.spawn_sp_on_cycle_chance = 0 end
      
      





したがって、この変数の確率である新しいchanceListも自動的に追加します。これにより、関数に必要な機能を追加できますonCycle







 function Player:onCycle() if self.chances.spawn_sp_on_cycle_chance:next() then self.area:addGameObject('SkillPoint') self.area:addGameObject('InfoText', self.x, self.y, {text = 'SP Spawn!', color = skill_point_color}) end end
      
      





そして、これは意図したとおりに動作するはずです:









敵を殺すときに並んで撃つチャンス



次のスキルはbarrage_on_kill_chance



まだわからないことは、「ラインナップ」の部分だけです。キルイベントは前のイベントと似ていますが、サイクルの実行中に呼び出すのではなくonKill



、敵が死亡したときにプレーヤー関数を呼び出す点が異なります。



したがって、最初に変数をPlayerに追加しますbarrage_on_kill_chance







 function Player:new(...) ... -- Chances self.barrage_on_kill_chance = 0 end
      
      





次に、関数を作成しonKill



、敵が死んだときに呼び出します。onKill



プレイヤーが死んだときのチャレンジは2つのアプローチがあります。 1つは、関数die



またはhit



各敵から関数を呼び出すだけです。ここでの問題は、新しい敵を追加するとき、それを引き起こすそれらのすべてに同じコードを追加しなければならないということonKill



です。 2番目のオプションはonKill



、敵と衝突したときにProjectileオブジェクトを呼び出すことです。ここでの問題は、一部のシェルが敵と衝突する可能性がありますが、敵を殺すことはできない(敵のHPが高いか、シェルのダメージが少ないため)ため、敵が実際に死んでいるかどうかを確認する方法を見つける必要があるということです。このチェックは非常に簡単なので、この方法を選択します。



 function Projectile:update(dt) ... if self.collider:enter('Enemy') then ... if object then object:hit(self.damage) self:die() if object.hp <= 0 then current_room.player:onKill() end end end end
      
      





hit



敵の関数を呼び出した後に行う必要があるのは、敵のHPがゼロに等しいかどうかを確認することだけです。等しい場合、それは彼が死んでいることを意味し、私たちはを呼び出すことができますonKill







それでは、行自体に進みましょう。デフォルトでは、0.05秒のショット間隔と-math.pi / 8から+ math.pi / 8の範囲の広がりで、8つのシェルがコードで起動されます。さらに、発射物のシェルには、プレイヤーが所有する攻撃があります。つまり、プレイヤーがホーミングシェルを撃った場合、キュー内のすべてのシェルもホーミングします。コードでは、これは次のように記述できます。



 function Player:onKill() if self.chances.barrage_on_kill_chance:next() then for i = 1, 8 do self.timer:after((i-1)*0.05, function() local random_angle = random(-math.pi/8, math.pi/8) local d = 2.2*self.w self.area:addGameObject('Projectile', self.x + d*math.cos(self.r + random_angle), self.y + d*math.sin(self.r + random_angle), {r = self.r + random_angle, attack = self.attack}) end) end self.area:addGameObject('InfoText', self.x, self.y, {text = 'Barrage!!!'}) end end
      
      





ほとんどのコードは非常に単純です。言及する価値がある唯一のことは、forループの内側を使用してafter



、0.05秒のポーズでシェルの作成を分割することです。他のすべての点で、私たちは単に与えられた制限で発射物を作成します。すべて次のようになります。









以下の演習(およびそれら以降のすべての演習)ではInfoText



、プレイヤーが何が起こっているのかを理解できるように、適切な色のオブジェクトを作成すること忘れないでください



133.(CONTENT)受動的スキルを実装しspawn_hp_on_cycle_chance



ます。



134.(CONTENT)受動的スキルを実装しregain_hp_on_cycle_chance



ます。 HP回復の数は25に等しいはずである



(コンテンツ)135パッシブスキルを実装しますregain_full_ammo_on_cycle_chance







136.(CONTENT)受動的スキルを実装しchange_attack_on_cycle_chance



ます。新しい攻撃がランダムに選択されます。



137.(CONTENT)受動的スキルを実装しspawn_haste_area_on_cycle_chance



ます。



138.(CONTENT)受動的スキルを実装しbarrage_on_cycle_chance



ます。



139.(コンテンツ)受動的スキルを実装するlaunch_homing_projectile_on_cycle_chance







140.(CONTENT)受動的スキルを実装しregain_ammo_on_kill_chance



ます。弾薬の回復可能な量は20でなければなりません



。141.(内容)受動的スキルを実装しlaunch_homing_projectile_on_kill_chance



ます。



142.(CONTENT)受動的スキルを実装しregain_boost_on_kill_chance



ます。復元される加速の量は40でなければなりません。143



.(CONTENT)受動的スキルを実装しspawn_boost_on_kill_chance



ます。



KillでのASPDアクセラレーションの取得



ASPD Accelerationに似たオブジェクトのようなパッシブスキルを既に実装していますHasteArea



今度は、敵を殺した後に攻撃速度を上げるチャンスがある別のものを実装したいと思います。ただし、以前のASPDアクセラレーションと同じ方法でこれを実装しようとすると、すぐに問題が発生します。メモリを更新するために、加速がどのように実装されているかの例を示しますHasteArea







 function HasteArea:update(dt) HasteArea.super.update(self, dt) local player = current_room.player if not player then return end local d = distance(self.x, self.y, player.x, player.y) if d < self.r and not player.inside_haste_area then player:enterHasteArea() elseif d >= self.r and player.inside_haste_area then player:exitHasteArea() end end
      
      





次にenterHasteArea



、これらexitHasteArea



は次のようになります。



 function Player:enterHasteArea() self.inside_haste_area = true self.pre_haste_aspd_multiplier = self.aspd_multiplier self.aspd_multiplier = self.aspd_multiplier/2 end function Player:exitHasteArea() self.inside_haste_area = false self.aspd_multiplier = self.pre_haste_aspd_multiplier self.pre_haste_aspd_multiplier = nil end
      
      





aspd_boost_on_kill_chance



同様の方法でパッシブスキルを実装しようとすると、次のようになります。



 function Player:onKill() ... if self.chances.aspd_boost_on_kill_chance:next() then self.pre_boost_aspd_multiplier = self.aspd_multiplier self.aspd_multiplier = self.aspd_multiplier/2 self.timer:after(4, function() self.aspd_multiplier = self.pre_boost_aspd_multiplier self.pre_boost_aspd_multiplier = nil end) end end
      
      





ここでは、HasteAreaのアクセラレーションと同じことを行います。現在の攻撃速度の乗数を保存して半分にし、指定された期間(この場合は4秒)後に元の値を復元します。このような実装の問題は、これらのボーナスのアクションを組み合わせたいときに発生します。



プレイヤーがHasteAreaに進入し、敵を殺した後にASPD加速を受けた状況を想像してください。ここでの問題は、プレイヤーが4秒前にHasteAreaを離れると、変数aspd_multiplier



がASPD加速前の値に復元されることです。つまり、エリアを離れると、攻撃速度を加速する他のすべてのボーナスがキャンセルされます。



プレーヤーがASPDを積極的に加速し、HasteAreaエリアに進入していると想像してください。加速アクションが完了すると、HasteArea攻撃の速度の増加を考慮しないように値pre_boost_aspd_multiplier



復元されるため、HasteAreaエフェクトもゼロにリセットされますaspd_multiplier



。しかし、より重要なことは、プレイヤーがHasteAreaを離れると、入り口での保存された攻撃速度がASPDアクセラレーションによって増加するため、常に攻撃速度が増加することです。



したがって、この問題はいくつかの変数を導入することで解決できます。



 function Player:new(...) ... self.base_aspd_multiplier = 1 self.aspd_multiplier = 1 self.additional_aspd_multiplier = {} end
      
      





1つの変数の代わりにaspd_multiplier



、とがbase_aspd_multiplier



ありadditional_aspd_multiplier



ます。変数aspd_multiplier



は、すべての加速度を考慮して、現在の乗数を格納します。base_aspd_multiplier



パーセントの増加のみを考慮した元の乗数が含まれます。つまり、ツリーから攻撃速度が50%増加すると、コンストラクター(c setStats



)に適用されbase_aspd_multiplier



ます。次にadditional_aspd_multiplier



、すべての加速度のすべての追加値が含まれます。つまり、プレーヤーがHasteAreaにいる場合、対応する値をこのテーブルに追加し、各フレームでその量にベースを乗算します。したがって、たとえば、更新関数は次のようになります。



 function Player:update(dt) ... self.additional_aspd_multiplier = {} if self.inside_haste_area then table.insert(self.additional_aspd_multiplier, -0.5) end if self.aspd_boosting then table.insert(self.additional_aspd_multiplier, -0.5) end local aspd_sum = 0 for _, aspd in ipairs(self.additional_aspd_multiplier) do aspd_sum = aspd_sum + aspd end self.aspd_multiplier = self.base_aspd_multiplier/(1 - aspd_sum) end
      
      





この方法では、各フレームaspd_multiplier



で、ベースの値とすべての加速度に従って変数を再計算します非常によく似た方法で機能するいくつかの要素があります。そのため、異なる変数名でコードを毎回繰り返すのは面倒なので、このための共通オブジェクトを作成します。



オブジェクトStat



は次のようになります。



 Stat = Object:extend() function Stat:new(base) self.base = base self.additive = 0 self.additives = {} self.value = self.base*(1 + self.additive) end function Stat:update(dt) for _, additive in ipairs(self.additives) do self.additive = self.additive + additive end if self.additive >= 0 then self.value = self.base*(1 + self.additive) else self.value = self.base/(1 - self.additive) end self.additive = 0 self.additives = {} end function Stat:increase(percentage) table.insert(self.additives, percentage*0.01) end function Stat:decrease(percentage) table.insert(self.additives, -percentage*0.01) end
      
      





そして、それを使用して、次のように攻撃速度の問題を解決します。



 function Player:new(...) ... self.aspd_multiplier = Stat(1) end function Player:update(dt) ... if self.inside_haste_area then self.aspd_multiplier:decrease(100) end if self.aspd_boosting then self.aspd_multiplier:decrease(100) end self.aspd_multiplier:update(dt) ... end
      
      





aspd_multiplier:update



参照するとaspd_multiplier.value



呼び出し後いつでも攻撃率乗数にアクセスできます。これは、ベースおよび適用されたすべての種類の加速に従って正しい結果を返します。したがって、変数の使用方法を変更する必要がありますaspd_multiplier







 function Player:update(dt) ... -- Shoot self.shoot_timer = self.shoot_timer + dt if self.shoot_timer > self.shoot_cooldown*self.aspd_multiplier.value then self.shoot_timer = 0 self:shoot() end end
      
      





ここでは、単純に置き換えるself.shoot_cooldown*self.aspd_multiplier



self.shoot_cooldown*self.aspd_multiplier.value



そうでない場合は何も動作しませんので、。さらに、ここで何か他のものを変更する必要があります。aspd_multiplier



これまでの変数の動作方法は、他のすべてのゲーム変数の動作方法と矛盾しています。 HPを10%増加したと言うと、hp_multiplier



1.1であることわかりますが、10%ASPD増加したと言うと、aspd_multiplier



実際には0.9です。これを変更して、aspd_multiplier



乗算の代わりに除算を実行することで、他の変数と同じように動作させることできますshoot_cooldown







 if self.shoot_timer > self.shoot_cooldown/self.aspd_multiplier.value then
      
      





したがって、ASPDが100%増加すると、その値は2に等しくなり、ショット間のポーズが半分になります。これはまさに達成したことです。さらに、ボーナスの適用方法を変更する必要があり、それらを呼び出す代わりに、decrease



次のように呼び出しますincrease







 function Player:update(dt) ... if self.inside_haste_area then self.aspd_multiplier:increase(100) end if self.aspd_boosting then self.aspd_multiplier:increase(100) end self.aspd_multiplier:update(dt) end
      
      





さらに、これaspd_multiplier



Stat



単なる数字ではなくオブジェクトであるため、ツリーを実装してその値をPlayerオブジェクトにインポートするとき、それらを異なる方法で処理する必要があることを覚えておく必要がありますしたがって、上記の関数では、treeToPlayer



これを考慮する必要があります。



それがそうであるように、この方法で、「killでASPDアクセラレーションを取得」を簡単に実装できます。



 function Player:new(...) ... -- Chances self.gain_aspd_boost_on_kill_chance = 0 end
      
      





 function Player:onKill() ... if self.chances.gain_aspd_boost_on_kill_chance:next() then self.aspd_boosting = true self.timer:after(4, function() self.aspd_boosting = false end) self.area:addGameObject('InfoText', self.x, self.y, {text = 'ASPD Boost!', color = ammo_color}) end end
      
      





関数enterHasteArea



とを削除しexitHasteArea



、HasteAreaオブジェクトの動作をわずかに変更することもできます。



 function HasteArea:update(dt) HasteArea.super.update(self, dt) local player = current_room.player if not player then return end local d = distance(self.x, self.y, player.x, player.y) if d < self.r then player.inside_haste_area = true elseif d >= self.r then player.inside_haste_area = false end end
      
      





以前に使用した複雑なロジックの代わりinside_haste_area



に、リージョン内にあるかどうかに応じてPlayerオブジェクトの属性をtrueまたはfalseに設定し、オブジェクトの実装方法によりStat



、HasteAreaから受信した攻撃の速度を加速するアプリケーションが実行されます自動的に。



144.(CONTENT)受動的スキルを実装しmvspd_boost_on_cycle_chance



ます。 「MVSPDの増加」は、プレーヤーに4秒間の移動速度を50%増加させます。また、変数mvspd_multiplier



実装し、適切な場所でそれを掛けます。



145.(CONTENT)受動的スキルを実装しpspd_boost_on_cycle_chance



ます。 「PSPD Boost」は、プレイヤーが作成したシェルの速度を4秒間100%増加させます。変数も実装しますpspd_multiplier



適切な場所でそれを掛けます。



146.(CONTENT)受動的スキルを実装しpspd_inhibit_on_cycle_chance



ます。「PSPD Decrease」は、プレイヤーが作成した発射体の速度を4秒間50%減少させます。



加速中



次に実装するパッシブスキルは、最新の「イベント確率」スキルです。以前に説明したスキルはすべて、何らかのイベント(キル中、サイクル中、リソース選択時)で何かを行う確率に関連しており、以下は加速中に何かを行う確率であるため、違いはありません(ブースト)船。



まず、実装していlaunch_homing_projectile_while_boosting_chance



ます。これは次のように動作します。通常、セルフガイドの発射体ショットの確率があり、この確率は加速(ブースト)を実行するときに0.2秒の間隔でチェックされます。これは、1秒間加速すると、「確率キューブ」が5回ロールされることを意味します。



これを実装するための良い方法は、2つの新しい関数を定義することである。onBoostStart



そしてonBoostEnd



加速が開始されるとパッシブスキルがアクティブになり、加速が終了するとパッシブスキルが非アクティブになります。これら2つの関数を追加するには、加速コードをわずかに変更する必要があります。



 function Player:update(dt) ... -- Boost ... if self.boost_timer > self.boost_cooldown then self.can_boost = true end ... if input:pressed('up') and self.boost > 1 and self.can_boost then self:onBoostStart() end if input:released('up') then self:onBoostEnd() end if input:down('up') and self.boost > 1 and self.can_boost then ... if self.boost <= 1 then self.boosting = false self.can_boost = false self.boost_timer = 0 self:onBoostEnd() end end if input:pressed('down') and self.boost > 1 and self.can_boost then self:onBoostStart() end if input:released('down') then self:onBoostEnd() end if input:down('down') and self.boost > 1 and self.can_boost then ... if self.boost <= 1 then self.boosting = false self.can_boost = false self.boost_timer = 0 self:onBoostEnd() end end ... end
      
      





ここでは、追加input:pressed



、およびinput:released



そのリターン真のみこれらのイベントの委員会であり、このために、我々は確認することができonBoostStart



、かつonBoostEnd



唯一のこれらのイベントの委員会に呼び出されます。またinput:down



onBoostEnd



、プレーヤーがボタンを放さなかった場合に条件構造内に追加しますが、プレーヤーが利用できる加速の量は終了するため、加速は終了します。



では、次の部分に進みましょうlaunch_homing_projectile_while_boosting_chance







 function Player:new(...) ... -- Chances self.launch_homing_projectile_while_boosting_chance = 0 end function Player:onBoostStart() self.timer:every('launch_homing_projectile_while_boosting_chance', 0.2, function() if self.chances.launch_homing_projectile_while_boosting_chance:next() then local d = 1.2*self.w self.area:addGameObject('Projectile', self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), {r = self.r, attack = 'Homing'}) self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'}) end end) end function Player:onBoostEnd() self.timer:cancel('launch_homing_projectile_while_boosting_chance') end
      
      





ここでは、加速の開始時に、timer:every



0.2秒ごとにホーミング発射体を発射する確率を確認し、加速が終了したらこのタイマーをキャンセルします。イベントが完了する確率が100%の場合、次のようになります。









147.(CONTENT)変数を実装しますcycle_speed_multiplier



。この変数は、その値に応じて、サイクルの速度を増減します。つまり、たとえば、cycle_speed_multiplier



2で、デフォルトのサイクル期間が5秒の場合、変数を使用するとサイクル期間が2.5秒に短縮されます。



148.(CONTENT)受動的スキルを実装しincreased_cycle_speed_while_boosting



ます。この変数はブール型である必要があり、プレーヤーが加速するときにサイクル速度を上げる必要があるかどうかを示します。加速は、サイクル速度係数を200%増加させる必要があります。



149.(CONTENT)受動的スキルを実装するinvulnerability_while_boosting



この変数はブール型で、加速中にプレーヤーが不死身であるべきかどうかを示します。invincible



プレーヤーの不死身の原因となっいる既存の属性使用します



加速運の増加



私たちが実装する「加速の」受動的スキルの最後のタイプは「加速の運の向上」です。実装する前に、parameterを実装する必要がありますluck_multiplier



運はゲームの主要なパラメーターの1つです。目的のイベントを実現する可能性が高まります。殺害中に10%の確率でミサイルがホーミングする可能性があるとします。luck_multiplier



2に等しい場合、この確率は20パーセントになります。



スキルを実装する方法は非常に簡単です。「確率」などのすべての受動的スキルは関数を通過するgenerateChances



ため、ここで簡単に実装できます。



 function Player:generateChances() self.chances = {} for k, v in pairs(self) do if k:find('_chance') and type(v) == 'number' then self.chances[k] = chanceList( {true, math.ceil(v*self.luck_multiplier)}, {false, 100-math.ceil(v*self.luck_multiplier)}) end end end
      
      





そして、ここで乗算v



するだけでluck_multiplier



、これは正確に機能するはずです。これによりincreased_luck_while_boosting



、次のようにパッシブスキル実装できます。



 function Player:onBoostStart() ... if self.increased_luck_while_boosting then self.luck_boosting = true self.luck_multiplier = self.luck_multiplier*2 self:generateChances() end end function Player:onBoostEnd() ... if self.increased_luck_while_boosting and self.luck_boosting then self.luck_boosting = false self.luck_multiplier = self.luck_multiplier/2 self:generateChances() end end
      
      





ここでは、オブジェクトに対して元々行っていた方法で実装しますHasteArea



。 Playerに幸運を与える他の受動的なスキルがないため、これを行うことができます。つまり、互いにオーバーライドできるいくつかのボーナスを心配する必要はありません。幸運を増す受動的なスキルがいくつかStat



ある場合、の場合と同様に、それらをオブジェクトにする必要がありますaspd_multiplier







また、運の乗数を変更するときは、generateChances



それ以外の場合、運の増加は何にも影響しません。このソリューションには欠点があります-すべてのリストがリセットされるため、一部のリストが一連の失敗した「スロー」を誤って選択してリセットされた場合、再びチャンスリストプロパティを使用する代わりに一連の失敗した「スロー」を選択できます。時間が経つにつれて、スロー成功の選択が次第に少なくなります。しかし、これは非常に小さな問題であり、個人的にはあまり気にしません。



HP作成確率乗数



ここでhp_spawn_chance_multiplier



、ディレクターが新しいリソースを作成したときに、このリソースがHPになる可能性を高める方法を検討します。ディレクターの仕組みを覚えていれば、この実装は非常に簡単です。



 function Player:new(...) ... -- Multipliers self.hp_spawn_chance_multiplier = 1 end
      
      





 function Director:new(...) ... self.resource_spawn_chances = chanceList({'Boost', 28}, {'HP', 14*current_room.player.hp_spawn_chance_multiplier}, {'SkillPoint', 58}) end
      
      





パート9では、各リソースの生成確率を作成することを検討しました。これらの確率はchanceList resource_spawn_chances



格納されるため、使用する必要があるのはhp_spawn_chance_multiplier



、乗数に従ってHPリソースが作成される可能性を高めることだけです。



さらに、DirectorはPlayerで使用可能な変数に依存しますが、PlayerはDirectorにまったく依存しないため、ここではPlayerの後にステージルームでディレクターを初期化することが重要です。



150.(CONTENT)受動的スキルを実装しspawn_sp_chance_multiplier



ます。



151.(CONTENT)受動的スキルを実装しspawn_boost_chance_multiplier



ます。



以前に実装したすべてを考慮すると、次の演習は困難に思えるかもしれません。私はそれらの実装のほとんどの側面を考慮しませんでしたが、それらは私たちが以前にやったものに比べて非常に単純なので、それらを書くのは簡単です。



152.(CONTENT)受動的スキルを実装しdrop_double_ammo_chance



ます。敵が死亡すると、1つではなく2つの弾薬オブジェクトを作成する可能性があります。



153.(CONTENT)受動的スキルを実装しattack_twice_chance



ます。プレーヤーが攻撃するとき、関数をshoot



2回呼び出す可能性があります



154.(CONTENT)受動的スキルを実装しspawn_double_hp_chance



ます。ディレクターがHPリソースを作成する場合、1つではなく、2つのHPオブジェクトを作成する可能性があります。



155.(コンテンツ)パッシブスキルの実装spawn_double_sp_chance



ディレクターがSkillPointリソースを作成するとき、代わりに2つのSkillPointオブジェクトを作成する可能性があります。



156.(CONTENT)受動的スキルを実装しgain_double_sp_chance



ます。プレイヤーがSkillPointリソースを取得すると、1つではなく2つのスキルポイントを獲得できる可能性があります。



敵の作成頻度



enemy_spawn_rate_multiplier



Directorが難易度を変更する速度を制御します。デフォルトでは、これは22秒ごとに発生しますが、enemy_spawn_rate_multiplier



2 秒の場合は11秒ごとに発生します。これの実装も非常に簡単です。



 function Player:new(...) ... -- Multipliers self.enemy_spawn_rate_multiplier = 1 end
      
      





 function Director:update(dt) ... -- Difficulty self.round_timer = self.round_timer + dt if self.round_timer > self.round_duration/self.stage.player.enemy_spawn_rate_multiplier then ... end end
      
      





つまり、ここでは単純に除算round_duration



enemy_spawn_rate_multiplier



て、ラウンドの所要時間を取得します。



157.(CONTENT)受動的スキルを実装しresource_spawn_rate_multiplier



ます。



158.(CONTENT)受動的スキルを実装しattack_spawn_rate_multiplier



ます。



そして、他の受動的なスキルのためのより多くの演習があります。基本的に、これらは上記の受動的スキルのクラスに起因するものではない要因ですが、それらの実装は非常にシンプルでなければなりません。



159.(CONTENT)受動的スキルを実装しturn_rate_multiplier



ます。この受動的なスキルは、プレイヤーの回転速度を増減させます。



160.(CONTENT)受動的スキルを実装するboost_effectiveness_multiplier



。この受動的なスキルは、加速の効果を増加または減少させます。これは、変数の値が2の場合、加速は2倍速くまたは遅く動作することを意味します。



161.(CONTENT)受動的スキルを実装しprojectile_size_multiplier



ます。これは、シェルのサイズを増減する受動的なスキルです。



162.(CONTENT)受動的スキルを実装しboost_recharge_rate_multiplier



ます。これは、リロードアクセラレーションの速度を増加または減少させる受動的なスキルです。



163.(CONTENT)受動的スキルを実装しinvulnerability_time_multiplier



ます。これは、ダメージが与えられたときにプレイヤーの不死身時間を増加または減少させる受動的なスキルです。



164.(コンテンツ)受動的スキルを実装するammo_consumption_multiplier



この受動的なスキルは、すべての攻撃で消費される弾薬の量を増減します。



165.(CONTENT)受動的スキルを実装しsize_multiplier



ます。この受動的なスキルは、プレイヤーの船のサイズを増減します。それに応じて、すべての船のすべての痕跡の位置と砲弾の位置を変更する必要があることに注意してください。



166.(CONTENT)受動的スキルを実装しstat_boost_duration_multiplier



ます。この受動的なスキルは、プレーヤーに与えられる一時的なボーナスの期間を増減します。



パッシブ発射スキル



ここで、いくつかの受動的発射スキルを見てみましょう。これらの受動的スキルは、シェルの動作を根本的に変えます。同じアイデアをオブジェクトEnemyProjectile



実装することもできます。その後、これらのスキルのいくつかを使用して敵を作成できます。たとえば、砲弾を直進させるのではなく、船の周りを回転させる受動的なスキルがあります。後で、敵の周りにシェルの山が飛ぶ敵を追加します。どちらの場合も同じ技術が使用されます。



90度の変化



この受動的スキルと呼びますprojectile_ninety_degree_change



彼は定期的に発射体の角度を90度変更します。次のようになります。









発射物はショット中に移動した方向とほぼ同じ方向に移動しますが、角度は毎回急速に90度変化することに注意してください。これは、角度を変更することは完全にランダムではないことを意味し、それについてよく考える必要があります。



簡単な方法はprojectile_ninety_degree_change



、真の場合に影響するブール変数を作成することです。このエフェクトをクラスに適用するProjectile



ためprojectile_ninety_degree_change



、プレーヤーから値を読み取る方法には、opts



関数に新しいシェルを作成するときにテーブル渡すか、プレーヤーからshoot



直接読み取るか、current_room.player



current_room.player



このコードの一部をに移動するときに別のものに置き換える必要があることを除いて、2番目の方法を使用しEnemyProjectile



ます。これはすべて次のようになります。



 function Player:new(...) ... -- Booleans self.projectile_ninety_degree_change = false end
      
      





 function Projectile:new(...) ... if current_room.player.projectile_ninety_degree_change then end end
      
      





次に、Projectileコンストラクターの条件付き設計内で毎回90度ずつ発射体の角度を変更する必要がありますが、その初期方向も考慮します。最初にできることは、角度をランダムに90度または-90度に変更することです。次のようになります。



 function Projectile:new(...) ... if current_room.player.projectile_ninety_degree_change then self.timer:after(0.2, function() self.ninety_degree_direction = table.random({-1, 1}) self.r = self.r + self.ninety_degree_direction*math.pi/2 end) end end
      
      











ここで、発射物を他の方向に回転させてから、回転させてもう一方の方向に戻し、それから再び回転させる方法を理解する必要があります。これは無限に繰り返される定期的なアクションなので、次を使用できますtimer:every







 function Projectile:new(...) ... if current_room.player.projectile_ninety_degree_change then self.timer:after(0.2, function() self.ninety_degree_direction = table.random({-1, 1}) self.r = self.r + self.ninety_degree_direction*math.pi/2 self.timer:every('ninety_degree_first', 0.25, function() self.r = self.r - self.ninety_degree_direction*math.pi/2 self.timer:after('ninety_degree_second', 0.1, function() self.r = self.r - self.ninety_degree_direction*math.pi/2 self.ninety_degree_direction = -1*self.ninety_degree_direction end) end) end) end end
      
      





まず、発射物を最初の回転と反対方向に回転させます。つまり、発射物は最初の角度に向けられます。次に、わずか0.1秒後に同じ方向に再び回転させ、最初の回転とは反対の方向に向けます。最初にショット中に右に向けられた場合、次のことが起こります:0.2秒後に上になり、0.25秒後に再び右になり、0.1秒後に下になり、0.25秒後にプロセスを繰り返し、最初に右に、次に上に、次に下に、など。



また、各サイクルの終わりにevery



彼が回転する方向を変更します。そうしないと、彼は上下方向の間で振動せず、直線ではなく上下に移動します。これを実装することにより、以下が得られます。









167.(CONTENT)projectile_random_degree_change



発射体の角度をランダムに変更するパッシブスキル実装します。90度の回転とは異なり、この場合のシェルは元の方向に戻らないはずです。



168.(CONTENT)受動的スキルを実装しangle_change_frequency_multiplier



ます。このスキルは、前の2つのパッシブスキルの角度の変化率を増減します。場合angle_change_frequency_multiplier



等しい、実施例2のために、代わりに0.25と0.1秒を介して角度を変化させる、それらは0.125と0.05秒を介して変更されます。



ウェーブシェル



発射体の角度を断続的に変更する代わりに、関数を使用してこれをスムーズに行うことができます。これtimer:tween



により、波の発射体の効果が得られます。









ここでの考え方は、前の例とほぼ同じですが、次のもののみを使用していtimer:tween



ます。



 function Projectile:new(...) ... if current_room.player.wavy_projectiles then local direction = table.random({-1, 1}) self.timer:tween(0.25, self, {r = self.r + direction*math.pi/8}, 'linear', function() self.timer:tween(0.25, self, {r = self.r - direction*math.pi/4}, 'linear') end) self.timer:every(0.75, function() self.timer:tween(0.25, self, {r = self.r + direction*math.pi/4}, 'linear', function() self.timer:tween(0.5, self, {r = self.r - direction*math.pi/4}, 'linear') end) end) end end
      
      





仕組みによりtimer:every



、コードでは、初期時間が終了するまで機能を実行しません。そのため、最初にループの1回の反復を手動で実行してから、各ループを実行します。最初の反復では、math.pi / 4の代わりに元の値math.pi / 8も使用します。これは、シェルが元々中央の位置にあったために、必要なだけ半分だけ振動させるためです。プレーヤーはちょうど解雇されました)



169.(CONTENT)受動的スキルを実装しprojectile_waviness_multiplier



ます。このスキルは、トゥイーンを行うときに発射体が達成しなければならないターゲット角度を増減します。たとえば、If projectile_waviness_multiplier



が2の場合、パスの円弧は通常の2倍になります。



シェルの加速と制動



次に、発射体の速度を変更するいくつかの受動的スキルに進みます。1つ目は「高速->ゆっくり」、2つ目は「低速->高速」です。つまり、発射物は高速または低速で始まり、その後低速または高速になります。「高速->低速」は次のようになります。









これをかなり簡単な方法で実装します。スキル「Fast-> Slow」は、初期値を2倍にして速度の高速トゥイーンを実現し、しばらくしてからトゥイーンを初期値の半分に減らします。また、別のスキルとして、逆の操作を行うだけです。



 function Projectile:new(...) ... if current_room.player.fast_slow then local initial_v = self.v self.timer:tween('fast_slow_first', 0.2, self, {v = 2*initial_v}, 'in-out-cubic', function() self.timer:tween('fast_slow_second', 0.3, self, {v = initial_v/2}, 'linear') end) end if current_room.player.slow_fast then local initial_v = self.v self.timer:tween('slow_fast_first', 0.2, self, {v = initial_v/2}, 'in-out-cubic', function() self.timer:tween('slow_fast_second', 0.3, self, {v = 2*initial_v}, 'linear') end) end end
      
      





170.(CONTENT)受動的スキルを実装しprojectile_acceleration_multiplier



ます。このスキルは、速度が元の値から増加するときに加速の量を制御します。



171.(CONTENT)受動的スキルを実装しprojectile_deceleration_multiplier



ます。このスキルは、速度が元の値から低下したときにブレーキの量を制御します。



シールドシェル



それらの実装は、より多くの可動部分があるため、他の実装よりも少し難しくなります。最終結果は次のようになります。









ご覧のとおり、シェルはプレイヤーを中心に回転し、プレイヤーの移動方向を借用しています。これは、円のパラメトリック方程式を使用して実装できます一般的な場合、Aを特定の半径RでBの周りに回転させたい場合、次のようなことができます。



 Ax = Bx + R*math.cos(time) Ay = By + R*math.sin(time)
      
      





どこにtime



値が時間とともに増加する変数があります。実装に着手する前に、残りを準備しましょう。shield_projectile_chance



これはブール変数ではなく、「確率」タイプの変数です。つまり、新しい発射物が作成されるたびに、プレーヤーの周りを回転し始めるように見えます。



 function Player:new(...) ... -- Chances self.shield_projectile_chance = 0 end function Player:shoot() ... local shield = self.chances.shield_projectile_chance:next() if self.attack == 'Neutral' then self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), {r = self.r, attack = self.attack, shield = shield}) ... end
      
      





ここでは、shield



この発射体がプレーヤーを中心に回転するかどうかに「キューブ」がスローされる変数を定義し、その後、それをopts



call tableに渡しますaddGameObject



ここでは、利用可能な攻撃ごとにこの手順を繰り返す必要があります。将来的に同様の変更が行われるため、代わりに次のようなことができます。



 function Player:shoot() ... local mods = { shield = self.chances.shield_projectile_chance:next() } if self.attack == 'Neutral' then self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), table.merge({r = self.r, attack = self.attack}, mods)) ... end
      
      





したがって、将来はすべてをテーブルに追加するだけで十分ですmods



関数table.merge



はまだ定義されていませんが、ここでの使用方法に基づいて、その機能を推測できます。



 function table.merge(t1, t2) local new_table = {} for k, v in pairs(t2) do new_table[k] = v end for k, v in pairs(t1) do new_table[k] = v end return new_table end
      
      





2つのテーブルとその値を単純にマージして新しいテーブルに戻し、それを返します。



これで、機能自体の実装を開始できますshield



最初に、半径、回転速度などの変数を定義します。今のところ、次のように定義します。



 function Projectile:new(...) ... if self.shield then self.orbit_distance = random(32, 64) self.orbit_speed = random(-6, 6) self.orbit_offset = random(0, 2*math.pi) end end
      
      





orbit_distance



プレーヤーの周囲の半径を示します。orbit_speed



乗算されtime



、その発射の絶対値よりも大きいがより速く移動し、以下であろう場合に、ある-遅いです。負の値を設定すると、発射物が他の方向に移動し、少しランダムになります。orbit_offset



各発射体の初期角変位です。また、少しランダム性が追加され、ほぼ同じ位置にすべてのシェルを作成することはできません。そして今、これをすべて決定したら、発射体の位置にパラメトリック円方程式を適用できます。



 function Projectile:update(dt) ... -- Shield if self.shield then local player = current_room.player self.collider:setPosition( player.x + self.orbit_distance*math.cos(self.orbit_speed*time + self.orbit_offset), player.y + self.orbit_distance*math.sin(self.orbit_speed*time + self.orbit_offset)) end ... end
      
      





に適用される他のすべての呼び出しの後にこのコードを挿入することが重要setLinearVelocity



です。そうしないと、何も機能しません。また、グローバル変数を追加time



し、各フレームでを増やすことを忘れないでくださいdt



すべてを正しく行うと、次のようになります。









タスクは完了しましたが、これはすべて正しく見えません。最も間違っているのは、プレーヤーの周りを回転させるときに、発射体の角度が考慮されないことです。これを修正する1つの方法は、発射体の位置の最後のフレームを保存し、前の位置の現在の位置から減算するベクトルの角度を取得することです。コードは千の言葉に値するので、それがどのように見えるかをよく見てみましょう:



 function Projectile:new(...) ... self.previous_x, self.previous_y = self.collider:getPosition() end function Projectile:update(dt) ... -- Shield if self.shield then ... local x, y = self.collider:getPosition() local dx, dy = x - self.previous_x, y - self.previous_y self.r = Vector(dx, dy):angle() end ... -- At the very end of the update function self.previous_x, self.previous_y = self.collider:getPosition() end
      
      





したがって、r



回転中に考慮される発射体の角度が保存される変数を設定しますsetLinearVelocity



この角度を使用しているため、発射物を引き込み、Projectile:draw



それVector(self.collider:getLinearVelocity()):angle())



使用して方向を取得する場合、変数の設定方法に従ってすべてが設定されr



ます。そして、これはすべて次のようになります。









今、すべてが正しく見えます。上記のGIFでは、1つの小さな問題に気付くことができます-シェルを発射した後、シェルになったときに、すぐには起こりません。1-2フレームの場合、それらは通常のシェルのように見え、その後消えて出現し、すでにプレーヤーの周りを回転します。この問題を解決する1つの方法は、1〜2フレームのシールドシェルをすべて非表示にしてから表示することです。



 function Projectile:new(...) ... if self.shield then ... self.invisible = true self.timer:after(0.05, function() self.invisible = false end) end end function Projectile:draw() if self.invisible then return end ... end
      
      





そして最後に、シールドは敵と衝突するまで永続的に存在すると強力すぎる武器になるため、発射体のライフタイムを追加する必要があります。その後、破壊する必要があります。



 function Projectile:new(...) ... if self.shield then ... self.timer:after(6, function() self:die() end) end end
      
      





したがって、6秒間存在すると、シールドシェルは破壊されます。



終了



この記事を書いているエディターは、そのボリュームのために速度が低下し始めるため、ここで終了します。次のパートでは、引き続き他のパッシブスキルを実装し、プレイヤー、敵、および関連するパッシブスキルのすべての攻撃を追加します。さらに、次の部分は、すべてのゲームコンテンツの実装の完了です。それ以降のすべての部分は、このコンテンツをプレーヤーに表示することを検討します(SkillTreeルームとコンソールルーム)。






この一連のチュートリアルをお楽しみいただける場合は、今後同様のことを書いてください。





itch.ioのチュートリアルを購入すると、ゲームの完全なソースコード、パート1から9の演習への回答、チュートリアルの一部に分割されたコード(コードは各パートの終わりに見えるはずです)およびキーにアクセスできます。 Steam上のゲーム。



All Articles