Love2dとLuaでスペースインベーダーを作成する





こんにちは 今日は、 Love2dエンジンでクラシックゲームSpace Invadersを作成します。 「すぐにコードを作成する」ファンの場合、ゲームの最終バージョンはgithubで見ることができます。 開発プロセスに興味のある方は、catへようこそ。



ここでは、最終版にあるすべてのことを説明することはできません。面白くなく、記事が無限になります。 ここで分析するものに加えて、ゲームにはさまざまなモード(一時停止、損失、勝利)が含まれ、デバッグ情報(オブジェクトの速度と数、メモリなど)を表示でき、プレーヤーには命があり、アカウントが保持されますゲームレベル(複雑さではなく、シーケンス)。 これはすべて、コードで見ることも、独自のオプションを開発することもできます。



だから、 作業計画:





準備する



main.luaで 、メインのlove2dメソッドへの呼び出しを追加します。 後で行うすべての要素または関数は、これらのメソッドに直接または間接的に関連付けられている必要があります。関連付けられていない場合、それらは見過ごされます。



function love.load() end function love.keyreleased( key ) end function love.draw() end function love.update( dt ) end
      
      





プレーヤーを追加



player.luaファイルをプロジェクトルートに追加します



 local player = {} player.position_x = 500 player.position_y = 550 player.speed_x = 300 player.width = 50 player.height = 50 function player.update( dt ) if love.keyboard.isDown( "right" ) and player.position_x < ( love.graphics.getWidth() - player.width ) then player.position_x = player.position_x + ( player.speed_x * dt ) end if love.keyboard.isDown( "left" ) and player.position_x > 0 then player.position_x = player.position_x - ( player.speed_x * dt ) end end function player.draw() love.graphics.rectangle( "fill", player.position_x, player.position_y, player.width, player.height ) end return player
      
      





また、 main.luaも更新します



 local player = require 'player' function love.draw() player.draw() end function love.update( dt ) player.update( dt ) end
      
      





ゲームを開始すると、下部に白い四角の黒い画面が表示され、「左」と「右」のキーを制御できます。 さらに、プレーヤーのコードの制限により、彼は画面を越えることができません。



 player.position.x < ( love.graphics.getWidth() - player.width ) player.position.x > 0
      
      





敵を追加する



外国の侵略者と戦うので、それらのファイルinvaders.luaと呼びます



 local invaders = {} invaders.rows = 5 invaders.columns = 9 invaders.top_left_position_x = 50 invaders.top_left_position_y = 50 invaders.invader_width = 40 invaders.invader_height = 40 invaders.horizontal_distance = 20 invaders.vertical_distance = 30 invaders.current_speed_x = 50 invaders.current_level_invaders = {} local initial_speed_x = 50 local initial_direction = 'right' function invaders.new_invader( position_x, position_y ) return { position_x = position_x, position_y = position_y, width = invaders.invader_width, height = invaders.invader_height } end function invaders.new_row( row_index ) local row = {} for col_index=1, invaders.columns - (row_index % 2) do local new_invader_position_x = invaders.top_left_position_x + invaders.invader_width * (row_index % 2) + (col_index - 1) * (invaders.invader_width + invaders.horizontal_distance) local new_invader_position_y = invaders.top_left_position_y + (row_index - 1) * (invaders.invader_height + invaders.vertical_distance) local new_invader = invaders.new_invader( new_invader_position_x, new_invader_position_y ) table.insert( row, new_invader ) end return row end function invaders.construct_level() invaders.current_speed_x = initial_speed_x for row_index=1, invaders.rows do local invaders_row = invaders.new_row( row_index ) table.insert( invaders.current_level_invaders, invaders_row ) end end function invaders.draw_invader( single_invader ) love.graphics.rectangle('line', single_invader.position_x, single_invader.position_y, single_invader.width, single_invader.height ) end function invaders.draw() for _, invader_row in pairs( invaders.current_level_invaders ) do for _, invader in pairs( invader_row ) do invaders.draw_invader( invader, is_miniboss ) end end end function invaders.update_invader( dt, single_invader ) single_invader.position_x = single_invader.position_x + invaders.current_speed_x * dt end function invaders.update( dt ) local invaders_rows = 0 for _, invader_row in pairs( invaders.current_level_invaders ) do invaders_rows = invaders_rows + 1 end if invaders_rows == 0 then invaders.no_more_invaders = true else for _, invader_row in pairs( invaders.current_level_invaders ) do for _, invader in pairs( invader_row ) do invaders.update_invader( dt, invader ) end end end end return invaders
      
      





