Otherwise, the easiest way to run the game is to drag the folder onto either love.exe or a shortcut to love.exe. Remember to drag the folder containing main.lua, and not main.lua itself.
function
love.load()
Used for initializing our game state at the very beginning of program execution. Load function肯定自带有,这是overwrite初始function. Love2D默认read main.lua file.
love.update(dt) - dt:delta time
Called each frame by LÖVE; dt will be the elapsed time in seconds since the last frame, and we can use this to scale any changes in our game for even behavior across frame rates.
love.draw()
Called each frame by LÖVE after update for drawing things to the screen once they’ve changed.
LÖVE2D expects these functions to be implemented in main.lua and calls them internally; if we don’t define them, it will still function, but our game will be fundamentally incomplete, at least if update or draw are missing!
love.graphics.printf(text, x, y, [width], [align])
Versatile print function that can align text left, right, or center on the screen.
love.window.setMode(width, height, params)
Used to initialize the window’s dimensions and to set parameters like vsync (vertical sync), whether we’re fullscreen or not, and whether the window is resizable after startup. Won’t be using past this example in favor of the push virtual resolution library, which has its own method like this, but useful to know if encountered in other code.
--[[ GD50 2018 Pong Remake pong-0 "The Day-0 Update" -- Main Program -- ]]
WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720
--[[ Runs when the game first starts up, only once; used to initialize the game. ]] functionlove.load() love.window.setMode(WINDOW_WIDTH, WINDOW_HEIGHT, { fullscreen = false, resizable = false, vsync = true }) end
--[[ Called after update by LÖVE2D, used to draw anything to the screen, updated or otherwise. ]] functionlove.draw() love.graphics.printf( 'Hello Pong!', -- text to render 0, -- starting X (0 since we're going to center it based on width) WINDOW_HEIGHT / 2 - 6, -- starting Y (halfway down the screen) WINDOW_WIDTH, -- number of pixels to center within (the entire screen here) 'center') -- alignment mode, can be 'center', 'left', or 'right' end
1
下一步更改了resolution.
function
love.graphics.setDefaultFilter(min, mag)
Sets the texture scaling filter when minimizing and magnifying textures and fonts; default is bilinear, which causes blurriness, and for our use cases we will typically want nearest-neighbor filtering (‘nearest’), which results in perfect pixel upscaling and downscaling, simulating a retro feel. 当love2D run text or image的时候,会apply一个default filter.
love.keypressed(key)
A LÖVE2D callback function that executes whenever we press a key, assuming we’ve implemented this in our main.lua, in the same vein as love.load(), love.update(dt), and love.draw().
--[[ GD50 2018 Pong Remake pong-1 "The Low-Res Update" -- Main Program -- ]]
-- push is a library that will allow us to draw our game at a virtual -- resolution, instead of however large our window is; used to provide -- a more retro aesthetic -- -- https://github.com/Ulydev/push -- 导入library
push = require'push'
WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720
VIRTUAL_WIDTH = 432 VIRTUAL_HEIGHT = 243
-- 这一步是原本432*243大小的图像之后render成 1280*720,看起来像老游戏
--[[ Runs when the game first starts up, only once; used to initialize the game. ]] functionlove.load() -- use nearest-neighbor filtering on upscaling and downscaling to prevent blurring of text -- and graphics; try removing this function to see the difference! love.graphics.setDefaultFilter('nearest', 'nearest')
-- initialize our virtual resolution, which will be rendered within our -- actual window no matter its dimensions; replaces our love.window.setMode call -- from the last example push:setupScreen(VIRTUAL_WIDTH, VIRTUAL_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT, { fullscreen = false, resizable = false, vsync = true }) end
--[[ Keyboard handling, called by LÖVE2D each frame; passes in the key we pressed so we can access. ]] functionlove.keypressed(key) -- keys can be accessed by string name if key == 'escape'then -- function LÖVE gives us to terminate application love.event.quit() end end
--[[ Called after update by LÖVE2D, used to draw anything to the screen, updated or otherwise. ]] functionlove.draw() -- begin rendering at virtual resolution push:apply('start')
-- condensed onto one line from last example -- note we are now using virtual width and height now for text placement love.graphics.printf('Hello Pong!', 0, VIRTUAL_HEIGHT / 2 - 6, VIRTUAL_WIDTH, 'center')
-- end rendering at virtual resolution push:apply('end') end
-- Copyright (c) 2020 Ulysse Ramage -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local love11 = love.getVersion() == 11 local getDPI = love11 and love.window.getDPIScale or love.window.getPixelScale local windowUpdateMode = love11 and love.window.updateMode orfunction(width, height, settings) local _, _, flags = love.window.getMode() for k, v inpairs(settings) do flags[k] = v end love.window.setMode(width, height, flags) end
for i = 1, #canvases do push:addCanvas(canvases[i]) end
returnself end functionpush:addCanvas(params) table.insert(self.canvases, { name = params.name, private = params.private, shader = params.shader, canvas = love.graphics.newCanvas(self._WWIDTH, self._WHEIGHT), stencil = params.stencil orself._stencil }) end
functionpush:setCanvas(name) ifnotself._canvas thenreturntrueend local canvasTable = self:getCanvasTable(name) return love.graphics.setCanvas({ canvasTable.canvas, stencil = canvasTable.stencil }) end functionpush:getCanvasTable(name) for i = 1, #self.canvases do ifself.canvases[i].name == name then returnself.canvases[i] end end end functionpush:setShader(name, shader) ifnot shader then self:getCanvasTable("_render").shader = name else self:getCanvasTable(name).shader = shader end end
functionpush:initValues() self._PSCALE = (not love11 andself._highdpi) and getDPI() or1
self._SCALE = { x = self._RWIDTH/self._WWIDTH * self._PSCALE, y = self._RHEIGHT/self._WHEIGHT * self._PSCALE }
ifself._stretched then--if stretched, no need to apply offset self._OFFSET = {x = 0, y = 0} else local scale = math.min(self._SCALE.x, self._SCALE.y) ifself._pixelperfect then scale = math.floor(scale) end
self._OFFSET = {x = (self._SCALE.x - scale) * (self._WWIDTH/2), y = (self._SCALE.y - scale) * (self._WHEIGHT/2)} self._SCALE.x, self._SCALE.y = scale, scale --apply same scale to X and Y end
functionpush:apply(operation, shader) self._drawFunctions[operation](self, shader) end
functionpush:start() ifself._canvas then love.graphics.push() love.graphics.setCanvas({ self.canvases[1].canvas, stencil = self.canvases[1].stencil })
else love.graphics.translate(self._OFFSET.x, self._OFFSET.y) love.graphics.setScissor(self._OFFSET.x, self._OFFSET.y, self._WWIDTH*self._SCALE.x, self._WHEIGHT*self._SCALE.y) love.graphics.push() love.graphics.scale(self._SCALE.x, self._SCALE.y) end end
functionpush:applyShaders(canvas, shaders) local _shader = love.graphics.getShader() if #shaders <= 1then love.graphics.setShader(shaders[1]) love.graphics.draw(canvas) else local _canvas = love.graphics.getCanvas()
local _tmp = self:getCanvasTable("_tmp") ifnot _tmp then--create temp canvas only if needed self:addCanvas({ name = "_tmp", private = true, shader = nil }) _tmp = self:getCanvasTable("_tmp") end
love.graphics.push() love.graphics.origin() local outputCanvas for i = 1, #shaders do local inputCanvas = i % 2 == 1and canvas or _tmp.canvas outputCanvas = i % 2 == 0and canvas or _tmp.canvas love.graphics.setCanvas(outputCanvas) love.graphics.clear() love.graphics.setShader(shaders[i]) love.graphics.draw(inputCanvas) love.graphics.setCanvas(inputCanvas) end love.graphics.pop()
love.graphics.setCanvas(_canvas) love.graphics.draw(outputCanvas) end love.graphics.setShader(_shader) end
functionpush:finish(shader) love.graphics.setBackgroundColor(unpack(self._borderColor)) ifself._canvas then local _render = self:getCanvasTable("_render")
love.graphics.pop()
local white = love11 and1or255 love.graphics.setColor(white, white, white)
--draw canvas love.graphics.setCanvas(_render.canvas) for i = 1, #self.canvases do--do not draw _render yet local _table = self.canvases[i] ifnot _table.private then local _canvas = _table.canvas local _shader = _table.shader self:applyShaders(_canvas, type(_shader) == "table"and _shader or { _shader }) end end love.graphics.setCanvas()
--draw render love.graphics.translate(self._OFFSET.x, self._OFFSET.y) local shader = shader or _render.shader love.graphics.push() love.graphics.scale(self._SCALE.x, self._SCALE.y) self:applyShaders(_render.canvas, type(shader) == "table"and shader or { shader }) love.graphics.pop()
--clear canvas for i = 1, #self.canvases do love.graphics.setCanvas(self.canvases[i].canvas) love.graphics.clear() end
love.graphics.setCanvas() love.graphics.setShader() else love.graphics.pop() love.graphics.setScissor() end end
functionpush:setBorderColor(color, g, b) self._borderColor = g and {color, g, b} or color end
functionpush:toGame(x, y) x, y = x - self._OFFSET.x, y - self._OFFSET.y local normalX, normalY = x / self._GWIDTH, y / self._GHEIGHT
x = (x >= 0and x <= self._WWIDTH * self._SCALE.x) and normalX * self._WWIDTH ornil y = (y >= 0and y <= self._WHEIGHT * self._SCALE.y) and normalY * self._WHEIGHT ornil
return x, y end
--doesn't work - TODO functionpush:toReal(x, y) return x + self._OFFSET.x, y + self._OFFSET.y end
functionpush:switchFullscreen(winw, winh) self._fullscreen = notself._fullscreen local windowWidth, windowHeight = love.window.getDesktopDimensions()
ifself._fullscreen then--save windowed dimensions for later self._WINWIDTH, self._WINHEIGHT = self._RWIDTH, self._RHEIGHT elseifnotself._WINWIDTH ornotself._WINHEIGHT then self._WINWIDTH, self._WINHEIGHT = windowWidth * .5, windowHeight * .5 end
self._RWIDTH = self._fullscreen and windowWidth or winw orself._WINWIDTH self._RHEIGHT = self._fullscreen and windowHeight or winh orself._WINHEIGHT
self:initValues()
love.window.setFullscreen(self._fullscreen, "desktop") ifnotself._fullscreen and (winw or winh) then windowUpdateMode(self._RWIDTH, self._RHEIGHT) --set window dimensions end end
functionpush:resize(w, h) ifself._highdpi then w, h = w / self._PSCALE, h / self._PSCALE end self._RWIDTH = w self._RHEIGHT = h self:initValues() end
functionpush:getWidth()returnself._WWIDTH end functionpush:getHeight()returnself._WHEIGHT end functionpush:getDimensions()returnself._WWIDTH, self._WHEIGHT end
return push
2
画出paddle和球.
function
love.graphics.newFont(path, size)
Loads a font file into memory at a specific path, setting it to a specific size, and storing it in an object we can use to globally change the currently active font that LÖVE2D is using to render text (functioning like a state machine). Deafult is Arial.
love.graphics.setFont(font)
Sets LÖVE2D’s currently active font (of which there can only be one at a time) to a passed-in font object that we can create using love.graphics.newFont.
love.graphics.clear(r, g, b, a)
Wipes the entire screen with a color defined by an RGBA set, each component of which being from 0-255.
love.graphics.rectangle(mode, x, y, width, height)
Draws a rectangle onto the screen using whichever our active color is (love.graphics.setColor, which we don’t need to use in this particular project since most everything is white, the default LÖVE2D color). mode can be set to ‘fill’ or ‘line’, which result in a filled or outlined rectangle, respectively, and the other four parameters are its position and size dimensions. This is the cornerstone drawing function of the entirety of our Pong implementation!
--[[ GD50 2018 Pong Remake pong-2 "The Rectangle Update" ]]
-- push is a library that will allow us to draw our game at a virtual -- resolution, instead of however large our window is; used to provide -- a more retro aesthetic -- -- https://github.com/Ulydev/push push = require'push'
WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720
VIRTUAL_WIDTH = 432 VIRTUAL_HEIGHT = 243
--[[ Runs when the game first starts up, only once; used to initialize the game. ]] functionlove.load() love.graphics.setDefaultFilter('nearest', 'nearest')
-- more "retro-looking" font object we can use for any text smallFont = love.graphics.newFont('font.ttf', 8)
-- set LÖVE2D's active font to the smallFont obect love.graphics.setFont(smallFont)
--[[ Keyboard handling, called by LÖVE2D each frame; passes in the key we pressed so we can access. ]] functionlove.keypressed(key) -- keys can be accessed by string name if key == 'escape'then -- function LÖVE gives us to terminate application love.event.quit() end end
--[[ Called after update by LÖVE2D, used to draw anything to the screen, updated or otherwise. ]] functionlove.draw() -- begin rendering at virtual resolution push:apply('start')
-- clear the screen with a specific color; in this case, a color similar -- to some versions of the original Pong love.graphics.clear(40/255, 45/255, 52/255, 255/255)
-- draw welcome text toward the top of the screen love.graphics.printf('Hello Pong!', 0, 20, VIRTUAL_WIDTH, 'center')
-- -- paddles are simply rectangles we draw on the screen at certain points, -- as is the ball --
-- end rendering at virtual resolution push:apply('end') end
3
让paddle动起来!
function
love.keyboard.isDown(key)
Returns true or false depending on whether the specified key is currently held down; differs from love.keypressed(key) in that this can be called arbitrarily and will continuously return true if the key is pressed down, where love.keypressed(key) will only fire its code once every time the key is initially pressed down. However, since we want to be able to move our paddles up and down by holding down the appropriate keys, we need a function to test for longer periods of input, hence the use of love.keyboard.isDown(key)! This returns true or false.
--[[ GD50 2018 Pong Remake pong-3 "The Paddle Update" ]]
-- push is a library that will allow us to draw our game at a virtual -- resolution, instead of however large our window is; used to provide -- a more retro aesthetic -- -- https://github.com/Ulydev/push push = require'push'
WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720
VIRTUAL_WIDTH = 432 VIRTUAL_HEIGHT = 243
-- paddle移动速度; multiplied by dt in update PADDLE_SPEED = 200
--[[ Runs when the game first starts up, only once; used to initialize the game. ]] functionlove.load() love.graphics.setDefaultFilter('nearest', 'nearest')
-- more "retro-looking" font object we can use for any text smallFont = love.graphics.newFont('font.ttf', 8)
-- larger font for drawing the score on the screen scoreFont = love.graphics.newFont('font.ttf', 32)
-- set LÖVE2D's active font to the smallFont obect love.graphics.setFont(smallFont)
-- initialize score variables, used for rendering on the screen and keeping -- track of the winner player1Score = 0 player2Score = 0
-- paddle positions on the Y axis (they can only move up or down) player1Y = 30 player2Y = VIRTUAL_HEIGHT - 50 end
--[[ Runs every frame, with "dt" passed in, our delta in seconds since the last frame, which LÖVE2D supplies us. ]] functionlove.update(dt) -- player 1 movement if love.keyboard.isDown('w') then -- add negative paddle speed to current Y scaled by deltaTime,这里的横线是负的意思 player1Y = player1Y + -PADDLE_SPEED * dt elseif love.keyboard.isDown('s') then -- add positive paddle speed to current Y scaled by deltaTime player1Y = player1Y + PADDLE_SPEED * dt end
-- player 2 movement if love.keyboard.isDown('up') then -- add negative paddle speed to current Y scaled by deltaTime player2Y = player2Y + -PADDLE_SPEED * dt elseif love.keyboard.isDown('down') then -- add positive paddle speed to current Y scaled by deltaTime player2Y = player2Y + PADDLE_SPEED * dt end end
--[[ Keyboard handling, called by LÖVE2D each frame; passes in the key we pressed so we can access. ]] functionlove.keypressed(key) -- keys can be accessed by string name if key == 'escape'then -- function LÖVE gives us to terminate application love.event.quit() end end
--[[ Called after update by LÖVE2D, used to draw anything to the screen, updated or otherwise. ]] functionlove.draw() -- begin rendering at virtual resolution push:apply('start')
-- clear the screen with a specific color; in this case, a color similar -- to some versions of the original Pong love.graphics.clear(40/255, 45/255, 52/255, 255/255)
-- draw welcome text toward the top of the screen love.graphics.setFont(smallFont) love.graphics.printf('Hello Pong!', 0, 20, VIRTUAL_WIDTH, 'center')
-- draw score on the left and right center of the screen -- need to switch font to draw before actually printing love.graphics.setFont(scoreFont) love.graphics.print(tostring(player1Score), VIRTUAL_WIDTH / 2 - 50, VIRTUAL_HEIGHT / 3) love.graphics.print(tostring(player2Score), VIRTUAL_WIDTH / 2 + 30, VIRTUAL_HEIGHT / 3)
-- render first paddle (left side), now using the players' Y variable love.graphics.rectangle('fill', 10, player1Y, 5, 20)
-- end rendering at virtual resolution push:apply('end') end
4
让球动起来!
function
math.randomseed(num)
”Seeds” the random number generator used by Lua (math.random) with some value such that its randomness is dependent on that supplied value, allowing us to pass in different numbers each playthrough to guarantee non-consistency across different program executions (or uniformity if we want consistent behavior for testing).
os.time()
Lua function that returns, in seconds, the time since 00:00:00 UTC, January 1, 1970, also known as Unix epoch time (https://en.wikipedia.org/wiki/Unix_time). 因为每次的时间不一样,所以可以拿来当作seed.
math.random(min, max)
Returns a random number, dependent on the seeded random number generator, between min and max, inclusive.
--[[ GD50 2018 Pong Remake pong-4 "The Ball Update" -- Main Program -- ]]
-- push is a library that will allow us to draw our game at a virtual -- resolution, instead of however large our window is; used to provide -- a more retro aesthetic -- -- https://github.com/Ulydev/push push = require'push'
WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720
VIRTUAL_WIDTH = 432 VIRTUAL_HEIGHT = 243
-- speed at which we will move our paddle; multiplied by dt in update PADDLE_SPEED = 200
--[[ Runs when the game first starts up, only once; used to initialize the game. ]] functionlove.load() love.graphics.setDefaultFilter('nearest', 'nearest')
-- "seed" the RNG so that calls to random are always random -- use the current time, since that will vary on startup every time math.randomseed(os.time())
-- more "retro-looking" font object we can use for any text smallFont = love.graphics.newFont('font.ttf', 8)
-- set LÖVE2D's active font to the smallFont obect love.graphics.setFont(smallFont)
-- paddle positions on the Y axis (they can only move up or down) player1Y = 30 player2Y = VIRTUAL_HEIGHT - 50
-- velocity and position variables for our ball when play starts ballX = VIRTUAL_WIDTH / 2 - 2 ballY = VIRTUAL_HEIGHT / 2 - 2
-- math.random returns a random value between the left and right number -- dx and dy are delta x and delta y, which represent velocity ballDX = math.random(2) == 1and100or-100 ballDY = math.random(-50, 50) -- by adding them one frame by one frame in update, ball will move
-- game state variable used to transition between different parts of the game -- (used for beginning, menus, main game, high score list, etc.) -- we will use this to determine behavior during render and update gameState = 'start' end
--[[ Runs every frame, with "dt" passed in, our delta in seconds since the last frame, which LÖVE2D supplies us. ]] functionlove.update(dt) -- player 1 movement if love.keyboard.isDown('w') then -- add negative paddle speed to current Y scaled by deltaTime -- now, we clamp our position between the bounds of the screen -- math.max returns the greater of two values; 0 and player Y -- will ensure we don't go above it player1Y = math.max(0, player1Y + -PADDLE_SPEED * dt)
-- 用max来锁住活动范围
elseif love.keyboard.isDown('s') then -- add positive paddle speed to current Y scaled by deltaTime -- math.min returns the lesser of two values; bottom of the egde minus paddle height -- and player Y will ensure we don't go below it player1Y = math.min(VIRTUAL_HEIGHT - 20, player1Y + PADDLE_SPEED * dt) end
-- player 2 movement if love.keyboard.isDown('up') then -- add negative paddle speed to current Y scaled by deltaTime player2Y = math.max(0, player2Y + -PADDLE_SPEED * dt) elseif love.keyboard.isDown('down') then -- add positive paddle speed to current Y scaled by deltaTime player2Y = math.min(VIRTUAL_HEIGHT - 20, player2Y + PADDLE_SPEED * dt) end
-- update our ball based on its DX and DY only if we're in play state; -- scale the velocity by dt so movement is framerate-independent if gameState == 'play'then ballX = ballX + ballDX * dt ballY = ballY + ballDY * dt end end
--[[ Keyboard handling, called by LÖVE2D each frame; passes in the key we pressed so we can access. ]] functionlove.keypressed(key) -- keys can be accessed by string name if key == 'escape'then -- function LÖVE gives us to terminate application love.event.quit() -- if we press enter during the start state of the game, we'll go into play mode -- during play mode, the ball will move in a random direction elseif key == 'enter'or key == 'return'then if gameState == 'start'then gameState = 'play' else gameState = 'start'
-- start ball's position in the middle of the screen ballX = VIRTUAL_WIDTH / 2 - 2 ballY = VIRTUAL_HEIGHT / 2 - 2
-- given ball's x and y velocity a random starting value -- the and/or pattern here is Lua's way of accomplishing a ternary operation -- in other programming languages like C ballDX = math.random(2) == 1and100or-100 -- 这跟在C里写 math.random(2) == 1 ? 100 : -100相同 ballDY = math.random(-50, 50) * 1.5 end end end
--[[ Called after update by LÖVE2D, used to draw anything to the screen, updated or otherwise. ]] functionlove.draw() -- begin rendering at virtual resolution push:apply('start')
-- clear the screen with a specific color; in this case, a color similar -- to some versions of the original Pong love.graphics.clear(40/255, 45/255, 52/255, 255/255)
-- draw different things based on the state of the game love.graphics.setFont(smallFont)
if gameState == 'start'then love.graphics.printf('Hello Start State!', 0, 20, VIRTUAL_WIDTH, 'center') else love.graphics.printf('Hello Play State!', 0, 20, VIRTUAL_WIDTH, 'center') end
-- render first paddle (left side), now using the players' Y variable love.graphics.rectangle('fill', 10, player1Y, 5, 20)
-- end rendering at virtual resolution push:apply('end') end
4
Class update,把paddle和球换成class. (Yeah this makes more sense.)
function
What is a class?
Blueprints for creating bundles of data and code that are related.
A “Car” class can have attributes that describe its brand, model, color, miles, and anything else descriptive; these are also known as “fields”.
A “Car” class can also have “methods” that define its behavior, such as “accelerate”, “turn”, “honk”, and more, which take the form of functions.
Objects are instantiated from these class blueprints, and it’s these concrete objects that are the physical “cars” you see on the road, as opposed to the blueprints that may exist in the factory.
Our Paddles and Ball are perfect simple use cases for taking some of our code and bundling it together into classes and objects.
--[[ GD50 2018 Pong Remake pong-5 "The Class Update" -- Main Program -- ]]
-- push is a library that will allow us to draw our game at a virtual -- resolution, instead of however large our window is; used to provide -- a more retro aesthetic -- -- https://github.com/Ulydev/push push = require'push'
-- the "Class" library we're using will allow us to represent anything in -- our game as code, rather than keeping track of many disparate variables and -- methods -- -- https://github.com/vrld/hump/blob/master/class.lua Class = require'class'
-- our Paddle class, which stores position and dimensions for each Paddle -- and the logic for rendering them require'Paddle'
-- our Ball class, which isn't much different than a Paddle structure-wise -- but which will mechanically function very differently require'Ball'
WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720
VIRTUAL_WIDTH = 432 VIRTUAL_HEIGHT = 243
-- speed at which we will move our paddle; multiplied by dt in update PADDLE_SPEED = 200
--[[ Runs when the game first starts up, only once; used to initialize the game. ]] functionlove.load() love.graphics.setDefaultFilter('nearest', 'nearest')
-- "seed" the RNG so that calls to random are always random -- use the current time, since that will vary on startup every time math.randomseed(os.time())
-- more "retro-looking" font object we can use for any text smallFont = love.graphics.newFont('font.ttf', 8)
-- set LÖVE2D's active font to the smallFont object love.graphics.setFont(smallFont)
-- initialize our player paddles; make them global so that they can be -- detected by other functions and modules player1 = Paddle(10, 30, 5, 20) player2 = Paddle(VIRTUAL_WIDTH - 10, VIRTUAL_HEIGHT - 30, 5, 20)
-- place a ball in the middle of the screen ball = Ball(VIRTUAL_WIDTH / 2 - 2, VIRTUAL_HEIGHT / 2 - 2, 4, 4)
-- game state variable used to transition between different parts of the game -- (used for beginning, menus, main game, high score list, etc.) -- we will use this to determine behavior during render and update gameState = 'start' end
--[[ Runs every frame, with "dt" passed in, our delta in seconds since the last frame, which LÖVE2D supplies us. ]] functionlove.update(dt) -- player 1 movement if love.keyboard.isDown('w') then player1.dy = -PADDLE_SPEED elseif love.keyboard.isDown('s') then player1.dy = PADDLE_SPEED else player1.dy = 0 end
-- player 2 movement if love.keyboard.isDown('up') then player2.dy = -PADDLE_SPEED elseif love.keyboard.isDown('down') then player2.dy = PADDLE_SPEED else player2.dy = 0 end
-- update our ball based on its DX and DY only if we're in play state; -- scale the velocity by dt so movement is framerate-independent if gameState == 'play'then ball:update(dt) end
player1:update(dt) player2:update(dt) end
--[[ Keyboard handling, called by LÖVE2D each frame; passes in the key we pressed so we can access. ]] functionlove.keypressed(key) -- keys can be accessed by string name if key == 'escape'then -- function LÖVE gives us to terminate application love.event.quit() -- if we press enter during the start state of the game, we'll go into play mode -- during play mode, the ball will move in a random direction elseif key == 'enter'or key == 'return'then if gameState == 'start'then gameState = 'play' else gameState = 'start'
-- ball's new reset method ball:reset() end end end
--[[ Called after update by LÖVE2D, used to draw anything to the screen, updated or otherwise. ]] functionlove.draw() -- begin rendering at virtual resolution push:apply('start')
-- clear the screen with a specific color; in this case, a color similar -- to some versions of the original Pong love.graphics.clear(40/255, 45/255, 52/255, 255/255)
-- draw different things based on the state of the game love.graphics.setFont(smallFont)
if gameState == 'start'then love.graphics.printf('Hello Start State!', 0, 20, VIRTUAL_WIDTH, 'center') else love.graphics.printf('Hello Play State!', 0, 20, VIRTUAL_WIDTH, 'center') end
-- render paddles, now using their class's render method player1:render() player2:render()
-- render ball using its class's render method ball:render()
-- end rendering at virtual resolution push:apply('end') end
--[[ GD50 2018 Pong Remake -- Ball Class -- Author: Colton Ogden cogden@cs50.harvard.edu Represents a ball which will bounce back and forth between paddles and walls until it passes a left or right boundary of the screen, scoring a point for the opponent. ]]
Ball = Class{}
functionBall:init(x, y, width, height) self.x = x self.y = y self.width = width self.height = height
-- these variables are for keeping track of our velocity on both the -- X and Y axis, since the ball can move in two dimensions self.dy = math.random(2) == 1and-100or100 self.dx = math.random(-50, 50) end
--[[ Places the ball in the middle of the screen, with an initial random velocity on both axes. ]] functionBall:reset() self.x = VIRTUAL_WIDTH / 2 - 2 self.y = VIRTUAL_HEIGHT / 2 - 2 self.dy = math.random(2) == 1and-100or100 self.dx = math.random(-50, 50) end
--[[ Simply applies velocity to position, scaled by deltaTime. ]] functionBall:update(dt) self.x = self.x + self.dx * dt self.y = self.y + self.dy * dt end
functionBall:render() love.graphics.rectangle('fill', self.x, self.y, self.width, self.height) end
--[[ GD50 2018 Pong Remake -- Paddle Class -- Author: Colton Ogden cogden@cs50.harvard.edu Represents a paddle that can move up and down. Used in the main program to deflect the ball back toward the opponent. ]]
Paddle = Class{}
--[[ The `init` function on our class is called just once, when the object is first created. Used to set up all variables in the class and get it ready for use. Our Paddle should take an X and a Y, for positioning, as well as a width and height for its dimensions. Note that `self` is a reference to *this* object, whichever object is instantiated at the time this function is called. Different objects can have their own x, y, width, and height values, thus serving as containers for data. In this sense, they're very similar to structs in C. ]] functionPaddle:init(x, y, width, height) self.x = x self.y = y self.width = width self.height = height self.dy = 0 end
functionPaddle:update(dt) -- math.max here ensures that we're the greater of 0 or the player's -- current calculated Y position when pressing up so that we don't -- go into the negatives; the movement calculation is simply our -- previously-defined paddle speed scaled by dt ifself.dy < 0then self.y = math.max(0, self.y + self.dy * dt) -- similar to before, this time we use math.min to ensure we don't -- go any farther than the bottom of the screen minus the paddle's -- height (or else it will go partially below, since position is -- based on its top left corner) else self.y = math.min(VIRTUAL_HEIGHT - self.height, self.y + self.dy * dt) end end
--[[ To be called by our main function in `love.draw`, ideally. Uses LÖVE2D's `rectangle` function, which takes in a draw mode as the first argument as well as the position and dimensions for the rectangle. To change the color, one must call `love.graphics.setColor`. As of the newest version of LÖVE2D, you can even draw rounded rectangles! ]] functionPaddle:render() love.graphics.rectangle('fill', self.x, self.y, self.width, self.height) end
--[[ Copyright (c) 2010-2013 Matthias Richter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]]--
localfunctioninclude_helper(to, from, seen) if from == nilthen return to elseiftype(from) ~= 'table'then return from elseif seen[from] then return seen[from] end
seen[from] = to for k,v inpairs(from) do k = include_helper({}, k, seen) -- keys might also be tables if to[k] == nilthen to[k] = include_helper({}, v, seen) end end return to end
-- deeply copies 'other' into 'class'. keys in 'other' that are already -- defined in 'class' are omitted localfunctioninclude(class, other) return include_helper(class, other, {}) end
-- returns a deep copy of 'other' localfunctionclone(other) returnsetmetatable(include({}, other), getmetatable(other)) end
localfunctionnew(class) -- mixins class = class or {} -- class can be nil local inc = class.__includes or {} ifgetmetatable(inc) then inc = {inc} end
for _, other inipairs(inc) do iftype(other) == "string"then other = _G[other] end include(class, other) end
-- class implementation class.__index = class class.init = class.init or class[1] orfunction()end class.include = class.include or include class.clone = class.clone or clone
-- constructor call returnsetmetatable(class, {__call = function(c, ...) local o = setmetatable({}, c) o:init(...) return o end}) end
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons). if class_commons ~= falseandnot common then common = {} functioncommon.class(name, prototype, parent) return new{__includes = {prototype, parent}} end functioncommon.instance(class, ...) return class(...) end end
-- the module returnsetmetatable({new = new, include = include, clone = clone}, {__call = function(_,...)return new(...) end})
5
The FPS(frame per second) Update.
function
love.window.setTitle(title) - 改个标题
Simply sets the title of our application window, adding a slight level of polish.
love.timer.getFPS()
Returns the current FPS of our application, making it easy to monitor when printed.
-- render paddles, now using their class's render method player1:render() player2:render()
-- render ball using its class's render method ball:render()
-- new function just to demonstrate how to see FPS in LÖVE2D displayFPS()
-- end rendering at virtual resolution push:apply('end') end
--[[ Renders the current FPS. ]] functiondisplayFPS() -- simple FPS display across all states love.graphics.setFont(smallFont) love.graphics.setColor(0, 255/255, 0, 255/255) love.graphics.print('FPS: ' .. tostring(love.timer.getFPS()), 10, 10) -- 这里的点点是合并string end
7
终于要做撞击设定了.
function
AABB Collision Detection
Relies on all colliding entities to have “alix-aligned bounding boxes”, which simply means their collision boxes contain no rotation in our world space, which allows us to use a simple math formula to test for collision:
1 2 3 4 5 6 7
if rect1.x is not > rect2.x + rect2.width and rect1.x + rect1.width is not < rect2.x and rect1.y is not > rect2.y + rect2.height and rect1.y + rect1.height is not < rect2.y: collision is true else collision is false
functionlove.update(dt) if gameState == 'play'then -- detect ball collision with paddles, reversing dx if true and -- slightly increasing it, then altering the dy based on the position of collision if ball:collides(player1) then ball.dx = -ball.dx * 1.03-- this 1.03 is for speed up the game ball.x = player1.x + 5-- this is for not overlap paddle collision box and the ball
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end end if ball:collides(player2) then ball.dx = -ball.dx * 1.03 ball.x = player2.x - 4-- in this case 4 is for width of the ball
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end end
-- detect upper and lower screen boundary collision and reverse if collided if ball.y <= 0then ball.y = 0 ball.dy = -ball.dy end
-- -4 to account for the ball's size if ball.y >= VIRTUAL_HEIGHT - 4then ball.y = VIRTUAL_HEIGHT - 4 ball.dy = -ball.dy end end
-- player 1 movement if love.keyboard.isDown('w') then player1.dy = -PADDLE_SPEED elseif love.keyboard.isDown('s') then player1.dy = PADDLE_SPEED else player1.dy = 0 end
-- player 2 movement if love.keyboard.isDown('up') then player2.dy = -PADDLE_SPEED elseif love.keyboard.isDown('down') then player2.dy = PADDLE_SPEED else player2.dy = 0 end
-- update our ball based on its DX and DY only if we're in play state; -- scale the velocity by dt so movement is framerate-independent if gameState == 'play'then ball:update(dt) end
player1:update(dt) player2:update(dt) end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
functionBall:collides(paddle) -- first, check to see if the left edge of either is farther to the right -- than the right edge of the other ifself.x > paddle.x + paddle.width or paddle.x > self.x + self.width then returnfalse end
-- then check to see if the bottom edge of either is higher than the top -- edge of the other ifself.y > paddle.y + paddle.height or paddle.y > self.y + self.height then returnfalse end
-- if the above aren't true, they're overlapping returntrue end
functionlove.update(dt) if gameState == 'play'then -- detect ball collision with paddles, reversing dx if true and -- slightly increasing it, then altering the dy based on the position of collision if ball:collides(player1) then ball.dx = -ball.dx * 1.03 ball.x = player1.x + 5
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end end if ball:collides(player2) then ball.dx = -ball.dx * 1.03 ball.x = player2.x - 4
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end end
-- detect upper and lower screen boundary collision and reverse if collided if ball.y <= 0then ball.y = 0 ball.dy = -ball.dy end
-- -4 to account for the ball's size if ball.y >= VIRTUAL_HEIGHT - 4then ball.y = VIRTUAL_HEIGHT - 4 ball.dy = -ball.dy end end
-- if we reach the left or right edge of the screen, -- go back to start and update the score if ball.x < 0then servingPlayer = 1 player2Score = player2Score + 1 ball:reset() gameState = 'start' end
if ball.x > VIRTUAL_WIDTH then servingPlayer = 2 player1Score = player1Score + 1 ball:reset() gameState = 'start' end
-- player 1 movement if love.keyboard.isDown('w') then player1.dy = -PADDLE_SPEED elseif love.keyboard.isDown('s') then player1.dy = PADDLE_SPEED else player1.dy = 0 end
-- player 2 movement if love.keyboard.isDown('up') then player2.dy = -PADDLE_SPEED elseif love.keyboard.isDown('down') then player2.dy = PADDLE_SPEED else player2.dy = 0 end
-- update our ball based on its DX and DY only if we're in play state; -- scale the velocity by dt so movement is framerate-independent if gameState == 'play'then ball:update(dt) end
functionlove.load() -- set love's default filter to "nearest-neighbor", which essentially -- means there will be no filtering of pixels (blurriness), which is -- important for a nice crisp, 2D look love.graphics.setDefaultFilter('nearest', 'nearest')
-- set the title of our application window love.window.setTitle('Pong')
-- "seed" the RNG so that calls to random are always random -- use the current time, since that will vary on startup every time math.randomseed(os.time())
-- more "retro-looking" font object we can use for any text smallFont = love.graphics.newFont('font.ttf', 8)
-- larger font for drawing the score on the screen scoreFont = love.graphics.newFont('font.ttf', 32)
-- set LÖVE2D's active font to the smallFont obect love.graphics.setFont(smallFont)
--[[ Runs every frame, with "dt" passed in, our delta in seconds since the last frame, which LÖVE2D supplies us. ]] functionlove.update(dt) if gameState == 'serve'then-- serve state -- before switching to play, initialize ball's velocity based -- on player who last scored ball.dy = math.random(-50, 50) if servingPlayer == 1then ball.dx = math.random(140, 200) else ball.dx = -math.random(140, 200) end elseif gameState == 'play'then -- detect ball collision with paddles, reversing dx if true and -- slightly increasing it, then altering the dy based on the position of collision if ball:collides(player1) then ball.dx = -ball.dx * 1.03 ball.x = player1.x + 5
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end end if ball:collides(player2) then ball.dx = -ball.dx * 1.03 ball.x = player2.x - 4
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end end
-- detect upper and lower screen boundary collision and reverse if collided if ball.y <= 0then ball.y = 0 ball.dy = -ball.dy end
-- -4 to account for the ball's size if ball.y >= VIRTUAL_HEIGHT - 4then ball.y = VIRTUAL_HEIGHT - 4 ball.dy = -ball.dy end end
-- if we reach the left or right edge of the screen, -- go back to start and update the score if ball.x < 0then servingPlayer = 1 player2Score = player2Score + 1 ball:reset() gameState = 'serve' end
if ball.x > VIRTUAL_WIDTH then servingPlayer = 2 player1Score = player1Score + 1 ball:reset() gameState = 'serve' end
-- player 1 movement if love.keyboard.isDown('w') then player1.dy = -PADDLE_SPEED elseif love.keyboard.isDown('s') then player1.dy = PADDLE_SPEED else player1.dy = 0 end
-- player 2 movement if love.keyboard.isDown('up') then player2.dy = -PADDLE_SPEED elseif love.keyboard.isDown('down') then player2.dy = PADDLE_SPEED else player2.dy = 0 end
-- update our ball based on its DX and DY only if we're in play state; -- scale the velocity by dt so movement is framerate-independent if gameState == 'play'then ball:update(dt) end
player1:update(dt) player2:update(dt) end
--[[ Keyboard handling, called by LÖVE2D each frame; passes in the key we pressed so we can access. ]] functionlove.keypressed(key)
if key == 'escape'then love.event.quit() -- if we press enter during either the start or serve phase, it should -- transition to the next appropriate state elseif key == 'enter'or key == 'return'then if gameState == 'start'then gameState = 'serve' elseif gameState == 'serve'then gameState = 'play' end end end
--[[ Called after update by LÖVE2D, used to draw anything to the screen, updated or otherwise. ]] functionlove.draw()
push:apply('start')
-- clear the screen with a specific color; in this case, a color similar -- to some versions of the original Pong love.graphics.clear(40/255, 45/255, 52/255, 255/255)
love.graphics.setFont(smallFont)
displayScore()
if gameState == 'start'then love.graphics.setFont(smallFont) love.graphics.printf('Welcome to Pong!', 0, 10, VIRTUAL_WIDTH, 'center') love.graphics.printf('Press Enter to begin!', 0, 20, VIRTUAL_WIDTH, 'center') elseif gameState == 'serve'then love.graphics.setFont(smallFont) love.graphics.printf('Player ' .. tostring(servingPlayer) .. "'s serve!", 0, 10, VIRTUAL_WIDTH, 'center') love.graphics.printf('Press Enter to serve!', 0, 20, VIRTUAL_WIDTH, 'center') elseif gameState == 'play'then -- no UI messages to display in play end
player1:render() player2:render() ball:render()
displayFPS()
push:apply('end') end
10
增加winning condition.
source code
当赢了的时候.
1 2 3 4 5 6 7
if player1Score == 10then winningPlayer = 1 gameState = 'done' else gameState = 'serve' ball:reset() end
if key == 'escape'then love.event.quit() -- if we press enter during either the start or serve phase, it should -- transition to the next appropriate state elseif key == 'enter'or key == 'return'then if gameState == 'start'then gameState = 'serve' elseif gameState == 'serve'then gameState = 'play' elseif gameState == 'done'then -- game is simply in a restart phase here, but will set the serving -- player to the opponent of whomever won for fairness! gameState = 'serve'
-- decide serving player as the opposite of who won if winningPlayer == 1then servingPlayer = 2 else servingPlayer = 1 end end end end
11
加音乐!
function
love.audio.newSource(path, [type])
Creates a LÖVE2D Audio object that we can play back at any point in our program. Can also be given a “type” of “stream” or “static”; streamed assets will be streamed from disk as needed, whereas static assets will be preserved in memory. For larger sound effects and music tracks, streaming is more memory-effective; in our examples, audio assets are static, since they’re so small that they won’t take up much memory at all.
software
Simple sound-generating program, freely available on major OSes.
Used to generate all sound effects for our Pong example and most examples going forward.
-- set up our sound effects; later, we can just index this table and -- call each entry's `play` method sounds = { ['paddle_hit'] = love.audio.newSource('sounds/paddle_hit.wav', 'static'), -- if static, it will load into and memory, if steam, they are loaded on the fly -- steam is helpful for saving memory(for example large music files) ['score'] = love.audio.newSource('sounds/score.wav', 'static'), ['wall_hit'] = love.audio.newSource('sounds/wall_hit.wav', 'static') }
需要的时候.
1
sounds['paddle_hit']:play()
12
The Resize Update.
function
love.resize(width, height)
Called by LÖVE every time we resize the application; logic should go in here if anything in the game (like a UI) is sized dynamically based on the window size. push:resize() needs to be called here for our use case so that it can dynamically rescale its internal canvas to fit our new window dimensions.
--[[ GD50 2018 Pong Remake -- Main Program -- Author: Colton Ogden cogden@cs50.harvard.edu Originally programmed by Atari in 1972. Features two paddles, controlled by players, with the goal of getting the ball past your opponent's edge. First to 10 points wins. This version is built to more closely resemble the NES than the original Pong machines or the Atari 2600 in terms of resolution, though in widescreen (16:9) so it looks nicer on modern systems. ]]
-- push is a library that will allow us to draw our game at a virtual -- resolution, instead of however large our window is; used to provide -- a more retro aesthetic -- -- https://github.com/Ulydev/push push = require'push'
-- the "Class" library we're using will allow us to represent anything in -- our game as code, rather than keeping track of many disparate variables and -- methods -- -- https://github.com/vrld/hump/blob/master/class.lua Class = require'class'
-- our Paddle class, which stores position and dimensions for each Paddle -- and the logic for rendering them require'Paddle'
-- our Ball class, which isn't much different than a Paddle structure-wise -- but which will mechanically function very differently require'Ball'
-- size of our actual window WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720
-- size we're trying to emulate with push VIRTUAL_WIDTH = 432 VIRTUAL_HEIGHT = 243
-- paddle movement speed PADDLE_SPEED = 200
--[[ Called just once at the beginning of the game; used to set up game objects, variables, etc. and prepare the game world. ]] functionlove.load() -- set love's default filter to "nearest-neighbor", which essentially -- means there will be no filtering of pixels (blurriness), which is -- important for a nice crisp, 2D look love.graphics.setDefaultFilter('nearest', 'nearest')
-- set the title of our application window love.window.setTitle('Pong')
-- seed the RNG so that calls to random are always random math.randomseed(os.time())
-- set up our sound effects; later, we can just index this table and -- call each entry's `play` method sounds = { ['paddle_hit'] = love.audio.newSource('sounds/paddle_hit.wav', 'static'), ['score'] = love.audio.newSource('sounds/score.wav', 'static'), ['wall_hit'] = love.audio.newSource('sounds/wall_hit.wav', 'static') }
-- initialize our virtual resolution, which will be rendered within our -- actual window no matter its dimensions push:setupScreen(VIRTUAL_WIDTH, VIRTUAL_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT, { fullscreen = false, resizable = true, vsync = true, canvas = false })
-- initialize our player paddles; make them global so that they can be -- detected by other functions and modules player1 = Paddle(10, 30, 5, 20) player2 = Paddle(VIRTUAL_WIDTH - 10, VIRTUAL_HEIGHT - 30, 5, 20)
-- place a ball in the middle of the screen ball = Ball(VIRTUAL_WIDTH / 2 - 2, VIRTUAL_HEIGHT / 2 - 2, 4, 4)
-- either going to be 1 or 2; whomever is scored on gets to serve the -- following turn servingPlayer = 1
-- player who won the game; not set to a proper value until we reach -- that state in the game winningPlayer = 0
-- the state of our game; can be any of the following: -- 1. 'start' (the beginning of the game, before first serve) -- 2. 'serve' (waiting on a key press to serve the ball) -- 3. 'play' (the ball is in play, bouncing between paddles) -- 4. 'done' (the game is over, with a victor, ready for restart) gameState = 'start' end
--[[ Called whenever we change the dimensions of our window, as by dragging out its bottom corner, for example. In this case, we only need to worry about calling out to `push` to handle the resizing. Takes in a `w` and `h` variable representing width and height, respectively. ]] functionlove.resize(w, h) push:resize(w, h) end
--[[ Called every frame, passing in `dt` since the last frame. `dt` is short for `deltaTime` and is measured in seconds. Multiplying this by any changes we wish to make in our game will allow our game to perform consistently across all hardware; otherwise, any changes we make will be applied as fast as possible and will vary across system hardware. ]] functionlove.update(dt) if gameState == 'serve'then -- before switching to play, initialize ball's velocity based -- on player who last scored ball.dy = math.random(-50, 50) if servingPlayer == 1then ball.dx = math.random(140, 200) else ball.dx = -math.random(140, 200) end elseif gameState == 'play'then -- detect ball collision with paddles, reversing dx if true and -- slightly increasing it, then altering the dy based on the position -- at which it collided, then playing a sound effect if ball:collides(player1) then ball.dx = -ball.dx * 1.03 ball.x = player1.x + 5
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end
sounds['paddle_hit']:play() end if ball:collides(player2) then ball.dx = -ball.dx * 1.03 ball.x = player2.x - 4
-- keep velocity going in the same direction, but randomize it if ball.dy < 0then ball.dy = -math.random(10, 150) else ball.dy = math.random(10, 150) end
sounds['paddle_hit']:play() end
-- detect upper and lower screen boundary collision, playing a sound -- effect and reversing dy if true if ball.y <= 0then ball.y = 0 ball.dy = -ball.dy sounds['wall_hit']:play() end
-- -4 to account for the ball's size if ball.y >= VIRTUAL_HEIGHT - 4then ball.y = VIRTUAL_HEIGHT - 4 ball.dy = -ball.dy sounds['wall_hit']:play() end
-- if we reach the left or right edge of the screen, go back to serve -- and update the score and serving player if ball.x < 0then servingPlayer = 1 player2Score = player2Score + 1 sounds['score']:play()
-- if we've reached a score of 10, the game is over; set the -- state to done so we can show the victory message if player2Score == 10then winningPlayer = 2 gameState = 'done' else gameState = 'serve' -- places the ball in the middle of the screen, no velocity ball:reset() end end
if ball.x > VIRTUAL_WIDTH then servingPlayer = 2 player1Score = player1Score + 1 sounds['score']:play()
if player1Score == 10then winningPlayer = 1 gameState = 'done' else gameState = 'serve' ball:reset() end end end
-- -- paddles can move no matter what state we're in -- -- player 1 if love.keyboard.isDown('w') then player1.dy = -PADDLE_SPEED elseif love.keyboard.isDown('s') then player1.dy = PADDLE_SPEED else player1.dy = 0 end
-- player 2 if love.keyboard.isDown('up') then player2.dy = -PADDLE_SPEED elseif love.keyboard.isDown('down') then player2.dy = PADDLE_SPEED else player2.dy = 0 end
-- update our ball based on its DX and DY only if we're in play state; -- scale the velocity by dt so movement is framerate-independent if gameState == 'play'then ball:update(dt) end
player1:update(dt) player2:update(dt) end
--[[ A callback that processes key strokes as they happen, just the once. Does not account for keys that are held down, which is handled by a separate function (`love.keyboard.isDown`). Useful for when we want things to happen right away, just once, like when we want to quit. ]] functionlove.keypressed(key) -- `key` will be whatever key this callback detected as pressed if key == 'escape'then -- the function LÖVE2D uses to quit the application love.event.quit() -- if we press enter during either the start or serve phase, it should -- transition to the next appropriate state elseif key == 'enter'or key == 'return'then if gameState == 'start'then gameState = 'serve' elseif gameState == 'serve'then gameState = 'play' elseif gameState == 'done'then -- game is simply in a restart phase here, but will set the serving -- player to the opponent of whomever won for fairness! gameState = 'serve'
-- decide serving player as the opposite of who won if winningPlayer == 1then servingPlayer = 2 else servingPlayer = 1 end end end end
--[[ Called each frame after update; is responsible simply for drawing all of our game objects and more to the screen. ]] functionlove.draw() -- begin drawing with push, in our virtual resolution push:start()
-- render different things depending on which part of the game we're in if gameState == 'start'then -- UI messages love.graphics.setFont(smallFont) love.graphics.printf('Welcome to Pong!', 0, 10, VIRTUAL_WIDTH, 'center') love.graphics.printf('Press Enter to begin!', 0, 20, VIRTUAL_WIDTH, 'center') elseif gameState == 'serve'then -- UI messages love.graphics.setFont(smallFont) love.graphics.printf('Player ' .. tostring(servingPlayer) .. "'s serve!", 0, 10, VIRTUAL_WIDTH, 'center') love.graphics.printf('Press Enter to serve!', 0, 20, VIRTUAL_WIDTH, 'center') elseif gameState == 'play'then -- no UI messages to display in play elseif gameState == 'done'then -- UI messages love.graphics.setFont(largeFont) love.graphics.printf('Player ' .. tostring(winningPlayer) .. ' wins!', 0, 10, VIRTUAL_WIDTH, 'center') love.graphics.setFont(smallFont) love.graphics.printf('Press Enter to restart!', 0, 30, VIRTUAL_WIDTH, 'center') end
-- show the score before ball is rendered so it can move over the text displayScore()
player1:render() player2:render() ball:render()
-- display FPS for debugging; simply comment out to remove displayFPS()
-- end our drawing to push push:finish() end
--[[ Simple function for rendering the scores. ]] functiondisplayScore() -- score display love.graphics.setFont(scoreFont) love.graphics.print(tostring(player1Score), VIRTUAL_WIDTH / 2 - 50, VIRTUAL_HEIGHT / 3) love.graphics.print(tostring(player2Score), VIRTUAL_WIDTH / 2 + 30, VIRTUAL_HEIGHT / 3) end
--[[ Renders the current FPS. ]] functiondisplayFPS() -- simple FPS display across all states love.graphics.setFont(smallFont) love.graphics.setColor(0, 255/255, 0, 255/255) love.graphics.print('FPS: ' .. tostring(love.timer.getFPS()), 10, 10) love.graphics.setColor(255, 255, 255, 255) end
Implement an AI-controlled paddle (either the left or the right will do) such that it will try to deflect the ball at all times. Since the paddle can move on only one axis (the Y axis), you will need to determine how to keep the paddle moving in relation to the ball. Currently, each paddle has its own chunk of code where input is detected by the keyboard; this feels like an excellent place to put the code we need! Once either the left or right paddle (or both, if desired) try to deflect the paddle on their own, you’ve done it!
At the time of the course’s lectures being filmed, we were using a now-outdated version of LÖVE (0.10.2); most of everything has remained the same into the newer version 11 series of the framework, but one core change is worth noting: love.graphics.clear and functions like it which take four arguments representing the red, green, blue, and alpha components of a color (or RGBA) formerly took integer values between 0 and 255:
love.graphics.clear(255, 0, 0, 255) However, the API has now changed to where all functions that formerly took integer values for color components now take a floating-point value between 0 and 1; for example, the above API call would translate to the following:
love.graphics.clear(1, 0, 0, 1) An easy way to normalize the old style to the new style is simply to divide components by 255. If, for example, we had a color we liked with the components 120, 30, 70, and 255, we could write it like the below: