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

画像






目次





13.スキルツリー



14.コンソール



15.最終



パート5:ゲームの基本



はじめに



このパートでは、最終的にゲーム自体を開始します。 まず、ゲームプレイの観点からゲームの構造を確認してから、ゲームのすべての部分に共通する基本事項、つまりピクセル化されたスタイル、カメラ、物理シミュレーションに焦点を当てます。 次に、プレーヤーの移動の基本を検討し、最後に、ガベージコレクションとオブジェクトのリークの可能性に対処します。



ゲームプレイ構造



ゲーム自体は、 Stage



Console



、およびSkillTree



3つの個別の部屋にのみ分かれています。



ステージルームでは、ゲームプレイ全体が行われます。 プレイヤー、敵、シェル、リソース、ボーナスなどのオブジェクトが含まれています。 ゲームプレイはBit Blaster XLに非常に似ており、実際には非常にシンプルです。 ゲームの別の側面(巨大なスキルツリー)に集中できるため、このようなシンプルなゲームプレイを選択しました。



GIF






「メニュー」に関連するものはすべて、コンソールルームで発生します。サウンドとビデオの設定の変更、実績の表示、船の選択、スキルツリーへのアクセスなどです。 コンソールは端末をエミュレートし、あなた(プレイヤー)が単に端末を介してゲームをプレイしていることを明確にするため、同様のスタイルのゲーム用に異なるメニューを作成する代わりに、「コンピューター」の外観(「怠programmerなプログラマーの芸術」とも呼ばれます)を与えることがより論理的です。



GIF






SkillTreeルームでは、すべてのパッシブスキルを取得できます。 ステージルームでは、プレイヤーはSP(スキルポイント)を獲得できます。SP(スキルポイント)は、ランダムに作成されるか、敵を倒したときに与えられます。 死亡後、プレーヤーはこれらのスキルポイントを使用してパッシブスキルを購入できます。 Path of Exileパッシブスキルツリーのスタイルで、何か巨大なものを実装したかったのですが、私はこれでかなり成功しているように思えます。 作成したスキルツリーには、約600〜800のノードがあります。 私の意見では、かなり良い。



GIF






スキルツリーのすべてのスキルを含め、これらの各部屋の作成について詳しく調べます。 ただし、私がやっていることから可能な限り逸脱することを強くお勧めします。 ゲームプレイに関して私が下した多くの決定は好みの問題であり、あなたは他のものを選ぶことができます。



たとえば、巨大なスキルツリーの代わりに、 Tree of Saviorで実装されているような多くの組み合わせを作成できる巨大なクラスシステムを選択できます。 したがって、パッシブスキルのツリーを構築する代わりに、すべてのパッシブスキルを実装し、これらのパッシブスキルを使用して独自のクラスシステムを構築できます。



これは単なるアイデアの1つです。 独自のバリエーションを選択できる多くの領域があります。 私はこれらのチュートリアルを演習で補足します。その際には、既存の資料をコピーするだけでなく、資料自体を扱うように人々に勧めます。 人々はこの方法でより良く学ぶように思えます。 したがって、自分のやり方で何かをする機会を見つけたら、そうすることをお勧めします。



ゲームサイズ



それではステージに行きましょう。 最初に必要なことは(ステージだけでなく、すべての部屋に当てはまります)、低解像度で部屋のピクセル化された外観を作成することです。 たとえば、次の円を見てください。









これを見てください:









私は2番目のオプションを好みます。 私の動機は純粋に美的であり、私自身の好みです。 ピクセル化されたビューを使用しない多くのゲームがありますが、同時に、これは単純な形状と色に制限されています。 つまり、スタイルの好みと、ゲームに投入する作業量に依存します。 しかし、私のゲームでは、ピクセル化されたビューを使用します。



それを実装する方法の1つは、非常に小さなデフォルト解像度を定義することです1920x1080



ゲームウィンドウの目標解像度に合わせてスケーリングすることが望ましいです。 このゲームでは、 1920x1080



を4で割った480x270



を選択します。ゲームのサイズをこの値に変更するには、 conf.lua



ファイルを使用する必要があります。これは、前のconf.lua



説明したように、LÖVEプロジェクトのパラメーターを定義する構成ファイルですデフォルトでは、ゲームを開始するウィンドウの解像度を含みます。



さらに、このファイルでは、基本解像度の幅と高さに対応する2つのグローバル変数gw



gh



、およびこの基本解像度に適用されるスケールに対応するsx



sy



変数も定義します。 conf.lua



ファイルはconf.lua



ファイルと同じフォルダーにある必要があり、同時に次のようになります。



 gw = 480 gh = 270 sx = 1 sy = 1 function love.conf(t) t.identity = nil --    () t.version = "0.10.2" --  LÖVE,      () t.console = false --   (boolean,   Windows) t.window.title = "BYTEPATH" --   () t.window.icon = nil --    ,     () t.window.width = gw --   () t.window.height = gh --   () t.window.borderless = false --       (boolean) t.window.resizable = true --      (boolean) t.window.minwidth = 1 --        () t.window.minheight = 1 --        () t.window.fullscreen = false --    (boolean) t.window.fullscreentype = "exclusive" --           () t.window.vsync = true --    (boolean) t.window.fsaa = 0 --      () t.window.display = 1 --  ,      () t.window.highdpi = false --    dpi     Retina (boolean) t.window.srgb = false --  - sRGB     (boolean) t.window.x = nil --  x      () t.window.y = nil --  y      () t.modules.audio = true --   (boolean) t.modules.event = true --    (boolean) t.modules.graphics = true --    (boolean) t.modules.image = true --    (boolean) t.modules.joystick = true --    (boolean) t.modules.keyboard = true --    (boolean) t.modules.math = true --    (boolean) t.modules.mouse = true --    (boolean) t.modules.physics = true --    (boolean) t.modules.sound = true --    (boolean) t.modules.system = true --    (boolean) t.modules.timer = true --    (boolean),    0 delta time  love.update    0 t.modules.window = true --    (boolean) t.modules.thread = true --    (boolean) end
      
      





今すぐゲームを開始すると、ウィンドウが小さくなっていることがわかります。



ピクセル化されたビューを取得するには、ウィンドウを拡大するときに、追加の作業を行う必要があります。 画面の中央に円( gw/2, gh/2



)を描くと、次のようになります。









そして、例えば3*gw



幅と3*gh



高さでlove.window.setMode



を呼び出すことにより、画面を直接スケーリングします。次のようになります:









ご覧のように、円は画面に合わせて拡大縮小されず、小さな円のままでした。 また、画面を中央にgh/2



ません。3倍にすると、 gw/2



gh/2



画面の中心でgh/2



なくなるためです。 画面を通常のモニターのサイズに拡大すると、円も比例してスケーリングされ(ピクセル化され)、その位置も比例して変わらないように、 480x270



基本解像度で小さな円を描画できるようにしたいと思います。 この問題を解決する最も簡単な方法はCanvas



を使用することですCanvas



は、他のエンジンではフレームバッファーまたはレンダーターゲットとも呼ばれます。 最初に、 Stage



クラスのコンストラクターで基本的な解像度を持つキャンバスを作成します。



 function Stage:new() self.area = Area(self) self.main_canvas = love.graphics.newCanvas(gw, gh) end
      
      





これにより、サイズが480x270



キャンバスが作成され、その上に描画できます。



 function Stage:draw() love.graphics.setCanvas(self.main_canvas) love.graphics.clear() love.graphics.circle('line', gw/2, gh/2, 50) self.area:draw() love.graphics.setCanvas() end
      
      





キャンバスの描画方法は、 Canvasページの例で見ることができます。 このページによると、キャンバスに何かを描画したい場合、 love.graphics.setCanvas



を呼び出すlove.graphics.setCanvas



があります。これlove.graphics.setCanvas



、すべての描画操作が現在設定されているキャンバスにリダイレクトされます。 次に、 love.graphics.clear



を呼び出しlove.graphics.clear



。これは、現在のフレームのキャンバスの内容をクリアします。これは、前のフレームにも描画されていたためです。 次に、必要なすべてを描画した後、 setCanvas



を再利用しsetCanvas



が、今回は何も渡さないため、ターゲットキャンバスが最新ではなくなり、描画操作のリダイレクトが実行されなくなります。



ここで停止すると、画面には何も起こりません。 これは、描画されたものすべてがキャンバスに移動したためですが、実際にはキャンバス自体をレンダリングしているわけではありません。 そのため、画面上にキャンバス自体を描画する必要があり、次のようになります。



 function Stage:draw() love.graphics.setCanvas(self.main_canvas) love.graphics.clear() love.graphics.circle('line', gw/2, gh/2, 50) self.area:draw() love.graphics.setCanvas() love.graphics.setColor(255, 255, 255, 255) love.graphics.setBlendMode('alpha', 'premultiplied') love.graphics.draw(self.main_canvas, 0, 0, 0, sx, sy) love.graphics.setBlendMode('alpha') end
      
      





love.graphics.draw



を使用して画面にキャンバスを描画し、さらにlove.graphics.setBlendMode



呼び出しでラップします。これはLÖVEwikiのCanvasページによると、誤ったブレンドを防ぐために使用されます。 ここでプログラムを実行すると、描画された円が表示されます。



Canvas sx



sy



を増やすために使用したものに注目してください。 これらの変数の値は1ですが、たとえば値を3に変更すると、次のようになります。









何も見えません! しかし、これは、キャンバス480x270



中央の円がキャンバス480x270