main.luaを更新する



 ... local invaders = require 'invaders' function love.load() invaders.construct_level() end function love.draw() ... invaders.draw() end function love.update( dt ) ... invaders.update( dt ) end
      
      





love.loadは、アプリケーションの最初に呼び出されます。 invaders.construct_levelメソッドを呼び出します。このメソッドは、 invaders.current_level_invadersテーブルを作成し、オブジェクトの高さと幅、および必要な水平距離と垂直距離を考慮して、列ごとに個別のインベーダーオブジェクトを設定します。 偶数行と奇数行のオフセットを取得するには、 invaders.new_rowメソッドを少し複雑にする必要がありました。 現在のデザインを置き換える場合:



 for col_index=1, invaders.columns - (row_index % 2) do local new_invader_position_x = invaders.top_left_position_x + invaders.invader_width * (row_index % 2) + (col_index - 1) * (invaders.invader_width + invaders.horizontal_distance)
      
      





このように:



 for col_index=1, invaders.columns do local new_invader_position_x = invaders.top_left_position_x + (col_index - 1) * (invaders.invader_width + invaders.horizontal_distance)
      
      





次に、この効果を削除して、長方形の塗りつぶしを返します。 写真での比較

現在のオプション 長方形オプション


インベーダーオブジェクトは、position_x、position_y、width、heightのプロパティを持つテーブルです。 これはすべてオブジェクトを描画するために必要であり、後でショットとの衝突をチェックするためにも必要になります。



love.drawinvaders.drawを呼び出し、 invaders.current_level_invadersテーブルのすべての行のすべてのオブジェクトが描画されます。



love.update 、そしてinvaders.updateは、現在の速度を考慮に入れて、各侵略者の現在の位置を更新します。



侵略者はすでに動き始めていますが、これまでのところ、画面の後ろの右側のみです。 今すぐ修正します。

壁と衝突を追加する



新しいwalls.luaファイル



 local walls = {} walls.wall_thickness = 1 walls.bottom_height_gap = 1/5 * love.graphics.getHeight() walls.current_level_walls = {} function walls.new_wall( position_x, position_y, width, height ) return { position_x = position_x, position_y = position_y, width = width, height = height } end function walls.construct_level() local left_wall = walls.new_wall( 0, 0, walls.wall_thickness, love.graphics.getHeight() - walls.bottom_height_gap ) local right_wall = walls.new_wall( love.graphics.getWidth() - walls.wall_thickness, 0, walls.wall_thickness, love.graphics.getHeight() - walls.bottom_height_gap ) local top_wall = walls.new_wall( 0, 0, love.graphics.getWidth(), walls.wall_thickness ) local bottom_wall = walls.new_wall( 0, love.graphics.getHeight() - walls.bottom_height_gap - walls.wall_thickness, love.graphics.getWidth(), walls.wall_thickness ) walls.current_level_walls["left"] = left_wall walls.current_level_walls["right"] = right_wall walls.current_level_walls["top"] = top_wall walls.current_level_walls["bottom"] = bottom_wall end function walls.draw_wall(wall) love.graphics.rectangle( 'line', wall.position_x, wall.position_y, wall.width, wall.height ) end function walls.draw() for _, wall in pairs( walls.current_level_walls ) do walls.draw_wall( wall ) end end return walls
      
      





