目次
- 記事1
- パート1.ゲームサイクル
- パート2.ライブラリ
- パート3.部屋とエリア
- パート4.演習
- セクション2
- パート5.ゲームの基本
- パート6.プレーヤークラスの基本
- セクション3
- パート7.プレーヤーのパラメーターと攻撃
- パート8.敵
- セクション4
- パート9.ディレクターとゲームサイクル
- パート10.コード作成のプラクティス
- パート11.受動的スキル
- セクション5
- パート12.その他の受動的スキル
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で。