の中央にあるために1440x810



。 画面自体のサイズは480x270



ため、画面よりも大きいため、Canvas全体を見ることができません。 これを修正するために、呼び出されたときに画面自体のサイズだけでなく、 sx



sy



を変更するresize



関数をmain.lua



作成できます。



 function resize(s) love.window.setMode(s*gw, s*gh) sx, sy = s, s end
      
      





したがって、 love.load



resize(3)



love.load



と、次のようになります。









これが私たちが達成したことです。 ただし、別の問題があります。円はピクセル化されず、ぼやけて見えます。



この理由は、LÖVEでレンダリングされたオブジェクトを増減するときにFilterModeを使用し、このフィルタリングモードがデフォルトで'linear'



です。 ゲームにピクセル化された外観を持たせるため、値を'nearest'



に変更する必要があります。 love.graphics.setDefaultFilter



の最初に引数'nearest'



を指定してlove.graphics.setDefaultFilter



を呼び出すと、問題が解決するはずです。 別の側面-LineStyle'rough'



に設定する必要があります。 デフォルトでは'smooth'



、LÖVEプリミティブはエイリアスを使用してレンダリングされますが、これはピクセルスタイルの作成には適していません。 これをすべて実行してコードを再度実行すると、画面は次のようになります。









壊れてピクセル化された外観が必要です! 最も重要なことは、1つの解像度を使用してゲーム全体を作成できることです。 画面の中央にオブジェクトを作成する場合、その位置x, y



gw/2, gh/2



に等しく、最終的な解像度に関係なく、オブジェクトは常に画面の中央にあることを報告できます。 これにより、プロセスが大幅に簡素化されます。つまり、ゲームがどのように見えるか、オブジェクトが画面上でどのように配布されるかを一度だけ心配するだけで済みます。



ゲームサイズの練習



65. Steamコンピュータ構成調査の「プライマリディスプレイ解像度」セクションをご覧ください。 Steamユーザーのほぼ半数が使用する最も一般的な解像度は1920x1080



です。 私たちのゲームの基本的な解像度はそれに応じて拡張されます。 しかし、2番目に人気のある解像度は1366x768



です。 480x270



はそれに480x270



しません。 ゲームをフルスクリーンモードに切り替えるときに、非標準の解像度で作業するためにどのようなオプションを提供できますか?



66.同じまたは類似のテクニックを使用するコレクションからゲームを選択します(基本解像度を低くします)。 通常、ピクセルグラフィックスを使用したゲームで使用されます。 ゲームの基本的な解像度は何ですか? 基本解像度を正しく入力できない非標準のアクセス許可をゲームはどのように処理しますか? デスクトップの解像度を数回変更し、毎回異なる解像度でゲームを起動して、変更を確認し、ゲームがバリエーションを処理する方法を理解します。



カメラ



3つの部屋すべてがカメラを使用しているため、今すぐ検討するのが論理的です。 チュートリアルの第2部では、タイマーにハンプライブラリを使用しました。 このライブラリには便利なカメラモジュールもあり、これも使用します。 しかし、私は画面を揺する機能を備えたわずかに修正されたバージョンを使用しています。 ここからファイルをダウンロードできますcamera.lua



ファイルをハンプライブラリフォルダーに配置し(そしてcamera.lua



の既存のバージョンを上書きします)、カメラのrequireモジュールをmain.lua



追加します。 Shake.lua



ファイルをobjects



フォルダーに配置します。



(さらに、私が書いたライブラリを使用することもできます。このライブラリはすでにすべての機能を備えています。チュートリアルの完了後にこのライブラリを作成しました。そのため、このライブラリは使用しません。このライブラリを使用する場合は、チュートリアルの作業を続行できます。ただし、このライブラリの機能を使用するためにいくつかの側面を引き継ぎます。)



カメラを追加したら、次の機能が必要です。



 function random(min, max) local min, max = min or 0, max or 1 return (min > max and (love.math.random()*(min - max) + max)) or (love.math.random()*(max - min) + min) end
      
      





任意の2つの数字の間で乱数を取得できます。 Shake.lua



ファイルがShake.lua



するため、これが必要です。 utils.lua



この関数を定義した後utils.lua



同様のことを試してください。



 function love.load() ... camera = Camera() input:bind('f3', function() camera:shake(4, 60, 1) end) ... end function love.update(dt) ... camera:update(dt) ... end
      
      





そして、 Stage



クラスで:



 function love.load() ... camera = Camera() input:bind('f3', function() camera:shake(4, 60, 1) end) ... end function love.update(dt) ... camera:update(dt) ... end
      
      





f3



を押すとf3



画面が揺れ始めます。









シェーキング機能は、 この記事で説明した機能に基づいています。 振幅(ピクセル単位)、周波数、持続時間を取得します。 画面の揺れは、指定された秒数と指定された周波数で、振幅から徐々に減衰して実行されます。 周波数が高いほど、画面は2つの制限(振幅、-振幅)の間でより活発に振動します。 低周波数は反対になります。



また、カメラはまだ特定のポイントに結び付けられていないことに注意することも重要です。そのため、カメラを振ると、すべての方向にスローされます。つまり、振動が完了すると、前のgifアニメーションで見られるように、別の場所に中央に配置されます。



この問題を解決する1つの方法は、カメラを中央に配置することです。これはカメラに実装できます:lockPosition function カメラモジュールの修正バージョンでは、最初にdt



引数を受け取るように、カメラのすべてのモーション関数を変更しました。 そして、次のようになります。



 function Stage:update(dt) camera.smoother = Camera.smooth.damped(5) camera:lockPosition(dt, gw/2, gh/2) self.area:update(dt) end
      
      





カメラをスムーズにするには、 damped



モードを5



damped



します。 試行錯誤によってこれらのパラメーターを推測しましたが、一般的にこれにより、カメラはスムーズで快適な方法でターゲットポイントに焦点を合わせることができます。 私たちは現在ステージルームで作業しているので、このコードをステージルーム内に配置します。このルームでは、カメラは常に画面の中央にあり、画面を振る瞬間を除いて移動することはありません。 その結果、次の結果が得られます。









ゲーム全体では、部屋ごとに個別のカメラインスタンスを作成する必要がないため、1つのグローバルカメラを使用します。 ステージルームでは、カメラは揺れ以外は使用されませんので、今のところここで停止します。 コンソールルームとSkillTreeルームでは、カメラはより複雑な方法で使用されますが、後で説明します。



プレイヤーの物理



これで、ゲーム自体を開始するために必要なものはすべて揃いました。 Playerオブジェクトから始めます。 Player.lua



というPlayer.lua



objects



フォルダーに新しいファイルを作成します。これは次のようになります。



 Player = GameObject:extend() function Player:new(area, x, y, opts) Player.super.new(self, area, x, y, opts) end function Player:update(dt) Player.super.update(self, dt) end function Player:draw() end
      
      





このようにして、デフォルトで新しいクラスのゲームオブジェクトを作成する必要があります。 それらはすべてGameObject



を継承し、同じコンストラクター構造を持ち、関数を更新および描画します。 次のように、ステージルームでこのPlayerオブジェクトをインスタンス化できます。



 function Stage:new() ... self.area:addGameObject('Player', gw/2, gh/2) end
      
      





インスタンス化の仕組みをテストし、Playerオブジェクトが更新され、 Area



描画されることを確認するには、その位置に円を描くだけです。



 function Player:draw() love.graphics.circle('line', self.x, self.y, 25) end
      
      





これにより、画面の中央に円が表示されます。 addGameObject



の呼び出しが作成されたオブジェクトを返すので、 self.player



self.player



ステージ内にプレーヤーへのリンクを保存し、必要に応じて、添付されたキーを持つPlayerオブジェクトのデスイベントを含めることができます。



 function Stage:new() ... self.player = self.area:addGameObject('Player', gw/2, gh/2) input:bind('f3', function() self.player.dead = true end) end
      
      





f3



キーを押すと、Playerオブジェクトが消滅する必要があります。つまり、円の描画が停止します。 これは、前のパートでArea



オブジェクトコードを構成した結果として発生します。 また、この方法でaddGameObject



によって返されたリンクを保存する場合、 nil



へのリンクが保存される変数を設定しないと、このオブジェクトは削除されないことに注意することも重要です。 さらに、オブジェクトをメモリから実際に削除する場合は、リンクをnil



(この場合、文字列self.player = nil



)に設定することを忘れないことが重要です(属性にdead



をtrueに設定することに加えて)。






それでは、物理学に移りましょう。 プレイヤー(敵、シェル、リソースなど)は物理オブジェクトになります。 このために、私はLÖVEでbox2d統合を使用しますが、box2dのような完全な物理エンジンを使用しても何の有用性も得られないため、一般的にこれはゲームには必要ありません。 私はそれに慣れているのでそれを使用します。 ただし、独自の衝突処理手順(このようなゲームでは非常に簡単です)を作成するか、これを行うライブラリを使用することをお勧めします。



チュートリアルでは、作成したwindfieldライブラリを使用します。これにより、box2dをLÖVEで使用するのがはるかに簡単になります。LÖVEには衝突も処理する他のライブラリがあります:HardonColliderまたはbump.luaです。



チュートリアルを繰り返すのではなく、衝突を自分で実装するか、これら2つのライブラリのいずれかを使用することを強くお勧めします。したがって、継続的に開発する必要がある能力を開発するように強制します。たとえば、さまざまなソリューションから選択し、ニーズに合ったソリューションを見つけて最適な方法で作業するだけでなく、問題に対する独自のソリューションを開発するだけでなく、チュートリアルに従うだけです。