main.luaに少し



 ... local walls = require 'walls' function love.load() ... walls.construct_level() end function love.draw() ... -- walls.draw() end
      
      





侵略者の作成と同様に、 walls.construct_levelは壁の作成を担当します。 侵略者の「衝突」とそれらとのショットをインターセプトするために必要なのは壁だけなので、それらを描く必要はありません。 ただし、これはデバッグ目的で必要になる場合があるため、Wallsオブジェクトにはdrawメソッドがあり、その呼び出しはmain.lua-> love.drawから標準になっていますが、これまでデバッグは必要ありません-それ(呼び出し)はコメント化されています。



ここから、ここから借用た衝突ハンドラーを作成します 。 だからcollisions.lua



 local collisions = {} function collisions.check_rectangles_overlap( a, b ) local overlap = false if not( ax + a.width < bx or bx + b.width < ax or ay + a.height < by or by + b.height < ay ) then overlap = true end return overlap end function collisions.invaders_walls_collision( invaders, walls ) local overlap, wall if invaders.current_speed_x > 0 then wall, wall_type = walls.current_level_walls['right'], 'right' else wall, wall_type = walls.current_level_walls['left'], 'left' end local a = { x = wall.position_x, y = wall.position_y, width = wall.width, height = wall.height } for _, invader_row in pairs( invaders.current_level_invaders ) do for _, invader in pairs( invader_row ) do local b = { x = invader.position_x, y = invader.position_y, width = invader.width, height = invader.height } overlap = collisions.check_rectangles_overlap( a, b ) if overlap then if wall_type == invaders.allow_overlap_direction then invaders.current_speed_x = -invaders.current_speed_x if invaders.allow_overlap_direction == 'right' then invaders.allow_overlap_direction = 'left' else invaders.allow_overlap_direction = 'right' end invaders.descend_by_row() end end end end end function collisions.resolve_collisions( invaders, walls ) collisions.invaders_walls_collision( invaders, walls ) end return collisions
      
      





いくつかのメソッドと変数をinvaders.luaに追加します



 invaders.allow_overlap_direction = 'right' function invaders.descend_by_row_invader( single_invader ) single_invader.position_y = single_invader.position_y + invaders.vertical_distance / 2 end function invaders.descend_by_row() for _, invader_row in pairs( invaders.current_level_invaders ) do for _, invader in pairs( invader_row ) do invaders.descend_by_row_invader( invader ) end end end
      
      





そして、 main.luaに衝突チェックを追加します



 local collisions = require 'collisions' function love.update( dt ) ... collisions.resolve_collisions( invaders, walls ) end
      
      





これで、侵略者はcollisions.invaders_walls_collisionの壁を越えて少し下に下がり、速度を反対に変更します。



私は、侵入者が遭遇した壁のタイプと有効なタイプが格納されている変数の同等性に関する追加のチェックを導入する必要がありました。



 if overlap then if wall_type == invaders.allow_overlap_direction then ...
      
      





すべての侵略者が極端な列から同時に壁を突破し、衝突ハンドラーが「全員のために働く」ことに成功し、侵略者が向きを変えて連絡先を離れる前にチーム全体を1列減らすことにより、行。 ここでは、次の衝突で衝突が発生した場合にブロックを配置するか、侵入者を正確に1つではなく他の1つの下に配置します。



プレーヤーが撃つ方法を学ぶ時が来ました