繰り返しますが、このチュートリアルで演習を行う主な理由の1つは、素材の習得に積極的に関与している場合にのみ学習することです。練習は、資料に慣れるもう1つの機会です。チュートリアルを繰り返しただけで、知らないことに対処することを学ばなければ、実際に学ぶことはありません。したがって、ここのチュートリアルから逸脱して、物理/衝突部分を自分で実装することを強くお勧めします。



可能であれば、ライブラリwindfield



ダウンロードして、そのrequireをファイルに追加できますmain.lua



。そのドキュメントによると、2つの主要な概念があります- World



Collider



ワールドはシミュレーションが行われる物理的な世界であり、コライダーはこの世界の中でシミュレートされる物理的なオブジェクトです。つまり、ゲームには物理的な世界の外観が必要であり、プレイヤーはこの世界内のコライダーになります。呼び出しを追加して



、クラス内に世界を作成Area



しますaddPhysicsWorld







 function Area:addPhysicsWorld() self.world = Physics.newWorld(0, 0, true) end
      
      





したがって.world



、物理世界を含む領域の属性を設定しますまた、このワールドが存在する場合は、このワールドを更新する必要があります(必要に応じて、デバッグのために描画します)。



 function Area:update(dt) if self.world then self.world:update(dt) end for i = #self.game_objects, 1, -1 do ... end end function Area:draw() if self.world then self.world:draw() end for _, game_object in ipairs(self.game_objects) do game_object:draw() end end
      
      





ゲームオブジェクトの更新情報を使用するため、すべてのゲームオブジェクトを更新する前に物理世界を更新します。これは、物理シミュレーションがこのフレームの前に実行される場合にのみ可能です。最初にゲームオブジェクトを更新する場合、前のフレームからの物理情報を使用し、これによりフレームが壊れます。実際、これはプログラムの動作に大きな影響を与えませんが、概念的な観点からはより混乱します。



チャレンジを通して平和を加えましたaddPhysicsWorld



すべてのエリアに物理的な世界を持たせたくないため、Areaコンストラクタに追加しただけではありません。たとえば、コンソールルームでもオブジェクトを使用してエンティティを管理しますが、この領域に物理的な世界をアタッチする必要はありません。したがって、単一の関数を呼び出すことにより、オプションにします。次のように、ステージルームエリアに物理世界のインスタンスを作成できます。



 function Stage:new() self.area = Area(self) self.area:addPhysicsWorld() ... end
      
      





そして、世界ができたので、Playerコライダーを追加できます。



 function Player:new(area, x, y, opts) Player.super.new(self, area, x, y, opts) self.x, self.y = x, y self.w, self.h = 12, 12 self.collider = self.area.world:newCircleCollider(self.x, self.y, self.w) self.collider:setObject(self) end
      
      





ここで、プレーヤーがエリアへのリンクを持っていることがどのように役立つかに注目してください。この方法で、エリアのワールドオブジェクトにアクセスして、新しいコライダーを追加できるからです。このようなパターン(エリア内のエンティティへのアクセス)は頻繁に繰り返されます。たとえば、すべてのオブジェクトGameObject



が同じコンストラクターを持つように作成し、Area



所属するオブジェクトへの参照を取得します



コンストラクタの属性を持つ、我々プレイヤーw



h



その幅と高さを定義し、我々は新しいを追加12次と等しいですCircleCollider



半径が幅に等しい。幅と高さを決定する場合、円の形でコライダーを作成することはあまり論理的ではありませんが、異なる種類の船を追加すると視覚的にすべての船の幅と高さが異なるため、物理的にコライダーは常に円になるため、将来的には有用ですすべての船が平等なチャンスを持ち、プレイヤーにとって予測可能な行動をとるようにします。



コライダーを追加した後setObject



、Playerオブジェクトを新しく作成されたコライダーにバインドする関数を呼び出します。これは、2つのコライダーが衝突したときに、オブジェクトではなくコライダーの観点から情報を取得できるため便利です。たとえば、PlayerがProjectileと衝突する場合、PlayerとProjectileを表す2つのコライダーがありますが、オブジェクト自体はない場合があります。setObject



(そしてgetObject



)Colliderが属するオブジェクトを設定および取得できます。



これで、最終的にPlayerをそのサイズに応じてレンダリングできます。



 function Player:draw() love.graphics.circle('line', self.x, self.y, self.w) end
      
      





今すぐゲームを開始すると、プレーヤーを表す小さな円が表示されます。









物理演習プレーヤー



自分で衝突を作成するか、衝突/物理の代替ライブラリの1つを選択する場合、これらの演習を実行する必要はありません。



67.物理世界のy軸の重力を512に変更します。Playerオブジェクトはどうなりますか?



68.呼び出しの3番目の引数は何を行い、.newWorld



falseに設定するとどうなりますか?true / falseを設定する利点はありますか?どれ?



プレイヤーの動き



このゲームでのプレーヤーの動きは次のように動作します。プレーヤーが動く一定の速度と、「左」または「右」を保持することで変更できる角度があります。これを実装するには、いくつかの変数が必要です。



 function Player:new(area, x, y, opts) Player.super.new(self, area, x, y, opts) ... self.r = -math.pi/2 self.rv = 1.66*math.pi self.v = 0 self.max_v = 100 self.a = 100 end
      
      





ここr



で、プレイヤーが移動する角度を定義します。最初は重要です-math.pi/2



、つまり、上向きです。 LÖVEの角度は時計回りに示されています。つまりmath.pi/2



、下向きで、a -math.pi/2



は上向きです(0は右向きです)。変数rv



は、プレーヤーが「左」または「右」を押したときに角度が変化する割合です。次に、がv



あり、プレーヤーの速度を示し、プレーヤーmax_v



の最大速度を示します。最後の属性はa



、プレーヤーの加速表す属性です。すべての値は試行錯誤によって取得されます。



これらすべての変数を考慮してプレーヤーの位置を更新するために、同様のことができます:



 function Player:update(dt) Player.super.update(self, dt) if input:down('left') then self.r = self.r - self.rv*dt end if input:down('right') then self.r = self.r + self.rv*dt end self.v = math.min(self.v + self.a*dt, self.max_v) self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end
      
      





最初の2行は、左右のキーを押したときに何が起こるかを決定します。使用する入力ライブラリによると、これらのバインディングは事前に定義する必要があり、ファイルでこれを行いましたmain.lua



(すべてにグローバル入力オブジェクトを使用するため)。



 function love.load() ... input:bind('left', 'left') input:bind('right', 'right') ... end
      
      





そして、プレーヤーが「左」または「右」を押すr



と、プレーヤーの角に対応する属性1.66*math.pi



が、対応する方向のラジアンに変わります。ここで、この値にを掛けるdt



、つまりこの値は1秒ごとに制御されることに注意することも重要です。つまり、角度の変化率は1.66*math.pi



ラジアン/秒で測定されます。これは、ゲームサイクルの仕組みの結果であり、チュートリアルの最初の部分で分析しました。



その後、属性を設定しますv



。それはもう少し複雑ですが、他の言語でそれをやった場合、それはあなたに馴染みがあるはずです。最初の計算の形式はself.v = self.v + self.a*dt



、つまり、加速度の分だけ速度を上げるだけです。この場合、1秒あたり100ずつ増やします。しかし、属性も定義しましたmax_v



、最大許容速度を制限する必要があります。制限しない場合、無期限にself.v = self.v + self.a*dt



増加v



し、プレイヤーはソニックになります。しかし、これは必要ありません!これを防ぐ1つの方法は次のとおりです。



 function Player:update(dt) ... self.v = self.v + self.a*dt if self.v >= self.max_v then self.v = self.max_v end ... end
      
      





さらに、v



それが大きくmax_v



なったら、この値に制限し、それを超えないようにします。これを記述する別の簡単な方法math.min



は、渡されたすべての引数の中で最小値を返す関数を使用することです。私たちのケースでは、我々は結果を渡すself.v + self.a*dt



と、self.max_v



加算結果が大きい場合にはmax_v



、その後、math.min



戻ってmax_v



それは小さい合計よりもあるとして、。これは、Lua(および他のプログラミング言語でも)で非常に一般的で便利なパターンです。



最後に、setLinearVelocity



xとyのコライダー速度を属性に設定しますv



オブジェクトの角度に応じて適切な値を掛けます。一般的なケースでは、特定の方向に何かを動かしたいときに角度を付けたい場合、それcos



使用してx軸にsin



沿って移動し、y軸に沿って移動する必要があります。これは、2Dゲームの開発でも非常に一般的なパターンです。あなたが学校でそれを理解したと仮定して、これについては説明しません(そうでない場合は、三角法の基本をGoogleで検索してください)。



クラスに最後の変更を加えることができますが、GameObject



これは非常に簡単です。物理エンジンを使用しているため、速度と位置など、いくつかの変数に2つの表現が格納されています。属性x, y



を使用してプレーヤーの位置と速度を取得し、を使用しv



てコライダーの位置と速度を取得しますgetPosition



およびgetLinearVelocity



これら2つのビューを同期することは論理的であり、これを自動的に実現する1つの方法は、すべてのゲームオブジェクトの親クラスを変更することです。



 function GameObject:update(dt) if self.timer then self.timer:update(dt) end if self.collider then self.x, self.y = self.collider:getPosition() end end
      
      





ここでは、次の処理が行われます。オブジェクトの属性が定義されている場合collider





それx



y



コライダーの位置にインストールされます。そして、コライダーの位置が変わると、オブジェクト自体のこの位置の表現もそれに応じて変わります。



ここでプログラムを実行すると、次のように表示されます。



GIF






そのため、左または右キーを押すと、通常、Playerオブジェクトは画面上を動き回り、方向を変えることがわかります。ここでも重要な点が1つありますworld:draw()



。Area オブジェクトでは、呼び出しによってコライダーが描画されます実際、コライダーだけでなく、この行をコメントアウトしてPlayerオブジェクトを直接描画することも論理的です。



 function Player:draw() love.graphics.circle('line', self.x, self.y, self.w) end
      
      





最後にできることは、プレイヤーが「見る」方向を視覚化することです。これは、プレイヤーの位置から彼が指示されている側に線を引くだけで実行できます。



 function Player:draw() love.graphics.circle('line', self.x, self.y, self.w) love.graphics.line(self.x, self.y, self.x + 2*self.w*math.cos(self.r), self.y + 2*self.w*math.sin(self.r)) end
      
      





そして、次のようになります。



GIF






これらは三角法の基本でもあり、ここでは以前に適用したのと同じ考え方を使用します。位置位置に対して特定の角度になるように、位置B



distance



単位とする位置を取得する場合、パターンは次のようになります。これは2Dゲームの開発で非常に頻繁に使用されます(少なくとも、私には思えます)。このパターンを直感的に理解することはあなたにとって有用です。A



B



angle



A



bx = ax + distance*math.cos(angle)



by = ay + distance*math.sin(angle)







プレイヤーの動きの練習



69.次の角度を度に変換し(精神的に)、どの象限に属するか(左上、右上、左下、または右下)を伝えます。LÖVEでは、学校で教えられたように、コーナーは反時計回りでカウントされることを忘れないでください。



 math.pi/2 math.pi/4 3*math.pi/4 -5*math.pi/6 0 11*math.pi/12 -math.pi/6 -math.pi/2 + math.pi/4 3*math.pi/4 + math.pi/3 math.pi
      
      





70.加速属性が必要a



ですか?存在しない場合、プレーヤーの更新機能はどのようになりますか?その存在に利点はありますか?



71.使用される角度が等しく、距離がの場合、位置から(x, y)



ポイントの位置を取得します。B



A



-math.pi/4



100













72.使用する角度が等しく、距離が等しい場合、位置から(x, y)



ポイントの位置を取得します。位置、およびそれらの間の距離と角度は、前の演習と同じままです。C



B



math.pi/4



50



A



B













73.前の2つの演習に基づいて、ある地点A



から特定の地点に到達する必要があるときに使用される一般的なパターンを教えてくださいC



。角度と距離を介して到達できる中間地点のみを使用できますか?



74.プレーヤー属性とコライダー属性ビューの同期で位置と速度が言及されていますが、回転はどうですか?コライダーには、を介してアクセスできるねじれがありますgetAngle



同様に属性を介して同期しないのはなぜr



ですか?



ごみ収集



物理エンジンと動きのコードを追加したので、これまで欠けていたもの、つまりメモリリークの処理に集中できます。プログラミング環境で発生する可能性のある問題の1つは、メモリリークです。これは、あらゆる種類の否定的な結果につながる可能性があります。 Luaなどのマネージコード言語では、完全なメモリ管理よりもブラックボックスにデータが隠れるため、これはさらに厄介な問題になります。



ガベージコレクターは次のように機能します。オブジェクトを参照する参照がない場合は削除されます。つまり、変数のみが参照するテーブルがあるa



場合、割り当てを実行するときにa = nil



ガベージコレクタは、参照されたテーブルを他の誰も参照していないことを理解するため、将来のガベージコレクションサイクルでメモリから削除できます。 1つのオブジェクトが複数回参照され、これらすべてのポイントからリンクを削除するのを忘れると、問題が発生します。



たとえば、を使用して新しいオブジェクトを作成するaddGameObject



と、オブジェクトがリストに追加されます.game_objects



。これは、このオブジェクトを指す単一のリンクと見なされます。ただし、オブジェクト自体もこの関数で返されます。したがって、以前はこのようなことを行いましたself.player = self.area:addGameObject('Player', ...)



。つまり、Areaオブジェクト内のリストにオブジェクトへのリンクを格納することに加えて、変数へのリンクも格納しますself.player



。つまり、私たちが言うときself.player.dead



PlayerオブジェクトはAreaオブジェクトのゲームオブジェクトのリストから削除されますが、それを指すため、メモリから削除することはできませんself.player



。つまり、この場合、実際にメモリからPlayerオブジェクトを削除するには、dead



値をtrue に設定してから実行する必要がありself.player = nil



ます。



これは起こりうることのほんの一例ですが、そのような問題はどこにでもあります。サードパーティのライブラリを使用する場合は特に注意する必要があります。たとえば、私が書いた物理ライブラリには関数がありますsetObject



Colliderがオブジェクトへの参照を保存するようにオブジェクトを渡します。オブジェクトが死んだ場合、メモリから削除されますか?いいえ、Colliderはまだリンクを保存しているためです。他の条件でのみ同じ問題。問題を解決する1つの方法はdestroy



、リンクの削除を処理するために作成された関数を使用してオブジェクトを明示的に削除することです



つまり、すべてのオブジェクトに次を追加できます。



 function GameObject:destroy() self.timer:destroy() if self.collider then self.collider:destroy() end self.collider = nil end
      
      





現在、すべてのオブジェクトにデフォルトでこの機能がありますdestroy



。この関数は、destroy



EnhancedTimerオブジェクトの関数、およびコライダー(Collider)の関数を呼び出します。これらの関数は、ユーザーがおそらくメモリから削除したいアイテムを逆参照します。たとえばCollider:destroy



、アクションの1つに呼び出しがありますself:setObject(nil)



。このオブジェクトを破棄するため、Colliderにリンクを保存する必要がなくなりました。



次のように、エリア更新機能を変更することもできます。



 function Area:update(dt) if self.world then self.world:update(dt) end for i = #self.game_objects, 1, -1 do local game_object = self.game_objects[i] game_object:update(dt) if game_object.dead then game_object:destroy() table.remove(self.game_objects, i) end end end
      
      





dead



オブジェクト属性がtrueの場合、ゲームオブジェクトのリストから削除することに加えて、そのdestroy関数を呼び出して、それへの参照を削除します。この概念を拡張して、物理世界自体にWorld:destroyがあることを認識でき、それを使用してAreaオブジェクトを破棄できます。



 function Area:destroy() for i = #self.game_objects, 1, -1 do local game_object = self.game_objects[i] game_object:destroy() table.remove(self.game_objects, i) end self.game_objects = {} if self.world then self.world:destroy() self.world = nil end end
      
      





エリアを破壊するとき、最初にエリア内のすべてのオブジェクトを破壊し、次に物理世界が存在する場合はそれを破壊します。次のアクションに合わせてステージルームを変更できます。



 function Stage:destroy() self.area:destroy() self.area = nil end
      
      





関数を変更することもできますgotoRoom







 function gotoRoom(room_type, ...) if current_room and current_room.destroy then current_room:destroy() end current_room = _G[room_type](...) end
      
      





current_room



既存の変数が属性であるかどうか、およびdestroy



実際に部屋が含まれているかどうかを確認します。その場合、destroy関数を呼び出します。そして、対象の部屋に行きます。



destroy関数を追加した後は、すべてのオブジェクトが次のパターンに従う必要があることを忘れないでください。



 NewGameObject = GameObject:extend() function NewGameObject:new(area, x, y, opts) NewGameObject.super.new(self, area, x, y, opts) end function NewGameObject:update(dt) NewGameObject.super.update(self, dt) end function NewGameObject:draw() end function NewGameObject:destroy() NewGameObject.super.destroy(self) end
      
      





もちろん、これはすべて良いことですが、実際にメモリからアイテムを削除したかどうかを確認するにはどうすればよいですか?私が好きな1つの投稿で答えを見つけましたが、リークを追跡する比較的簡単な解決策もあります:



 function count_all(f) local seen = {} local count_table count_table = function(t) if seen[t] then return end f(t) seen[t] = true for k,v in pairs(t) do if type(v) == "table" then count_table(v) elseif type(v) == "userdata" then f(v) end end end count_table(_G) end function type_count() local counts = {} local enumerate = function (o) local t = type_name(o) counts[t] = (counts[t] or 0) + 1 end count_all(enumerate) return counts end global_type_table = nil function type_name(o) if global_type_table == nil then global_type_table = {} for k,v in pairs(_G) do global_type_table[v] = k end global_type_table[0] = "table" end return global_type_table[getmetatable(o) or 0] or "Unknown" end
      
      





このコードは投稿で説明されているため解析しませんが、に追加してからmain.lua



love.load



次のコードを内部に追加します。



 function love.load() ... input:bind('f1', function() print("Before collection: " .. collectgarbage("count")/1024) collectgarbage() print("After collection: " .. collectgarbage("count")/1024) print("Object count: ") local counts = type_count() for k, v in pairs(counts) do print(k, v) end print("-------------------------------------") end) ... end
      
      





このコードの機能:ユーザーがをクリックf1



すると、ガベージコレクションサイクルの前後のメモリ量が表示され、メモリ内のオブジェクトのタイプも表示されます。これは、たとえば、オブジェクトが内部にある新しいステージルームを作成して削除し、ステージが作成される前とメモリが同じ(またはほぼ同じ)であることを確認できるようになったため便利です。同じままである場合、メモリリークはありません。メモリリークがない場合は、問題が発生しているため、それらのソースを探す必要があります。