新しいファイルとクラスbullets.lua



 local bullets = {} bullets.current_speed_y = -200 bullets.width = 2 bullets.height = 10 bullets.current_level_bullets = {} function bullets.destroy_bullet( bullet_i ) bullets.current_level_bullets[bullet_i] = nil end function bullets.new_bullet(position_x, position_y) return { position_x = position_x, position_y = position_y, width = bullets.width, height = bullets.height } end function bullets.fire( player ) local position_x = player.position_x + player.width / 2 local position_y = player.position_y local new_bullet = bullets.new_bullet( position_x, position_y ) table.insert(bullets.current_level_bullets, new_bullet) end function bullets.draw_bullet( bullet ) love.graphics.rectangle( 'fill', bullet.position_x, bullet.position_y, bullet.width, bullet.height ) end function bullets.draw() for _, bullet in pairs(bullets.current_level_bullets) do bullets.draw_bullet( bullet ) end end function bullets.update_bullet( dt, bullet ) bullet.position_y = bullet.position_y + bullets.current_speed_y * dt end function bullets.update( dt ) for _, bullet in pairs(bullets.current_level_bullets) do bullets.update_bullet( dt, bullet ) end end return bullets
      
      





ここで、メインのメソッドはbullets.fireです。 Playerをそれに渡します。なぜなら 弾丸が飛び出すようにしたいので、その位置を知る必要があります。 なぜなら カートリッジは1つではありませんが、キュー全体を使用できます。それをbullets.current_level_bulletsテーブルに格納し、そのカートリッジと各カートリッジのdrawメソッドとupdateメソッドを呼び出します。 bullets.destroy_bulletメソッド 、侵入者または天井と接触したときに、メモリから余分なカートリッジを削除するために必要です。



bullet-invaderとbullet-ceilingの衝突処理を追加します。



collisions.lua

 function collisions.invaders_bullets_collision( invaders, bullets ) local overlap for b_i, bullet in pairs( bullets.current_level_bullets) do local a = { x = bullet.position_x, y = bullet.position_y, width = bullet.width, height = bullet.height } for i_i, invader_row in pairs( invaders.current_level_invaders ) do for i_j, invader in pairs( invader_row ) do local b = { x = invader.position_x, y = invader.position_y, width = invader.width, height = invader.height } overlap = collisions.check_rectangles_overlap( a, b ) if overlap then invaders.destroy_invader( i_i, i_j ) bullets.destroy_bullet( b_i ) end end end end end function collisions.bullets_walls_collision( bullets, walls ) local overlap local wall = walls.current_level_walls['top'] local a = { x = wall.position_x, y = wall.position_y, width = wall.width, height = wall.height } for b_i, bullet in pairs( bullets.current_level_bullets) do local b = { x = bullet.position_x, y = bullet.position_y, width = bullet.width, height = bullet.height } overlap = collisions.check_rectangles_overlap( a, b ) if overlap then bullets.destroy_bullet( b_i ) end end end function collisions.resolve_collisions( invaders, walls, bullets ) ... collisions.invaders_bullets_collision( invaders, bullets ) collisions.bullets_walls_collision( bullets, walls ) end
      
      





侵入者にメソッドを追加して破壊し、一般的な侵入者テーブルの特定の行に侵入者が存在するかどうかを確認します。誰も残っていない場合、行自体が削除されます。 また、キル中に艦隊全体の速度を上げます。



invaders.lua

 ... invaders.speed_x_increase_on_destroying = 10 function invaders.destroy_invader( row, invader ) invaders.current_level_invaders[row][invader] = nil local invaders_row_count = 0 for _, invader in pairs( invaders.current_level_invaders[row] ) do invaders_row_count = invaders_row_count + 1 end if invaders_row_count == 0 then invaders.current_level_invaders[row] = nil end if invaders.allow_overlap_direction == 'right' then invaders.current_speed_x = invaders.current_speed_x + invaders.speed_x_increase_on_destroying else invaders.current_speed_x = invaders.current_speed_x - invaders.speed_x_increase_on_destroying end end ...
      
      