ガベージコレクションの演習



75.キーf2



バインドして、電話で新しいステージルームを作成してアクティブにしますgotoRoom







76.f3



現在の部屋の破壊の縛る



77.を押して、使用メモリ量を数回確認しf1



ます。その後f2



キーキーを数回押してf3



新しい部屋を作成および破壊します。もう一度を数回押して、使用されているメモリの量を確認しf1



ます。メモリーの量は以前のままですか、それともそれ以上ですか?



78.ステージルームを設定して、1つではなく100個のPlayerオブジェクトを作成し、同様のことを行います。



 function Stage:new() ... for i = 1, 100 do self.area:addGameObject('Player', gw/2 + random(-4, 4), gh/2 + random(-4, 4)) end end
      
      





また、プレーヤーの更新機能を変更して、プレーヤーオブジェクトが移動しないようにします(モーションコードをコメントアウトします)。使用メモリ量は変更されましたか?全体的な結果は変わりましたか?






パート6:プレーヤークラスの基本



はじめに



このセクションでは、Playerクラスに機能を追加することに焦点を当てます。まず、プレイヤーの攻撃とProjectileオブジェクトを確認します。次に、プレーヤーの2つの主な特性である、ブーストとサイクル/ティックに焦点を当てます。そして最後に、ゲームに追加されるコンテンツの最初の部分、つまりさまざまなプレイヤーの船の作成を開始します。このパートから始めて、ゲームプレイに関連する瞬間のみに焦点を当てます(前の5つのパートは準備段階でした)。



プレイヤー攻撃



このゲームのプレイヤーは次のように攻撃します。毎秒n



、攻撃が起動し、自動的に開始されます。最終的に、16種類の攻撃を受けますが、それらのほとんどは、プレイヤーの船の視線の方向への砲撃に関連しています。たとえば、これは誘導ミサイルによる攻撃です。



GIF






この攻撃はより速く発射されますが、ランダムな角度で:



GIF






攻撃とシェルにはあらゆる種類の特性があり、さまざまな側面の影響を受けますが、それらの基礎は常に同じです。



まず、プレイヤーに毎秒攻撃させる必要がありn



ます。n



攻撃によって異なる数値ですが、デフォルトでは重要0.24



です。これは、前のパートで説明したタイマーライブラリを使用して簡単に実装できます。



 function Player:new() ... self.timer:every(0.24, function() self:shoot() end) end
      
      





したがって、関数shoot



を0.24秒ごとに呼び出し、この関数内に、発射物オブジェクトの作成を処理するコードを配置します。



これで、撮影機能内で何が起こるかを指定できます。まず、小さな効果があり、ショットを示します。実際には、次のルールを開発しました。ゲームからエンティティを作成または削除するとき、付随する効果が表示され、エンティティが画面からどこにも表示されたり消えたりするという事実を隠します。さらに、全体的な外観を改善する必要があります。



この新しい効果を作成するには、最初に新しいゲームオブジェクトを作成する必要がありますShootEffect



(今、あなたはすでにこれを行う方法を知っているはずです)。この効果は、発射物が作成された位置の隣に短時間画面上に残る単純な正方形である必要があります。これを実現する最も簡単な方法は、次のようなものを使用することです。



 function Player:shoot() self.area:addGameObject('ShootEffect', self.x + 1.2*self.w*math.cos(self.r), self.y + 1.2*self.w*math.sin(self.r)) end
      
      





 function ShootEffect:new(...) ... self.w = 8 self.timer:tween(0.1, self, {w = 0}, 'in-out-cubic', function() self.dead = true end) end function ShootEffect:draw() love.graphics.setColor(default_color) love.graphics.rectangle('fill', self.x - self.w/2, self.y - self.w/2, self.w, self.w) end
      
      





そして、次のようになります。



GIF






エフェクトコードは非常にシンプルです。これは、幅が8の正方形で、0.1秒生存していますtween



この時間の間に幅は、関数を使用して0になります。これまでのところ、1つの問題があります。エフェクトの位置は静的であり、プレーヤーに追従しません。効果の持続時間が短いため、これは重要ではないように見えますが、0.5秒以上に変更してみてください。



この問題を解決する1つの方法は、ShootEffectオブジェクトをPlayerオブジェクトへの参照として渡すことです。したがって、ShootEffectオブジェクトは、その位置をPlayerオブジェクトと同期できます。



 function Player:shoot() local d = 1.2*self.w self.area:addGameObject('ShootEffect', self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), {player = self, d = d}) end
      
      





 function ShootEffect:update(dt) ShootEffect.super.update(self, dt) if self.player then self.x = self.player.x + self.d*math.cos(self.player.r) self.y = self.player.y + self.d*math.sin(self.player.r) end end function ShootEffect:draw() pushRotate(self.x, self.y, self.player.r + math.pi/4) love.graphics.setColor(default_color) love.graphics.rectangle('fill', self.x - self.w/2, self.y - self.w/2, self.w, self.w) love.graphics.pop() end
      
      





player



ShootEffectオブジェクトの属性にはopts



、プレーヤーのシュート機能のテーブル介して値が割り当てられますself



。これはself.player



、ShootEffectオブジェクトでPlayerオブジェクトへのリンクへのアクセスを取得できることを意味します。通常、オブジェクトは他のオブジェクトの関数から作成されるため、オブジェクトへの参照をこの方法で互いに渡します。つまり、を渡すとself



、必要なものが得られます。さらにd



、Playerオブジェクトの中心からエフェクトが表示される距離値を属性に割り当てます。これもテーブルを使用して実装されますopts







次に、ShootEffectの更新関数で、その位置をプレーヤーの位置の値に割り当てます。アクセスする変数が設定されているかどうかを確認することは常に重要です(if self.player then



)、そうでない場合、エラーが発生するためです。また、ゲームの今後の作成中に、エンティティが他の場所から参照される場合が非常に頻繁にあり、それらの値にアクセスしようとしますが、既に死んでいるため、これらの値は設定されず、エラーが発生します。このことを忘れずに、お互いの内部のエンティティをこのように参照することが重要です。



最後に、ここで最後に行うことは、正方形をプレーヤーの角度と同期させ、45度回転させて見栄えを良くすることです。これを行うにはpushRotate



、次のような関数を使用します



 function pushRotate(x, y, r) love.graphics.push() love.graphics.translate(x, y) love.graphics.rotate(r or 0) love.graphics.translate(-x, -y) end
      
      





これは、遷移を遷移スタックに転送する単純な関数です。本質的に、彼女は私たちが電話をするまでr



、ポイントの周りですべてをオンx, y



にしますlove.graphics.pop



つまり、この例では、正方形があり、プレーヤーの角度に45度(pi / 4ラジアン)を足した数だけ中心の周りを回転させます。完全を期すために、スケーリングを含むこの関数の別のバージョンを表示できます。



 function pushRotateScale(x, y, r, sx, sy) love.graphics.push() love.graphics.translate(x, y) love.graphics.rotate(r or 0) love.graphics.scale(sx or 1, sy or sx or 1) love.graphics.translate(-x, -y) end
      
      





これらの関数は非常に有用であり、ゲーム全体で使用するため、それらを試してよく理解してください!



プレイヤー攻撃演習



80.現時点では、プレーヤーのコンストラクターで初期タイマー呼び出しのみを使用します。これにより、0.24秒ごとにシュート関数が呼び出されます。Playerには、self.attack_speed



5秒ごとに1〜2の範囲のランダムな値に変化する属性があるとします



 function Player:new(...) ... self.attack_speed = 1 self.timer:every(5, function() self.attack_speed = random(1, 2) end) self.timer:every(0.24, function() self:shoot() end)
      
      





プレーヤーのオブジェクトをどのように変更して、0.24秒ごとに発砲するのではなく、1秒ごとに撃つようにし0.24/self.attack_speed



ますか?every



シュート機能の呼び出しの簡単な変更は機能しないことに注意してください



81。前の部分では、ガベージコレクションを見て、忘れられたリンクが危険であり、リークにつながる可能性があるという事実について話しました。この部分では、PlayerおよびShootEffectオブジェクトの例を使用して、相互にオブジェクトを参照できることを説明しました。この場合、ShootEffectがPlayerへのリンクを含む短命のオブジェクトである場合、このオブジェクトをガベージコレクターによって削除できるようにPlayerへのリンクを逆参照することを心配する必要がありますか?より一般的には、この方法で相互に参照するオブジェクトの逆参照をいつ処理する必要がありますか?



82.使用して、pushRotate



プレーヤーを中心に180度回転させます。次のようになります。



GIF






83.使用して、pushRotate



ラインを90度回転させ、プレーヤーの中心を中心とした動きの方向を示します。次のようになります。



GIF






84.使用して、pushRotate



プレーヤーがプレーヤーの中心の周りを移動している方向に線を90度回転します。次のようになります。



GIF






85.使用して、pushRotate



ShootEffectオブジェクトをプレーヤーの中心の周りに90度回転します(プレーヤーの方向に対して既に回転しているという事実に加えて)。次のようになります。



GIF






プレイヤーシェル



これで射撃効果が得られたので、シェル自体に進むことができます。発射体には、プレイヤーのメカニズムと非常によく似た移動メカニズムがあります。これは、この角度に応じて速度を設定できる角度を持つ物理オブジェクトです。開始するには、シュート関数内に呼び出しを記述しましょう:



 function Player:shoot() ... 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}) end
      
      





そして、予期しないことは何もありません。Projectileの初期位置を設定するにd



は、以前に定義した変数同じ変数を使用し、プレーヤーの角度を属性として渡しますr



ShootEffectオブジェクトとは異なり、Projectileオブジェクトは作成時にプレーヤーのコーナー以外を必要としないため、プレーヤーをリンクとして渡す必要はありません。



それでは、Projectileコンストラクターを見てみましょう。発射物オブジェクトには、サークルコライダー(プレーヤーなど)、移動の速度と方向もあります。



 function Projectile:new(area, x, y, opts) Projectile.super.new(self, area, x, y, opts) self.s = opts.s or 2.5 self.v = opts.v or 200 self.collider = self.area.world:newCircleCollider(self.x, self.y, self.s) self.collider:setObject(self) self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end
      
      





この属性s



はコライダーの半径ですr



。この変数は移動角度に既に使用されているため、として指定されていません。一般的に、私は、変数を使用するオブジェクトのサイズを設定するにはw



h



r



またはをs



。オブジェクトが長方形の場合は最初の2つ、円の場合は最後の2つ。変数r



がすでに方向に使用されている場合(この場合のように)、radiusとして使用されs



ます。ほとんどの場合、これらのオブジェクトには、衝突に関連するすべての作業を行うコライダーが既にあるため、これらの属性は主に視覚化に使用されます。



ここで使用する別の側面は、前述の設計opts.attribute or default_value



です。方法のおかげでor



Luaで動作します。このコンストラクトを使用して、以下を簡単に渡すことができます。



 if opts.attribute then self.attribute = opts.attribute else self.attribute = default_value end
      
      





属性が存在するかどうかを確認し、この属性に変数を設定します。存在しない場合は、デフォルト値を割り当てます。その場合self.s



opts.s



が定義されている場合は値が割り当てられ、定義されていない場合は値が割り当てられます2.5



同じことが当てはまりself.v



ます。最後に、で発射体の速度を設定します。これは、発射体setLinearVelocity



の初期速度とプレイヤーから送信される角度を示します。プレーヤーの動きと同じアプローチを使用するため、これをすでに理解している必要があります。



次のように発射物を更新およびレンダリングしている場合:



 function Projectile:update(dt) Projectile.super.update(self, dt) self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end function Projectile:draw() love.graphics.setColor(default_color) love.graphics.circle('line', self.x, self.y, self.s) end
      
      





次のようになります。



GIF






プレイヤーシェル演習



86.プレイヤーのシュート機能で、5で作成された発射体のサイズ/半径を変更し、速度を150で変更します。87



.シュート機能を変更して、1つではなく3つのシェルを作成します。 30度。次のようになります。



GIF






88.シュート関数を変更して、1つのシェルではなく3つのシェルを作成し、各サイドシェルの位置が中央のシェルに対して8ピクセルだけオフセットされるようにします。次のようになります。



GIF






89.発射物の初期速度を100に変更し、作成後0.5秒以内に400に加速します。



プレイヤーと発射体の死



プレーヤーが移動して簡単な攻撃を行えるようになったので、ゲームの追加ルールに注意する必要があります。これらのルールの1つは、プレイヤーがゲームエリアの端に触れた場合、彼は死ななければならないということです。同じことがシェルにも当てはまります。なぜなら、シェルは作成されても破壊されることはなく、ある時点で非常に多くのシェルが存在するため、ゲームの速度が大幅に低下するからです。



Projectileオブジェクトから始めましょう:



 function Projectile:update(dt) ... if self.x < 0 then self:die() end if self.y < 0 then self:die() end if self.x > gw then self:die() end if self.y > gh then self:die() end end
      
      





プレイエリアの中心がにあることgw/2, gh/2



、つまり左上隅が0, 0



にあり、右下隅がにあることがわかりgw, gh



ます。そして、発射体の更新関数にいくつかの条件構造を追加し、その位置を確認する必要がありますdie



境界の外側にある場合は、関数を呼び出す必要があります



同じロジックがPlayerオブジェクトに適用されます。



 function Player:update(dt) ... if self.x < 0 then self:die() end if self.y < 0 then self:die() end if self.x > gw then self:die() end if self.y > gh then self:die() end end
      
      





では、関数に進みましょうdie



。それは非常に単純であり、本質的に、dead



エンティティの属性をtrueに設定してから視覚効果を作成するだけです。発射物の場合、作成されるエフェクトは呼び出されProjectileDeathEffect



ます。 ShootEffectの場合のように、これは短時間画面上に残り、その後消えますが、いくつかの違いがあります。主な違いは、ProjectileDeathEffectがしばらくちらつき、その後通常の色に戻ってフェードすることです。これにより、軽くて面白い綿の効果が生まれます。したがって、コンストラクターは次のようになります。



 function ProjectileDeathEffect:new(area, x, y, opts) ProjectileDeathEffect.super.new(self, area, x, y, opts) self.first = true self.timer:after(0.1, function() self.first = false self.second = true self.timer:after(0.15, function() self.second = false self.dead = true end) end) end
      
      





効果がどの段階にあるかを示す2つの属性- first



を特定second



しました。彼が最初の段階にいる場合、彼は白い色を持ち、2番目の段階で彼は彼の本当の色を取ります。2番目のステージが完了した後、エフェクトは「消滅」しdead



ます。これは、trueに設定することによって行われます。これはすべて0.25秒(0.1 + 0.15)以内に発生します。つまり、非常に短命で迅速な効果です。エフェクトは、ShootEffectレンダリングメソッドと非常によく似た方法でレンダリングされます。



 function ProjectileDeathEffect:draw() if self.first then love.graphics.setColor(default_color) elseif self.second then love.graphics.setColor(self.color) end love.graphics.rectangle('fill', self.x - self.w/2, self.y - self.w/2, self.w, self.w) end
      
      





ここでは、エフェクトの段階に応じて色を設定し、この色の長方形を描画します。この効果をdie



Projectileオブジェクトの関数実装します。



 function Projectile:die() self.dead = true self.area:addGameObject('ProjectileDeathEffect', self.x, self.y, {color = hp_color, w = 3*self.s}) end
      
      





ゲームの色数には限りがあることを以前に言及するのを忘れていました。私はアーティストではないので、色について考えるのに多くの時間を費やしたくありません。そのため、いくつかのよく一致する色を選択して、ゲーム全体で使用しました。これらの色はで定義されglobals.lua



、次のようなります。



 default_color = {222, 222, 222} background_color = {16, 16, 16} ammo_color = {123, 200, 164} boost_color = {76, 195, 217} hp_color = {241, 103, 69} skill_point_color = {255, 198, 93}
      
      





発射体の死のエフェクトでは、色hp_color



(赤)を使用してエフェクトの外観を示しますが、将来的には発射体オブジェクトの色を正しく使用する予定です。攻撃の種類によって色が異なるため、死の効果も攻撃に応じて色が異なります。現在の効果は次のとおりです。



GIF









それでは、Playerのデス効果に移りましょう。最初に行うことはdie



、Projectileオブジェクトの関数コピーしdead



、プレーヤーが画面の境界に達したときに属性をtrueに設定することです。これにより、視覚効果を死に追加できます。プレイヤーが死亡したときの主な特殊効果はExplodeParticle



、爆発のようなものと呼ばれる粒子ビームです。一般的な場合、粒子は初期位置からランダムな角度で移動し、長さが徐々に減少する線になります。これはほぼこの方法で実装できます。



 function ExplodeParticle:new(area, x, y, opts) ExplodeParticle.super.new(self, area, x, y, opts) self.color = opts.color or default_color self.r = random(0, 2*math.pi) self.s = opts.s or random(2, 3) self.v = opts.v or random(75, 150) self.line_width = 2 self.timer:tween(opts.d or random(0.3, 0.5), self, {s = 0, v = 0, line_width = 0}, 'linear', function() self.dead = true end) end
      
      





ここで、いくつかの属性を特定しました。それらのほとんどは、それ自体が語っています。さらに、0.3〜0.5秒の間隔で、トゥイーンを使用してラインのサイズ、速度、幅を0に変更し、トランジションが完了すると、パーティクルは「消滅」します。パーティクルモーションコードはProjectileとPlayerに似ているため、スキップします。彼女はちょうど彼女の速度で角度に従います。



そして最後に、粒子は線として描かれます:



 function ExplodeParticle:draw() pushRotate(self.x, self.y, self.r) love.graphics.setLineWidth(self.line_width) love.graphics.setColor(self.color) love.graphics.line(self.x - self.s, self.y, self.x + self.s, self.y) love.graphics.setColor(255, 255, 255) love.graphics.setLineWidth(1) love.graphics.pop() end
      
      





通常、回転するもの(この場合はパーティクルの方向の角度)を描画する必要がある場合、角度0(右方向)であるかのように描画します。つまり、この場合、左から右に線を引く必要があり、その中心が回転位置になります。つまりs



、実際には行のサイズの半分であり、フルサイズではありません。また、このlove.graphics.setLineWidth



ラインを使用して、最初は太字にし、時間とともに細くします。



パーティクルは非常に簡単な方法で作成されます。関数でそれらの乱数を作成するだけですdie







 function Player:die() self.dead = true for i = 1, love.math.random(8, 12) do self.area:addGameObject('ExplodeParticle', self.x, self.y) end end
      
      