そして、 mail.luaを更新します 。新しいクラスを追加し、それをコリジョンハンドラーに送信し、Spaceキーでシューティングコールを切断します。



 ... local bullets = require 'bullets' function love.keyreleased( key ) if key == 'space' then bullets.fire( player ) end end function love.draw() ... bullets.draw() end function love.update( dt ) ... collisions.resolve_collisions( invaders, walls, bullets ) bullets.update( dt ) end
      
      





さらなる作業には既存のコードの変更が含まれるため、この段階で取得したものはバージョン0.5として保存されます。



:gitのコードは、ここで解析されたコードとは異なります。 ハンプライブラリは、もともとベクターの操作に使用されていました。 しかし、その後、それなしで実行できることが明らかになり、最終バージョンではライブラリを見つけました。 コードはあちこちで同様に機能します。githubからコードを実行するには、サブモジュールを初期化する必要があります。



 git submodule update --init
      
      





ハングテクスチャ





これらは3人の標準的な敵と1人のミニボスであり、そのデバイスはここでは考慮されませんが、最終バージョンにあります。 そして戦車プレイヤー自身。



ゲームのテクスチャはannnushkkkaによって親切に提供されました



すべての画像は、プロジェクトのルートのimagesディレクトリにあります。 player.luaでプレーヤーを変更する



 ... player.image = love.graphics.newImage('images/Hero.png') -- from https://love2d.org/forums/viewtopic.php?t=79756 function getImageScaleForNewDimensions( image, newWidth, newHeight ) local currentWidth, currentHeight = image:getDimensions() return ( newWidth / currentWidth ), ( newHeight / currentHeight ) end local scaleX, scaleY = getImageScaleForNewDimensions( player.image, player.width, player.height ) function player.draw() --   love.graphics.draw(player.image, player.position_x, player.position_y, rotation, scaleX, scaleY ) end ...
      
      





ここから見たgetImageScaleForNewDimensions関数は、 player.width、player.heightで指定した寸法に画像を調整します。 ここと敵の両方で使用されますが、後で別のutils.luaモジュールに配置します。 player.draw関数置き換えられました。



起動時に、元スクエアプレイヤーは戦車になりました!



invaders.luaを変更します



 ... invaders.images = {love.graphics.newImage('images/bad_1.png'), love.graphics.newImage('images/bad_2.png'), love.graphics.newImage('images/bad_3.png') } -- from https://love2d.org/forums/viewtopic.php?t=79756 function getImageScaleForNewDimensions( image, newWidth, newHeight ) local currentWidth, currentHeight = image:getDimensions() return ( newWidth / currentWidth ), ( newHeight / currentHeight ) end local scaleX, scaleY = getImageScaleForNewDimensions( invaders.images[1], invaders.invader_width, invaders.invader_height ) function invaders.new_invader(position_x, position_y ) --  local invader_image_no = math.random(1, #invaders.images) invader_image = invaders.images[invader_image_no] return ({position_x = position_x, position_y = position_y, width = invaders.invader_width, height = invaders.invader_height, image = invader_image}) end function invaders.draw_invader( single_invader ) --  love.graphics.draw(single_invader.image, single_invader.position_x, single_invader.position_y, rotation, scaleX, scaleY ) end
      
      





テーブルに敵の写真を追加し、getImageScaleForNewDimensionsを使用してサイズを調整します。 新しいインベーダーを作成するとき、画像テーブルからランダムな画像が画像属性でそれに割り当てられます。 そして、レンダリング方法自体を変更します。



起こったことは次のとおりです。







ゲームを数回実行すると、敵のランダムな組み合わせが毎回同じであることがわかります。 これを回避するには、ゲームを開始する前にmath.randomseedを定義する必要があります。 os.timeを引数として渡してこれを行うのは良いことです。 これをmain.luaに追加します



 function love.load() ... math.randomseed( os.time() ) ... end
      
      





これで、 バージョン0.75のほぼ完全なゲームができました。 計画されていたすべてを解体しました。



レビュー、コメント、ヒントを喜んでいます!



All Articles