最後にできることは、キーをバインドしてdie



Player 関数をトリガーすることです。これは、画面の端ではエフェクトを完全に見ることができないためです。



 function Player:new(...) ... input:bind('f4', function() self:die() end) end
      
      





そして、次のようになります。



GIF






しかし、写真はあまり印象的ではありませんでした。瞬間をより劇的にするために、時間を少し遅くすることができます。ほとんどの人はこれに気付かないでしょうが、注意深く見れば、多くのゲームはプレイヤーがダメージを受けるか死ぬ時間をわずかに遅くします。ダウンウェルが良い例です。このビデオはゲームプレイを示しています。あなたが自分でそれを観察して気付くように、私は損傷が行われた時間に注意しました。



これを自分で実装するのは非常に簡単です。最初に、グローバル変数slow_amount



定義love.load



して初期値1を割り当てます。この変数を使用して、すべての更新関数で渡されたデルタを乗算します。したがって、時間を50%遅くする必要がある場合は、slow_amount



0.5の値。この乗算の実行は次のようになります。



 function love.update(dt) timer:update(dt*slow_amount) camera:update(dt*slow_amount) if current_room then current_room:update(dt*slow_amount) end end
      
      





そして今、それを起動させる関数を決定する必要があります。一般的なケースでは、短時間で時間の膨張を元に戻す必要があります。したがって、この関数にスローダウンレベルとともにその期間を追加することは論理的です。



 function slow(amount, duration) slow_amount = amount timer:tween('slow', duration, _G, {slow_amount = 1}, 'in-out-cubic') end
      
      





つまり、呼び出しslow(0.5, 1)



とは、ゲームの速度が最初に50%に低下し、1秒後に最高速度に戻ることを意味します。ここで、tween関数は文字列を使用することに注意することが重要'slow'



です。前の部分で説明したように、これは、別のスロー関数のトゥイーンがまだアクティブな状態でスロー関数が呼び出されると、この前のトゥイーンがキャンセルされ、新しいトゥイーンが継続することを意味します。これにより、1つの変数で2つのトゥイーン関数が同時に実行されなくなります。プレイヤーの死亡中



に電話をかけるとslow(0.15, 1)



、次のものが得られます。



GIF






ここで、画面の揺れを追加することもできます。カメラモジュールには既にfunction :shake



があるため、次を追加できます。



 function Player:die() ... camera:shake(6, 60, 0.4) ... end
      
      





最後に、いくつかのフレームで画面をちらつくことができます。これは、気づかないかもしれない多くのゲームで使用されている別の効果ですが、全体として視覚効果の良い印象を作り出します。この効果は非常に単純です。呼び出されるflash(n)



と、画面がnフレームの間背景でちらつきます。そのような可能性を実現する一つの方法は、グローバル変数を定義することflash_frames



love.load



最初にゼロであるが。flash_frames



nilに等しい場合、これは効果が非アクティブであることを意味し、nilに等しくない場合、アクティブになります。フリッカー関数は次のようになります。



 function flash(frames) flash_frames = frames end
      
      





これで関数で設定できますlove.draw







 function love.draw() if current_room then current_room:draw() end if flash_frames then flash_frames = flash_frames - 1 if flash_frames == -1 then flash_frames = nil end end if flash_frames then love.graphics.setColor(background_color) love.graphics.rectangle('fill', 0, 0, sx*gw, sy*gh) love.graphics.setColor(255, 255, 255) end end
      
      





最初に、各フレームflash_frames



で1 ずつ減少し、次に、到達-1



するとエフェクトが完了しているため、nilを割り当てます。そして、効果が完了しない場合、background_color



画面全体を覆う色の大きな長方形を描くだけです。これを関数に追加するとdie



次のようになります。



 function Player:die() self.dead = true flash(4) camera:shake(6, 60, 0.4) slow(0.15, 1) for i = 1, love.math.random(8, 12) do self.area:addGameObject('ExplodeParticle', self.x, self.y) end end
      
      





そうすることで、以下が得られます。



GIF






これは非常に弱くて微妙な効果ですが、そのような小さなディテールは全体像をより強力で美しいものにします。



デスプレーヤー/発射体の練習



90.first



および属性second



を使用せずに、新しい属性のみを使用せずにProjectileDeathEffectオブジェクトの色を変更する効果を他にどのように実現できますcurrent_color



か?



91.関数を変更して、flash



フレームではなく秒単位で期間を取得します。どちらが良いですか、それとも単なる好みの問題ですか?タイマーは秒ではなくフレームを使用して継続時間を測定できますか?



プレイヤータクト



次に、Playerのもう1つの重要な側面であるサイクルメカニズムに進みます。ゲームは、パッシブスキルのツリーに、各サイクルでトリガーされる可能性のあるスキルがあるように機能します。サイクルは、n秒ごとに機能する単なるカウンターです。主なタスクを設定する必要があります。このため、tick



5秒ごとに関数呼び出しを行います



 function Player:new(...) ... self.timer:every(5, function() self:tick() end) end
      
      





tick関数では、まずTickEffect



、各メジャーで機能する小さな視覚効果追加します。この効果は、Downwellの更新効果に似ています(上記のDownwellに関するビデオを参照)。これは、プレイヤーと短時間オーバーラップする大きな長方形です。次のようになります。



GIF






最初に気付くのは、大きな長方形がプレーヤーを覆い、時間とともに小さくなることです。しかし、ShootEffectと同様に、彼もプレイヤーをフォローします。つまり、TickEffectオブジェクトへの参照としてPlayerオブジェクトを渡す必要があることを理解しています。



 function Player:tick() self.area:addGameObject('TickEffect', self.x, self.y, {parent = self}) end
      
      





 function TickEffect:update(dt) ... if self.parent then self.x, self.y = self.parent.x, self.parent.y end end
      
      





また、長方形は時間の経過とともに小さくなりますが、高さだけが小さくなります。これを実装する最も簡単な方法は次のとおりです。



 function TickEffect:new(area, x, y, opts) TickEffect.super.new(self, area, x, y, opts) self.w, self.h = 48, 32 self.timer:tween(0.13, self, {h = 0}, 'in-out-cubic', function() self.dead = true end) end
      
      





しかし、これを行おうとすると、長方形が本来のように立ち上がらず、プレーヤーの中心付近で単純に小さくなることがわかります。この問題を解決する1つの方法は、時間y_offset



とともに増加する属性を導入することです。これはy



、TickEffectオブジェクトの位置から減算されます。



 function TickEffect:new(...) ... self.y_offset = 0 self.timer:tween(0.13, self, {h = 0, y_offset = 32}, 'in-out-cubic', function() self.dead = true end) end function TickEffect:update(dt) ... if self.parent then self.x, self.y = self.parent.x, self.parent.y - self.y_offset end end
      
      





このようにして、目的の効果を得ることができます。今のところ、ティック関数はこれですべてです。後で特性と受動的スキルを追加し、新しいコードが表示されます。



プレイヤーの加速



ゲームプレイのもう1つの重要な側面は加速です。ユーザーが押し上げると、プレーヤーはより速く動き始めなければなりません。そして、ユーザーが「下」を押すと、プレーヤーはよりゆっくりと動き始めなければなりません。この加速メカニズムはゲームプレイの基本的な部分です。メジャーと同様に、まず基本を作成し、次にそれらに新しい機能を追加します。



まず、キー管理を構成する必要があります。プレーヤーにはmax_v



、プレーヤーが移動できる最大速度を設定する属性があります。上/下を押すと、この値が変化し、大きく/小さくなります。ここでの問題は、キーを放した後、通常の値に戻す必要があることです。したがって、ベース値を格納する別の変数と、現在の値を含む別の変数が必要です。



修飾子によって変更する必要がある(つまり、基本値と現在の値が必要)ゲーム内の特性(速度など)の存在は、非常に一般的なパターンです。後で、ゲームに新しい特性と受動的なスキルを追加し、これをより詳細に検討します。ただし、現時点ではbase_max_v



、最大速度の初期値/基本値を含む属性追加します。通常の属性max_v



には、可能なすべての修飾子が適用される現在の最大速度が含まれます(加速度など)。



 function Player:new(...) ... self.base_max_v = 100 self.max_v = self.base_max_v end function Player:update(dt) ... self.max_v = self.base_max_v if input:down('up') then self.max_v = 1.5*self.base_max_v end if input:down('down') then self.max_v = 0.5*self.base_max_v end end
      
      





このコードでは、各フレームmax_v



を割り当て、base_max_v



上下キーが押されているかどうかを確認し、それに応じて変更しますmax_v



。注意することが重要です-これは、setLinearVelocity



使用後に呼び出しmax_v



が発生する必要があることを意味します。そうしないと、すべてがバラバラになります。基本的な加速機能ができたので、視覚効果を追加できます。これを行うには、プレイヤーのオブジェクトに排気トレースを追加します。







GIF






トレース作成は一般的なパターンに従います。各フレームごとに新しいオブジェクトを作成し、一定時間、トゥイーン関数を使用してオブジェクトのサイズを縮小します。時間が経つにつれて、オブジェクトの後にオブジェクトを作成し、それらが隣同士に描画されます。前に作成されたものは小さくなりますが、作成されたばかりのものは大きくなります。それらはすべてプレイヤーの下部に作成され、プレイヤーが移動すると、必要なトレース効果が得られます。



これを実装するためTrailParticle



に、特定の半径の円である新しいオブジェクト作成し、特定の時間、トゥイーン関数で縮小することができます



 function TrailParticle:new(area, x, y, opts) TrailParticle.super.new(self, area, x, y, opts) self.r = opts.r or random(4, 6) self.timer:tween(opts.d or random(0.3, 0.5), self, {r = 0}, 'linear', function() self.dead = true end) end
      
      





たとえば、'in-out-cubic'



代わり'linear'



さまざまな遷移モードを使用すると、トラックの形状が変わります。私は最も美しいように見えるので、私は線形を使用しましたが、別のものを選択できます。 draw関数は、属性を使用して、対応する色と半径の円を描くだけですr







Playerオブジェクトの側から、次のような新しいTrailParticleを作成できます。



 function Player:new(...) ... self.trail_color = skill_point_color self.timer:every(0.01, function() self.area:addGameObject('TrailParticle', self.x - self.w*math.cos(self.r), self.y - self.h*math.sin(self.r), {parent = self, r = random(2, 4), d = random(0.15, 0.25), color = self.trail_color}) end)
      
      





つまり、0.01秒ごと(つまり、各フレーム内)に、2〜4のランダムな半径、0.15〜0.25秒のランダムな持続時間、および色skill_point_color



(黄色)を持つプレーヤー用の新しいTrailParticleオブジェクトを作成します



また、「上」または「下」を押すと、粒子の色を青に変更できます。これを行うには、アクセラレーションコードにロジックを追加する必要があります。つまり、アクセラレーションがいつ発生するかを通知する必要があり、そのためにattributeが必要ですboosting



この属性を使用すると、加速がいつ発生するかを確認し、それに応じて参照される色を変更できますtrail_color







 function Player:update(dt) ... self.max_v = self.base_max_v self.boosting = false if input:down('up') then self.boosting = true self.max_v = 1.5*self.base_max_v end if input:down('down') then self.boosting = true self.max_v = 0.5*self.base_max_v end self.trail_color = skill_point_color if self.boosting then self.trail_color = boost_color end end
      
      





そのため、プレーヤーが加速すると、色が(青)から青に変わることになりましtrail_color



boost_color







プレイヤーシップグラフィックス



このパートで最後に見るのは船です!ゲームには多くの異なる種類の船があり、それぞれに独自の特性、受動的なスキル、および外観があります。ここまでは、外観のみに焦点を合わせて1隻の船を追加し、演習ではさらに7隻を作成する必要があります。



また、コンテンツに言及する価値があります:ゲームにコンテンツを追加する必要がある場合-船、パッシブスキル、さまざまなメニューオプション、スキルツリーの視覚的構築など、ほとんどの作業を自分で行う必要があります。チュートリアルでは、これを1つの例のみで行います。その後、新しい類似コンテンツを手動で手動で追加する必要があるため、この作業を演習に入れます。



これを行う理由は2つあります。まず、すべての詳細な説明に時間がかかりすぎて、チュートリアルが非常に長くなります。第二に、ゲームにコンテンツを自分で追加することにより、手動作業を行う方法を学ぶ必要があります。ゲームの開発のほとんどは、「新しい」ものを作成せずにコンテンツを追加するだけです。気に入らないかもしれない多くの仕事をしなければならないので、あなたはこれを好まないかもしれません。これは、後で理解するよりも、早く理解することをお勧めします。これが望ましくない場合は、たとえば、多くの手作業を必要としないゲームの作成に集中できます。しかし、私のゲームはまったく別のケースです。スキルツリーには約800のノードがあり、それらはすべて手動で設定する必要があります(ツリーが同じ大きさの場合は同じことをする必要があります)。これは、この種の作業が好きかどうかを理解する絶好の機会です。かどうか。



なるほど、1隻の船から始めましょう。表示は次のとおりです。



GIF






ご覧のとおり、本体と2つの翼の3つの部分で構成されています。単純なポリゴンのセットから描画します。つまり、3つの別々のポリゴンを定義する必要があります。船が右に曲がったかのようにポリゴンの位置を決定します(上で説明したように、これは角度0です)。次のようなものが得られます。



 function Player:new(...) ... self.ship = 'Fighter' self.polygons = {} if self.ship == 'Fighter' then self.polygons[1] = { ... } self.polygons[2] = { ... } self.polygons[3] = { ... } end end
      
      





各ポリゴンテーブル内で、ポリゴンの頂点を定義します。これらのポリゴンを描画するには、いくつかの作業を行う必要があります。最初に、プレーヤーの中心の周りにポリゴンを回転させる必要があります。



 function Player:draw() pushRotate(self.x, self.y, self.r) love.graphics.setColor(default_color) --    love.graphics.pop() end
      
      





その後、各ポリゴンを検​​討する必要があります。



 function Player:draw() pushRotate(self.x, self.y, self.r) love.graphics.setColor(default_color) for _, polygon in ipairs(self.polygons) do --     end love.graphics.pop() end
      
      





次に、各ポリゴンを描画します。



 function Player:draw() pushRotate(self.x, self.y, self.r) love.graphics.setColor(default_color) for _, polygon in ipairs(self.polygons) do local points = fn.map(polygon, function(k, v) if k % 2 == 1 then return self.x + v + random(-1, 1) else return self.y + v + random(-1, 1) end end) love.graphics.polygon('line', points) end love.graphics.pop() end
      
      





最初に行うことは、すべてのポイントを正しく順序付けることです。各ポリゴンはローカルに定義する必要があります。つまり、その中心からの距離は等しいと見なされ0, 0



ます。これは、各ポリゴンがまだ世界のどの位置にいるかをまだ知らないことを意味します。



関数fn.map



は、テーブル内の各要素をバイパスし、関数を適用します。この場合、関数はインデックスのパリティをチェックします。奇数の場合、コンポーネントxを示し、偶数の場合、コンポーネントyを示します。つまり、これらの各ケースでは、プレイヤーのxまたはy位置を頂点に追加するだけでなく、-1から1の範囲の乱数を追加することで、船がもう少しファジーで面白く見えるようにします。そして最後に、love.graphics.polygon



これらすべてのポイントを描画するために呼び出されます。



各ポリゴンの定義は次のとおりです。



 self.polygons[1] = { self.w, 0, -- 1 self.w/2, -self.w/2, -- 2 -self.w/2, -self.w/2, -- 3 -self.w, 0, -- 4 -self.w/2, self.w/2, -- 5 self.w/2, self.w/2, -- 6 } self.polygons[2] = { self.w/2, -self.w/2, -- 7 0, -self.w, -- 8 -self.w - self.w/2, -self.w, -- 9 -3*self.w/4, -self.w/4, -- 10 -self.w/2, -self.w/2, -- 11 } self.polygons[3] = { self.w/2, self.w/2, -- 12 -self.w/2, self.w/2, -- 13 -3*self.w/4, self.w/4, -- 14 -self.w - self.w/2, self.w, -- 15 0, self.w, -- 16 }
      
      





最初は本体、2番目は上翼、3番目は下翼です。すべての頂点は反時計回りに順番に決定されます。ラインの最初のポイントは常にコンポーネントxを示し、2番目のポイントはコンポーネントyを示します。上記の数字のペアへの各頂点のバインドは次のようになります。









ご覧のとおり、最初の点は右端にあり、中心に揃えられています。つまり、座標がありself.w, 0



ます。次の要素は、最初の要素の少し左上にあります。つまり、その座標self.w/2, -self.w/2



などです。



最後に、ポイントを追加した後、トラックを船に一致させることができます。この場合、上記のgifからわかるように、1つではなく2つのトラックがあります。



 function Player:new(...) ... self.timer:every(0.01, function() if self.ship == 'Fighter' then self.area:addGameObject('TrailParticle', self.x - 0.9*self.w*math.cos(self.r) + 0.2*self.w*math.cos(self.r - math.pi/2), self.y - 0.9*self.w*math.sin(self.r) + 0.2*self.w*math.sin(self.r - math.pi/2), {parent = self, r = random(2, 4), d = random(0.15, 0.25), color = self.trail_color}) self.area:addGameObject('TrailParticle', self.x - 0.9*self.w*math.cos(self.r) + 0.2*self.w*math.cos(self.r + math.pi/2), self.y - 0.9*self.w*math.sin(self.r) + 0.2*self.w*math.sin(self.r + math.pi/2), {parent = self, r = random(2, 4), d = random(0.15, 0.25), color = self.trail_color}) end end) end
      
      





ここでは、次のテクニックを使用します。ゴールに到達するために必要な角度に基づいて、ポイントからポイントに渡します。必要なターゲットポイントはプレーヤーの後ろ(後ろ0.9*self.w



)にありますが、それぞれ0.2*self.w



がプレーヤーの動きとは反対の軸に沿ってわずかな距離()移動します。



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



GIF








船のグラフィック演習



小さなメモ:ラベル(CONTENT)は、それ自体がゲームのコンテンツであるエクササイズを示します。このようにマークされたエクササイズには答えがありませんので、完全に自分で行う必要があります!この瞬間から、ますます多くのエクササイズがこのようになります。ゲーム自体に進み始めており、その大部分はコンテンツを手動で追加するだけだからです。



92.(コンテンツ)さらに7種類の船を追加します。新しいタイプの船を追加するにelseif self.ship == 'ShipName' then



は、ポリゴンの定義とトレースの定義の両方に別の条件付き設計を追加するだけです。これは私が作成した船の外観です(ただし、もちろん、自分で作成して独自のデザインを作成できます)。



GIF











これらのチュートリアルが好きで、将来似たようなものを書くように刺激したい場合:





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



All Articles