diff --git a/cfg/binds.lst b/cfg/binds.lst index 7941ba33..117b9b13 100644 --- a/cfg/binds.lst +++ b/cfg/binds.lst @@ -19,6 +19,7 @@ Communication ============= "Chat" chat +"Voice" +voice Development =========== diff --git a/cfg/binds_default.cfg b/cfg/binds_default.cfg index 93bac599..4394c979 100644 --- a/cfg/binds_default.cfg +++ b/cfg/binds_default.cfg @@ -8,4 +8,5 @@ tab +gamemenu wu zoomin wd zoomout y chat +k +voice \ toggleconsole diff --git a/class.lua b/class.lua index e384af73..f1816018 100644 --- a/class.lua +++ b/class.lua @@ -15,6 +15,7 @@ local rawget = rawget local ipairs = ipairs local module = module local _G = _G +local _R = debug.getregistry() ------------------------------------------------------------------------------- -- new() @@ -30,20 +31,20 @@ end ------------------------------------------------------------------------------- -- getbaseclass() --- Purpose: Get a base class --- Input: class - Class metatable --- Output: class +-- Purpose: Gets a base class +-- Input: metatable +-- Output: metatable ------------------------------------------------------------------------------- -local function getbaseclass( class ) - local name = class.__base - return package.loaded[ name ] +local function getbaseclass( metatable ) + local base = metatable.__base + return package.loaded[ base ] end _G.getbaseclass = getbaseclass ------------------------------------------------------------------------------- -- eventnames --- Purpose: Provide a list of all inheritable internal event names +-- Purpose: Provides a list of all inheritable internal event names ------------------------------------------------------------------------------- local eventnames = { "__add", "__sub", "__mul", "__div", "__mod", @@ -54,15 +55,15 @@ local eventnames = { ------------------------------------------------------------------------------- -- metamethod() --- Purpose: Creates a filler metamethod for metamethod inheritance --- Input: class - Class metatable --- eventname - Event name +-- Purpose: Creates a placeholder metamethod for metamethod inheritance +-- Input: metatable +-- eventname -- Output: function ------------------------------------------------------------------------------- -local function metamethod( class, eventname ) +local function metamethod( metatable, eventname ) return function( ... ) local event = nil - local base = getbaseclass( class ) + local base = getbaseclass( metatable ) while ( base ~= nil ) do if ( base[ eventname ] ) then event = base[ eventname ] @@ -86,7 +87,7 @@ end ------------------------------------------------------------------------------- -- setproxy() --- Purpose: Set a proxy for __gc +-- Purpose: Sets a proxy for __gc -- Input: object ------------------------------------------------------------------------------- local function setproxy( object ) @@ -102,13 +103,16 @@ end _G.setproxy = setproxy ------------------------------------------------------------------------------- --- package.class --- Purpose: Turns a module into a class --- Input: module - Module table +-- classinit +-- Purpose: Initializes a class +-- Input: module ------------------------------------------------------------------------------- -function package.class( module ) +local function classinit( module ) module.__index = module module.__type = string.gsub( module._NAME, module._PACKAGE, "" ) + module._M = nil + module._NAME = nil + module._PACKAGE = nil -- Create a shortcut to name() setmetatable( module, { __call = function( self, ... ) @@ -132,11 +136,11 @@ function package.class( module ) end ------------------------------------------------------------------------------- --- package.inherit +-- inherit -- Purpose: Sets a base class --- Input: base - Class name +-- Input: base - Name of metatable ------------------------------------------------------------------------------- -function package.inherit( base ) +local function inherit( base ) return function( module ) -- Set our base class module.__base = base @@ -169,15 +173,16 @@ end ------------------------------------------------------------------------------- -- class() -- Purpose: Creates a class --- Input: name - Name of class +-- Input: modname ------------------------------------------------------------------------------- -function class( name ) - local function setmodule( name ) - module( name, package.class ) - end setmodule( name ) +function class( modname ) + local function setmodule( modname ) + module( modname, classinit ) + end setmodule( modname ) + _R[ modname ] = package.loaded[ modname ] -- For syntactic sugar, return a function to set inheritance return function( base ) - local _M = package.loaded[ name ] - package.inherit( base )( _M ) + local metatable = package.loaded[ modname ] + inherit( base )( metatable ) end end diff --git a/common/vector.lua b/common/vector.lua index 3e831117..944abf5d 100644 --- a/common/vector.lua +++ b/common/vector.lua @@ -15,6 +15,18 @@ function vector:vector( x, y ) self.y = y or 0 end +function vector:approximately( b ) + return math.approximately( self.x, b.x ) and + math.approximately( self.y, b.y ) +end + +function vector:dot( b ) + local ret = 0 + ret = ret + self.x * b.x + ret = ret + self.y * b.y + return ret +end + function vector:length() return math.sqrt( self:lengthSqr() ) end @@ -25,6 +37,10 @@ end function vector:normalize() local length = self:length() + if ( length == 0 ) then + return vector() + end + return vector( self.x / length, self.y / length ) end @@ -56,6 +72,14 @@ function vector.__mul( a, b ) end end +function vector.__div( a, b ) + if ( type( b ) == "number" ) then + return vector( a.x / b, a.y / b ) + else + return vector( a.x / b.x, a.y / b.y ) + end +end + function vector.__eq( a, b ) return a.x == b.x and a.y == b.y end diff --git a/conf.lua b/conf.lua index b00f159f..a8744c71 100644 --- a/conf.lua +++ b/conf.lua @@ -1,9 +1,12 @@ ---=========== Copyright © 2018, Planimeter, All rights reserved. ===========-- +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- -- -- Purpose: -- --==========================================================================-- +require( "engine.shared.profile" ) +profile.push( "load" ) + argv = {} for _, v in ipairs( arg ) do argv[ v ] = true end @@ -22,7 +25,7 @@ end function love.conf( c ) c.title = "Grid Engine" - c.version = "11.1" + c.version = "11.3" if ( _DEDICATED ) then c.modules.keyboard = false c.modules.mouse = false diff --git a/engine/client/camera.lua b/engine/client/camera.lua index 44f67899..3f1f124e 100644 --- a/engine/client/camera.lua +++ b/engine/client/camera.lua @@ -181,12 +181,12 @@ end concommand( "zoomin", "Zooms the camera in", function() local scale = getZoom() - setZoom( scale * 2 ) + setZoom( scale + 1 ) end ) concommand( "zoomout", "Zooms the camera out", function() local scale = getZoom() - setZoom( scale / 2 ) + setZoom( scale - 1 ) end ) function update( dt ) diff --git a/engine/client/canvas.lua b/engine/client/canvas.lua index e981af0d..a97f8ce4 100644 --- a/engine/client/canvas.lua +++ b/engine/client/canvas.lua @@ -31,7 +31,7 @@ end function canvas.invalidateCanvases() for _, v in ipairs( canvas._canvases ) do if ( typeof( v, "fullscreencanvas" ) ) then - newCanvas( v ) + newCanvas( v, unpack( v._args ) ) end if ( v:shouldAutoRedraw() ) then @@ -44,7 +44,7 @@ local function noop() end function canvas:canvas( ... ) - local args = { ... } + self._args = { ... } self._drawFunc = noop self.needsRedraw = false self.autoRedraw = true @@ -78,6 +78,17 @@ function canvas:invalidate() self.needsRedraw = true end +function canvas:remove() + for i, v in ipairs( canvas._canvases ) do + if ( v == self ) then + table.remove( canvas._canvases, i ) + end + end + + collectgarbage() + collectgarbage() +end + function canvas:renderTo( func ) self._drawFunc = func render( self ) @@ -93,16 +104,12 @@ function canvas:__tostring() return s end -function canvas:__gc() - for i, v in ipairs( canvas._canvases ) do - if ( v == self ) then - table.remove( canvas._canvases, i ) - end - end -end +canvas.__gc = canvas.remove class "fullscreencanvas" ( "canvas" ) -function fullscreencanvas:fullscreencanvas() - canvas.canvas( self ) +function fullscreencanvas:fullscreencanvas( ... ) + canvas.canvas( self, ... ) end + +fullscreencanvas.__gc = canvas.remove diff --git a/engine/client/gui/autoloader.lua b/engine/client/gui/autoloader.lua index d298d8ca..7e583010 100644 --- a/engine/client/gui/autoloader.lua +++ b/engine/client/gui/autoloader.lua @@ -16,17 +16,23 @@ local _G = _G module( "gui" ) local metatable = {} +local panelDirectories = { + "game.client", + "engine.client" +} function metatable.__index( t, k ) + -- Ignore private members. local privateMember = string.sub( k, 1, 1 ) == "_" if ( privateMember ) then return end - for _, module in ipairs( { - "game.client", - "engine.client" - } ) do + -- Look in `/game/client/gui` and `/engine/client/gui` for + -- panels not yet required and require them. + -- + -- Otherwise, return a standard Lua error. + for _, module in ipairs( panelDirectories ) do local library = module .. ".gui." .. k local status, err = pcall( require, library ) if ( status == true ) then @@ -40,6 +46,7 @@ function metatable.__index( t, k ) end end + -- Return pass-through. local v = rawget( t, k ) if ( v ~= nil ) then return v diff --git a/engine/client/gui/box/init.lua b/engine/client/gui/box/init.lua index 6b7e118c..d01fa896 100644 --- a/engine/client/gui/box/init.lua +++ b/engine/client/gui/box/init.lua @@ -5,16 +5,68 @@ -- --==========================================================================-- +require( "engine.client.gui.panel" ) + class "gui.box" ( "gui.panel" ) require( "engine.client.gui.box.properties" ) +local function applyProps( panel, props ) + table.foreach( props, function( k, v ) + -- Apply styles + if ( k == "styles" ) then + applyProps( panel, v ) + return + end + + -- Modify key for mutator + local callback = string.find( k, "^on%u" ) + if ( not callback ) then + k = "set" .. string.capitalize( k ) + end + + -- Set nil + if ( v == "nil" ) then + v = nil + end + + -- Apply callback or call mutator + if ( callback ) then + panel[ k ] = v + else + panel[ k ]( panel, v ) + end + end ) +end + +function gui.createElement( type, props, children ) + local panel = gui[ type ]() + + if ( props ) then + applyProps( panel, props ) + end + + if ( children ) then + for _, v in ipairs( children ) do + v:setParent( panel ) + end + end + + return panel +end + local box = gui.box function box:box( parent, name ) gui.panel.panel( self, parent, name ) + self:setScheme( "Default" ) + self.props = { + style = {}, + children = {} + } + -- 8.5 Border properties -- https://www.w3.org/TR/CSS21/box.html#border-properties -- 8.5.1 Border width: 'border-top-width', 'border-right-width', @@ -37,6 +89,16 @@ function box:box( parent, name ) -- self.borderRightStyle = "none" -- self.borderBottomStyle = "none" -- self.borderLeftStyle = "none" + + if ( props ) then + for k, v in pairs( props ) do + self.props[ k ] = v + end + end + + if ( children ) then + self.props.children = children + end end function box:preDraw() @@ -50,7 +112,7 @@ function box:preDraw() end local scale = self:getScale() - local width, height = self:getSize() + local width, height = self:getDimensions() love.graphics.push() if ( self:getPosition() == "absolute" ) then @@ -78,7 +140,7 @@ function box:draw() end for _, v in ipairs( children ) do - v:createFramebuffer() + v:createCanvas() end local formattingContext = self:getFormattingContext() @@ -88,45 +150,86 @@ function box:draw() local y = self:getBorderTopWidth() + self:getPaddingTop() - for _, v in ipairs( children ) do - x, y = self:drawChild( _, v, formattingContext, x, y ) + for i, v in ipairs( children ) do + x, y = self:drawChild( i, v, formattingContext, x, y ) end end -function box:drawChild( _, v, formattingContext, x, y ) +function box:drawChild( i, v, formattingContext, x, y ) love.graphics.push() + -- 9.4 Normal flow local position = v:getPosition() if ( position == "static" ) then + -- 9.4.1 Block formatting contexts if ( formattingContext == "block" ) then - y = y + v:getMarginTop() + if ( i == 1 ) then + y = y + v:getMarginTop() + end end + -- 9.4.2 Inline formatting contexts if ( formattingContext == "inline" ) then x = x + v:getMarginLeft() end end - if ( position == "static" or position == "relative" ) then - if ( position == "relative" ) then - -- TODO: Calculate relative position. - end + local rx = 0 + local ry = 0 - love.graphics.translate( x, y ) + -- 9.4.3 Relative positioning + if ( position == "relative" ) then + rx = self:getLeft() + ry = self:getTop() end - v:setOffsetLeft( x ) - v:setOffsetTop( y ) + local ax = self:getX() + local ay = self:getY() + + -- 9.6 Absolute positioning + if ( position == "absolute" ) then + v:setOffsetLeft( nil ) + v:setOffsetTop( nil ) + + -- love.graphics.translate( ax, ay ) + else + local dx = x + rx + local dy = y + ry + + if ( formattingContext == "block" ) then + dx = dx + v:getMarginLeft() + end + + if ( formattingContext == "inline" ) then + dy = dy + v:getMarginTop() + end + + v:setOffsetLeft( dx ) + v:setOffsetTop( dy ) + + love.graphics.translate( dx, dy ) + end v:preDraw() - v:drawFramebuffer() + v:drawCanvas() v:postDraw() if ( position == "static" ) then if ( formattingContext == "block" ) then y = y + v:getHeight() - y = y + v:getMarginBottom() + + -- Vertical margins between adjacent + -- block-level boxes in a block formatting + -- context collapse. + local marginBottom = v:getMarginBottom() + local sibling = v:getNextSibling() + if ( sibling ) then + marginBottom = math.max( + marginBottom, sibling:getMarginTop() + ) + end + y = y + marginBottom end if ( formattingContext == "inline" ) then @@ -175,7 +278,7 @@ function box:drawSelection() -- Content love.graphics.setColor( color.content ) local t, r, b, l = self:getPadding() - local width, height = self:getSize() + local width, height = self:getDimensions() love.graphics.rectangle( "fill", 0, 0, width, height ) -- love.graphics.rectangle( "fill", l, t, width - l - r, height - t - b ) end @@ -213,6 +316,12 @@ function box:getFormattingContext() end for _, v in ipairs( children ) do + if ( v.getDisplay == nil ) then + error( "attempt to return formatting context for panel '" + .. tostring( v ) .. + "'" ) + end + if ( v:getDisplay() == "block" ) then return "block" end @@ -224,17 +333,35 @@ end function box:getOffsetWidth() local children = self:getChildren() + local formattingContext = self:getFormattingContext() + local w = self:getBorderLeftWidth() + self:getPaddingLeft() local maxWidth = 0 if ( children ) then - for _, v in ipairs( children ) do - local w = 0 - w = w + v:getMarginLeft() - w = w + v:getWidth() - w = w + v:getMarginRight() - maxWidth = math.max( maxWidth, w ) + for i, v in ipairs( children ) do + if ( formattingContext == "block" ) then + local w = 0 + w = w + v:getMarginLeft() + w = w + v:getWidth() + w = w + v:getMarginRight() + maxWidth = math.max( maxWidth, w ) + end + + if ( formattingContext == "inline" ) then + w = w + v:getMarginLeft() + w = w + v:getWidth() + + local marginRight = v:getMarginRight() + local sibling = children[ i + 1 ] + if ( sibling ) then + marginRight = math.max( + marginRight, sibling:getMarginLeft() + ) + end + w = w + marginRight + end end end w = w + maxWidth @@ -255,11 +382,19 @@ function box:getOffsetHeight() local maxHeight = 0 if ( children ) then - for _, v in ipairs( children ) do + for i, v in ipairs( children ) do if ( formattingContext == "block" ) then h = h + v:getMarginTop() h = h + v:getHeight() - h = h + v:getMarginBottom() + + local marginBottom = v:getMarginBottom() + local sibling = children[ i + 1 ] + if ( sibling ) then + marginBottom = math.max( + marginBottom, sibling:getMarginTop() + ) + end + h = h + marginBottom end if ( formattingContext == "inline" ) then diff --git a/engine/client/gui/button.lua b/engine/client/gui/button.lua index 59afce20..b29c7ad4 100644 --- a/engine/client/gui/button.lua +++ b/engine/client/gui/button.lua @@ -17,7 +17,7 @@ function button:button( parent, name, text ) self:setBorderWidth( 1 ) self.width = 216 self.height = 46 - self.text = text or "Button" + self.text = gui.text( self, text ) self.disabled = false self:setScheme( "Default" ) @@ -39,9 +39,10 @@ function button:drawBorder() return end - if ( self.mousedown and ( self.mouseover or self:isChildMousedOver() ) ) then + local mouseover = ( self.mouseover or self:isChildMousedOver() ) + if ( self.mousedown and mouseover ) then color = self:getScheme( "button.mousedown.borderColor" ) - elseif ( self.mousedown or ( self.mouseover or self:isChildMousedOver() ) or self.focus ) then + elseif ( self.mousedown or mouseover or self.focus ) then color = self:getScheme( "button.mouseover.borderColor" ) end @@ -55,14 +56,7 @@ function button:drawText() color = self:getScheme( "button.disabled.textColor" ) end - love.graphics.setColor( color ) - - local font = self:getScheme( "font" ) - love.graphics.setFont( font ) - local text = self:getText() - local x = math.round( self:getWidth() / 2 - font:getWidth( text ) / 2 ) - local y = math.round( self:getHeight() / 2 - font:getHeight() / 2 ) - love.graphics.print( text, x, y ) + self.text:setColor( color ) end gui.accessor( button, "text" ) @@ -81,14 +75,16 @@ function button:keypressed( key, scancode, isrepeat ) end function button:mousepressed( x, y, button, istouch ) - if ( ( self.mouseover or self:isChildMousedOver() ) and button == 1 ) then + local mouseover = ( self.mouseover or self:isChildMousedOver() ) + if ( mouseover and button == 1 ) then self.mousedown = true self:invalidate() end end function button:mousereleased( x, y, button, istouch ) - if ( ( self.mousedown and ( self.mouseover or self:isChildMousedOver() ) ) and not self:isDisabled() ) then + local mouseover = ( self.mouseover or self:isChildMousedOver() ) + if ( ( self.mousedown and mouseover ) and not self:isDisabled() ) then self:onClick() end @@ -102,18 +98,10 @@ function button:onClick() end function button:setText( text ) - if ( type( self.text ) ~= "string" ) then - self.text:setText( text ) - else - self.text = text - end + self.text:set( text ) self:invalidate() end function button:getText() - if ( type( self.text ) ~= "string" ) then - return self.text:getText() - else - return self.text - end + return self.text:get() end diff --git a/engine/client/gui/checkbox.lua b/engine/client/gui/checkbox.lua index be5044b8..26457299 100644 --- a/engine/client/gui/checkbox.lua +++ b/engine/client/gui/checkbox.lua @@ -10,9 +10,14 @@ local checkbox = gui.checkbox function checkbox:checkbox( parent, name, text ) gui.button.button( self, parent, name, text ) - self.height = 24 + self.width = nil + self.height = nil + self:setPadding( 4, 0, 3 ) + self:setBorderWidth( 0 ) + self.text:setDisplay( "inline-block" ) + self.text:set( text ) + self.text:setMarginLeft( 34 ) self.icon = self:getScheme( "checkbox.icon" ) - self.text = text or "Checkbox Label" self.checked = false end @@ -21,7 +26,7 @@ function checkbox:draw() self:drawBorder() self:drawLabel() - gui.panel.draw( self ) + gui.box.draw( self ) end function checkbox:drawCheck() @@ -38,18 +43,19 @@ function checkbox:drawCheck() love.graphics.setColor( color ) local height = self:getHeight() - local x = height / 2 - self.icon:getWidth() / 2 - local y = height / 2 - self.icon:getHeight() / 2 + local x = math.round( height / 2 - self.icon:getWidth() / 2 ) + local y = math.round( height / 2 - self.icon:getHeight() / 2 ) love.graphics.draw( self.icon, x, y ) end function checkbox:drawBorder() + local mouseover = ( self.mouseover or self:isChildMousedOver() ) local color = self:getScheme( "checkbox.borderColor" ) if ( not self:isDisabled() ) then - if ( self.mousedown and self.mouseover ) then + if ( self.mousedown and mouseover ) then color = self:getScheme( "checkbox.mousedown.borderColor" ) - elseif ( self.mousedown or self.mouseover or self.focus ) then + elseif ( self.mousedown or mouseover or self.focus ) then color = self:getScheme( "checkbox.mouseover.borderColor" ) end end @@ -74,15 +80,7 @@ function checkbox:drawLabel() color = self:getScheme( "checkbox.disabled.textColor" ) end - love.graphics.setColor( color ) - - local font = self:getScheme( "font" ) - love.graphics.setFont( font ) - local height = self:getHeight() - local marginLeft = 9 - local x = math.round( height + marginLeft ) - local y = math.round( height / 2 - font:getHeight() / 2 ) - love.graphics.print( self:getText(), x, y ) + self.text:setColor( color ) end accessor( checkbox, "checked", "is" ) @@ -95,15 +93,16 @@ function checkbox:keypressed( key, scancode, isrepeat ) if ( key == "return" or key == "kpenter" or key == "space" ) then - self:onClick() self:setChecked( not self:isChecked() ) + self:onClick() end end function checkbox:mousereleased( x, y, button, istouch ) - if ( ( self.mousedown and self.mouseover ) and not self:isDisabled() ) then - self:onClick() + local mouseover = ( self.mouseover or self:isChildMousedOver() ) + if ( ( self.mousedown and mouseover ) and not self:isDisabled() ) then self:setChecked( not self:isChecked() ) + self:onClick() end if ( self.mousedown ) then diff --git a/engine/client/gui/closebutton.lua b/engine/client/gui/closebutton.lua index f7378e65..bc237107 100644 --- a/engine/client/gui/closebutton.lua +++ b/engine/client/gui/closebutton.lua @@ -12,9 +12,10 @@ closebutton.canFocus = false function closebutton:closebutton( parent, name ) gui.button.button( self, parent, name ) - local margin = 36 - self.width = 2 * margin + 8 - 1 - self.height = 2 * margin + 16 - 2 + local padding = 36 + self:setPadding( padding ) + self.width = 2 * padding + 8 - 1 + self.height = 2 * padding + 16 - 2 self.icon = self:getScheme( "icon" ) end diff --git a/engine/client/gui/commandbutton.lua b/engine/client/gui/commandbutton.lua index a436ab33..2c0d4331 100644 --- a/engine/client/gui/commandbutton.lua +++ b/engine/client/gui/commandbutton.lua @@ -10,13 +10,14 @@ local commandbutton = gui.commandbutton function commandbutton:commandbutton( parent, name, text ) gui.button.button( self, parent, name, text ) - self:setDisplay( "inline" ) + self.width = nil + self.height = nil + self:setDisplay( "inline-block" ) self:setPosition( "static" ) + self:setPadding( 15, 18 ) + self:setBorderWidth( 0 ) if ( text ) then - local font = self:getScheme( "font" ) - local padding = 18 - self:setWidth( font:getWidth( text ) + 2 * padding ) parent:invalidateLayout() end end @@ -39,11 +40,12 @@ function commandbutton:drawBackground() width = isFirstChild and width - 2 or width - 1 height = height - 1 - if ( self.mousedown and self.mouseover ) then + local mouseover = ( self.mouseover or self:isChildMousedOver() ) + if ( self.mousedown and mouseover ) then color = self:getScheme( "button.mousedown.backgroundColor" ) love.graphics.setColor( color ) love.graphics.rectangle( "fill", x, 1, width, height ) - elseif ( self.mousedown or self.mouseover ) then + elseif ( self.mousedown or mouseover ) then color = self:getScheme( "button.mouseover.backgroundColor" ) love.graphics.setColor( color ) love.graphics.rectangle( "fill", x, 1, width, height ) diff --git a/engine/client/gui/console/init.lua b/engine/client/gui/console/init.lua index 4865efad..c5be0af9 100644 --- a/engine/client/gui/console/init.lua +++ b/engine/client/gui/console/init.lua @@ -121,6 +121,12 @@ local function autocomplete( text ) end suggestions = table.unique( suggestions ) + + local limit = 5 + while ( #suggestions > limit ) do + table.remove( suggestions, limit + 1 ) + end + return #suggestions > 0 and suggestions or nil end @@ -129,15 +135,13 @@ local keypressed = function( itemGroup, key, isrepeat ) return end - -- BUGBUG: We never get here anymore due to the introduction of - -- cascadeInputToChildren. local history = console._commandHistory for i, v in ipairs( history ) do if ( itemGroup:getValue() == v ) then table.remove( history, i ) local item = itemGroup:getSelectedItem() - itemGroup:removeItem( item ) - return + item:remove() + return true end end end @@ -147,10 +151,11 @@ function console:console( parent, name, title ) name = name or "Console" title = title or name gui.frame.frame( self, parent, name, title ) - self.width = 661 + self.width = 668 self.minHeight = 178 self.output = console.textbox( self, name .. " Output Text Box", "" ) + self.output:setMaxLength( 80 * 50 ) self.input = gui.textbox( self, name .. " Input Text Box", "" ) self.input.onEnter = function( textbox, text ) text = string.trim( text ) @@ -237,7 +242,7 @@ end ) concommand( "clear", "Clears the console", function() if ( love.system.getOS() == "Windows" ) then - -- This breaks the LOVE console. :( + -- This breaks the LOVE console. -- os.execute( "cls" ) else os.execute( "clear" ) diff --git a/engine/client/gui/console/textboxautocompleteitemgroup.lua b/engine/client/gui/console/textboxautocompleteitemgroup.lua index 77167cf4..b9dacb58 100644 --- a/engine/client/gui/console/textboxautocompleteitemgroup.lua +++ b/engine/client/gui/console/textboxautocompleteitemgroup.lua @@ -13,8 +13,6 @@ function textboxautocompleteitemgroup:textboxautocompleteitemgroup( parent, name end function textboxautocompleteitemgroup:invalidateLayout() - self:updatePos() - local itemWidth = 0 local font = self:getScheme( "font" ) local maxWidth = 0 @@ -37,7 +35,7 @@ function textboxautocompleteitemgroup:invalidateLayout() for _, listItem in ipairs( listItems ) do listItem:setWidth( maxWidth ) end - - self:setHeight( y + 1 ) end + + self:updatePos() end diff --git a/engine/client/gui/debugoverlaypanel.lua b/engine/client/gui/debugoverlaypanel.lua index 41449af8..2a8de40c 100644 --- a/engine/client/gui/debugoverlaypanel.lua +++ b/engine/client/gui/debugoverlaypanel.lua @@ -4,15 +4,15 @@ -- --==========================================================================-- -class "gui.debugoverlaypanel" ( "gui.panel" ) +class "gui.debugoverlaypanel" ( "gui.box" ) local debugoverlaypanel = gui.debugoverlaypanel function debugoverlaypanel:debugoverlaypanel( parent ) - gui.panel.panel( self, parent, "Debug Overlay" ) + gui.box.box( self, parent, "Debug Overlay" ) self.width = love.graphics.getWidth() self.height = love.graphics.getHeight() - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) self.overlays = {} end @@ -66,10 +66,7 @@ function debugoverlaypanel:preDrawWorld() end function debugoverlaypanel:invalidateLayout() - self:setSize( - love.graphics.getWidth(), - love.graphics.getHeight() - ) + self:setDimensions( love.graphics.getWidth(), love.graphics.getHeight() ) gui.panel.invalidateLayout( self ) end diff --git a/engine/client/gui/dropdownlist.lua b/engine/client/gui/dropdownlist.lua index 7c643106..56c55224 100644 --- a/engine/client/gui/dropdownlist.lua +++ b/engine/client/gui/dropdownlist.lua @@ -10,6 +10,8 @@ local dropdownlist = gui.dropdownlist function dropdownlist:dropdownlist( parent, name ) gui.button.button( self, parent, name ) + self.height = nil + self:setPadding( 14, 18, 13 ) self.icon = self:getScheme( "dropdownlist.icon" ) self.listItemGroup = gui.dropdownlistitemgroup( self, name .. " Item Group" ) self.active = false @@ -71,8 +73,8 @@ function dropdownlist:drawIcon() love.graphics.setColor( color ) - local padding = 18 - local x = self:getWidth() - self.icon:getWidth() - padding + local t, r, b, l = self:getPadding() + local x = self:getWidth() - self.icon:getWidth() - r local y = self:getHeight() / 2 - self.icon:getHeight() / 2 love.graphics.draw( self.icon, x, y ) end @@ -190,3 +192,10 @@ function dropdownlist:setActive( active ) self.active = active gui.setFocusedPanel( self, active ) end + +function dropdownlist:setValue( value ) + local listItemGroup = self:getListItemGroup() + if ( listItemGroup ) then + return listItemGroup:setValue( value ) + end +end diff --git a/engine/client/gui/dropdownlistitem.lua b/engine/client/gui/dropdownlistitem.lua index 93ed25e6..713b02d8 100644 --- a/engine/client/gui/dropdownlistitem.lua +++ b/engine/client/gui/dropdownlistitem.lua @@ -8,14 +8,20 @@ class "gui.dropdownlistitem" ( "gui.radiobutton" ) local dropdownlistitem = gui.dropdownlistitem -function dropdownlistitem:dropdownlistitem( name, text ) +function dropdownlistitem:dropdownlistitem( parent, name, text ) gui.radiobutton.radiobutton( self, nil, name, text ) self:setPadding( 15, 18, 14 ) self:setDisplay( "block" ) self:setPosition( "static" ) - self.width = 214 - self.height = nil - self.text = gui.text( self, name .. " Text Node", text or "Drop-Down List Item" ) + + parent:addItem( self ) + + local parent = self:getParent() + self.width = nil + self.height = nil + self.text:set( text ) + + self:invalidateLayout() end function dropdownlistitem:draw() @@ -74,3 +80,10 @@ function dropdownlistitem:mousepressed( x, y, button, istouch ) parentFrame:setFocusedFrame( true ) end end + +function dropdownlistitem:invalidateLayout() + local parent = self:getParent() + local t, r, b, l = parent:getBorderWidth() + self:setWidth( parent:getWidth() - r - l ) + gui.panel.invalidateLayout( self ) +end diff --git a/engine/client/gui/dropdownlistitemgroup.lua b/engine/client/gui/dropdownlistitemgroup.lua index bfd55a30..2a36ce42 100644 --- a/engine/client/gui/dropdownlistitemgroup.lua +++ b/engine/client/gui/dropdownlistitemgroup.lua @@ -10,15 +10,15 @@ local dropdownlistitemgroup = gui.dropdownlistitemgroup function dropdownlistitemgroup:dropdownlistitemgroup( parent, name ) gui.radiobuttongroup.radiobuttongroup( self, nil, name ) + self:setParent( parent:getRootPanel() ) self.height = nil self:setBorderWidth( 1 ) self:setBorderColor( self:getScheme( "dropdownlistitem.borderColor" ) ) self:setDisplay( "block" ) self:setPosition( "absolute" ) self.width = parent:getWidth() - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) self.dropDownList = parent - self:setScheme( "Default" ) end function dropdownlistitemgroup:addItem( item, default ) @@ -45,6 +45,7 @@ accessor( dropdownlistitemgroup, "dropDownList" ) function dropdownlistitemgroup:invalidateLayout() self:updatePos() self:setWidth( self:getDropDownList():getWidth() ) + gui.panel.invalidateLayout( self ) end function dropdownlistitemgroup:isVisible() diff --git a/engine/client/gui/frame.lua b/engine/client/gui/frame.lua index f1238eb1..711b37f1 100644 --- a/engine/client/gui/frame.lua +++ b/engine/client/gui/frame.lua @@ -28,9 +28,9 @@ function frame:frame( parent, name, title ) self.movable = true self.closeButton = gui.closebutton( self, name .. " Close Button" ) - local margin = 36 + local t, r, b, l = self:getPadding() self.closeButton:setPos( - self.width - 2 * margin - 16, + self.width - 2 * r - 16, 1 ) @@ -39,11 +39,11 @@ function frame:frame( parent, name, title ) local font = self:getScheme( "titleFont" ) local titleWidth = font:getWidth( self:getTitle() ) local closeButtonWidth = self.closeButton:getWidth() - self.minWidth = 2 * margin + titleWidth + closeButtonWidth + self.minWidth = 2 * l + titleWidth + closeButtonWidth local titleBarHeight = 86 self.minHeight = titleBarHeight - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) end local FRAME_ANIM_SCALE = 0.93 @@ -119,9 +119,9 @@ function frame:drawTitle() love.graphics.setColor( color ) local font = self:getScheme( "titleFont" ) love.graphics.setFont( font ) - local margin = 36 - local x = math.round( margin ) - local y = math.round( margin - 4 ) + local t, r, b, l = self:getPadding() + local x = math.round( l ) + local y = math.round( t - 4 ) love.graphics.print( self:getTitle(), x, y ) end @@ -136,7 +136,10 @@ accessor( frame, "title" ) function frame:invalidateLayout() if ( self.closeButton ) then - self.closeButton:setX( self:getWidth() - 2 * 36 - 16 ) + local r = self:getPaddingRight() + self.closeButton:setX( + self:getWidth() - self.closeButton:getWidth() - 8 + ) end gui.panel.invalidateLayout( self ) diff --git a/engine/client/gui/framerate.lua b/engine/client/gui/framerate.lua index 6ba46d2a..fec11103 100644 --- a/engine/client/gui/framerate.lua +++ b/engine/client/gui/framerate.lua @@ -10,6 +10,12 @@ local framerate = gui.framerate function framerate:framerate( parent, name ) gui.label.label( self, parent, name, text ) + + self:setScheme( "Console" ) + self.font = self:getScheme( "font" ) + self.height = self.font:getHeight() + + self:setScheme( "Default" ) self:setTextAlign( "right" ) self:invalidateLayout() end @@ -23,8 +29,12 @@ function framerate:update( dt ) self:setOpacity( 1 ) end - self:setText( self:getFramerate() ) - self:invalidate() + local framerate = self:getFramerate() + local text = self:getText() + if ( text ~= framerate ) then + self:setText( self:getFramerate() ) + self:invalidate() + end end function framerate:getFramerate() diff --git a/engine/client/gui/frametab.lua b/engine/client/gui/frametab.lua index 8b0189ed..1ceccfc5 100644 --- a/engine/client/gui/frametab.lua +++ b/engine/client/gui/frametab.lua @@ -11,36 +11,36 @@ local frametab = gui.frametab function frametab:frametab( parent, name, text ) gui.radiobutton.radiobutton( self, parent, name, text ) self:setDisplay( "inline-block" ) - self.text = text or "Frame Tab" - local font = self:getScheme( "font" ) - local padding = 24 - self.width = font:getWidth( self:getText() ) + 2 * padding - self.height = 61 + self:setPadding( 22 ) + self.text:set( text ) + self.width = nil + self.height = nil end function frametab:draw() self:drawBackground() self:drawText() - - gui.panel.draw( self ) + gui.box.draw( self ) end function frametab:drawBackground() - local color = self:getScheme( "frametab.backgroundColor" ) - local width = self:getWidth() - local height = self:getHeight() + local color = self:getScheme( "frametab.backgroundColor" ) + local mouseover = self.mouseover or self:isChildMousedOver() + local width = self:getWidth() + local height = self:getHeight() + if ( self:isSelected() ) then color = self:getScheme( "frametab.selected.backgroundColor" ) - elseif ( self.mouseover ) then + elseif ( mouseover ) then gui.panel.drawBackground( self, color ) color = self:getScheme( "frametab.mouseover.backgroundColor" ) end love.graphics.setColor( color ) - local selected = self.mouseover or self:isSelected() - local mouseover = self.mouseover and not self:isSelected() + local selected = mouseover or self:isSelected() + mouseover = mouseover and not self:isSelected() love.graphics.rectangle( "fill", 0, @@ -78,7 +78,7 @@ function frametab:drawBackground() end function frametab:mousepressed( x, y, button, istouch ) - if ( self.mouseover and button == 1 ) then + if ( ( self.mouseover or self:isChildMousedOver() ) and button == 1 ) then self.mousedown = true if ( not self:isDisabled() ) then diff --git a/engine/client/gui/frametabpanel.lua b/engine/client/gui/frametabpanel.lua index 18eab63c..8af46b60 100644 --- a/engine/client/gui/frametabpanel.lua +++ b/engine/client/gui/frametabpanel.lua @@ -4,22 +4,23 @@ -- --==========================================================================-- -class "gui.frametabpanel" ( "gui.panel" ) +class "gui.frametabpanel" ( "gui.box" ) local frametabpanel = gui.frametabpanel function frametabpanel:frametabpanel( parent, name ) - gui.panel.panel( self, parent, name ) - self:setScheme( "Default" ) + gui.box.box( self, parent, name ) + self:setDisplay( "block" ) + self:setPosition( "absolute" ) end function frametabpanel:draw() gui.panel.drawBackground( self, self:getScheme( "frame.backgroundColor" ) ) - gui.panel.draw( self ) + gui.box.draw( self ) end function frametabpanel:invalidateLayout() - self:setSize( self:getParent():getSize() ) + self:setDimensions( self:getParent():getDimensions() ) gui.panel.invalidateLayout( self ) end diff --git a/engine/client/gui/frametabpanels.lua b/engine/client/gui/frametabpanels.lua index 498fd8cf..dc1a675a 100644 --- a/engine/client/gui/frametabpanels.lua +++ b/engine/client/gui/frametabpanels.lua @@ -4,23 +4,27 @@ -- --==========================================================================-- -class "gui.frametabpanels" ( "gui.panel" ) +class "gui.frametabpanels" ( "gui.box" ) local frametabpanels = gui.frametabpanels function frametabpanels:frametabpanels( parent, name, text ) - gui.panel.panel( self, parent, name ) + gui.box.box( self, parent, name ) + self:setDisplay( "block" ) + self:setPosition( "absolute" ) self.width = parent:getWidth() self.height = parent:getHeight() - 62 end function frametabpanels:addPanel( frametabpanel, default ) - frametabpanel:setSize( self:getSize() ) - frametabpanel:setParent( self ) + local panel = frametabpanel( self ) + panel:setDimensions( self:getDimensions() ) if ( not default and #self:getChildren() ~= 1 ) then - frametabpanel:setVisible( false ) + panel:setVisible( false ) end + + return panel end function frametabpanels:setSelectedChild( i ) diff --git a/engine/client/gui/handlers.lua b/engine/client/gui/handlers.lua index 7f598c8a..aecfb200 100644 --- a/engine/client/gui/handlers.lua +++ b/engine/client/gui/handlers.lua @@ -23,7 +23,7 @@ local function updateFramerate( convar ) return end - local framerate = gui.framerate( nil, "Frame Rate" ) + local framerate = gui.framerate( _rootPanel, "Frame Rate" ) _G.g_Framerate = framerate else _G.g_Framerate:remove() @@ -44,13 +44,17 @@ local function updateNetGraph( convar ) local netgraph = gui.netgraph( nil, "Net Graph" ) _G.g_NetGraph = netgraph else + if ( _G.g_NetGraph == nil ) then + return + end + _G.g_NetGraph:remove() _G.g_NetGraph = nil end end local perf_draw_net_graph = convar( "perf_draw_net_graph", "0", nil, nil, - "Draws the net graph", updateNetGraph ) + "Draws the net graph", updateNetGraph ) function load() -- Initialize root panel @@ -79,36 +83,37 @@ function load() end end -local function updateTranslucencyFramebuffer( convar ) +local function updateTranslucencyCanvas( convar ) local enabled = convar:getBoolean() if ( enabled ) then require( "shaders.gaussianblur" ) - _translucencyFramebuffer = _G.shader.getShader( "gaussianblur" ) - _translucencyFramebuffer:set( "sigma", love.window.toPixels( 20 ) ) + _translucencyCanvas = _G.shader.getShader( "gaussianblur" ) + _translucencyCanvas:set( "sigma", love.window.toPixels( 20 ) ) else - _translucencyFramebuffer = nil + _translucencyCanvas = nil end end local gui_draw_translucency = convar( "gui_draw_translucency", "1", nil, nil, "Toggles gui translucency", - updateTranslucencyFramebuffer ) + updateTranslucencyCanvas, + { "archive" } ) function draw() - if ( _viewportFramebuffer and gui_draw_translucency:getBoolean() ) then - if ( _translucencyFramebuffer == nil ) then + if ( _viewportCanvas and gui_draw_translucency:getBoolean() ) then + if ( _translucencyCanvas == nil ) then require( "shaders.gaussianblur" ) - _translucencyFramebuffer = _G.shader.getShader( "gaussianblur" ) - _translucencyFramebuffer:set( "sigma", love.window.toPixels( 12 ) ) + _translucencyCanvas = _G.shader.getShader( "gaussianblur" ) + _translucencyCanvas:set( "sigma", love.window.toPixels( 20 ) ) end - _translucencyFramebuffer:renderTo( function() + _translucencyCanvas:renderTo( function() love.graphics.clear() - _viewportFramebuffer:draw() + _viewportCanvas:draw() end ) end - _rootPanel:createFramebuffer() + _rootPanel:createCanvas() _rootPanel:draw() end @@ -169,6 +174,9 @@ function update( dt ) end _rootPanel:update( dt ) + + panel._drawcalls = 0 + panel._invalidations = 0 end function wheelmoved( x, y ) diff --git a/game/client/gui/hudframe.lua b/engine/client/gui/hudframe.lua similarity index 71% rename from game/client/gui/hudframe.lua rename to engine/client/gui/hudframe.lua index 9fa6c929..defd2c0d 100644 --- a/game/client/gui/hudframe.lua +++ b/engine/client/gui/hudframe.lua @@ -19,7 +19,7 @@ function hudframe:hudframe( parent, name, title ) self.closeButton = nil end - self:setUseFullscreenFramebuffer( false ) + self:setUseFullscreenCanvas( false ) self:invalidateLayout() end @@ -60,30 +60,16 @@ function hudframe:draw() end function hudframe:drawBackground() - if ( gui._translucencyFramebuffer == nil ) then - gui.panel.drawBackground( self, self:getScheme( "frame.backgroundColor" ) ) + if ( gui._translucencyCanvas == nil ) then + gui.panel.drawBackground( self, self:getScheme( + "frame.backgroundColor" + ) ) return end gui.box.drawBackground( self ) end -function hudframe:drawTranslucency() - if ( gui._translucencyFramebuffer == nil ) then - return - end - - gui.panel._maskedPanel = self - love.graphics.stencil( gui.panel.drawMask ) - love.graphics.setStencilTest( "greater", 0 ) - love.graphics.push() - local x, y = self:localToScreen() - love.graphics.translate( -x, -y ) - gui._translucencyFramebuffer:draw() - love.graphics.pop() - love.graphics.setStencilTest() -end - function hudframe:drawTitle() local property = "frame.titleTextColor" love.graphics.setColor( self:getScheme( property ) ) @@ -95,7 +81,7 @@ function hudframe:drawTitle() end function hudframe:update( dt ) - if ( gui._translucencyFramebuffer and self:isVisible() ) then + if ( gui._translucencyCanvas and self:isVisible() ) then self:invalidate() end diff --git a/engine/client/gui/hudprofiler.lua b/engine/client/gui/hudprofiler.lua new file mode 100644 index 00000000..e2ec1fec --- /dev/null +++ b/engine/client/gui/hudprofiler.lua @@ -0,0 +1,86 @@ +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- +-- +-- Purpose: Profiler HUD +-- +--==========================================================================-- + +class "gui.hudprofiler" ( "gui.hudframe" ) + +local hudprofiler = gui.hudprofiler + +function hudprofiler:hudprofiler( parent ) + local name = "HUD Profiler" + gui.hudframe.hudframe( self, parent, name, name ) + self.width = 320 -- - 31 + self.height = 432 + self:setBorderColor( self:getScheme( "borderColor" ) ) + + local budgets = profile._stack + for i, budget in ipairs( budgets ) do + local box = gui.box( self, budget.name .. " Budget Info" ) + box:setDisplay( "block" ) + box:setMargin( 16, 0 ) + local text = gui.text( box, budget.name ) + text:setColor( self:getScheme( "textColor" ) ) + gui.progressbar( box ) + end + + self:invalidateLayout() +end + +function hudprofiler:draw() + self:drawTranslucency() + self:drawBackground() + + gui.box.draw( self ) + + self:drawTitle() + -- self:drawBorder( self:getScheme( "borderColor" ) ) + + if ( convar.getConvar( "gui_draw_frame_focus" ):getBoolean() and + self.focus ) then + self:drawSelection() + end +end + +function hudprofiler:getTitle() + return "Profiler" +end + +function hudprofiler:invalidateLayout() + local x = love.graphics.getWidth() - self:getWidth() - 18 + local y = love.graphics.getHeight() - self:getHeight() - 18 + self:setPos( x, y ) + gui.frame.invalidateLayout( self ) +end + +concommand( "+profiler", "Opens the profiler", function() + local visible = _G.g_Profiler:isVisible() + if ( not visible ) then + _G.g_Profiler:activate() + end +end, { "game" } ) + +concommand( "-profiler", "Closes the profiler", function() + local visible = _G.g_Profiler:isVisible() + if ( visible ) then + _G.g_Profiler:close() + end +end, { "game" } ) + +local function onReloadScript() + local profiler = g_Profiler + if ( profiler == nil ) then + return + end + + local visible = profiler:isVisible() + profiler:remove() + profiler = gui.hudprofiler( g_Viewport ) + g_Profiler = profiler + if ( visible ) then + profiler:activate() + end +end + +onReloadScript() diff --git a/engine/client/gui/hudvoice.lua b/engine/client/gui/hudvoice.lua new file mode 100644 index 00000000..623562d8 --- /dev/null +++ b/engine/client/gui/hudvoice.lua @@ -0,0 +1,17 @@ +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- +-- +-- Purpose: Voice HUD +-- +--==========================================================================-- + +class "gui.hudvoice" ( "gui.box" ) + +local hudvoice = gui.hudvoice + +function hudvoice:hudvoice( parent, name ) + gui.box.box( self, parent, name ) +end + +function hudvoice:draw() + gui.box.draw( self ) +end diff --git a/engine/client/gui/init.lua b/engine/client/gui/init.lua index 2947622c..77ec8551 100644 --- a/engine/client/gui/init.lua +++ b/engine/client/gui/init.lua @@ -36,9 +36,14 @@ end function invalidateTree() _rootPanel:invalidateLayout() - _rootPanel:invalidateFramebuffer() - _viewportFramebuffer = nil - _translucencyFramebuffer = nil + _rootPanel:invalidateCanvas() + + if ( _viewportCanvas ) then + _viewportCanvas:remove() + end + + _viewportCanvas = nil + _translucencyCanvas = nil end function preDrawWorld() diff --git a/engine/client/gui/netgraph.lua b/engine/client/gui/netgraph.lua index 425eb4c8..275efa2d 100644 --- a/engine/client/gui/netgraph.lua +++ b/engine/client/gui/netgraph.lua @@ -13,6 +13,7 @@ function netgraph:netgraph( parent, name ) self:setDisplay( "block" ) self:setPosition( "absolute" ) + self:setScheme( "Console" ) self.font = self:getScheme( "font" ) self.width = 216 self.height = 3 * self.font:getHeight() @@ -42,6 +43,7 @@ function netgraph:drawSentReceived() return end + self:setScheme( "Default" ) love.graphics.setColor( self:getScheme( "label.textColor" ) ) local font = self:getFont() diff --git a/engine/client/gui/optionsmenu/audiooptionspanel.lua b/engine/client/gui/optionsmenu/audiooptionspanel.lua index 87dcbb08..3169fc6d 100644 --- a/engine/client/gui/optionsmenu/audiooptionspanel.lua +++ b/engine/client/gui/optionsmenu/audiooptionspanel.lua @@ -9,7 +9,6 @@ class "gui.audiooptionspanel" ( "gui.frametabpanel" ) local audiooptionspanel = gui.audiooptionspanel function audiooptionspanel:audiooptionspanel( parent, name ) - parent = parent or nil name = name or "Audio Options Panel" gui.frametabpanel.frametabpanel( self, parent, name ) local options = {} diff --git a/engine/client/gui/optionsmenu/bindlistheader.lua b/engine/client/gui/optionsmenu/bindlistheader.lua index c4ed5280..f0341fb6 100644 --- a/engine/client/gui/optionsmenu/bindlistheader.lua +++ b/engine/client/gui/optionsmenu/bindlistheader.lua @@ -4,12 +4,12 @@ -- --==========================================================================-- -class "gui.bindlistheader" ( "gui.panel" ) +class "gui.bindlistheader" ( "gui.box" ) local bindlistheader = gui.bindlistheader function bindlistheader:bindlistheader( parent, name, text ) - gui.panel.panel( self, parent, name ) + gui.box.box( self, parent, name ) self.width = parent:getWidth() self.height = 46 self.text = text or "Bind List Header" @@ -38,7 +38,7 @@ function bindlistheader:draw() local height = 1 love.graphics.rectangle( "fill", x, y, width, height ) - gui.panel.draw( self ) + gui.box.draw( self ) end accessor( bindlistheader, "text" ) diff --git a/engine/client/gui/optionsmenu/bindlistitem.lua b/engine/client/gui/optionsmenu/bindlistitem.lua index 46e5107c..a11c8203 100644 --- a/engine/client/gui/optionsmenu/bindlistitem.lua +++ b/engine/client/gui/optionsmenu/bindlistitem.lua @@ -10,8 +10,11 @@ local bindlistitem = gui.bindlistitem function bindlistitem:bindlistitem( parent, name, text, key, concommand ) gui.button.button( self, parent, name, text ) + self:setPosition( "static" ) self.width = parent:getWidth() - self.height = 30 + self.height = nil + self:setPadding( 7, 18 ) + self:setBorderWidth( 0 ) self.key = key self.concommand = concommand end @@ -34,7 +37,8 @@ function bindlistitem:drawBackground() return end - if ( self.mousedown and self.mouseover ) then + local mouseover = self.mouseover or self:isChildMousedOver() + if ( self.mousedown and mouseover ) then color = self:getScheme( "button.mousedown.backgroundColor" ) love.graphics.setColor( color ) love.graphics.rectangle( @@ -44,7 +48,7 @@ function bindlistitem:drawBackground() width - 2, height ) - elseif ( self.mousedown or self.mouseover ) then + elseif ( self.mousedown or mouseover ) then color = self:getScheme( "button.mouseover.backgroundColor" ) love.graphics.setColor( color ) love.graphics.rectangle( @@ -68,13 +72,13 @@ function bindlistitem:drawText() end love.graphics.setColor( color ) + self.text:setColor( color ) local font = self:getScheme( "font" ) love.graphics.setFont( font ) local margin = 18 local x = math.round( margin ) local y = math.round( self:getHeight() / 2 - font:getHeight() / 2 ) - love.graphics.print( self:getText(), x, y ) local label = "Key or Button" local key = self:getKey() diff --git a/engine/client/gui/optionsmenu/bindlistpanel.lua b/engine/client/gui/optionsmenu/bindlistpanel.lua index b6ddf805..f561d351 100644 --- a/engine/client/gui/optionsmenu/bindlistpanel.lua +++ b/engine/client/gui/optionsmenu/bindlistpanel.lua @@ -17,9 +17,9 @@ function bindlistpanel:bindlistpanel( parent, name ) end function bindlistpanel:draw() - self:drawBackground( self:getScheme( "bindlistpanel.backgroundColor" ) ) - gui.panel.draw( self ) - self:drawBorder( self:getScheme( "bindlistpanel.borderColor" ) ) + gui.panel.drawBackground( self, self:getScheme( "backgroundColor" ) ) + gui.box.draw( self ) + gui.panel.drawBorder( self, self:getScheme( "borderColor" ) ) end local function getLastY( self ) @@ -39,7 +39,6 @@ function bindlistpanel:addHeader( label ) local name = label .. " Bind List Header" local y = getLastY( panel ) local label = gui.bindlistheader( panel, name, label ) - label:setY( y ) self:setInnerHeight( getLastY( panel ) ) end @@ -48,7 +47,6 @@ function bindlistpanel:addBinding( text, key, concommand ) local name = text .. " Bind List Item" local y = getLastY( panel ) local binding = gui.bindlistitem( panel, name, text, key, concommand ) - binding:setY( y ) self:setInnerHeight( getLastY( panel ) ) end diff --git a/engine/client/gui/optionsmenu/init.lua b/engine/client/gui/optionsmenu/init.lua index 7397e6da..84ca8501 100644 --- a/engine/client/gui/optionsmenu/init.lua +++ b/engine/client/gui/optionsmenu/init.lua @@ -47,13 +47,16 @@ function optionsmenu:optionsmenu( parent ) end require( "engine.client.gui.optionsmenu.keyboardoptionspanel" ) - self:addTab( "Keyboard", gui.keyboardoptionspanel() ) + self:addTab( "Keyboard", gui.keyboardoptionspanel ) require( "engine.client.gui.optionsmenu.videooptionspanel" ) - self:addTab( "Video", gui.videooptionspanel() ) + self:addTab( "Video", gui.videooptionspanel ) require( "engine.client.gui.optionsmenu.audiooptionspanel" ) - self:addTab( "Audio", gui.audiooptionspanel() ) + self:addTab( "Audio", gui.audiooptionspanel ) + + -- require( "engine.client.gui.optionsmenu.multiplayeroptionspanel" ) + -- self:addTab( "Multiplayer", gui.multiplayeroptionspanel() ) end function optionsmenu:activate() diff --git a/engine/client/gui/optionsmenu/keyboardoptionspanel.lua b/engine/client/gui/optionsmenu/keyboardoptionspanel.lua index e2f9515b..03aaefc2 100644 --- a/engine/client/gui/optionsmenu/keyboardoptionspanel.lua +++ b/engine/client/gui/optionsmenu/keyboardoptionspanel.lua @@ -13,15 +13,14 @@ class "gui.keyboardoptionspanel" ( "gui.frametabpanel" ) local keyboardoptionspanel = gui.keyboardoptionspanel function keyboardoptionspanel:keyboardoptionspanel( parent, name ) - parent = parent or nil name = name or "Keyboard Options Panel" gui.frametabpanel.frametabpanel( self, parent, name ) self.bindList = gui.bindlistpanel( self ) local margin = 24 local height = 348 - margin - self.bindList:setSize( 640 - 2 * margin, height ) - self.bindList:setPos( margin, margin ) + self.bindList:setDimensions( 640 - 2 * margin, height ) + self.bindList:setMargin( margin ) self.bindList:readBinds() local name = "Keyboard Options" diff --git a/engine/client/gui/optionsmenu/multiplayeroptionspanel.lua b/engine/client/gui/optionsmenu/multiplayeroptionspanel.lua new file mode 100644 index 00000000..1736d25e --- /dev/null +++ b/engine/client/gui/optionsmenu/multiplayeroptionspanel.lua @@ -0,0 +1,85 @@ +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- +-- +-- Purpose: Multiplayer Options Panel class +-- +--==========================================================================-- + +class "gui.multiplayeroptionspanel" ( "gui.frametabpanel" ) + +local multiplayeroptionspanel = gui.multiplayeroptionspanel + +function multiplayeroptionspanel:multiplayeroptionspanel( parent, name ) + name = name or "Multiplayer Options Panel" + gui.frametabpanel.frametabpanel( self, parent, name ) + local options = {} + self.options = options + local c = config.getConfig() + + local e = gui.createElement + + local panel = e( "box", { + parent = self, + position = "absolute", + margin = 36 + }, { + e( "text", { text = "Name", marginBottom = 9 } ), + e( "textbox", { position = "static", text = "Unnamed" } ) + } ) + + panel:setPos( panel:getMarginLeft(), panel:getMarginTop() ) + + -- name = "Play Sound in Desktop" + -- local desktopSound = gui.checkbox( self, name, name ) + -- self.desktopSound = desktopSound + -- options.desktopSound = c.sound.desktop + -- desktopSound:setChecked( c.sound.desktop ) + -- desktopSound.onCheckedChanged = function( checkbox, checked ) + -- options.desktopSound = checked + -- c.sound.desktop = checked + -- end + -- x = 2 * x + self.name:getWidth() + -- y = margin + label:getHeight() + marginBottom + -- desktopSound:setPos( x, y ) + + -- name = "Tickrates" + -- local radiobuttongroup = gui.radiobuttongroup( self, name ) + -- name = "20" + -- local radiobutton = gui.radiobutton( self, name .. " 1" ) + -- radiobuttongroup:addItem( radiobutton ) + -- x = x + -- radiobutton:setPos( x, y ) + -- radiobutton:setDefault( true ) +end + +function multiplayeroptionspanel:activate() + self:saveControlStates() +end + +function multiplayeroptionspanel:onOK() + self:updateOptions() +end + +function multiplayeroptionspanel:onCancel() + self:resetControlStates() +end + +multiplayeroptionspanel.onApply = multiplayeroptionspanel.onOK + +function multiplayeroptionspanel:saveControlStates() + local controls = {} + self.controls = controls + -- controls.name = self.name:getText() + -- controls.desktopSound = self.desktopSound:isChecked() +end + +function multiplayeroptionspanel:resetControlStates() + local controls = self.controls + -- self.name:setText( controls.name ) + -- self.desktopSound:setChecked( controls.desktopSound ) + table.clear( controls ) +end + +function multiplayeroptionspanel:updateOptions() + local options = self.options + convar.setConvar( "name", options.name ) +end diff --git a/engine/client/gui/optionsmenu/videooptionspanel.lua b/engine/client/gui/optionsmenu/videooptionspanel.lua index 331a4829..a688cda9 100644 --- a/engine/client/gui/optionsmenu/videooptionspanel.lua +++ b/engine/client/gui/optionsmenu/videooptionspanel.lua @@ -55,7 +55,6 @@ function videooptionspanel.getFullscreenModes( x, y ) end function videooptionspanel:videooptionspanel( parent, name ) - parent = parent or nil name = name or "Video Options Panel" gui.frametabpanel.frametabpanel( self, parent, name ) local options = {} @@ -109,9 +108,7 @@ function videooptionspanel:videooptionspanel( parent, name ) local customHeight = self.customHeight customHeight:setDisabled( not checked ) - if ( not checked ) then - self:updateResolutions() - else + if ( checked ) then local resolution = options.resolution if ( resolution ) then local width = tonumber( customWidth:getText() ) @@ -120,6 +117,8 @@ function videooptionspanel:videooptionspanel( parent, name ) local height = tonumber( customHeight:getText() ) resolution.height = height or resolution.height end + else + self:updateResolutions() end end y = y + aspectRatios:getHeight() + margin @@ -164,19 +163,59 @@ function videooptionspanel:videooptionspanel( parent, name ) self:updateAspectRatios() self:updateResolutions() - name = "Fullscreen" - local fullscreen = gui.checkbox( self, name, name ) - self.fullscreen = fullscreen + name = "Fullscreen Type" + label = gui.label( self, name, name ) + x = 2 * x + resolutions:getWidth() + y = margin + label:setPos( x, y ) + label:setFont( self:getScheme( "fontBold" ) ) + + name = "Fullscreen Types Drop-Down List" + local fullscreentype = gui.dropdownlist( self, name ) + self.fullscreentype = fullscreentype local window = c.window options.fullscreen = window.fullscreen - fullscreen:setChecked( window.fullscreen ) - fullscreen.onCheckedChanged = function( checkbox, checked ) - options.fullscreen = checked - window.fullscreen = checked + options.fullscreentype = window.fullscreentype + fullscreentype.onValueChanged = function( dropdownlist, oldValue, newValue ) + if ( newValue ) then + options.fullscreen = true + window.fullscreen = true + options.fullscreentype = newValue + window.fullscreentype = newValue + else + options.fullscreen = false + window.fullscreen = false + options.fullscreentype = "desktop" + window.fullscreentype = "desktop" + end + + local disabled = newValue == "desktop" + if ( disabled ) then + customResolution:setChecked( false ) + end + + aspectRatios:setDisabled( disabled ) + resolutions:setDisabled( disabled ) + customResolution:setDisabled( disabled ) end - x = 2 * x + resolutions:getWidth() - y = margin + label:getHeight() + marginBottom - fullscreen:setPos( x, y ) + local marginBottom = 9 + y = y + label:getHeight() + marginBottom + fullscreentype:setPos( x, y ) + + for i, v in ipairs( { + { text = "Windowed", mode = nil }, + { text = "Desktop", mode = "desktop" }, + { text = "Exclusive", mode = "exclusive" } + } ) do + local dropdownlistitem = gui.dropdownlistitem( + fullscreentype, name .. " " .. i, v.text + ) + dropdownlistitem:setValue( v.mode ) + end + + fullscreentype:setValue( + window.fullscreen and window.fullscreentype or nil + ) name = "Vertical Synchronization" local vsync = gui.checkbox( self, name, name ) @@ -187,7 +226,8 @@ function videooptionspanel:videooptionspanel( parent, name ) options.vsync = checked window.vsync = checked end - y = y + 2 * fullscreen:getHeight() + 4 + -- y = y + 2 * fullscreentype:getHeight() + 4 + y = resolutions:getY() vsync:setPos( x, y ) name = "Borderless Window" @@ -199,21 +239,42 @@ function videooptionspanel:videooptionspanel( parent, name ) options.borderless = checked window.borderless = checked end - y = y + 2 * vsync:getHeight() + 3 + y = y + vsync:getHeight() + marginBottom + 7 borderless:setPos( x, y ) name = "High-DPI" - local highdpi = gui.checkbox( self, name, name ) + label = gui.label( self, name, name ) + y = customResolution:getY() + 4 + label:setPos( x, y ) + label:setFont( self:getScheme( "fontBold" ) ) + + name = "High-DPI Drop-Down List" + local highdpi = gui.dropdownlist( self, name ) + if ( love.system.getOS() == "Windows" ) then + highdpi:setDisabled( true ) + end self.highdpi = highdpi - options.highdpi = window.highdpi - highdpi:setChecked( window.highdpi ) - highdpi.onCheckedChanged = function( checkbox, checked ) - options.highdpi = checked - window.highdpi = checked + options.highdpi = convar.getConfig( "r_window_highdpi" ) + highdpi.onValueChanged = function( dropdownlist, oldValue, newValue ) + options.highdpi = newValue + window.highdpi = toboolean( newValue ) end - y = customResolution:getY() + y = customWidth:getY() highdpi:setPos( x, y ) + for i, v in ipairs( { + { text = "Low Resolution", value = 0 }, + { text = "Native", value = 1 }, + { text = "@1x", value = 2 } + } ) do + local dropdownlistitem = gui.dropdownlistitem( + highdpi, name .. " " .. i, v.text + ) + dropdownlistitem:setValue( v.value ) + end + + highdpi:setValue( tonumber( options.highdpi ) ) + name = "High-DPI Label" local text = "Changing high-DPI requires restarting the game." label = gui.label( self, name, text ) @@ -280,20 +341,20 @@ function videooptionspanel:saveControlStates() self.controls = controls controls.aspectRatios = self.aspectRatios:getListItemGroup():getSelectedId() controls.customResolution = self.customResolution:isChecked() - controls.fullscreen = self.fullscreen:isChecked() + controls.fullscreentype = self.fullscreentype:getValue() controls.vsync = self.vsync:isChecked() controls.borderless = self.borderless:isChecked() - controls.highdpi = self.highdpi:isChecked() + controls.highdpi = self.highdpi:getValue() end function videooptionspanel:resetControlStates() local controls = self.controls self.aspectRatios:getListItemGroup():setSelectedId( controls.aspectRatios ) self.customResolution:setChecked( controls.customResolution ) - self.fullscreen:setChecked( controls.fullscreen ) + self.fullscreentype:setValue( controls.fullscreentype ) self.vsync:setChecked( controls.vsync ) self.borderless:setChecked( controls.borderless ) - self.highdpi:setChecked( controls.highdpi ) + self.highdpi:setValue( controls.highdpi ) table.clear( controls ) self:clearCustomResolution() @@ -306,12 +367,13 @@ function videooptionspanel:updateMode() return end - convar.setConvar( "r_window_width", resolution.width ) - convar.setConvar( "r_window_height", resolution.height ) - convar.setConvar( "r_window_fullscreen", options.fullscreen and 1 or 0 ) - convar.setConvar( "r_window_vsync", options.vsync and 1 or 0 ) - convar.setConvar( "r_window_borderless", options.borderless and 1 or 0 ) - convar.setConvar( "r_window_highdpi", options.highdpi and 1 or 0 ) + convar.setConvar( "r_window_width", resolution.width ) + convar.setConvar( "r_window_height", resolution.height ) + convar.setConvar( "r_window_fullscreen", options.fullscreen and 1 or 0 ) + convar.setConvar( "r_window_fullscreentype", options.fullscreentype ) + convar.setConvar( "r_window_vsync", options.vsync and 1 or 0 ) + convar.setConvar( "r_window_borderless", options.borderless and 1 or 0 ) + convar.setConvar( "r_window_highdpi", options.highdpi ) local flags = table.copy( config.getConfig().window ) flags.width = nil @@ -331,6 +393,8 @@ function videooptionspanel:updateAspectRatios() local supportedAspectRatios = { { x = 4, y = 3 }, { x = 16, y = 9 }, + -- See https://en.wikipedia.org/wiki/Graphics_display_resolution + -- #WXGA_.281366x768_and_similar.29 -- { x = 683, y = 384 }, { x = 16, y = 10 }, { x = 21, y = 9 } @@ -340,17 +404,20 @@ function videooptionspanel:updateAspectRatios() local text = "" local arx, ary = videooptionspanel.getAspectRatio() for i, mode in ipairs( supportedAspectRatios ) do - local hasModes = #videooptionspanel.getFullscreenModes( mode.x, mode.y ) ~= 0 - -- HACKHACK: Include 683:384 when performing 16:9 lookups. + local hasModes = #videooptionspanel.getFullscreenModes( + mode.x, mode.y + ) ~= 0 + -- Include 683:384 when performing 16:9 lookups. if ( mode.x == 16 and mode.y == 9 and not hasModes ) then hasModes = #videooptionspanel.getFullscreenModes( 683, 384 ) ~= 0 end if ( hasModes ) then text = mode.x .. ":" .. mode.y - dropdownlistitem = gui.dropdownlistitem( name .. " " .. i, text ) + dropdownlistitem = gui.dropdownlistitem( + self.aspectRatios, name .. " " .. i, text + ) dropdownlistitem:setValue( mode ) - self.aspectRatios:addItem( dropdownlistitem ) local options = self.options if ( mode.x == arx and mode.y == ary ) then dropdownlistitem:setDefault( true ) @@ -378,7 +445,7 @@ function videooptionspanel:updateResolutions() local r = options.aspectRatio local modes = videooptionspanel.getFullscreenModes( r.x, r.y ) - -- HACKHACK: Include 683:384 when performing 16:9 lookups. + -- Include 683:384 when performing 16:9 lookups. if ( r.x == 16 and r.y == 9 ) then table.append( modes, videooptionspanel.getFullscreenModes( 683, 384 ) ) table.sort( modes, function( a, b ) @@ -395,9 +462,10 @@ function videooptionspanel:updateResolutions() for i, mode in ipairs( modes ) do text = scale > 1 and "Looks like " or "" text = text .. mode.width .. " × " .. mode.height - dropdownlistitem = gui.dropdownlistitem( name .. " " .. i, text ) + dropdownlistitem = gui.dropdownlistitem( + resolutions, name .. " " .. i, text + ) dropdownlistitem:setValue( mode ) - resolutions:addItem( dropdownlistitem ) if ( mode.width == width and mode.height == height ) then dropdownlistitem:setDefault( true ) foundMode = true @@ -408,8 +476,9 @@ function videooptionspanel:updateResolutions() end if ( not foundMode ) then - self.customWidth:setText( tostring( width ) ) - self.customHeight:setText( tostring( height ) ) + local scale = love.graphics.getDPIScale() + self.customWidth:setText( tostring( scale * width ) ) + self.customHeight:setText( tostring( scale * height ) ) else self:clearCustomResolution() end diff --git a/engine/client/gui/panel.lua b/engine/client/gui/panel.lua index e2d623c1..d716b786 100644 --- a/engine/client/gui/panel.lua +++ b/engine/client/gui/panel.lua @@ -13,17 +13,20 @@ local panel = gui.panel function panel.drawMask() local self = panel._maskedPanel - love.graphics.rectangle( "fill", 0, 0, self:getSize() ) + love.graphics.rectangle( "fill", 0, 0, self:getDimensions() ) end function panel:panel( parent, name ) self.x = 0 self.y = 0 self.name = name or "" - self:setParent( parent or g_RootPanel ) self.visible = true self.scale = 1 self.opacity = 1 + + if ( parent ) then + self:setParent( parent ) + end end function panel:animate( properties, duration, easing, complete ) @@ -59,33 +62,45 @@ function panel:animate( properties, duration, easing, complete ) table.insert( self.animations, animation ) end -function panel:createFramebuffer( width, height ) +function panel:createCanvas( width, height ) if ( width == nil and height == nil ) then - width, height = self:getSize() + width, height = self:getDimensions() end if ( width == 0 or height == 0 ) then width, height = nil, nil end - if ( self.framebuffer and not self.needsRedraw ) then + if ( self.canvas and not self.needsRedraw ) then return end - if ( self.framebuffer == nil ) then + if ( self.canvas == nil ) then + local dpiscale = love.graphics.getDPIScale() + local r_window_highdpi = convar.getConvar( "r_window_highdpi" ) + if ( r_window_highdpi:getNumber() == 2 ) then + dpiscale = 1 + end + require( "engine.client.canvas" ) - if ( self:shouldUseFullscreenFramebuffer() ) then - self.framebuffer = fullscreencanvas() + if ( self:shouldUseFullscreenCanvas() ) then + self.canvas = fullscreencanvas( nil, nil, { + dpiscale = dpiscale + } ) else - self.framebuffer = canvas( width, height ) + self.canvas = canvas( width, height, { + dpiscale = dpiscale + } ) end end - if ( self.framebuffer:shouldAutoRedraw() ) then - self.framebuffer:setAutoRedraw( false ) + self.canvas:setFilter( "nearest", "nearest" ) + + if ( self.canvas:shouldAutoRedraw() ) then + self.canvas:setAutoRedraw( false ) end - self.framebuffer:renderTo( function() + self.canvas:renderTo( function() love.graphics.clear() self:draw() end ) @@ -94,6 +109,8 @@ function panel:createFramebuffer( width, height ) end function panel:draw() + panel._drawcalls = ( panel._drawcalls or 0 ) + 1 + if ( not self:isVisible() ) then return end @@ -104,24 +121,24 @@ function panel:draw() end for _, v in ipairs( children ) do - v:createFramebuffer() + v:createCanvas() end for _, v in ipairs( children ) do v:preDraw() - v:drawFramebuffer() + v:drawCanvas() v:postDraw() end end function panel:drawBackground( color ) - local width, height = self:getSize() + local width, height = self:getDimensions() love.graphics.setColor( color ) love.graphics.rectangle( "fill", 0, 0, width, height ) end -function panel:drawSelection() - love.graphics.setColor( color.red ) +function panel:drawBorder( color ) + love.graphics.setColor( color ) local lineWidth = 1 love.graphics.setLineWidth( lineWidth ) love.graphics.rectangle( @@ -133,21 +150,27 @@ function panel:drawSelection() ) end -function panel:drawName() - local font = scheme.getProperty( "Default", "font" ) - love.graphics.setFont( font ) - local text = self:getName() - local width = font:getWidth( text ) - local height = font:getHeight() - love.graphics.setColor( color.red ) - love.graphics.rectangle( "fill", 0, 0, width + 6, height + 2 ) +function panel:drawCanvas() + if ( not self:isVisible() ) then + return + end - love.graphics.setColor( color.white ) - love.graphics.print( text, 3, 1 ) + if ( self.canvas == nil ) then + self:createCanvas() + end + + love.graphics.push() + local b = love.graphics.getBlendMode() + love.graphics.setBlendMode( "alpha", "premultiplied" ) + local a = self:getOpacity() + love.graphics.setColor( a, a, a, a ) + self.canvas:draw() + love.graphics.setBlendMode( b ) + love.graphics.pop() end -function panel:drawBorder( color ) - love.graphics.setColor( color ) +function panel:drawSelection() + love.graphics.setColor( color.red ) local lineWidth = 1 love.graphics.setLineWidth( lineWidth ) love.graphics.rectangle( @@ -159,23 +182,20 @@ function panel:drawBorder( color ) ) end -function panel:drawFramebuffer() - if ( not self:isVisible() ) then +function panel:drawTranslucency() + if ( gui._translucencyCanvas == nil ) then return end - if ( self.framebuffer == nil ) then - self:createFramebuffer() - end - - love.graphics.push() - local b = love.graphics.getBlendMode() - love.graphics.setBlendMode( "alpha", "premultiplied" ) - local a = self:getOpacity() - love.graphics.setColor( a, a, a, a ) - self.framebuffer:draw() - love.graphics.setBlendMode( b ) - love.graphics.pop() + gui.panel._maskedPanel = self + love.graphics.stencil( gui.panel.drawMask ) + love.graphics.setStencilTest( "greater", 0 ) + love.graphics.push() + local x, y = self:localToScreen() + love.graphics.translate( -x, -y ) + gui._translucencyCanvas:draw() + love.graphics.pop() + love.graphics.setStencilTest() end local filtered = function( panel, func, ... ) @@ -213,7 +233,7 @@ accessor( panel, "children" ) accessor( panel, "name" ) gui.accessor( panel, "opacity" ) accessor( panel, "parent" ) -accessor( panel, "scale" ) +gui.accessor( panel, "scale" ) function panel:getScheme( property ) return scheme.getProperty( self.scheme, property ) @@ -222,24 +242,63 @@ end gui.accessor( panel, "width", nil, nil, 0 ) gui.accessor( panel, "height", nil, nil, 0 ) -function panel:getSize() +function panel:getDimensions() return self:getWidth(), self:getHeight() end -accessor( panel, "x" ) -accessor( panel, "y" ) +gui.accessor( panel, "x" ) +gui.accessor( panel, "y" ) function panel:getPos() return self:getX(), self:getY() end +function panel:getRootPanel() + local panel = self + while ( panel ~= nil ) do + panel = panel:getParent() + if ( panel:getParent() == nil ) then + return panel + end + end +end + +function panel:getPrevSibling() + local parent = self:getParent() + if ( parent == nil ) then + return + end + + local children = parent:getChildren() + for i, v in ipairs( children ) do + if ( i < 1 and v == self ) then + return children[ i - 1 ] + end + end +end + +function panel:getNextSibling() + local parent = self:getParent() + local parent = self:getParent() + if ( parent == nil ) then + return + end + + local children = parent:getChildren() + for i, v in ipairs( children ) do + if ( i < #children and v == self ) then + return children[ i + 1 ] + end + end +end + function panel:getTopMostChildAtPos( x, y ) if ( not self:isVisible() ) then return nil end local sx, sy = self:localToScreen() - local w, h = self:getSize() + local w, h = self:getDimensions() if ( not math.pointinrect( x, y, sx, sy, w, h ) ) then return nil end @@ -258,6 +317,8 @@ function panel:getTopMostChildAtPos( x, y ) end function panel:invalidate() + panel._invalidations = ( panel._invalidations or 0 ) + 1 + self.needsRedraw = true local parent = self:getParent() @@ -267,17 +328,17 @@ function panel:invalidate() end end -function panel:invalidateFramebuffer() +function panel:invalidateCanvas() local children = self:getChildren() if ( children ) then for _, v in ipairs( children ) do - v:invalidateFramebuffer() + v:invalidateCanvas() end end - if ( self:shouldUseFullscreenFramebuffer() ) then - self.framebuffer = nil - self:createFramebuffer() + if ( self:shouldUseFullscreenCanvas() ) then + self:removeCanvas() + self:createCanvas() end end @@ -454,7 +515,7 @@ function panel:preDraw() end local scale = self:getScale() - local width, height = self:getSize() + local width, height = self:getDimensions() love.graphics.push() love.graphics.translate( self:getX(), self:getY() ) love.graphics.scale( scale ) @@ -480,7 +541,6 @@ function panel:postDraw() if ( gui_element_selection:getBoolean() ) then if ( self.mouseover ) then self:drawSelection() - self:drawName() end end @@ -504,17 +564,21 @@ function panel:remove() local parent = self:getParent() if ( parent ) then local children = parent:getChildren() - for i, v in ipairs( children ) do - if ( v == self ) then - table.remove( children, i ) + if ( children ~= nil ) then + for i, v in ipairs( children ) do + if ( v == self ) then + table.remove( children, i ) + end end - end - if ( #children == 0 ) then - parent.children = nil + if ( #children == 0 ) then + parent.children = nil + end end end + self:removeCanvas() + self:onRemove() end @@ -528,6 +592,13 @@ function panel:removeChildren() self:invalidate() end +function panel:removeCanvas() + if ( self.canvas ) then + self.canvas:remove() + self.canvas = nil + end +end + function panel:screenToLocal( x, y ) local posX, posY = 0, 0 local panel = self @@ -543,8 +614,8 @@ function panel:screenToLocal( x, y ) return x, y end -function panel:setUseFullscreenFramebuffer( useFullscreenFramebuffer ) - self.useFullscreenFramebuffer = useFullscreenFramebuffer and true or nil +function panel:setUseFullscreenCanvas( useFullscreenCanvas ) + self.useFullscreenCanvas = useFullscreenCanvas and true or nil end function panel:setNextThink( nextThink ) @@ -571,11 +642,6 @@ function panel:setParent( panel ) self.parent = panel end -function panel:setScale( scale ) - self.scale = scale - self:invalidate() -end - function panel:setScheme( name ) if ( not scheme.isLoaded( name ) ) then scheme.load( name ) @@ -584,16 +650,16 @@ function panel:setScheme( name ) end function panel:setWidth( width ) - self.width = math.round( width ) + self.width = type( width ) == "number" and math.round( width ) or width self:invalidate() end function panel:setHeight( height ) - self.height = math.round( height ) + self.height = type( height ) == "number" and math.round( height ) or height self:invalidate() end -function panel:setSize( width, height ) +function panel:setDimensions( width, height ) self:setWidth( width ) self:setHeight( height ) end @@ -617,7 +683,7 @@ function panel:setPos( x, y ) self:setY( y ) end -accessor( panel, "useFullscreenFramebuffer", "should" ) +accessor( panel, "useFullscreenCanvas", "should" ) function panel:textinput( text ) return cascadeInputToChildren( self, "textinput", text ) diff --git a/engine/client/gui/progressbar.lua b/engine/client/gui/progressbar.lua index 4e462a63..d05440e3 100644 --- a/engine/client/gui/progressbar.lua +++ b/engine/client/gui/progressbar.lua @@ -11,7 +11,6 @@ local progressbar = gui.progressbar function progressbar:progressbar( parent, name ) gui.box.box( self, parent, name ) self:setDisplay( "block" ) - self:setPosition( "absolute" ) self:setBackgroundColor( self:getScheme( "progressbar.backgroundColor" ) ) self.width = 216 self.height = 2 diff --git a/engine/client/gui/radiobutton.lua b/engine/client/gui/radiobutton.lua index 48e7e69b..9bda9042 100644 --- a/engine/client/gui/radiobutton.lua +++ b/engine/client/gui/radiobutton.lua @@ -14,7 +14,7 @@ function radiobutton:radiobutton( parent, name, text ) self.height = 24 self.icon = self:getScheme( "radiobutton.icon" ) self.foreground = self:getScheme( "radiobutton.foreground" ) - self.text = text or "Radio Button Label" + self.text:set( text ) self.value = nil self.selected = false self.id = -1 @@ -92,7 +92,8 @@ end gui.accessor( radiobutton, "selected", "is" ) function radiobutton:mousereleased( x, y, button, istouch ) - if ( ( self.mousedown and ( self.mouseover or self:isChildMousedOver() ) ) and not self:isDisabled() ) then + local mouseover = ( self.mouseover or self:isChildMousedOver() ) + if ( ( self.mousedown and mouseover ) and not self:isDisabled() ) then local radiobuttongroup = self:getGroup() if ( radiobuttongroup ) then radiobuttongroup:setSelectedId( self.id ) diff --git a/engine/client/gui/radiobuttongroup.lua b/engine/client/gui/radiobuttongroup.lua index d6b47dbf..600bd4d7 100644 --- a/engine/client/gui/radiobuttongroup.lua +++ b/engine/client/gui/radiobuttongroup.lua @@ -78,5 +78,14 @@ function radiobuttongroup:setSelectedId( selectedId, default ) end end +function radiobuttongroup:setValue( value ) + local items = self:getItems() + for i, v in ipairs( items ) do + if ( v:getValue() == value ) then + self:setSelectedId( i ) + end + end +end + function radiobuttongroup:onValueChanged( oldValue, newValue ) end diff --git a/engine/client/gui/rootpanel.lua b/engine/client/gui/rootpanel.lua index adb25368..6de4e1ad 100644 --- a/engine/client/gui/rootpanel.lua +++ b/engine/client/gui/rootpanel.lua @@ -4,9 +4,9 @@ -- --==========================================================================-- -require( "engine.client.gui.panel" ) +require( "engine.client.gui.box" ) -class "gui.rootpanel" ( "gui.panel" ) +class "gui.rootpanel" ( "gui.box" ) local rootpanel = gui.rootpanel @@ -20,11 +20,11 @@ function rootpanel:rootpanel() self.children = {} self.scale = 1 self.opacity = 1 - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) end function rootpanel:invalidateLayout() - self:setSize( love.graphics.getWidth(), love.graphics.getHeight() ) + self:setDimensions( love.graphics.getWidth(), love.graphics.getHeight() ) gui.panel.invalidateLayout( self ) end diff --git a/engine/client/gui/scrollablepanel.lua b/engine/client/gui/scrollablepanel.lua index 9fd94f3e..56339c71 100644 --- a/engine/client/gui/scrollablepanel.lua +++ b/engine/client/gui/scrollablepanel.lua @@ -4,13 +4,15 @@ -- --==========================================================================-- -class "gui.scrollablepanel" ( "gui.panel" ) +class "gui.scrollablepanel" ( "gui.box" ) local scrollablepanel = gui.scrollablepanel function scrollablepanel:scrollablepanel( parent, name ) - gui.panel.panel( self, parent, name ) - self.panel = gui.panel( self, self:getName() .. " Inner Panel" ) + gui.box.box( self, parent, name ) + self.panel = gui.box( self, self:getName() .. " Inner Panel" ) + self.panel:setPosition( "absolute" ) + self.scrollbar = gui.scrollbar( self, self:getName() .. " Scrollbar" ) self.scrollbar.onValueChanged = function( _, oldValue, newValue ) self.panel:setY( -newValue ) @@ -24,7 +26,7 @@ accessor( scrollablepanel, "innerPanel", nil, "panel" ) accessor( scrollablepanel, "scrollbar" ) function scrollablepanel:invalidateLayout() - self:setSize( self:getSize() ) + self:setDimensions( self:getDimensions() ) gui.panel.invalidateLayout( self ) end diff --git a/engine/client/gui/scrollbar.lua b/engine/client/gui/scrollbar.lua index bab5d876..17a9729c 100644 --- a/engine/client/gui/scrollbar.lua +++ b/engine/client/gui/scrollbar.lua @@ -20,8 +20,7 @@ function scrollbar:scrollbar( parent, name ) self.rangeWindow = 0 self.value = 0 - self:setScheme( "Default" ) - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) end function scrollbar:draw() diff --git a/engine/client/gui/tabbedframe.lua b/engine/client/gui/tabbedframe.lua index 10c1e208..1cf4e763 100644 --- a/engine/client/gui/tabbedframe.lua +++ b/engine/client/gui/tabbedframe.lua @@ -19,7 +19,7 @@ function tabbedframe:tabbedframe( parent, name, title ) local width = 2 * padding + iconWidth - 1 - 8 local height = 2 * padding + iconWidth - 3 - self.closeButton:setSize( width, height ) + self.closeButton:setDimensions( width, height ) local font = self:getScheme( "titleFont" ) local titleWidth = font:getWidth( self.title ) @@ -36,7 +36,7 @@ end function tabbedframe:addTab( tabName, tabPanel, default ) self.tabGroup:addTab( tabName, default ) - self.tabPanels:addPanel( tabPanel, default ) + local panel = self.tabPanels:addPanel( tabPanel, default ) local padding = 24 local font = self:getScheme( "titleFont" ) @@ -51,7 +51,7 @@ function tabbedframe:addTab( tabName, tabPanel, default ) + 1 + 8 ) - tabPanel:invalidateLayout() + panel:invalidateLayout() end function tabbedframe:drawBackground() @@ -131,16 +131,17 @@ accessor( tabbedframe, "tabPanels" ) function tabbedframe:invalidateLayout() local padding = 24 - local iconWidth = 16 if ( self.closeButton ) then - self.closeButton:setX( self:getWidth() - 2 * padding - iconWidth ) + self.closeButton:setX( + self:getWidth() - self.closeButton:getWidth() - 8 + ) end local font = self:getScheme( "titleFont" ) local titleWidth = font:getWidth( self.title ) self.tabGroup:setPos( 2 * padding + titleWidth, 1 ) - self.tabPanels:setSize( self:getWidth(), self:getHeight() - 62 ) + self.tabPanels:setDimensions( self:getWidth(), self:getHeight() - 62 ) gui.panel.invalidateLayout( self ) end diff --git a/engine/client/gui/testframe.lua b/engine/client/gui/testframe.lua index 7c820289..6fb4aa6f 100644 --- a/engine/client/gui/testframe.lua +++ b/engine/client/gui/testframe.lua @@ -28,7 +28,7 @@ function testframe:createTestPanels() panelName = "Scrollable Panel" local panel = gui.scrollablepanel( tab, getDebugName() ) panel.invalidateLayout = function( self ) - self:setSize( self:getParent():getSize() ) + self:setDimensions( self:getParent():getDimensions() ) gui.panel.invalidateLayout( self ) end panel:setInnerHeight( 1386 ) @@ -90,12 +90,9 @@ function testframe:createTestPanels() panelName = "Drop-Down List Item" local dropdownlistitem = nil item = getDebugName() - dropdownlistitem = gui.dropdownlistitem( item .. " 1", panelName .. " 1" ) - dropdownlist:addItem( dropdownlistitem ) - dropdownlistitem = gui.dropdownlistitem( item .. " 2", panelName .. " 2" ) - dropdownlist:addItem( dropdownlistitem ) - -- dropdownlistitem = gui.dropdownlistitem( item .. " 3", panelName .. " 3" ) - -- dropdownlist:addItem( dropdownlistitem ) + dropdownlistitem = gui.dropdownlistitem( dropdownlist, item .. " 1", panelName .. " 1" ) + dropdownlistitem = gui.dropdownlistitem( dropdownlist, item .. " 2", panelName .. " 2" ) + -- dropdownlistitem = gui.dropdownlistitem( dropdownlist, item .. " 3", panelName .. " 3" ) panelName = "Slider" local slider = gui.slider( panel, getDebugName() ) @@ -123,7 +120,7 @@ function testframe:createTestPanels() local image = gui.imagepanel( panel, "Image", nil ) x = margin image:setPos( x, 1386 - margin - 32 ) - image:setSize( 32, 32 ) + image:setDimensions( 32, 32 ) self:addTab( "Tab", tab, true ) end diff --git a/engine/client/gui/text.lua b/engine/client/gui/text.lua index d834266a..6aca5629 100644 --- a/engine/client/gui/text.lua +++ b/engine/client/gui/text.lua @@ -8,29 +8,38 @@ class "gui.text" ( "gui.box" ) local text = gui.text -function text:text( parent, name, text ) - gui.box.box( self, parent, name ) - - local width, height = parent:getSize() - if ( width == nil and height == nil ) then - local font = self:getFont() - width, height = font:getWidth( text ), font:getHeight() - end - - self:createFramebuffer( width, height ) +function text:text( parent, text ) + gui.box.box( self, parent, nil ) + self:set( text ) +end - self:setText( text ) +function text:createCanvas() end function text:draw() + assert( false ) love.graphics.setColor( self:getColor() ) - love.graphics.setFont( self:getFont() ) - love.graphics.printf( self:getText(), 0, 0, self:getWidth() ) + love.graphics.draw( self._text ) +end + +function text:drawCanvas() + if ( not self:isVisible() ) then + return + end + + love.graphics.push() + local a = self:getOpacity() + local c = self:getColor() + love.graphics.setColor( + a * c[ 1 ], a * c[ 2 ], a * c[ 3 ], a * c[ 4 ] + ) + love.graphics.draw( self._text ) + love.graphics.pop() end function text:getWidth() local font = self:getFont() - return font:getWidth( self:getText() ) + return font:getWidth( self:get() ) end function text:getHeight() @@ -38,14 +47,26 @@ function text:getHeight() return font:getHeight() end -gui.accessor( text, "text" ) +function text:set( text ) + self.text = text + + if ( self._text ) then + self._text:set( text or "" ) + else + self._text = love.graphics.newText( self:getFont(), text ) + end +end + +text.setText = text.set -function text:getText() +function text:get() return rawget( self, "text" ) or "" end -gui.accessor( text, "font" ) +function text:setFont( font ) + self._text:setFont( font ) +end function text:getFont() - return self.font or self:getScheme( "font" ) + return self._text and self._text:getFont() or self:getScheme( "font" ) end diff --git a/engine/client/gui/textbox.lua b/engine/client/gui/textbox.lua index b12480d0..ccfc843b 100644 --- a/engine/client/gui/textbox.lua +++ b/engine/client/gui/textbox.lua @@ -43,8 +43,7 @@ function textbox:textbox( parent, name, placeholder ) self.editable = true self.multiline = false - self:setScheme( "Default" ) - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) end function textbox:draw() @@ -215,9 +214,10 @@ local function updateAutocomplete( self, suggestions ) local dropdownlistitem = nil local name = "Autocomplete Drop-Down List Item" for i, suggestion in pairs( suggestions ) do - dropdownlistitem = gui.dropdownlistitem( name .. " " .. i, suggestion ) + dropdownlistitem = gui.dropdownlistitem( + self.autocompleteItemGroup, name .. " " .. i, suggestion + ) dropdownlistitem:setValue( suggestion ) - self.autocompleteItemGroup:addItem( dropdownlistitem ) end end @@ -326,6 +326,7 @@ end accessor( textbox, "autocomplete" ) accessor( textbox, "defocusOnEnter" ) +accessor( textbox, "maxLength" ) accessor( textbox, "placeholder" ) accessor( textbox, "text" ) @@ -341,13 +342,9 @@ local function updateScrollbarRange( self ) end function textbox:insertText( text ) - local underflow = getTextWidth( self ) < getInnerWidth( self ) - local font = self:getScheme( "font" ) - local sub1 = utf8sub( self.text, self.cursorPos + 1 ) - local sub2 = utf8sub( self.text, 1, self.cursorPos ) - if ( self.cursorPos == 0 ) then - sub2 = "" - end + local underflow = getTextWidth( self ) < getInnerWidth( self ) + local font = self:getScheme( "font" ) + local resetScrollOffset = false if ( getTextWidth( self ) + font:getWidth( text ) > getInnerWidth( self ) ) then @@ -361,12 +358,27 @@ function textbox:insertText( text ) end end - self.text = sub2 .. text .. sub1 + -- Text after cursor. + local sub1 = utf8sub( self.text, 1, self.cursorPos ) + if ( self.cursorPos == 0 ) then + sub1 = "" + end + + -- Text after cursor. + local sub2 = utf8sub( self.text, self.cursorPos + 1 ) + + -- Insert text. + self.text = sub1 .. text .. sub2 + + -- Truncate the head. + local maxLength = self:getMaxLength() + if ( maxLength ) then + self.text = utf8sub( self.text, -maxLength ) + end + updateOverflow( self ) if ( resetScrollOffset ) then self.scrollOffset = 0 - underflow = false - resetScrollOffset = false end self.cursorPos = self.cursorPos + utf8len( text ) @@ -502,6 +514,14 @@ function textbox:keypressed( key, scancode, isrepeat ) if ( key == "backspace" ) then self:doBackspace( controlDown and math.abs( nextWord( self, -1 ) ) or 1 ) elseif ( key == "delete" ) then + if ( self.autocompleteItemGroup ) then + if ( self.autocompleteItemGroup:keypressed( + key, scancode, isrepeat + ) ) then + return true + end + end + self:doDelete( controlDown and nextWord( self, 1 ) or 1 ) elseif ( key == "end" ) then if ( getTextWidth( self ) + getTextX( self ) > @@ -701,6 +721,15 @@ function textbox:setEditable( editable ) self.canFocus = editable end +function textbox:setMaxLength( maxLength ) + self.maxLength = math.max( 0, maxLength ) + + local text = self:getText() + if ( string.utf8len( text ) > maxLength ) then + self:setText( utf8sub( text, 1, maxLength ) ) + end +end + function textbox:setMultiline( multiline ) self.multiline = multiline if ( multiline ) then @@ -714,6 +743,11 @@ function textbox:setMultiline( multiline ) end function textbox:setText( text ) + local maxLength = self:getMaxLength() + if ( maxLength ) then + text = utf8sub( text, 1, maxLength ) + end + self.text = text self.cursorPos = utf8len( text ) diff --git a/engine/client/gui/textboxautocompleteitemgroup.lua b/engine/client/gui/textboxautocompleteitemgroup.lua index 1b8e415c..fadfd0bb 100644 --- a/engine/client/gui/textboxautocompleteitemgroup.lua +++ b/engine/client/gui/textboxautocompleteitemgroup.lua @@ -4,8 +4,6 @@ -- --==========================================================================-- -local gui = gui - class "gui.textboxautocompleteitemgroup" ( "gui.dropdownlistitemgroup" ) local textboxautocompleteitemgroup = gui.textboxautocompleteitemgroup @@ -35,22 +33,6 @@ end accessor( textboxautocompleteitemgroup, "textbox" ) -function textboxautocompleteitemgroup:invalidateLayout() - self:updatePos() - self:setWidth( self:getTextbox():getWidth() ) - - local listItems = self:getItems() - if ( listItems ) then - local y = 0 - for _, listItem in ipairs( listItems ) do - listItem:setY( y ) - listItem:setWidth( self:getWidth() ) - y = y + listItem:getHeight() - end - self:setHeight( y ) - end -end - function textboxautocompleteitemgroup:isVisible() local textbox = self:getTextbox() local children = self:getChildren() @@ -74,11 +56,3 @@ end function textboxautocompleteitemgroup:onValueChanged( oldValue, newValue ) end - -function textboxautocompleteitemgroup:updatePos() - local textbox = self:getTextbox() - if ( textbox ) then - local sx, sy = textbox:localToScreen() - self:setPos( sx, sy + textbox:getHeight() ) - end -end diff --git a/engine/client/gui/throbber.lua b/engine/client/gui/throbber.lua index 1af92ef7..c56c873a 100644 --- a/engine/client/gui/throbber.lua +++ b/engine/client/gui/throbber.lua @@ -10,7 +10,7 @@ local throbber = gui.throbber function throbber:throbber( parent, name, image ) gui.imagepanel.imagepanel( self, parent, name, image or "images/gui/throbber.png" ) - self:setSize( 16, 16 ) + self:setDimensions( 16, 16 ) self:setOpacity( 0 ) end diff --git a/engine/client/gui/viewport.lua b/engine/client/gui/viewport.lua index ef5676f4..9dbf16f5 100644 --- a/engine/client/gui/viewport.lua +++ b/engine/client/gui/viewport.lua @@ -4,20 +4,20 @@ -- --==========================================================================-- -class "gui.viewport" ( "gui.panel" ) +class "gui.viewport" ( "gui.box" ) local viewport = gui.viewport function viewport:viewport( parent ) - gui.panel.panel( self, parent, "Viewport" ) + gui.box.box( self, parent, "Viewport" ) self.width = love.graphics.getWidth() self.height = love.graphics.getHeight() - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) self:moveToBack() end function viewport:invalidateLayout() - self:setSize( love.graphics.getWidth(), love.graphics.getHeight() ) + self:setDimensions( love.graphics.getWidth(), love.graphics.getHeight() ) gui.panel.invalidateLayout( self ) end diff --git a/engine/client/gui/watch.lua b/engine/client/gui/watch.lua new file mode 100644 index 00000000..c63ab7d1 --- /dev/null +++ b/engine/client/gui/watch.lua @@ -0,0 +1,59 @@ +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- +-- +-- Purpose: Watch class +-- +--==========================================================================-- + +class "gui.watch" ( "gui.label" ) + +local watch = gui.watch + +function watch:watch( parent, name ) + gui.label.label( self, parent, name, text ) + + self:setScheme( "Console" ) + self.font = self:getScheme( "font" ) + local margin = gui.scale( 96 ) + self.width = love.graphics.getWidth() - 2 * margin + self.height = self.font:getHeight() + + self:setScheme( "Default" ) + self:invalidateLayout() +end + +function watch:update( dt ) + -- HACKHACK: Fade this out for readability. + if ( g_HudMoveIndicator and g_HudMoveIndicator._entity ) then + self:setOpacity( 0 ) + else + self:setOpacity( 1 ) + end + + local f, err = loadstring( "return " .. self:getExpression() ) + local ret = "?" + if ( f ) then + local status, err = pcall( f ) + if ( status == false ) then + self:invalidate() + return + end + else + ret = err + end + + self:setText( self:getExpression() .. " = " .. tostring( + f and f() or ret + ) ) + self:invalidate() +end + +gui.accessor( watch, "expression" ) + +function watch:invalidateLayout() + local margin = gui.scale( 96 ) + local x = margin + local y = margin + 28 + self:setPos( x, y ) + + gui.panel.invalidateLayout( self ) +end diff --git a/engine/client/handlers.lua b/engine/client/handlers.lua index 41437664..166f6b06 100644 --- a/engine/client/handlers.lua +++ b/engine/client/handlers.lua @@ -156,12 +156,19 @@ function update( dt ) local _SERVER = _SERVER or _G._SERVER if ( _CLIENT and not _SERVER ) then - _G.map.update( dt ) - end - - local game = _G.game and _G.game.client or nil - if ( game ) then - game.update( dt ) + local game = _G.game and _G.game.client or nil + local entity = _G.entity + + if ( game ) then + game.update( dt ) + + if ( entity ) then + local entities = entity.getAll() + for _, entity in ipairs( entities ) do + entity:update( dt ) + end + end + end end local network = engine.client.network @@ -179,18 +186,21 @@ function draw() end if ( isInGame() ) then - if ( gui._viewportFramebuffer == nil ) then + if ( gui._viewportCanvas == nil ) then require( "engine.client.canvas" ) - gui._viewportFramebuffer = _G.fullscreencanvas() + gui._viewportCanvas = _G.fullscreencanvas( nil, nil, { + dpiscale = 1 + } ) + gui._viewportCanvas:setFilter( "nearest", "nearest" ) end - gui._viewportFramebuffer:renderTo( function() + gui._viewportCanvas:renderTo( function() love.graphics.clear() _G.game.client.draw() end ) love.graphics.setColor( _G.color.white ) - gui._viewportFramebuffer:draw() + gui._viewportCanvas:draw() end gui.draw() diff --git a/engine/client/init.lua b/engine/client/init.lua index ef770aeb..5e471428 100644 --- a/engine/client/init.lua +++ b/engine/client/init.lua @@ -9,12 +9,15 @@ require( "engine.client.gui" ) require( "engine.shared.network.payload" ) local concommand = concommand +local convar = convar local engine = engine local gui = gui +local ipairs = ipairs local love = love local payload = payload local pcall = pcall local print = print +local profile = profile local require = require local tostring = tostring local unrequire = unrequire @@ -152,8 +155,9 @@ function onDisconnect( event ) -- Shutdown game local game = _G.game and _G.game.client or nil if ( game ) then - gui._viewportFramebuffer = nil - gui._translucencyFramebuffer = nil + gui._viewportCanvas:remove() + gui._viewportCanvas = nil + gui._translucencyCanvas = nil game.shutdown() unrequire( "game.client" ) _G.game.client = nil @@ -184,16 +188,97 @@ function onDisconnect( event ) engine.client.network = nil end -function onTick( timestep ) - local game = _G.game and _G.game.client or nil - if ( game ) then - game.onTick( timestep ) - end -end - function sendClientInfo() local payload = payload( "clientInfo" ) payload:set( "graphicsWidth", love.graphics.getWidth() ) payload:set( "graphicsHeight", love.graphics.getHeight() ) payload:sendToServer() end + +local voice_loopback = convar( "voice_loopback", "0", nil, nil ) + +function broadcastVoiceRecording() + local recordingDevice = love.audio.getRecordingDevices()[ 1 ] + + if ( source == nil ) then + source = love.audio.newQueueableSource( 32000, 16, 1 ) + end + + local data = recordingDevice:getData() + if ( _voiceRecording and data ) then + if ( voice_loopback:getBoolean() ) then + source:queue( data ) + love.audio.play( source ) + end + + local payload = payload( "voice" ) + payload:set( "data", data:getString() ) + payload:broadcast() + end +end + +function sendVoiceRecording() + local recordingDevice = love.audio.getRecordingDevices()[ 1 ] + + if ( source == nil ) then + source = love.audio.newQueueableSource( 32000, 16, 1 ) + end + + local data = recordingDevice:getData() + if ( _voiceRecording and data ) then + if ( voice_loopback:getBoolean() ) then + source:queue( data ) + love.audio.play( source ) + end + + local payload = payload( "voice" ) + payload:set( "data", data:getString() ) + payload:sendToServer() + end +end + +function startVoiceRecording() + local recordingDevice = love.audio.getRecordingDevices()[ 1 ] + if ( not recordingDevice:isRecording() ) then + recordingDevice:start( 32768, 32000 ) + end + _voiceRecording = true +end + +function stopVoiceRecording() + -- BUGBUG: This is expensive. + -- local recordingDevice = love.audio.getRecordingDevices()[ 1 ] + -- recordingDevice:stop() + _voiceRecording = false +end + +concommand( "+voice", "Starts recording voice", startVoiceRecording ) +concommand( "-voice", "Stops recording voice", stopVoiceRecording ) + +function tick( timestep ) + local game = _G.game and _G.game.client or nil + local entity = _G.entity + local map = _G.map + + if ( game == nil ) then + return + end + + game.tick( timestep ) + + if ( entity ) then + local entities = entity.getAll() + for _, entity in ipairs( entities ) do + entity:tick( timestep ) + end + end + + map.tick( timestep ) + + if ( entity ) then + local entities = entity.getAll() + for _, entity in ipairs( entities ) do + entity:onPostWorldUpdate( timestep ) + end + end +end diff --git a/engine/client/network/init.lua b/engine/client/network/init.lua index c153358b..ef61debf 100644 --- a/engine/client/network/init.lua +++ b/engine/client/network/init.lua @@ -93,8 +93,7 @@ end local cl_updaterate = convar( "cl_updaterate", 20, nil, nil, "Sets the client tick rate" ) --- local timestep = 1/20 -_accumulator = _accumulator or 0 +_accumulator = _accumulator or 0 function update( dt ) if ( _host == nil ) then @@ -102,21 +101,11 @@ function update( dt ) end local timestep = 1 / cl_updaterate:getNumber() - _accumulator = _accumulator + dt + _accumulator = _accumulator + dt while ( _accumulator >= timestep ) do + engine.client.tick( timestep ) pollEvents() - - local entity = _G.entity - if ( entity ) then - local entities = entity.getAll() - for _, entity in ipairs( entities ) do - entity:onTick( timestep ) - end - end - - engine.client.onTick( timestep ) - _accumulator = _accumulator - timestep end end @@ -131,8 +120,8 @@ function updateSentReceived() _prevTotalRcvdData = _totalRcvdData or 0 _totalSentData = _host:total_sent_data() _totalRcvdData = _host:total_received_data() - _avgSentData = ( _totalSentData - _prevTotalSentData ) - _avgRcvdData = ( _totalRcvdData - _prevTotalRcvdData ) + _avgSentData = _totalSentData - _prevTotalSentData + _avgRcvdData = _totalRcvdData - _prevTotalRcvdData _sentRcvdUpdateTime = love.timer.getTime() + 1 end diff --git a/engine/client/payloads.lua b/engine/client/payloads.lua index adb60aaa..db1f26b7 100644 --- a/engine/client/payloads.lua +++ b/engine/client/payloads.lua @@ -14,6 +14,7 @@ local function onReceivePlayerInitialized( payload ) camera.setZoom( 2 ) if ( not _SERVER ) then + localplayer:setGraphicsSize( love.graphics.getDimensions() ) localplayer:initialSpawn() end end diff --git a/engine/init.lua b/engine/init.lua index 66a0daaf..df95e9be 100644 --- a/engine/init.lua +++ b/engine/init.lua @@ -28,7 +28,10 @@ local os = os local package = package local pairs = pairs local print = print +local profile = profile local require = require +local string = string +local table = table local _DEBUG = _DEBUG local _CLIENT = _CLIENT local _SERVER = _SERVER @@ -61,59 +64,67 @@ for k in pairs( love.handlers ) do end end -local fps_max = convar( "fps_max", "300", nil, nil, "Frame rate limiter" ) - -function love.run() - if love.load then love.load(love.arg.parseGameArguments(arg), arg) end - - -- We don't want the first frame's dt to include time taken by love.load. - if love.timer then love.timer.step() end - - local dt = 0 - local startTime = 0 - local endTime = 0 - local duration = 0 - local remaining = 0 - - -- Main loop time. - return function() - if love.timer then startTime = love.timer.getTime() end - - -- Process events. - if love.event then - love.event.pump() - for name, a,b,c,d,e,f in love.event.poll() do - if name == "quit" then - if not love.quit or not love.quit() then - return a or 0 - end - end - love.handlers[name](a,b,c,d,e,f) - end - end - - -- Update dt, as we'll be passing it to update - if love.timer then dt = love.timer.step() end - - -- Call update and draw - if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled - - if love.graphics and love.graphics.isActive() then - love.graphics.origin() - love.graphics.clear(love.graphics.getBackgroundColor()) - - if love.draw then love.draw() end - - love.graphics.present() - end - - if love.timer then endTime = love.timer.getTime() end - duration = endTime - startTime - remaining = math.max( 0, 1 / fps_max:getNumber() - duration ) - if love.timer and remaining > 0 then love.timer.sleep(remaining) end - end - -end +-- local fps_max = convar( "fps_max", "300", nil, nil, "Frame rate limiter" ) +-- +-- function love.run() +-- if love.load then love.load(love.arg.parseGameArguments(arg), arg) end +-- +-- -- We don't want the first frame's dt to include time taken by love.load. +-- if love.timer then love.timer.step() end +-- +-- -- Main loop time. +-- return function() +-- engine.run() +-- end +-- +-- end +-- +-- function run() +-- local dt = 0 +-- local startTime = 0 +-- local endTime = 0 +-- local duration = 0 +-- local remaining = 0 +-- +-- if love.timer then startTime = love.timer.getTime() end +-- +-- -- Process events. +-- if love.event then +-- love.event.pump() +-- for name, a,b,c,d,e,f in love.event.poll() do +-- if name == "quit" then +-- if not love.quit or not love.quit() then +-- return a or 0 +-- end +-- end +-- love.handlers[name](a,b,c,d,e,f) +-- end +-- end +-- +-- -- Update dt, as we'll be passing it to update +-- if love.timer then dt = love.timer.step() end +-- +-- -- Call update and draw +-- if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled +-- +-- if love.graphics and love.graphics.isActive() then +-- profile.push( "draw" ) +-- love.graphics.origin() +-- love.graphics.clear(love.graphics.getBackgroundColor()) +-- +-- if love.draw then love.draw() end +-- profile.pop( "draw" ) +-- +-- profile.push( "present" ) +-- love.graphics.present() +-- profile.pop( "present" ) +-- end +-- +-- if love.timer then endTime = love.timer.getTime() end +-- duration = endTime - startTime +-- remaining = math.max( 0, 1 / fps_max:getNumber() - duration ) +-- if love.timer and remaining > 0 then love.timer.sleep(remaining) end +-- end function love.focus( focus ) if ( focus ) then @@ -140,21 +151,34 @@ end function love.load( arg ) math.randomseed( os.time() ) - if ( _SERVER ) then - engine.server.load( arg ) - end - if ( _CLIENT ) then engine.client.load( arg ) end - print( "Grid Engine 8" ) - print( "Made by Planimeter in Arizona" ) + if ( _SERVER ) then + engine.server.load( arg ) + end + + print( "Grid Engine [Version 9]" ) + print( "© 2019 Planimeter. All rights reserved.\r\n" ) require( "engine.shared.addon" ) _G.addon.load( arg ) require( "engine.shared.map" ) + + if ( _CLIENT ) then + table.foreachi( arg, function( i, v ) + local name = string.match( v, "^%+(.-)$" ) + if ( name == nil ) then + return + end + + concommand.run( name .. " " .. arg[ i + 1 ] ) + end ) + end + + profile.pop( "load" ) end function love.quit() @@ -184,41 +208,29 @@ end ) local host_timescale = convar( "host_timescale", "1", nil, nil, "Prescales the clock by this amount" ) -local timestep = 1/100 -local accumulator = 0 function love.update( dt ) + profile.push( "update" ) + dt = host_timescale:getNumber() * dt if ( _DEBUG and _DEDICATED ) then package.update( dt ) end - accumulator = accumulator + dt - - while ( accumulator >= timestep ) do - local entity = _G.entity - local _CLIENT = _CLIENT - local _SERVER = _SERVER or _G._SERVER - - if ( entity ) then - local entities = entity.getAll() - for _, entity in ipairs( entities ) do - entity:update( timestep ) - end - end - - if ( _SERVER ) then - engine.server.update( timestep ) - end + local _CLIENT = _CLIENT + local _SERVER = _SERVER or _G._SERVER - if ( _CLIENT ) then - engine.client.update( timestep ) - end + if ( _CLIENT ) then + engine.client.update( dt ) + end - accumulator = accumulator - timestep + if ( _SERVER ) then + engine.server.update( dt ) end if ( _CLIENT ) then gui.update( dt ) end + + profile.pop( "update" ) end diff --git a/engine/server/handlers.lua b/engine/server/handlers.lua index f6540a6f..efdd4e33 100644 --- a/engine/server/handlers.lua +++ b/engine/server/handlers.lua @@ -61,11 +61,18 @@ function quit() end function update( dt ) - _G.map.update( dt ) + local game = _G.game and _G.game.server or nil + local entity = _G.entity - local game = _G.game and _G.game.server or nil if ( game ) then game.update( dt ) + + if ( entity ) then + local entities = entity.getAll() + for _, entity in ipairs( entities ) do + entity:update( dt ) + end + end end local network = engine.server.network diff --git a/engine/server/init.lua b/engine/server/init.lua index 266866eb..583a37e9 100644 --- a/engine/server/init.lua +++ b/engine/server/init.lua @@ -6,11 +6,15 @@ require( "engine.shared.network.payload" ) +local convar = convar +local engine = engine +local ipairs = ipairs local payload = payload local print = print local map = map local require = require local tostring = tostring +local _CLIENT = _CLIENT local _G = _G module( "engine.server" ) @@ -65,13 +69,6 @@ function onDisconnect( event ) print( tostring( event.peer ) .. " has disconnected." ) end -function onTick( timestep ) - local game = _G.game and _G.game.server or nil - if ( game ) then - game.onTick( timestep ) - end -end - function sendServerInfo( player ) local payload = payload( "serverInfo" ) payload:set( "map", _G.game.initialMap ) @@ -80,7 +77,40 @@ end shutdown = quit +function tick( timestep ) + local game = _G.game and _G.game.server or nil + local entity = _G.entity + local map = _G.map + + if ( game == nil ) then + return + end + + game.tick( timestep ) + + if ( entity ) then + local entities = entity.getAll() + for _, entity in ipairs( entities ) do + entity:tick( timestep ) + end + end + + map.tick( timestep ) + + if ( entity ) then + local entities = entity.getAll() + for _, entity in ipairs( entities ) do + entity:onPostWorldUpdate( timestep ) + end + end + + if ( _CLIENT ) then + engine.client.broadcastVoiceRecording() + end +end + function upload( filename, peer ) + -- TODO: Split by MTU of less than 1500 bytes. local payload = payload( "upload" ) payload:set( "filename", filename ) payload:set( "file", love.filesystem.read( filename ) ) diff --git a/engine/server/network/init.lua b/engine/server/network/init.lua index 36bd72df..9af7b1ed 100644 --- a/engine/server/network/init.lua +++ b/engine/server/network/init.lua @@ -78,33 +78,22 @@ function shutdownServer() collectgarbage() end -local sv_updaterate = convar( "sv_updaterate", 20, nil, nil, - "Sets the server tick rate" ) +local tickrate = convar( "tickrate", 20, nil, nil, + "Sets the server tick rate" ) --- local timestep = 1/20 -_accumulator = _accumulator or 0 +_accumulator = _accumulator or 0 function update( dt ) if ( _host == nil ) then return end - local timestep = 1 / sv_updaterate:getNumber() - _accumulator = _accumulator + dt + local timestep = 1 / tickrate:getNumber() + _accumulator = _accumulator + dt while ( _accumulator >= timestep ) do + engine.server.tick( timestep ) pollEvents() - - local entity = _G.entity - if ( entity ) then - local entities = entity.getAll() - for _, entity in ipairs( entities ) do - entity:onTick( timestep ) - end - end - - engine.server.onTick( timestep ) - _accumulator = _accumulator - timestep end end @@ -119,12 +108,20 @@ function updateSentReceived() _prevTotalRcvdData = _totalRcvdData or 0 _totalSentData = _host:total_sent_data() _totalRcvdData = _host:total_received_data() - _avgSentData = ( _totalSentData - _prevTotalSentData ) / 1000 - _avgRcvdData = ( _totalRcvdData - _prevTotalRcvdData ) / 1000 + _avgSentData = _totalSentData - _prevTotalSentData + _avgRcvdData = _totalRcvdData - _prevTotalRcvdData _sentRcvdUpdateTime = love.timer.getTime() + 1 end function pollEvents() + local entity = _G.entity + if ( entity ) then + local entities = entity.getAll() + for _, entity in ipairs( entities ) do + entity:broadcastNetworkVarChanges() + end + end + local event = _host:service() while ( event ~= nil ) do if ( event.type == "connect" ) then diff --git a/engine/shared/baselib.lua b/engine/shared/baselib.lua index aca2f2db..5baa35a5 100644 --- a/engine/shared/baselib.lua +++ b/engine/shared/baselib.lua @@ -57,6 +57,11 @@ if ( rawprint == nil and rawtype == nil ) then end end +function toboolean( v ) + local n = tonumber( v ) + return n ~= nil and n ~= 0 +end + function typeof( object, class ) if ( type( object ) == class ) then return true @@ -144,3 +149,25 @@ concommand( "lua_dostring", "Loads and runs the given string", end end ) + +concommand( "lua_watch", "Watches an expression", + function( self, player, command, argString, argTable ) + if ( argTable[ 1 ] == nil ) then + print( "lua_watch " ) + return + end + + if ( _G.g_Watch ) then + _G.g_Watch:remove() + end + + if ( argTable[ 1 ] == "nil" ) then + return + end + + -- Initialize watch expression + local watch = gui.watch( _G.g_Viewport ) + watch:setExpression( argString ) + _G.g_Watch = watch + end +) diff --git a/engine/shared/concommand.lua b/engine/shared/concommand.lua index d81e328c..7107e88a 100644 --- a/engine/shared/concommand.lua +++ b/engine/shared/concommand.lua @@ -32,12 +32,10 @@ function concommand.dispatch( player, name, argString, argTable ) return true end end - - concommand:callback( player, name, argString, argTable ) - else - concommand:callback( player, name, argString, argTable ) end + concommand:callback( player, name, argString, argTable ) + if ( flags ) then local networked = table.hasvalue( flags, "network" ) if ( _CLIENT and networked ) then @@ -55,7 +53,7 @@ if ( _CLIENT ) then function concommand.run( name ) local command = string.match( name, "^([^%s]+)" ) if ( command == nil ) then - return + return false end local _, endPos = string.find( name, command, 1, true ) @@ -63,7 +61,16 @@ if ( _CLIENT ) then local argTable = string.parseargs( argString ) if ( concommand.getConcommand( command ) ) then concommand.dispatch( localplayer, command, argString, argTable ) + return true + end + + local convar = convar.getConvar( command ) + if ( convar ) then + convar:setValue( argString ) + return true end + + return false end end diff --git a/engine/shared/config.lua b/engine/shared/config.lua index cd706193..17c8cf16 100644 --- a/engine/shared/config.lua +++ b/engine/shared/config.lua @@ -6,9 +6,10 @@ require( "engine.shared.convar" ) -local convar = convar -local love = love -local tonumber = tonumber +local convar = convar +local love = love +local math = math +local tonumber = tonumber module( "config" ) @@ -21,12 +22,14 @@ local function createConvars() "Sets the height of the window on load", nil, { "archive" } ) convar( "r_window_fullscreen", "0", nil, nil, "Toggles fullscreen mode", nil, { "archive" } ) + convar( "r_window_fullscreentype", "desktop", nil, nil, + "Sets the fullscreen type on load", nil, { "archive" } ) convar( "r_window_vsync", "1", nil, nil, "Toggles vertical synchronization", nil, { "archive" } ) convar( "r_window_borderless", "0", nil, nil, "Toggles borderless mode", nil, { "archive" } ) convar( "r_window_highdpi", "1", nil, nil, - "Toggles high-dpi mode", nil, { "archive" } ) + "Sets the high-dpi mode on load", nil, { "archive" } ) local function updateVolume( convar ) local volume = convar:getNumber() @@ -57,25 +60,30 @@ local function toboolean( v ) end function setWindow( c ) - local r_window_width = convar.getConfig( "r_window_width" ) - local r_window_height = convar.getConfig( "r_window_height" ) - local r_window_fullscreen = convar.getConfig( "r_window_fullscreen" ) - local r_window_vsync = convar.getConfig( "r_window_vsync" ) - local r_window_borderless = convar.getConfig( "r_window_borderless" ) - local r_window_highdpi = convar.getConfig( "r_window_highdpi" ) + local r_window_width = convar.getConfig( "r_window_width" ) + local r_window_height = convar.getConfig( "r_window_height" ) + local r_window_fullscreen = convar.getConfig( "r_window_fullscreen" ) + local r_window_fullscreentype = convar.getConfig( "r_window_fullscreentype" ) + local r_window_vsync = convar.getConfig( "r_window_vsync" ) + local r_window_borderless = convar.getConfig( "r_window_borderless" ) + local r_window_highdpi = convar.getConfig( "r_window_highdpi" ) if ( r_window_width ) then - c.window.width = tonumber( r_window_width ) + c.window.width = math.max( 800, tonumber( r_window_width ) ) end if ( r_window_height ) then - c.window.height = tonumber( r_window_height ) + c.window.height = math.max( 600, tonumber( r_window_height ) ) end if ( r_window_fullscreen ) then c.window.fullscreen = toboolean( r_window_fullscreen ) end + if ( r_window_fullscreentype ) then + c.window.fullscreentype = r_window_fullscreentype + end + if ( r_window_vsync ) then c.window.vsync = toboolean( r_window_vsync ) end diff --git a/engine/shared/convar.lua b/engine/shared/convar.lua index b506e568..eb9a3696 100644 --- a/engine/shared/convar.lua +++ b/engine/shared/convar.lua @@ -161,25 +161,25 @@ end function convar:setValue( value ) local oldValue = self.value - self.value = value - - local numberValue = tonumber( self.value ) - if ( ( type( self.value ) == "number" or numberValue ) and - ( self.min and self.max ) ) then - self.value = math.min( self.max, math.max( self.min, numberValue ) ) + local numValue = tonumber( value ) + local isNumber = type( value ) == "number" or numValue + local min, max = self:getMin(), self:getMax() + if ( isNumber and min and max ) then + self.value = math.clamp( numValue, min, max ) + else + self.value = value end + self:onValueChange( oldValue, self.value ) + if ( _SERVER ) then local shouldNotify = self:isFlagSet( "notify" ) if ( shouldNotify ) then local name = self:getName() local text = "Server cvar " .. name .. " changed to " .. self.value player.sendTextAll( text ) - return true end end - - self:onValueChange( oldValue, value ) end function convar:__tostring() diff --git a/engine/shared/entities/character.lua b/engine/shared/entities/character.lua index a18eca0c..39af97c1 100644 --- a/engine/shared/entities/character.lua +++ b/engine/shared/entities/character.lua @@ -23,102 +23,10 @@ function character:addTask( task, name ) } ) end -function character:move( dt ) - if ( self._path == nil ) then - return - end - - -- Get direction to move - local start = self:getPosition() - local next = self._path[ 1 ] - local direction = ( next - start ) - direction:normalizeInPlace() - - -- Apply move speed to directional vector - local velocity = ( self:getNetworkVar( "moveSpeed" ) * dt ) * direction - - -- Where we'll move to - local newPosition = start + velocity - - -- If we change direction, don't apply linear impulse - local applyLinearImpulse = true - - -- Ensure we're not passing the next tile by comparing the - -- distance traveled to the distance to the next tile - if ( velocity:length() >= ( next - start ):length() ) then - newPosition = next - table.remove( self._path, 1 ) - - self:onMoveTo( newPosition ) - - -- If we're holding down a movement key, or we clicked-to-move, - -- `self._nextPosition` was set - if ( self._nextPosition ) then - local path = path.getPath( newPosition, self._nextPosition ) - if ( path ) then - self._path = path - applyLinearImpulse = false - end - - self._nextPosition = nil - end - end - - -- Move - local body = self:getBody() - if ( body and not body:isDestroyed() ) then - if ( applyLinearImpulse ) then - local initialVelocity = vector( body:getLinearVelocity() ) - local delta = velocity - initialVelocity - local mass = body:getMass() - body:applyLinearImpulse( delta.x * mass, delta.y * mass ) - else - body:setLinearVelocity( 0, 0 ) - end - - body:setPosition( newPosition.x, newPosition.y ) - end - - -- We've reached our goal - if ( self._path and #self._path == 0 ) then - local body = self:getBody() - if ( body ) then - body:setLinearVelocity( 0, 0 ) - body:setPosition( newPosition.x, newPosition.y ) - end - - self._path = nil - self:onFinishMove() - end -end - function character:moveTo( position, callback ) - local cl_predict = convar.getConvar( "cl_predict" ) - if ( _CLIENT and not _SERVER and not cl_predict:getBoolean() ) then - return - end - - if ( self._nextPosition ) then - return false - end - - local from = self:getPosition() - local to = position - local fromX = from.x - local fromY = from.y - local toX = to.x - local toY = to.y - fromX, fromY = map.roundToGrid( fromX, fromY ) - toX, toY = map.roundToGrid( toX, toY ) - if ( fromX == toX and fromY == toY ) then - self._moveCallback = callback - self:onFinishMove() - return false + if ( callback ) then + callback() end - - self._nextPosition = position - self._moveCallback = callback - return true end function character:nextTask() @@ -130,117 +38,13 @@ function character:nextTask() end end -function character:onFinishMove() - if ( self._moveCallback ) then - self._moveCallback() - self._moveCallback = nil - end -end - -function character:onMoveTo( position ) -end - -function character:onTick( dt ) - entity.onTick( self ) - - -- Don't animate if we can't calculate directional velocity - local body = self:getBody() - if ( body == nil or body:isDestroyed() ) then - self._lastDirection = nil - self._lastVelocity = nil - return - end - - -- Remember our last velocity for determining "idle" animation - local currentVelocity = vector( body:getLinearVelocity() ) - local moving = currentVelocity:lengthSqr() ~= 0 - if ( moving ) then - self._lastDirection = currentVelocity:normalize() - end - self._lastVelocity = currentVelocity -end - function character:removeTasks() self._tasks = nil end -function character:update( dt ) +function character:tick( timestep ) self:updateTasks() - self:updateMovement( dt ) - self:updateAnimation() - - entity.update( self, dt ) -end - -function character:updateAnimation() - local cl_predict = convar.getConvar( "cl_predict" ) - if ( _CLIENT and not _SERVER and not cl_predict:getBoolean() ) then - return - end - - -- Don't animate if we can't calculate directional velocity - local body = self:getBody() - if ( body == nil or body:isDestroyed() ) then - return - end - - -- Set "idle" animation if we haven't moved for two frames - local lastVelocity = self._lastVelocity or vector.origin - local currentVelocity = vector( body:getLinearVelocity() ) - if ( lastVelocity == vector.origin and - currentVelocity == vector.origin ) then - -- Find our nearest animation direction then set it - local direction = self._lastDirection or vector( 0, -1 ) - local angle = math.nearestmult( math.deg( direction:toAngle() ), 90 ) - if ( angle == -90 ) then - self:setAnimation( "idlenorth" ) - elseif ( angle == 0 ) then - self:setAnimation( "idleeast" ) - elseif ( angle == 90 ) then - self:setAnimation( "idlesouth" ) - elseif ( angle == 180 or angle == -180 ) then - self:setAnimation( "idlewest" ) - end - end - - -- Find our nearest animation direction then set it - local angle = math.nearestmult( math.deg( currentVelocity:toAngle() ), 45 ) - local moving = currentVelocity:lengthSqr() ~= 0 - if ( moving ) then - if ( angle == -90 ) then - self:setAnimation( "walknorth" ) - elseif ( angle == -45 ) then - self:setAnimation( "walknortheast" ) - elseif ( angle == 0 ) then - self:setAnimation( "walkeast" ) - elseif ( angle == 45 ) then - self:setAnimation( "walksoutheast" ) - elseif ( angle == 90 ) then - self:setAnimation( "walksouth" ) - elseif ( angle == 135 ) then - self:setAnimation( "walksouthwest" ) - elseif ( angle == 180 or angle == -180 ) then - self:setAnimation( "walkwest" ) - elseif ( angle == -135 ) then - self:setAnimation( "walknorthwest" ) - end - end -end - -function character:updateMovement( dt ) - if ( self._path ) then - self:move( dt ) - return - end - - if ( self._nextPosition ) then - require( "engine.shared.path" ) - local path = path.getPath( self:getPosition(), self._nextPosition ) - if ( path ) then - self._path = path - end - self._nextPosition = nil - end + entity.tick( self, timestep ) end function character:updateTasks() diff --git a/engine/shared/entities/client-side-prediction-live-demo.html b/engine/shared/entities/client-side-prediction-live-demo.html new file mode 100644 index 00000000..4618999a --- /dev/null +++ b/engine/shared/entities/client-side-prediction-live-demo.html @@ -0,0 +1,641 @@ + + + + + + +Fast-Paced Multiplayer: Sample Code and Live Demo - Gabriel Gambetta + + + + + + + + + +
+ +
+

Fast-Paced Multiplayer: Sample Code and Live Demo

+
+

Client-Server Game Architecture · Client-Side Prediction and Server Reconciliation · Entity Interpolation · Lag Compensation · Live Demo

+ +

This is a sample implementation of a client-server architecture demonstrating the main concepts explained in my Fast-Paced Multiplayer series of articles. It won’t make much sense unless you’ve read the articles first.

+

The code is pure JavaScript and it’s fully contained in this page. It’s less than 500 lines of code, including a lot of comments, showing that once you really understand the concepts, implementing them is relatively straightforward.

+

Although it’s not production-quality code, you may use this code in your own applications. Credit is appreciated although not required.

+
+

Player 1 view - move with LEFT and RIGHT arrow keys
Lag = ms · Prediction · Reconciliation · Interpolation

+ + +
+Waiting for connection… +
+
+
+ +
+
+

Server view · Update times per second

+ + +
+ +
+
+
+ +
+
+

Player 2 view - move with A and D keys
Lag = ms · Prediction · Reconciliation · Interpolation

+ + +
+Waiting for connection… +
+
+ +

Guided Tour

+

The views above show the state of the game world according to the server, and what two clients are rendering. You can move the blue ball, controlled by Player 1, with the LEFT and RIGHT arrow keys; and the red ball, controlled by Player 2, with the A and D keys.

+

Move the blue ball. There’s considerable delay between pressing the arrow keys and the blue ball actually moving. Without client-side prediction, the client only renders the new position of the ball only after a round-trip to the server. Because of the 250ms lag, this takes a while.

+

Set the player 1 Lag to 0ms, and try again. Now the client and the server move in sync because there’s no delay between them, but the movement isn’t smooth, because the server only updates its internal state 3 times per second. If you increase the update rate of the server to 60, we get smooth movement.

+

But this is not a very realistic scenario. Set the player 1 lag back to 250ms, and the server update rate back to 3. This is closer to the awful conditions where a real game still needs to work.

+

Client-side prediction and server reconciliation to the rescue! Enable both of them for Player 1 and move the blue ball. Now the movement is very smooth, and there’s no perceptible delay between pressing the arrow keys and moving the ball.

+

This still works if you make the conditions even worse - try setting the player 1 lag to 500ms and the server update rate to 1.

+

Now things look fantastic for player 1’s own entity, the blue ball. However, player 2’s view of this same entity looks terrible. Because the low update rate of the server, player 2 only gets a new position for player 1’s entity once per second, so the movement is very jumpy.

+

Enabling client-side prediction and server reconciliation for player 2 do nothing to smooth the movement of the blue ball, because these techniques only affect how a player renders its own entity. It does make a difference if you move the red ball, but now we have the same jumpiness in player 1’s view.

+

To solve this, we use entity interpolation. Enable entity interpolation for player 2 and move the blue ball. Now it moves smoothly, but is always rendered “in the past” compared to player 1 and to the server.

+

You may notice the speed of the interpolated entities may vary. This is an artifact of the interpolation, caused by setting the server update rate too low in relationship with the speeds. This effect should disappear almost entirely if you set the server update rate to 10, which is still pretty low.

+

Summary

+

Client-Side Prediction and Server Reconciliation are very powerful techniques to make multiplayer games feel responsive even under extremely bad network conditions. Therefore, they are a fundamental part of almost any client/server multiplayer network architecture.

+ + + +
+ +
+ + + diff --git a/engine/shared/entities/entity.lua b/engine/shared/entities/entity.lua index 2db2e617..162c6923 100644 --- a/engine/shared/entities/entity.lua +++ b/engine/shared/entities/entity.lua @@ -13,7 +13,7 @@ entity._entities = entity._entities or {} entity._lastEntIndex = entity._lastEntIndex or 0 if ( _CLIENT ) then - entity._shadowFramebuffer = entity._shadowFramebuffer or nil + entity._shadowCanvas = entity._shadowCanvas or nil end function entity.create( classname ) @@ -33,28 +33,45 @@ if ( _CLIENT ) then local renderables = {} local clear = table.clear local append = table.append + local sort = table.sort - local shallowcopy = function( from, to ) - for k, v in pairs( from ) do - to[ k ] = v + -- local shallowcopy = function( to, from ) + -- for i, v in ipairs( from ) do + -- to[ i ] = v + -- end + -- end + + local filter = function( t, comp ) + local world = map.getWorld() + if ( world == nil ) then + return end + + -- TODO: Change to camera bounds. + local min, max = localplayer:getGraphicsBounds() + world:queryBoundingBox( min.x, max.y, max.x, min.y, comp ) end - local depthSort = function( t ) - table.sort( t, function( a, b ) - local ay = a:getPosition().y - local al = a.getLocalPosition and a:getLocalPosition() - if ( al ) then - ay = ay + al.y - end + local visible = function( fixture ) + local body = fixture:getBody() + local entity = body:getUserData() + table.insert( renderables, entity ) + return true + end - local by = b:getPosition().y - local bl = b.getLocalPosition and b:getLocalPosition() - if ( bl ) then - by = by + bl.y - end - return ay < by - end ) + local depth = function( a, b ) + local ay = a:getPosition().y + local al = a.getLocalPosition and a:getLocalPosition() + if ( al ) then + ay = ay + al.y + end + + local by = b:getPosition().y + local bl = b.getLocalPosition and b:getLocalPosition() + if ( bl ) then + by = by + bl.y + end + return ay < by end local r_draw_bounding_boxes = convar( "r_draw_bounding_boxes", "0", nil, @@ -154,9 +171,10 @@ if ( _CLIENT ) then function entity.drawAll() clear( renderables ) - shallowcopy( entity._entities, renderables ) + -- shallowcopy( renderables, entity._entities ) + filter( renderables, visible ) append( renderables, camera.getWorldContexts() ) - depthSort( renderables ) + sort( renderables, depth ) -- Draw bounding boxes if ( r_draw_bounding_boxes:getBoolean() ) then @@ -167,31 +185,42 @@ if ( _CLIENT ) then -- Draw shadows if ( r_draw_shadows:getBoolean() ) then - if ( entity._shadowFramebuffer == nil ) then + if ( entity._shadowCanvas == nil ) then require( "engine.client.canvas" ) - entity._shadowFramebuffer = love.graphics.newCanvas() - entity._shadowFramebuffer:setFilter( "nearest", "nearest" ) + entity._shadowCanvas = fullscreencanvas( nil, nil, { + dpiscale = dpiscale + } ) + entity._shadowCanvas:setFilter( "nearest", "nearest" ) end love.graphics.push() - local canvas = love.graphics.getCanvas() - love.graphics.setCanvas( entity._shadowFramebuffer ) - love.graphics.clear() love.graphics.origin() - local x, y = camera.screenToWorld( 0, 0 ) - love.graphics.translate( -x, -y ) - for _, v in ipairs( renderables ) do - drawShadow( v ) - end - love.graphics.setCanvas( canvas ) + + entity._shadowCanvas:renderTo( function() + love.graphics.clear() + + -- Setup camera + local scale = camera.getZoom() + love.graphics.scale( scale ) + local x, y = camera.getTranslation() + love.graphics.translate( x, y ) + + for _, v in ipairs( renderables ) do + drawShadow( v ) + end + end ) love.graphics.pop() love.graphics.push() - local x, y = camera.screenToWorld( 0, 0 ) - love.graphics.translate( x, y ) + love.graphics.origin() love.graphics.setColor( color( color.white, 0.14 * 255 ) ) - love.graphics.draw( entity._shadowFramebuffer ) + entity._shadowCanvas:draw() love.graphics.pop() + else + if ( entity._shadowCanvas ) then + entity._shadowCanvas:remove() + entity._shadowCanvas = nil + end end -- Draw entities @@ -261,12 +290,27 @@ function entity:entity() self:networkString( "name", nil ) self:networkVector( "position", vector() ) + self:networkVector( "velocity", vector() ) self:networkNumber( "worldIndex", 1 ) self:networkString( "animation", nil ) table.insert( entity._entities, self ) end +if ( _SERVER ) then + function entity:broadcastNetworkVarChanges() + if ( self._networkVarChanges == nil ) then + return + end + + for i, v in ipairs( self._networkVarChanges ) do + v.payload:broadcast() + end + + table.clear( self._networkVarChanges ) + end +end + accessor( entity, "body" ) if ( _CLIENT ) then @@ -282,11 +326,11 @@ if ( _CLIENT ) then love.graphics.scale( 1 / camera.getZoom() ) -- Set color - local color = color( color.white, 0.14 * 255 ) + local color = color( color.white, 0.3 * 255 ) love.graphics.setColor( color ) -- Set font - local font = scheme.getProperty( "Default", "fontSmall" ) + local font = scheme.getProperty( "Console", "font" ) love.graphics.setFont( font ) -- Print text @@ -353,6 +397,14 @@ if ( _CLIENT ) then end end +if ( _CLIENT ) then + function entity:emitSound( filename ) + require( "engine.client.source" ) + local source = source( filename ) + source:play() + end +end + if ( _CLIENT ) then function entity:getAnimation() local sprite = self:getSprite() @@ -404,7 +456,14 @@ function entity:getPosition() return self:getNetworkVar( "position" ) end -accessor( entity, "predicted", "is" ) +function entity:getVelocity() + local body = self:getBody() + if ( body and not body:isDestroyed() ) then + return vector( body:getLinearVelocity() ) + end + return self:getNetworkVar( "velocity" ) +end + accessor( entity, "properties" ) accessor( entity, "map" ) @@ -416,6 +475,8 @@ function entity:getWorldIndex() return self:getNetworkVar( "worldIndex" ) end +local sv_friction = convar( "sv_friction", 4, nil, nil, "World friction." ) + function entity:initializePhysics( type ) local world = map.getWorld() local position = self:getPosition() @@ -424,10 +485,71 @@ function entity:initializePhysics( type ) self.body = love.physics.newBody( world, x, y, type ) self.body:setUserData( self ) self.body:setFixedRotation( true ) - self.body:setLinearDamping( 16 ) + self.body:setLinearDamping( sv_friction:getNumber() ) + return self.body end +if ( _SERVER ) then + function entity:insertNetworkVarChange( t ) + self._networkVarChanges = self._networkVarChanges or {} + + local hasChange = false + for i, v in ipairs( self._networkVarChanges ) do + if ( v.name == t.name ) then + self._networkVarChanges[ i ] = t + hasChange = true + end + end + + if ( not hasChange ) then + table.insert( self._networkVarChanges, t ) + end + end +end + +function entity:interpolate() + -- No point in interpolating this client's entity. + if ( self == localplayer ) then + return + end + + -- Find the two authoritative positions surrounding the rendering timestamp. + local buffer = self._interpolationBuffer + if ( buffer == nil ) then + return + end + + -- Compute render timestamp. + local cl_updaterate = convar.getConvar( "cl_updaterate" ) + local now = love.timer.getTime() + local renderTimestamp = now - ( 1 / cl_updaterate:getNumber() ) + + -- Drop older positions. + while ( #buffer >= 2 and buffer[ 2 ].time <= renderTimestamp ) do + table.remove( buffer, 1 ) + end + + local body = self:getBody() + if ( body == nil ) then + return + end + + -- Interpolate between the two surrounding authoritative positions. + if ( #buffer >= 2 and + buffer[ 1 ].time <= renderTimestamp and + renderTimestamp <= buffer[ 2 ].time ) then + local p1 = buffer[ 1 ].value + local p2 = buffer[ 2 ].value + local t1 = buffer[ 1 ].time + local t2 = buffer[ 2 ].time + local dt = ( renderTimestamp - t1 ) / ( t2 - t1 ) + local x = math.lerp( p1.x, p2.x, dt ) + local y = math.lerp( p1.y, p2.y, dt ) + body:setPosition( x, y ) + end +end + function entity:isMoving() local body = self:getBody() if ( body == nil ) then @@ -442,12 +564,12 @@ function entity:isOnTile() return vector( map.snapToGrid( position.x, position.y ) ) == position end -if ( _CLIENT ) then - function entity:emitSound( filename ) - require( "engine.client.source" ) - local source = source( filename ) - source:play() - end +function entity:localToWorld( v ) + return v + self:getPosition() +end + +function entity:worldToLocal( v ) + return v - self:getPosition() end function entity:networkVar( name, initialValue ) @@ -464,12 +586,12 @@ do "boolean", "number", "string", - "vector" + "vector", + "entity" } - for _, type in ipairs( networkableTypes ) do - entity[ "network" .. string.capitalize( type ) ] = - function( self, name, initialValue ) + local networkType = function( type ) + return function( self, name, initialValue ) local mt = getmetatable( self ) local keys = rawget( mt, "networkVarKeys" ) or {} rawset( mt, "networkVarKeys", keys ) @@ -482,6 +604,10 @@ do end end + if ( type == "number" ) then + type = "float" + end + if ( not keyExists ) then table.insert( keys, { name = name, @@ -492,11 +618,15 @@ do self:networkVar( name, initialValue ) end end + + for _, type in ipairs( networkableTypes ) do + entity[ "network" .. string.capitalize( type ) ] = networkType( type ) + end end function entity:setNetworkVar( name, value ) if ( self._networkVars == nil or not self:hasNetworkVar( name ) ) then - error( "attempt to set nonexistent networkvar '" .. name .. "'", 2 ) + error( "attempt to set networkvar '" .. name .. "' (a nil value)", 2 ) end self._networkVars[ name ]:setValue( value ) @@ -539,40 +669,43 @@ function entity:getNetworkVarTypeLenValues() return networkVars end -local cl_interpolate = convar( "cl_interpolate", "1", nil, nil, +local cl_interpolate = convar( "cl_interpolate", "0", nil, nil, "Perform client-side interpolation" ) -local cl_predict = convar( "cl_predict", "1", nil, nil, - "Perform client-side prediction" ) - +-- Process all messages from the server, i.e. world updates. +-- If enabled, do server reconciliation. function entity:onNetworkVarChanged( networkvar ) + -- Received the authoritative position of this client's entity. if ( networkvar:getName() == "position" ) then - if ( _CLIENT and not _SERVER and cl_interpolate:getBoolean() ) then - self._lastPosition = { - value = vector.copy( networkvar:getValue() ), - time = love.timer.getTime() - } - else + if ( self == localplayer or + ( self ~= localplayer and not cl_interpolate:getBoolean() ) or + _SERVER ) then if ( _CLIENT and not _SERVER ) then - if ( self._lastPosition ) then - self._lastPosition = nil - end - - if ( self._interpolationBuffer ) then - self._interpolationBuffer = nil - end + self:removeInterpolationBuffer() end + -- Entity interpolation is disabled - just accept + -- the server's position. local body = self:getBody() if ( body ) then local position = networkvar:getValue() body:setPosition( position.x, position.y ) end + else + if ( _CLIENT and not _SERVER ) then + -- Add it to the position buffer. + self:updateInterpolationBuffer( { + value = vector.copy( networkvar:getValue() ), + time = love.timer.getTime() + } ) + end end end - if ( _CLIENT and networkvar:getName() == "animation" ) then - self:setAnimation( networkvar:getValue() ) + if ( networkvar:getName() == "animation" ) then + if ( _CLIENT ) then + self:setAnimation( networkvar:getValue() ) + end end if ( _SERVER ) then @@ -584,15 +717,17 @@ function entity:onNetworkVarChanged( networkvar ) networkVar:set( networkvar:getName(), networkvar:getValue() ) payload:set( "networkVars", networkVar ) - payload:broadcast() + self:insertNetworkVarChange( { + name = networkvar:getName(), + payload = payload + } ) end end -function entity:localToWorld( v ) - return self:getPosition() + v -end - if ( _CLIENT ) then + function entity:onNetworkVarReceived( k, v ) + end + function entity:onAnimationEnd( animation ) end @@ -600,22 +735,7 @@ if ( _CLIENT ) then end end -function entity:onTick( timestep ) - if ( _SERVER ) then - return - end - - local lastPosition = self._lastPosition - if ( lastPosition == nil ) then - return - end - - if ( self._interpolationBuffer == nil ) then - self._interpolationBuffer = {} - end - - table.insert( self._interpolationBuffer, lastPosition ) - self._lastPosition = nil +function entity:onPostWorldUpdate( timestep ) end function entity:remove() @@ -652,6 +772,18 @@ if ( _CLIENT ) then payload.setHandler( onEntityRemoved, "entityRemoved" ) end +if ( _CLIENT ) then + function entity:removeInterpolationBuffer() + if ( self._lastPosition ) then + self._lastPosition = nil + end + + if ( self._interpolationBuffer ) then + self._interpolationBuffer = nil + end + end +end + function entity:setAnimation( animation ) if ( _CLIENT ) then local sprite = self:getSprite() @@ -682,17 +814,40 @@ function entity:setCollisionBounds( min, max ) fixture:destroy() end - -- BUGBUG: We use magic numbers here to shrink the bounding box of the - -- fixture. - local dimensions = max - min - dimensions.y = -dimensions.y - local width = dimensions.x - 0.6 - local height = dimensions.y - 0.6 - local x = width / 2 + 0.3 - local y = -height / 2 - 0.3 - local shape = love.physics.newRectangleShape( x, y, width, height ) - local fixture = love.physics.newFixture( body, shape ) - fixture:setFilterData( 1 --[[ COLLISION_GROUP_NONE ]], 0, 0 ) + local dimensions = max - min + dimensions.y = -dimensions.y + -- Remove polygon skin + -- See @erincatto/Box2D/blob/master/Box2D/Common/b2Settings.h#L81 + local linearSlop = 0.005 + local polygonRadius = ( 2.0 * linearSlop ) + local pixels = polygonRadius * love.physics.getMeter() + local width = dimensions.x - 2 * pixels + local height = dimensions.y - 2 * pixels + local x = width / 2 + pixels + local y = -height / 2 - pixels + local shape = love.physics.newRectangleShape( x, y, width, height ) + local fixture = love.physics.newFixture( body, shape ) + fixture:setUserData( self ) + -- fixture:setFriction( 0.3 * love.physics.getMeter() ) + -- fixture:setFilterData( 1 --[[ COLLISION_GROUP_NONE ]], 0, 0 ) + + if ( self.body:getType() == "dynamic" ) then + local gravity = 10.0 * love.physics.getMeter() + local I = self.body:getInertia() + local mass = self.body:getMass() + + local radius = math.sqrt( 2.0 * I / mass ) + + -- local joint = love.physics.newFrictionJoint( + -- map.getGroundBody(), + -- self.body, + -- 0, + -- 0, + -- true + -- ) + -- joint:setMaxForce( mass * gravity ) + -- joint:setMaxTorque( mass * radius * gravity ) + end end if ( _CLIENT ) then @@ -767,6 +922,12 @@ function entity:spawn() end end +function entity:startTouch( other, contact ) +end + +function entity:endTouch( other, contact ) +end + function entity:testPoint( x, y ) local body = self:getBody() if ( body and not body:isDestroyed() ) then @@ -798,24 +959,12 @@ function entity:testPoint( x, y ) return false end -function entity:updateNetworkVars( payload ) - local struct = self:getNetworkVarsStruct() - local networkVars = payload:get( "networkVars" ) - networkVars:setStruct( struct ) - networkVars:deserialize() - for k, v in pairs( networkVars:getData() ) do - self:setNetworkVar( k, v ) - end -end - -function entity:update( dt ) - if ( _CLIENT and not _SERVER and cl_interpolate:getBoolean() ) then - self:updatePosition( self ) - end - - local body = self:getBody() - if ( body and not body:isDestroyed() ) then - self:setNetworkVar( "position", vector( body:getPosition() ) ) +function entity:tick( timestep ) + if ( self.think and + self.nextThink and + self.nextThink <= love.timer.getTime() ) then + self.nextThink = nil + self:think() end local currentMap = self:getMap() @@ -824,11 +973,19 @@ function entity:update( dt ) self:setMap( map ) end - if ( self.think and - self.nextThink and - self.nextThink <= love.timer.getTime() ) then - self.nextThink = nil - self:think() + if ( _SERVER ) then + local body = self:getBody() + if ( body and not body:isDestroyed() ) then + self:setNetworkVar( "position", vector( body:getPosition() ) ) + self:setNetworkVar( "velocity", vector( body:getLinearVelocity() ) ) + end + end +end + +function entity:update( dt ) + -- Interpolate other entities. + if ( _CLIENT and not _SERVER and cl_interpolate:getBoolean() ) then + self:interpolate() end if ( _CLIENT ) then @@ -839,36 +996,26 @@ function entity:update( dt ) end end -function entity:updatePosition() - local buffer = self._interpolationBuffer - if ( buffer == nil ) then - return - end - - local cl_updaterate = convar.getConvar( "cl_updaterate" ) - local now = love.timer.getTime() - local renderTimestamp = now - ( 1 / cl_updaterate:getNumber() ) - - while ( #buffer >= 2 and buffer[ 2 ].time <= renderTimestamp ) do - table.remove( buffer, 1 ) - end +if ( _CLIENT ) then + function entity:updateInterpolationBuffer( position ) + if ( self._interpolationBuffer == nil ) then + self._interpolationBuffer = {} + end - local body = self:getBody() - if ( body == nil ) then - return + table.insert( self._interpolationBuffer, position ) end - if ( #buffer >= 2 and - buffer[ 1 ].time <= renderTimestamp and - renderTimestamp <= buffer[ 2 ].time ) then - local p1 = buffer[ 1 ].value - local p2 = buffer[ 2 ].value - local t1 = buffer[ 1 ].time - local t2 = buffer[ 2 ].time - local dt = ( renderTimestamp - t1 ) / ( t2 - t1 ) - local x = math.round( math.lerp( p1.x, p2.x, dt ) ) - local y = math.round( math.lerp( p1.y, p2.y, dt ) ) - body:setPosition( x, y ) + function entity:updateNetworkVars( payload ) + local struct = self:getNetworkVarsStruct() + local networkVars = payload:get( "networkVars" ) + networkVars:setStruct( struct ) + networkVars:deserialize() + for k, v in pairs( networkVars:getData() ) do + -- Should we reject the network var update? + if ( self:onNetworkVarReceived( k, v ) ~= false ) then + self:setNetworkVar( k, v ) + end + end end end diff --git a/engine/shared/entities/init.lua b/engine/shared/entities/init.lua index 83475d9f..16afd07e 100644 --- a/engine/shared/entities/init.lua +++ b/engine/shared/entities/init.lua @@ -173,7 +173,8 @@ end function shutdown() if ( _G.entity ) then - _G.entity._shadowFramebuffer = nil + _G.entity._shadowCanvas:remove() + _G.entity._shadowCanvas = nil _G.entity.removeAll() _G.entity._lastEntIndex = 0 end diff --git a/engine/shared/entities/player.lua b/engine/shared/entities/player.lua index 5cb3366f..079119db 100644 --- a/engine/shared/entities/player.lua +++ b/engine/shared/entities/player.lua @@ -73,7 +73,9 @@ function player:player() character.character( self ) self:networkNumber( "id", player._lastPlayerId + 1 ) - self:networkNumber( "moveSpeed", 66 ) + -- 1 tile @16px * 10 in 6 seconds. + self:networkNumber( "moveSpeed", ( 16 * 10 ) / 6 ) + -- Last processed input for each client. self:networkNumber( "lastCommandNumber", 0 ) if ( _SERVER ) then @@ -85,6 +87,7 @@ function player:player() self._lastButtons = 0 self._buttonsPressed = 0 self._buttonsReleased = 0 + self._idle = true if ( _CLIENT ) then require( "engine.client.sprite" ) @@ -96,6 +99,60 @@ function player:player() table.insert( player._players, self ) end +if ( _CLIENT ) then + local name = convar( "name", "Unnamed", nil, nil, "Sets your player name", + nil, { "archive", "userinfo" } ) + + local r_draw_network_position = convar( "r_draw_network_position", "0", nil, + nil, "Draws network position" ) + + function player:draw() + if ( r_draw_network_position:getBoolean() ) then + if ( self._networkPosition ) then + local position = self._networkPosition + position = self:worldToLocal( position ) + love.graphics.push() + local x, y = position.x, position.y + love.graphics.translate( x, y ) + love.graphics.setColor( color( color.white, 0.3 * 255 ) ) + entity.draw( self ) + love.graphics.pop() + end + end + + love.graphics.setColor( color.white ) + entity.draw( self ) + end +end + +function player:keypressed( key ) +end + +function player:keyreleased( key ) +end + +local keys = { + "forward", + "back", + "left", + "right", + "speed", + "use" +} + +-- Apply user's input to this entity. +function player:applyInput() + for _, key in ipairs( keys ) do + if ( self:isKeyPressed( key ) ) then + self:keypressed( key ) + end + + if ( self:isKeyReleased( key ) ) then + self:keyreleased( key ) + end + end +end + function player:getName() return entity.getName( self ) or "Unnamed" end @@ -136,66 +193,38 @@ function player:initialSpawn() game.call( "shared", "onPlayerInitialSpawn", self ) end -function player:isKeyDown( button ) - return bit.band( self._buttons, button ) ~= 0 -end +local buttonKeys = { + forward = _E.IN_FORWARD, + back = _E.IN_BACK, + left = _E.IN_LEFT, + right = _E.IN_RIGHT, + speed = _E.IN_SPEED, + use = _E.IN_USE +} + +local function isKeySet( self, key, ... ) + local buttons = { ... } + for _, button in ipairs( buttons ) do + button = buttonKeys[ button ] + local set = bit.band( self[ key ], button ) ~= 0 + if ( set ) then + return true + end + end -function player:isMoveKeyDown() - return self:isKeyDown( bit.bor( - _E.IN_FORWARD, - _E.IN_BACK, - _E.IN_LEFT, - _E.IN_RIGHT, - _E.IN_SPEED - ) ) + return false end -function player:isKeyPressed( button ) - return bit.band( self._buttonsPressed, button ) ~= 0 +function player:isKeyDown( ... ) + return isKeySet( self, "_buttons", ... ) end -function player:isKeyReleased( button ) - return bit.band( self._buttonsReleased, button ) ~= 0 +function player:isKeyPressed( ... ) + return isKeySet( self, "_buttonsPressed", ... ) end -if ( _SERVER ) then - function player:kick( message ) - local payload = payload( "kick" ) - payload:set( "message", message ) - self.peer:send( payload:serialize() ) - self.peer:disconnect_later() - end -end - -function player:moveTo( position, callback ) - local moving = character.moveTo( self, position, callback ) - - if ( _CLIENT and not _SERVER ) then - local payload = payload( "playerMove" ) - payload:set( "position", position ) - payload:sendToServer() - end - - if ( _CLIENT ) then - require( "engine.client.camera" ) - if ( moving and camera.getParentEntity() == self ) then - -- TODO: Reset on the client if we moved from the server. - camera.resetZoom() - end - end - - return moving -end - -if ( _SERVER ) then - local function onPlayerMove( payload ) - local player = payload:getPlayer() - local position = payload:get( "position" ) - player:removeTasks() - player:moveTo( position ) - end - - payload.setHandler( onPlayerMove, "playerMove" ) +function player:isKeyReleased( ... ) + return isKeySet( self, "_buttonsReleased", ... ) end concommand( "+forward", "Start moving player forward", function( _, player ) @@ -246,6 +275,65 @@ concommand( "-use", "Stop using an entity", function( _, player ) player._buttons = bit.band( player._buttons, bit.bnot( _E.IN_USE ) ) end, { "game" } ) +if ( _CLIENT ) then + function player:isPredictedPosition( v ) + if ( self ~= localplayer ) then + return false + end + + if ( self._predictionBuffer == nil ) then + return false + end + + local k = nil + local u = nil + for i, w in ipairs( self._predictionBuffer ) do + if ( v == w ) then + k = i + elseif ( v:approximately( w ) ) then + k = i + elseif ( u and math.pointonlinesegment( + u.x, u.y, + w.x, w.y, + v.x, v.y + ) ) then + k = i - 1 + end + u = w + end + + if ( k ~= nil ) then + return k + end + + return false + end +end + +if ( _SERVER ) then + function player:kick( message ) + local payload = payload( "kick" ) + payload:set( "message", message ) + self.peer:send( payload:serialize() ) + self.peer:disconnect_later() + end +end + +function player:moveTo( position, callback ) + character.moveTo( self, position, callback ) +end + +if ( _SERVER ) then + local function onPlayerMove( payload ) + local player = payload:getPlayer() + local position = payload:get( "position" ) + player:removeTasks() + player:moveTo( position ) + end + + payload.setHandler( onPlayerMove, "playerMove" ) +end + if ( _CLIENT ) then local function updateStepSound( self, event ) if ( event ~= "leftfootstep" and @@ -298,63 +386,215 @@ function player:onDisconnect() end end -local cl_server_reconciliation = convar( "cl_server_reconciliation", "1", nil, - nil, - "Perform server reconciliation" ) +local r_draw_prediction_buffer = nil -local function updateMovement( self ) - if ( _CLIENT and self ~= localplayer ) then - return +if ( _CLIENT ) then + r_draw_prediction_buffer = convar( "r_draw_prediction_buffer", "0", nil, + nil, "Draws prediction buffer" ) + + function player:onNetworkVarReceived( k, v ) + if ( self ~= localplayer ) then + return + end + + if ( k == "position" ) then + -- Track last known network position + if ( self._networkPosition == nil ) then + self._networkPosition = vector.copy( self:getPosition() ) + end + + -- Draw diff + if ( r_draw_prediction_buffer:getBoolean() ) then + local oldPos = self._networkPosition + local newPos = v + require( "engine.client.debugoverlay" ) + if ( oldPos ) then + local i = self:getNetworkVar( "worldIndex" ) + local x = newPos.x - oldPos.x + local y = newPos.y - oldPos.y + debugoverlay.line( + i, + oldPos.x + 0.5, oldPos.y - 0.5 + 1, + { 0, 0, x, y }, color.server, 30 + ) + end + end + + -- Update last known network position + self._networkPosition.x = v.x + self._networkPosition.y = v.y + + -- Prevent rubberbanding. + local i = self:isPredictedPosition( v ) + + -- This position was predicted, so dequeue from + -- the prediction buffer. + if ( i ~= false ) then + table.remove( self._predictionBuffer, 1 ) + return false + else + -- We didn't predict this. The buffer + -- is now invalid. + if ( self._predictionBuffer ) then + table.clear( self._predictionBuffer ) + end + end + end end +end +function player:onPostWorldUpdate( timestep ) if ( _CLIENT and not _SERVER ) then - local payload = payload( "usercmd" ) - self._commandNumber = self._commandNumber + 1 - payload:set( "commandNumber", self._commandNumber ) + if ( self ~= localplayer ) then + return + end + + self:updatePredictionBuffer() + end +end + +if ( _SERVER ) then + local function onPlayerUse( payload ) + local entity = payload:get( "entity" ) + local activator = payload:getPlayer() + local value = payload:get( "value" ) + + local canUse = game.call( + "server", "onPlayerUse", activator, entity, value + ) + if ( canUse == false ) then + return + end + + entity:use( activator, value ) + end + + payload.setHandler( onPlayerUse, "playerUse" ) +end + +local move = { + forward = vector( 0, -1 ), + back = vector( 0, 1 ), + left = vector( -1, 0 ), + right = vector( 1, 0 ) +} + +local cl_predict = convar( "cl_predict", "1", nil, nil, + "Perform client-side prediction", updatePrediction ) + +-- Get inputs and send them to the server. +-- If enabled, do client-side prediction. +local function processInputs( self, dt ) + -- Package player's input. + local movement = vector() + if ( self:isKeyDown( "forward" ) ) then + movement = movement + move[ "forward" ] + end + + if ( self:isKeyDown( "back" ) ) then + movement = movement + move[ "back" ] + end + + if ( self:isKeyDown( "left" ) ) then + movement = movement + move[ "left" ] + end + + if ( self:isKeyDown( "right" ) ) then + movement = movement + move[ "right" ] + end + + movement:normalizeInPlace() + + if ( self:isKeyDown( "speed" ) ) then + movement = 2 * movement + end + + -- Send the input to the server. + local sendToServer = false + local payload = payload( "usercmd" ) + local commandNumber = self._commandNumber + 1 + payload:set( "commandNumber", self._commandNumber ) + + -- Send move data if moving, or if we haven't been + -- idle, and need to send idle data to the server. + local moving = movement ~= vector.origin + if ( moving or ( not moving and not self._idle ) ) then + payload:set( "move", movement ) + sendToServer = true + end + + if ( self._buttons ~= 0 ) then payload:set( "buttons", self._buttons ) - payload:sendToServer() + sendToServer = true end - if ( not self:isMoveKeyDown() ) then - return + if ( sendToServer ) then + if ( _CLIENT and not _SERVER ) then + self._commandNumber = commandNumber + end + + payload:sendToServer() end - local position = vector.copy( self:getPosition() ) - position.x, position.y = map.roundToGrid( position.x, position.y ) - if ( self:isKeyDown( _E.IN_FORWARD ) ) then - position = position + vector( 0, -game.tileSize ) - elseif ( self:isKeyDown( _E.IN_BACK ) ) then - position = position + vector( 0, game.tileSize ) - elseif ( self:isKeyDown( _E.IN_LEFT ) ) then - position = position + vector( -game.tileSize, 0 ) - elseif ( self:isKeyDown( _E.IN_RIGHT ) ) then - position = position + vector( game.tileSize, 0 ) + -- Do client-side prediction. + if ( cl_predict:getBoolean() ) then + self:applyInput() end - self:moveTo( position ) + return sendToServer and movement or nil end -if ( _SERVER ) then - local function onUserCmd( payload ) - local player = payload:getPlayer() - local commandNumber = payload:get( "commandNumber" ) - local buttons = payload:get( "buttons" ) - player._commandNumber = commandNumber - player:updateButtonState( buttons ) - updateMovement( player ) +function player:tick( timestep ) + local movement = nil + local buttons = self._buttons + + if ( _CLIENT and self == localplayer ) then + -- Process inputs. + movement = processInputs( self, timestep ) end - payload.setHandler( onUserCmd, "usercmd" ) + if ( _SERVER ) then + if ( self._userCmds and #self._userCmds > 0 ) then + local payload = self._userCmds[ 1 ] + movement = payload:get( "move" ) + buttons = payload:get( "buttons" ) + self._commandNumber = self._commandNumber + 1 + self:setNetworkVar( "lastCommandNumber", self._commandNumber ) + table.remove( self._userCmds, 1 ) + else + movement = vector() + end + end + + -- Get button states + if ( buttons ) then + self:updateButtonState( buttons ) + end + + -- Let the game do the movement. + if ( movement ) then + self:updateAnimation( timestep, movement ) + self:updateMovement( timestep, movement ) + self._idle = movement == vector.origin or nil + end + + -- Copy output + if ( buttons ) then + self._lastButtons = buttons + end + + character.tick( self, timestep ) end -function player:onNetworkVarChanged( networkvar ) - if ( _CLIENT and networkvar:getName() == "health" ) then - if ( g_HudHealth ) then - g_HudHealth:invalidateLayout() - end +if ( _SERVER ) then + local function onUserCmd( payload ) + -- Process all pending messages from clients. + local player = payload:getPlayer() + player._userCmds = player._userCmds or {} + table.insert( player._userCmds, payload ) end - entity.onNetworkVarChanged( self, networkvar ) + payload.setHandler( onUserCmd, "usercmd" ) end concommand( "say", "Display player message", @@ -413,41 +653,172 @@ function player:spawn() self:initializePhysics( "dynamic" ) self:setCollisionBounds( min, max ) + local body = self:getBody() + if ( body ) then + body:setMass( 89.76593 ) + end + game.call( "shared", "onPlayerSpawn", self ) end -function player:update( dt ) - updateMovement( self ) - character.update( self, dt ) +function player:updateAnimation( timestep, movement ) end function player:updateButtonState( buttons ) - self._lastButtons = self._buttons - self._buttons = buttons local buttonsChanged = bit.bxor( self._lastButtons, self._buttons ) self._buttonsPressed = bit.band( buttonsChanged, buttons ) - self._buttonsReleased = bit.band( buttonsChanged, bit.bnot( buttons ) ) + self._buttonsReleased = bit.band( buttonsChanged, self._lastButtons ) end -if ( _SERVER ) then - local function onPlayerUse( payload ) - local entity = payload:get( "entity" ) - local activator = payload:getPlayer() - local value = payload:get( "value" ) +local directions = { + north = vector( 0, -1 ), + east = vector( 1, 0 ), + south = vector( 0, 1 ), + west = vector( -1, 0 ), + northeast = vector( 1, -1 ):normalize(), + southeast = vector( 1, 1 ):normalize(), + southwest = vector( -1, 1 ):normalize(), + northwest = vector( -1, -1 ):normalize() +} - local canUse = game.call( - "server", "onPlayerUse", activator, entity, value - ) - if ( canUse == false ) then +function player:updateMovement( timestep, movement ) + if ( _CLIENT and not _SERVER ) then + if ( self == localplayer and not cl_predict:getBoolean() ) then return end + end - entity:use( activator, value ) + if ( movement ~= vector.origin ) then + -- Moving. + if ( _CLIENT ) then + if ( self == localplayer ) then + -- camera.resetZoom() + end + end + + -- TODO: Set animation based on velocity, instead. + for direction, v in pairs( directions ) do + if ( movement:normalize() == v ) then + self:setAnimation( "walk" .. direction ) + self._lastDirection = direction + end + end + + local moveSpeed = self:getNetworkVar( "moveSpeed" ) + movement = moveSpeed * movement + + local body = self:getBody() + if ( body ) then + -- Convert pixels to meters + movement = movement / love.physics.getMeter() + + -- Uniform acceleration + -- v = u + at + if ( body:getType() == "dynamic" ) then + -- Initial velocity + local u = vector( body:getLinearVelocity() ) + u = u / love.physics.getMeter() + + -- Synchronize timestep + -- local m = ( 1 / 60 ) / timestep + -- local h = m * timestep + local h = timestep + + -- Revert linear damping + local sv_friction = convar.getConvar( "sv_friction" ) + u = u / ( 1.0 + h * sv_friction:getNumber() ) + + -- Acceleration + local a = ( movement - u ) / h + + -- F = m * a + movement = body:getMass() * a + + -- Negate damping + movement = movement * ( 1.0 + h * sv_friction:getNumber() ) + end + + -- Convert meters to pixels + movement = movement * love.physics.getMeter() + + if ( body:getType() == "kinematic" ) then + body:setLinearVelocity( movement.x, movement.y ) + end + + if ( body:getType() == "dynamic" ) then + body:applyForce( movement.x, movement.y ) + end + else + local position = self:getPosition() + self:setPosition( position + timestep * movement ) + end + else + -- Idle. + -- TODO: Set animation based on velocity, instead. + local lastDirection = self._lastDirection + if ( lastDirection ) then + self:setAnimation( "idle" .. lastDirection ) + self._lastDirection = nil + end + + local body = self:getBody() + if ( body == nil ) then + return + end + + if ( body:getType() == "kinematic" ) then + body:setLinearVelocity( 0, 0 ) + end + + if ( body:getType() == "dynamic" ) then + local vx, vy = body:getLinearVelocity() + body:applyLinearImpulse( + body:getMass() * -vx, + body:getMass() * -vy + ) + end end +end - payload.setHandler( onPlayerUse, "playerUse" ) +if ( _CLIENT ) then + function player:updatePredictionBuffer() + -- Only store to the prediction buffer if predicting + if ( not cl_predict:getBoolean() ) then + if ( self._predictionBuffer ) then + self._predictionBuffer = nil + end + return + end + + -- Inititalize prediction buffer + self._predictionBuffer = self._predictionBuffer or {} + + -- Store last position + local buffer = self._predictionBuffer + local oldPos = buffer[ #buffer ] + local newPos = self:getPosition() + if ( newPos ~= oldPos ) then + newPos = vector.copy( newPos ) + table.insert( buffer, newPos ) + end + + -- Draw the buffer + if ( r_draw_prediction_buffer:getBoolean() ) then + require( "engine.client.debugoverlay" ) + if ( oldPos ) then + local i = self:getNetworkVar( "worldIndex" ) + local x = newPos.x - oldPos.x + local y = newPos.y - oldPos.y + debugoverlay.line( + i, + oldPos.x + 0.5, oldPos.y - 0.5, + { 0, 0, x, y }, color.client, 30 + ) + end + end + end end function player:__tostring() diff --git a/engine/shared/entities/trigger_changelevel.lua b/engine/shared/entities/trigger_changelevel.lua index f2ae85b5..12f77f71 100644 --- a/engine/shared/entities/trigger_changelevel.lua +++ b/engine/shared/entities/trigger_changelevel.lua @@ -41,7 +41,7 @@ function trigger_changelevel:removeMap() end end -function trigger_changelevel:update( dt ) +function trigger_changelevel:tick( timestep ) for _, player in ipairs( player.getAll() ) do if ( self:isVisibleToPlayer( player ) ) then if ( not self.loaded ) then diff --git a/engine/shared/entities/trigger_transition.lua b/engine/shared/entities/trigger_transition.lua index 3732d146..501b87e0 100644 --- a/engine/shared/entities/trigger_transition.lua +++ b/engine/shared/entities/trigger_transition.lua @@ -121,19 +121,23 @@ end function trigger_transition:removeMap() local properties = self:getProperties() - if ( properties ) then - local name = properties[ "map" ] - local r = map.getByName( name ) - if ( r ) then - local players = player.getInOrNearMap( r ) - if ( players == nil ) then - map.unload( name ) - end - end + if ( properties == nil ) then + return + end + + local name = properties[ "map" ] + local r = map.getByName( name ) + if ( r == nil ) then + return + end + + local players = player.getInOrNearMap( r ) + if ( players == nil ) then + map.unload( name ) end end -function trigger_transition:update( dt ) +function trigger_transition:tick( timestep ) for _, player in ipairs( player.getAll() ) do if ( self:isVisibleToPlayer( player ) ) then if ( not self.loaded ) then diff --git a/engine/shared/map/init.lua b/engine/shared/map/init.lua index 2ae74f28..a1bd5c57 100644 --- a/engine/shared/map/init.lua +++ b/engine/shared/map/init.lua @@ -11,6 +11,8 @@ class( "map" ) map._maps = map._maps or {} if ( _CLIENT ) then + local r_draw_world = convar( "r_draw_world", "1", nil, nil, "Draws world" ) + local function drawMap( map ) local worldIndex = camera.getWorldIndex() if ( worldIndex ~= map:getWorldIndex() ) then @@ -38,8 +40,10 @@ if ( _CLIENT ) then love.graphics.translate( x, y ) -- Draw maps - for _, map in ipairs( map._maps ) do - drawMap( map ) + if ( r_draw_world:getBoolean() ) then + for _, map in ipairs( map._maps ) do + drawMap( map ) + end end -- Draw entities @@ -93,6 +97,10 @@ function map.getWorld() return map._world end +function map.getGroundBody() + return map._groundBody +end + function map.load( name, x, y, worldIndex ) x = x or 0 y = y or 0 @@ -155,9 +163,8 @@ end function map.unloadAll() for i = #map._maps, 1, -1 do - local map = map._maps[ i ] - unrequire( "maps." .. map:getName() ) - table.remove( map._maps, i ) + local m = map._maps[ i ] + map.unload( m:getName() ) end end @@ -251,11 +258,13 @@ function map:map( name, x, y, worldIndex ) self:parse() - require( "engine.client.canvas" ) - local width = self:getPixelWidth() - local height = self:getPixelHeight() - self.canvas = canvas( width, height ) - self.canvas:setFilter( "nearest", "nearest" ) + if ( _CLIENT ) then + require( "engine.client.canvas" ) + local width = self:getPixelWidth() + local height = self:getPixelHeight() + self.canvas = canvas( width, height, { dpiscale = 1 } ) + self.canvas:setFilter( "nearest", "nearest" ) + end self.needsRedraw = true end @@ -372,12 +381,68 @@ accessor( map, "worldIndex" ) accessor( map, "x" ) accessor( map, "y" ) +local function beginContact( a, b, contact ) + a = a:getUserData() + b = b:getUserData() + if ( a ) then + a:startTouch( b, contact ) + end +end + +local function endContact( a, b, contact ) + a = a:getUserData() + b = b:getUserData() + if ( a ) then + a:endTouch( b, contact ) + end +end + +local function preSolve( a, b, contact ) +end + +local function postSolve( a, b, contact, normalimpulse, tangentimpulse ) +end + function map.initializeWorld() if ( map._world ) then return end - map._world = love.physics.newWorld() + map._world = love.physics.newWorld() + map._groundBody = love.physics.newBody( map.getWorld(), 0, 0 ) + + local world = map.getWorld() + world:setCallbacks( beginContact, endContact, preSolve, postSolve ) +end + +function map:initializePhysics() + -- If the map has trigger_transitions, let level designers set + -- up nodraws themselves. + if ( entity.findByClassname( "trigger_transition", self ) ) then + return false + end + + local x = self:getX() + local y = self:getY() + local width = self:getPixelWidth() + local height = self:getPixelHeight() + local ground = map.getGroundBody() + + -- Top boundary + local shape = love.physics.newEdgeShape( x + 0, y + 0, x + width, y + 0 ) + local fixture = love.physics.newFixture( ground, shape ) + + -- Right boundary + shape = love.physics.newEdgeShape( x + width, y + 0, x + width, y + height ) + fixture = love.physics.newFixture( ground, shape ) + + -- Bottom boundary + shape = love.physics.newEdgeShape( x + width, y + height, x + 0, y + height ) + fixture = love.physics.newFixture( ground, shape ) + + -- Left boundary + shape = love.physics.newEdgeShape( x + 0, y + height, x + 0, y + 0 ) + fixture = love.physics.newFixture( ground, shape ) end local px = 0 @@ -496,17 +561,26 @@ function map:parse() self:setProperties( table.copy( data[ "properties" ] ) ) map.initializeWorld() + -- self:initializePhysics() + self:loadTilesets( data[ "tilesets" ] ) self:loadLayers( data[ "layers" ] ) + -- NOTE: Do this here to detect trigger_transitions, instead. + self:initializePhysics() + self.data = nil end function map:remove() self:cleanup() - local world = self:getWorld() + local world = map.getWorld() if ( world and #map._maps == 1 ) then + local ground = map.getGroundBody() + ground:destroy() + map._groundBody = nil + world:destroy() map._world = nil end @@ -529,10 +603,10 @@ function map:getTileSize() return self:getTileWidth(), self:getTileHeight() end -function map.update( dt ) +function map.tick( timestep ) local world = map.getWorld() if ( world ) then - world:update( dt ) + world:update( timestep ) end end diff --git a/engine/shared/mathlib.lua b/engine/shared/mathlib.lua index 6efda7a9..11829405 100644 --- a/engine/shared/mathlib.lua +++ b/engine/shared/mathlib.lua @@ -13,10 +13,25 @@ function math.aabbsintersect( minA, maxA, minB, maxB ) minA.y >= maxB.y end +math.fepsilon = 1e-5 + +function math.approximately( a, b ) + -- Calculate the difference. + local diff = math.abs( a - b ) + a = math.abs( a ) + b = math.abs( b ) + -- Find the largest + local largest = ( b > a ) and b or a + + if ( diff <= largest * math.fepsilon ) then + return true + end + + return false +end + function math.clamp( n, l, u ) - return n < l and ( l ) or - ( n > u and ( u ) or - ( n ) ) + return n < l and l or ( n > u and u or n ) end function math.gcd( a, b ) @@ -50,6 +65,30 @@ function math.pointinrect( px, py, x, y, width, height ) py < y + height end +function math.pointonline( x1, y1, x2, y2, px, py ) + local m = ( y2 - y1 ) / ( x2 - x1 ) + local b = y1 - m * x1 + return py == m * px + b +end + +function math.pointonlinesegment( x1, y1, x2, y2, px, py ) + -- Test x out of bounds + if ( x2 > x1 and px > x2 ) then + return false + elseif ( x2 < x1 and px < x2 ) then + return false + end + + -- Test y out of bounds + if ( y2 > y1 and py > y2 ) then + return false + elseif ( y2 < y1 and py < y2 ) then + return false + end + + return math.pointonline( x1, y1, x2, y2, px, py ) +end + function math.remap( n, inMin, inMax, outMin, outMax ) return ( n / ( inMax - inMin ) ) * ( outMax - outMin ) + outMin end diff --git a/engine/shared/network/payloads.lua b/engine/shared/network/payloads.lua index 4b0da9c0..bc912d3d 100644 --- a/engine/shared/network/payloads.lua +++ b/engine/shared/network/payloads.lua @@ -36,15 +36,15 @@ structs[ "upload" ] = { structs[ "clientInfo" ] = { keys = { - { name = "graphicsWidth", type = "number" }, - { name = "graphicsHeight", type = "number" } + { name = "graphicsWidth", type = "float" }, + { name = "graphicsHeight", type = "float" } } } structs[ "entitySpawned" ] = { keys = { { name = "classname", type = "string" }, - { name = "entIndex", type = "number" }, + { name = "entIndex", type = "float" }, { name = "networkVars", type = "typelenvalues" } } } @@ -52,7 +52,7 @@ structs[ "entitySpawned" ] = { structs[ "playerInitialized" ] = { keys = { { name = "player", type = "entity" }, - { name = "id", type = "number" } + { name = "id", type = "float" } } } @@ -97,8 +97,9 @@ structs[ "playerMove" ] = { structs[ "usercmd" ] = { keys = { - { name = "commandNumber", type = "number" }, - { name = "buttons", type = "number" } + { name = "commandNumber", type = "float" }, + { name = "move", type = "vector" }, + { name = "buttons", type = "float" } } } @@ -109,6 +110,12 @@ structs[ "playerUse" ] = { } } +structs[ "voice" ] = { + keys = { + { name = "data", type = "string" } + } +} + -- if ( _G._VADVENTURE ) then structs[ "playerPickup" ] = { keys = { @@ -119,7 +126,7 @@ structs[ "playerUse" ] = { structs[ "playerGotItem" ] = { keys = { { name = "item", type = "string" }, - { name = "count", type = "number" } + { name = "count", type = "float" } } } diff --git a/engine/shared/profile.lua b/engine/shared/profile.lua index 12551b9f..045d456b 100644 --- a/engine/shared/profile.lua +++ b/engine/shared/profile.lua @@ -4,15 +4,57 @@ -- --==========================================================================-- -class( "profile" ) +require( "love.timer" ) -profile._profiles = profile._profiles or {} +local ipairs = ipairs +local love = love +local print = print +local string = string +local table = table +local _G = _G -function profile.start( name ) - profile._profiles[ name ] = love.timer.getTime() +module( "profile" ) + +_stack = _stack or {} +_parent = _parent or _stack + +local function getChildren() + -- `children` == `_stack` or budget. + local children = _stack + if ( _parent ~= _stack ) then + _parent.children = _parent.children or {} + children = _parent.children + end + return children +end + +local function getBudget( children, name ) + for _, budget in ipairs( children ) do + if ( budget.name == name ) then + return budget + end + end end -function profile.stop( name ) - local duration = love.timer.getTime() - profile._profiles[ name ] - print( name .. " took " .. string.format( "%.3fs", duration ) ) +function push( name ) + local children = getChildren() + local budget = getBudget( children, name ) + if ( budget == nil ) then + budget = { name = name, parent = _parent } + table.insert( children, budget ) + end + + _parent = budget + budget.startTime = love.timer.getTime() +end + +function pop( name ) + _parent.endTime = love.timer.getTime() + _parent.duration = _parent.endTime - _parent.startTime + _parent = _parent.parent + + if ( _parent == nil ) then + _parent = _stack + end + -- print( name .. " took " .. string.format( "%.3fms", 1000 * duration ) ) end diff --git a/engine/shared/tablib.lua b/engine/shared/tablib.lua index a9ffbc88..b0ab6e85 100644 --- a/engine/shared/tablib.lua +++ b/engine/shared/tablib.lua @@ -117,6 +117,14 @@ function table.hasvalue( t, value ) return nil end +function table.keys( t ) + local keys = {} + for k in pairs( t ) do + table.insert( keys, k ) + end + return keys +end + function table.len( t ) if ( type( t ) ~= "table" ) then typerror( 1, "table", t ) diff --git a/engine/shared/typelenvalues.lua b/engine/shared/typelenvalues.lua index edaf9fe4..0c37c091 100644 --- a/engine/shared/typelenvalues.lua +++ b/engine/shared/typelenvalues.lua @@ -19,7 +19,6 @@ function typelenvalues.bytesToNumber( bytes ) for i = 6, 1, -1 do mantissa = mantissa * 256 + byte( bytes, i ) end - if ( byte( bytes, 8 ) > 127 ) then sign = -1 end @@ -34,6 +33,42 @@ function typelenvalues.bytesToNumber( bytes ) return ldexp( mantissa, exponent - 1023 ) end +function typelenvalues.bytesToFloat( bytes ) + bytes = reverse( bytes ) + + local sign = 1 + local mantissa = byte( bytes, 3 ) % 128 + for i = 2, 1, -1 do + mantissa = mantissa * 256 + byte( bytes, i ) + end + if ( byte( bytes, 4 ) > 127 ) then + sign = -1 + end + + local exponent = ( byte( bytes, 4 ) % 128 ) * 2 + + floor( byte( bytes, 3 ) / 128 ) + if ( exponent == 0 ) then + return 0 + end + + mantissa = sign * ( ldexp( mantissa, -23 ) + 1 ) + return ldexp( mantissa, exponent - 127 ) +end + +function typelenvalues.bytesToShort( bytes ) + bytes = reverse( bytes ) + + local number = 0 + number = number + byte( bytes, 1 ) + number = number + byte( bytes, 2 ) * ( 2 ^ 8 ) + + if ( number >= 2 ^ ( 2 * 8 - 1 ) ) then + number = number - 2 ^ ( 2 * 8 ) + end + + return number +end + local char = string.char local function getByte( v ) @@ -44,7 +79,6 @@ local frexp = math.frexp function typelenvalues.numberToBytes( number ) local sign = 0 - if ( number < 0 ) then sign = 1 number = -number @@ -59,20 +93,61 @@ function typelenvalues.numberToBytes( number ) exponent = exponent + 1022 end - local v = "" - local byte = 0 - number = mantissa + local bytes = "" + local byte = 0 + number = mantissa for i = 1, 6 do number, byte = getByte( number ) - v = v .. byte + bytes = bytes .. byte end number, byte = getByte( exponent * 16 + number ) - v = v .. byte + bytes = bytes .. byte number, byte = getByte( sign * 128 + number ) - v = v .. byte - return reverse( v ) + bytes = bytes .. byte + return reverse( bytes ) +end + +function typelenvalues.floatToBytes( number ) + local sign = 0 + if ( number < 0 ) then + sign = 1 + number = -number + end + + local mantissa, exponent = frexp( number ) + if ( number == 0 ) then + mantissa = 0 + exponent = 0 + else + mantissa = ( mantissa * 2 - 1 ) * ldexp( 0.5, 24 ) + exponent = exponent + 126 + end + + local bytes = "" + local byte = 0 + number, byte = getByte( mantissa ) + bytes = bytes .. byte + + number, byte = getByte( number ) + bytes = bytes .. byte + + number, byte = getByte( exponent * 128 + number ) + bytes = bytes .. byte + + number, byte = getByte( sign * 128 + number ) + bytes = bytes .. byte + return reverse( bytes ) +end + +function typelenvalues.shortToBytes( number ) + number = floor( number ) + + local bytes = char( number % ( 2 ^ 8 ) ) + number = floor( number / ( 2 ^ 8 ) ) + bytes = bytes .. char( number % ( 2 ^ 8 ) ) + return reverse( bytes ) end local pairs = pairs @@ -146,26 +221,28 @@ function typelenvalues:serialize() -- Insert length if necessary if ( key.type == "string" ) then local size = len( value ) - insert( data, typelenvalues.numberToBytes( size ) ) + insert( data, typelenvalues.shortToBytes( size ) ) elseif ( key.type == "typelenvalues" ) then local size = len( value:serialize() ) - insert( data, typelenvalues.numberToBytes( size ) ) + insert( data, typelenvalues.shortToBytes( size ) ) end -- Insert data if ( key.type == "boolean" ) then insert( data, char( value and 1 or 0 ) ) - elseif ( key.type == "number" ) then - insert( data, typelenvalues.numberToBytes( value ) ) + elseif ( key.type == "float" ) then + insert( data, typelenvalues.floatToBytes( value ) ) + elseif ( key.type == "short" ) then + insert( data, typelenvalues.shortToBytes( value ) ) elseif ( key.type == "string" ) then insert( data, value ) elseif ( key.type == "vector" ) then - insert( data, typelenvalues.numberToBytes( value.x ) ) - insert( data, typelenvalues.numberToBytes( value.y ) ) + insert( data, typelenvalues.floatToBytes( value.x ) ) + insert( data, typelenvalues.floatToBytes( value.y ) ) elseif ( key.type == "typelenvalues" ) then insert( data, value:serialize() ) elseif ( key.type == "entity" ) then - insert( data, typelenvalues.numberToBytes( value and value.entIndex or 0 ) ) + insert( data, typelenvalues.shortToBytes( value and value.entIndex or 0 ) ) else print( "Can't serialize " .. key.type .. " for " .. self:getStructName() .. "!" ) @@ -220,39 +297,43 @@ function typelenvalues:deserialize() if ( key ) then if ( key.type == "boolean" ) then size = 1 - elseif ( key.type == "number" ) then - size = 8 + elseif ( key.type == "float" ) then + size = 4 + elseif ( key.type == "short" ) then + size = 2 elseif ( key.type == "string" ) then - size = typelenvalues.bytesToNumber( sub( data, index, index + 7 ) ) - index = index + 8 + size = typelenvalues.bytesToShort( sub( data, index, index + 1 ) ) + index = index + 2 elseif ( key.type == "vector" ) then - size = 2 * 8 + size = 2 * 4 elseif ( key.type == "typelenvalues" ) then - size = typelenvalues.bytesToNumber( sub( data, index, index + 7 ) ) - index = index + 8 + size = typelenvalues.bytesToShort( sub( data, index, index + 1 ) ) + index = index + 2 elseif ( key.type == "entity" ) then - size = 8 + size = 2 end -- Get data bytes = sub( data, index, index + size - 1 ) if ( key.type == "boolean" ) then self.data[ key.name ] = byte( bytes ) ~= 0 - elseif ( key.type == "number" ) then - self.data[ key.name ] = typelenvalues.bytesToNumber( bytes ) + elseif ( key.type == "float" ) then + self.data[ key.name ] = typelenvalues.bytesToFloat( bytes ) + elseif ( key.type == "short" ) then + self.data[ key.name ] = typelenvalues.bytesToShort( bytes ) elseif ( key.type == "string" ) then self.data[ key.name ] = bytes elseif ( key.type == "vector" ) then self.data[ key.name ] = vector( - typelenvalues.bytesToNumber( sub( bytes, 1, 8 ) ), --x - typelenvalues.bytesToNumber( sub( bytes, 8, 16 ) ) --y + typelenvalues.bytesToFloat( sub( bytes, 1, 4 ) ), --x + typelenvalues.bytesToFloat( sub( bytes, 4, 8 ) ) --y ) elseif ( key.type == "typelenvalues" ) then local tlvs = typelenvalues() tlvs.data = bytes self.data[ key.name ] = tlvs elseif ( key.type == "entity" ) then - local entIndex = typelenvalues.bytesToNumber( bytes ) + local entIndex = typelenvalues.bytesToShort( bytes ) entities.require( "entity" ) self.data[ key.name ] = entity.getByEntIndex( entIndex ) end diff --git a/game/client/gui/closedialog.lua b/game/client/gui/closedialog.lua index f9aea9e1..8f7dfce8 100644 --- a/game/client/gui/closedialog.lua +++ b/game/client/gui/closedialog.lua @@ -26,6 +26,8 @@ function closedialog:closedialog( parent, name ) label:setWidth( font:getWidth( text ) ) local buttonYes = gui.button( self, "Close Dialog Yes Button", "Yes" ) + buttonYes.height = nil + buttonYes:setPadding( 14, 18, 13 ) buttonYes:setPos( 36, 86 + label:getHeight() + 18 ) buttonYes.onClick = function() love._shouldQuit = true @@ -33,6 +35,8 @@ function closedialog:closedialog( parent, name ) end local buttonNo = gui.button( self, "Close Dialog No Button", "No" ) + buttonNo.height = nil + buttonNo:setPadding( 14, 18, 13 ) buttonNo:setPos( 288, 86 + label:getHeight() + 18 ) buttonNo.onClick = function() self:close() diff --git a/game/client/gui/hudabout.lua b/game/client/gui/hudabout.lua index 33b457cc..f5d7abd3 100644 --- a/game/client/gui/hudabout.lua +++ b/game/client/gui/hudabout.lua @@ -4,10 +4,10 @@ -- --==========================================================================-- -class "gui.hudabout" ( "gui.panel" ) +class "gui.hudabout" ( "gui.box" ) local hudabout = gui.hudabout function hudabout:hudabout( parent ) - gui.panel.panel( self, parent, "HUD About" ) + gui.box.box( self, parent, "HUD About" ) end diff --git a/game/client/gui/hudchat.lua b/game/client/gui/hudchat.lua index 7d6c4ca9..fabe0b0d 100644 --- a/game/client/gui/hudchat.lua +++ b/game/client/gui/hudchat.lua @@ -15,6 +15,7 @@ function hudchat:hudchat( parent ) self.height = gui.scale( 404 ) self.output = gui.hudchattextbox( self, name .. " Output Text Box", "" ) + self.output:setMaxLength( 127 * 100 ) self.input = gui.textbox( self, name .. " Input Text Box", "" ) self.input:setPlaceholder( "Chat" ) self.input.onEnter = function( textbox, text ) @@ -30,7 +31,7 @@ function hudchat:hudchat( parent ) self.initializing = true self:invalidateLayout() - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) self:dock() end diff --git a/game/client/gui/huddialogue.lua b/game/client/gui/huddialogue.lua index eac77733..86f729fa 100644 --- a/game/client/gui/huddialogue.lua +++ b/game/client/gui/huddialogue.lua @@ -19,12 +19,12 @@ function huddialogue:huddialogue( parent ) box:setHeight( self.height ) box:setPadding( 18 ) - local header = gui.text( box, name .. " Header", "h1" ) + local header = gui.text( box, "h1" ) header:setDisplay( "block" ) header:setMarginBottom( 8 ) header:setFont( self:getScheme( "fontBold" ) ) - local dialogue = gui.text( box, name .. " Dialogue", "p" ) + local dialogue = gui.text( box, "p" ) dialogue:setDisplay( "block" ) self:invalidateLayout() diff --git a/game/client/gui/hudgamemenu/inventory.lua b/game/client/gui/hudgamemenu/inventory.lua index 01c305bb..76420990 100644 --- a/game/client/gui/hudgamemenu/inventory.lua +++ b/game/client/gui/hudgamemenu/inventory.lua @@ -13,11 +13,11 @@ local hudgamemenuinventory = gui.hudgamemenuinventory function hudgamemenuinventory:hudgamemenuinventory( parent ) gui.box.box( self, parent, "Inventory" ) self:setScheme( "Default" ) - self:setSize( parent:getSize() ) + self:setDimensions( parent:getDimensions() ) self.grid = gui.itemgrid( self, "Inventory Item Grid" ) self.grid:setPos( 36, 86 + 31 + 18 ) - self.grid:setSize( parent:getWidth() - 2 * 36, 314 ) + self.grid:setDimensions( parent:getWidth() - 2 * 36, 314 ) self.grid:setColumns( 4 ) self.grid:setRows( 7 ) diff --git a/game/client/gui/hudgamemenu/itembutton.lua b/game/client/gui/hudgamemenu/itembutton.lua index 699db0c2..a1871e47 100644 --- a/game/client/gui/hudgamemenu/itembutton.lua +++ b/game/client/gui/hudgamemenu/itembutton.lua @@ -118,10 +118,9 @@ local function addOptions( itembutton, classname ) local n = 0 for k, option in pairs( options ) do local panelName = name .. " " .. n - optionsitem = gui.optionsitem( panelName, option.name ) + optionsitem = gui.optionsitem( itembutton, panelName, option.name ) optionsitem:setEntity( item.data.name ) optionsitem:setValue( option.value ) - itembutton:addItem( optionsitem ) n = n + 1 end return n diff --git a/game/client/gui/hudgamemenu/navigationbutton.lua b/game/client/gui/hudgamemenu/navigationbutton.lua index c63e921c..553774ad 100644 --- a/game/client/gui/hudgamemenu/navigationbutton.lua +++ b/game/client/gui/hudgamemenu/navigationbutton.lua @@ -10,13 +10,12 @@ local hudgamemenunavigationbutton = gui.hudgamemenunavigationbutton function hudgamemenunavigationbutton:hudgamemenunavigationbutton( parent, name ) gui.radiobutton.radiobutton( self, parent, name, name ) - self:setDisplay( "inline" ) + self.width = nil + self.height = nil + self:setDisplay( "inline-block" ) self:setPosition( "static" ) self:setMarginRight( 18 ) - - local font = self:getScheme( "font" ) - self:setWidth( font:getWidth( self:getText() ) ) - self:setHeight( 45 ) + self:setPaddingTop( 13 ) self:setValue( name ) end @@ -45,18 +44,12 @@ end function hudgamemenunavigationbutton:drawLabel() if ( self:isDisabled() ) then - love.graphics.setColor( self:getScheme( "radiobutton.disabled.textColor" ) ) + self.text:setColor( self:getScheme( "radiobutton.disabled.textColor" ) ) elseif ( self:isSelected() ) then - love.graphics.setColor( self:getScheme( "hudgamemenunavigationbutton.borderColor" ) ) - elseif ( self.mouseover ) then - love.graphics.setColor( self:getScheme( "hudgamemenunavigationbutton.mouseover.textColor" ) ) + self.text:setColor( self:getScheme( "hudgamemenunavigationbutton.borderColor" ) ) + elseif ( self.mouseover or self:isChildMousedOver() ) then + self.text:setColor( self:getScheme( "hudgamemenunavigationbutton.mouseover.textColor" ) ) else - love.graphics.setColor( self:getScheme( "hudgamemenunavigationbutton.textColor" ) ) + self.text:setColor( self:getScheme( "hudgamemenunavigationbutton.textColor" ) ) end - - local font = self:getScheme( "font" ) - love.graphics.setFont( font ) - local x = 0 - local y = math.round( self:getHeight() / 2 - font:getHeight() / 2 - 1 ) - love.graphics.print( self:getText(), x, y ) end diff --git a/game/client/gui/hudgamemenu/stat.lua b/game/client/gui/hudgamemenu/stat.lua index e82ec13a..e544a24e 100644 --- a/game/client/gui/hudgamemenu/stat.lua +++ b/game/client/gui/hudgamemenu/stat.lua @@ -19,6 +19,7 @@ function hudgamemenustat:hudgamemenustat( parent, name, stat ) self:setScheme( "Default" ) local progressbar = gui.progressbar( self, "Stat Progress" ) + progressbar:setPosition( "absolute" ) progressbar:setY( 23 ) self.progressbar = progressbar diff --git a/game/client/gui/hudgamemenu/stats.lua b/game/client/gui/hudgamemenu/stats.lua index 8fe4f0ec..a2c2046e 100644 --- a/game/client/gui/hudgamemenu/stats.lua +++ b/game/client/gui/hudgamemenu/stats.lua @@ -4,17 +4,17 @@ -- --==========================================================================-- -class "gui.hudgamemenustats" ( "gui.panel" ) +class "gui.hudgamemenustats" ( "gui.box" ) local hudgamemenustats = gui.hudgamemenustats function hudgamemenustats:hudgamemenustats( parent ) - gui.panel.panel( self, parent, "Stats" ) - self:setScheme( "Default" ) - self:setSize( parent:getSize() ) + gui.box.box( self, parent, "Stats" ) + self:setDimensions( parent:getDimensions() ) local panel = gui.scrollablepanel( self, "Stats Scrollable Panel" ) - panel:setSize( self:getWidth(), self:getHeight() - 135 ) + panel:setPosition( "absolute" ) + panel:setDimensions( self:getWidth(), self:getHeight() - 135 ) panel:setInnerHeight( 738 ) panel:setY( 86 + 31 + 18 ) panel = panel:getInnerPanel() diff --git a/game/client/gui/hudhealth.lua b/game/client/gui/hudhealth.lua index b000aab8..0c28ac59 100644 --- a/game/client/gui/hudhealth.lua +++ b/game/client/gui/hudhealth.lua @@ -14,12 +14,12 @@ function hudhealth:hudhealth( parent ) self:setDisplay( "block" ) self:setPosition( "absolute" ) - self.text = gui.text( self, name .. " Text Node", "" ) + self.text = gui.text( self, "" ) self.text:setDisplay( "block" ) self.text:setColor( self:getScheme( "hudmoveindicator.textColor" ) ) self.text:setFont( self:getScheme( "entityFont" ) ) - local label = gui.text( self, name .. " Text Node", "Health" ) + local label = gui.text( self, "Health" ) label:setColor( self:getScheme( "hudmoveindicator.smallTextColor" ) ) self:invalidateLayout() @@ -33,7 +33,7 @@ end function hudhealth:drawHealth() local health = localplayer:getNetworkVar( "health" ) - self.text:setText( health ) + self.text:set( health ) end function hudhealth:invalidateLayout() diff --git a/game/client/gui/hudmana.lua b/game/client/gui/hudmana.lua index 0058d153..e5d88312 100644 --- a/game/client/gui/hudmana.lua +++ b/game/client/gui/hudmana.lua @@ -14,12 +14,12 @@ function hudmana:hudmana( parent ) self:setDisplay( "block" ) self:setPosition( "absolute" ) - self.text = gui.text( self, name .. " Text Node", "" ) + self.text = gui.text( self, "" ) self.text:setDisplay( "block" ) self.text:setColor( self:getScheme( "hudmoveindicator.textColor" ) ) self.text:setFont( self:getScheme( "entityFont" ) ) - local label = gui.text( self, name .. " Text Node", "Mana" ) + local label = gui.text( self, "Mana" ) label:setColor( self:getScheme( "hudmoveindicator.smallTextColor" ) ) self:invalidateLayout() @@ -33,7 +33,7 @@ end function hudmana:drawMana() local mana = localplayer:getNetworkVar( "mana" ) - self.text:setText( mana ) + self.text:set( mana ) end function hudmana:invalidateLayout() diff --git a/game/client/gui/hudmoveindicator.lua b/game/client/gui/hudmoveindicator.lua index 7c221bd7..05ae2fec 100644 --- a/game/client/gui/hudmoveindicator.lua +++ b/game/client/gui/hudmoveindicator.lua @@ -13,7 +13,7 @@ function hudmoveindicator:hudmoveindicator( parent ) gui.box.box( self, parent, name ) self.width = love.graphics.getWidth() self.height = love.graphics.getHeight() - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) self:setPadding( gui.scale( 96 ) ) self:setDisplay( "block" ) @@ -90,7 +90,7 @@ local function drawLabel( self, x, y ) love.graphics.push() love.graphics.scale( 1 / camera.getZoom() ) - local font = self:getScheme( "fontSmall" ) + local font = scheme.getProperty( "Console", "font" ) love.graphics.setFont( font ) local position = vector( x, y ) + vector( 0, game.tileSize ) @@ -353,25 +353,22 @@ local function onRightClick( self, x, y ) for i, entity in pairs( opts ) do for j, option in pairs( entity.options ) do local panelName = name .. " " .. n - optionsitem = gui.optionsitem( panelName, option.name ) + optionsitem = gui.optionsitem( options, panelName, option.name ) optionsitem:setEntity( entity.entity ) optionsitem:setValue( option.value ) - options:addItem( optionsitem ) n = n + 1 end end x, y = camera.screenToWorld( x, y ) - optionsitem = gui.optionsitem( name .. " " .. n, "Walk here" ) + optionsitem = gui.optionsitem( options, name .. " " .. n, "Walk here" ) optionsitem:setValue( function() moveTo( self, x, y ) end ) - options:addItem( optionsitem ) n = n + 1 - optionsitem = gui.optionsitem( name .. " " .. n, "Cancel" ) + optionsitem = gui.optionsitem( options, name .. " " .. n, "Cancel" ) optionsitem:setValue( noop ) - options:addItem( optionsitem ) n = n + 1 return n > 1 @@ -404,14 +401,21 @@ end function hudmoveindicator:update( dt ) local mx, my = love.mouse.getPosition() local entity = getEntitiesAtMousePos( mx, my )[ 1 ] - self._entity = entity + local needsRedraw = false + if ( self._entity ~= entity ) then + self._entity = entity + needsRedraw = true + end local sprites = self._sprites if ( sprites ) then for _, indicator in ipairs( sprites ) do indicator.sprite:update( dt ) end + needsRedraw = true end - self:invalidate() + if ( needsRedraw ) then + self:invalidate() + end end diff --git a/game/client/gui/hudspeechballoons.lua b/game/client/gui/hudspeechballoons.lua index a274eabc..fa67618b 100644 --- a/game/client/gui/hudspeechballoons.lua +++ b/game/client/gui/hudspeechballoons.lua @@ -4,12 +4,12 @@ -- --==========================================================================-- -class "gui.hudspeechballoons" ( "gui.panel" ) +class "gui.hudspeechballoons" ( "gui.box" ) local hudspeechballoons = gui.hudspeechballoons function hudspeechballoons:hudspeechballoons( parent ) - gui.panel.panel( self, parent, "HUD Speech Balloons" ) + gui.box.box( self, parent, "HUD Speech Balloons" ) self:setScheme( "Default" ) self:addChatHook() @@ -46,7 +46,7 @@ end function hudspeechballoons:draw() self:drawBalloons() - gui.panel.draw( self ) + gui.box.draw( self ) end function hudspeechballoons:drawBalloons() @@ -93,7 +93,10 @@ function hudspeechballoons:update( dt ) end self:updateBalloons() - self:invalidate() + + if ( self.speechBalloons ~= nil ) then + self:invalidate() + end end function hudspeechballoons:updateBalloons() @@ -106,4 +109,8 @@ function hudspeechballoons:updateBalloons() self.speechBalloons[ player ] = nil end end + + if ( table.count( self.speechBalloons ) == 0 ) then + self.speechBalloons = nil + end end diff --git a/game/client/gui/mainmenu.lua b/game/client/gui/mainmenu.lua index 16705841..2f775b24 100644 --- a/game/client/gui/mainmenu.lua +++ b/game/client/gui/mainmenu.lua @@ -10,14 +10,14 @@ class "gui.mainmenu" ( "gui.box" ) local mainmenu = gui.mainmenu -function mainmenu:mainmenu() +function mainmenu:mainmenu( parent, name ) gui.box.box( self, g_RootPanel, "Main Menu" ) self:setDisplay( "block" ) self:setPosition( "absolute" ) self.width = love.graphics.getWidth() self.height = love.graphics.getHeight() - self:setUseFullscreenFramebuffer( true ) + self:setUseFullscreenCanvas( true ) self.logo = self:getScheme( "logo" ) self.logoSmall = self:getScheme( "logoSmall" ) @@ -39,12 +39,15 @@ local MAINMENU_ANIM_TIME = 0.2 function mainmenu:activate() if ( not self:isVisible() ) then - self:setOpacity( 0 ) + -- self:setOpacity( 0 ) self:animate( { opacity = 1 -- y = 0 -- scale = 1 - }, MAINMENU_ANIM_TIME, "easeOutQuint" ) + }, { + duration = MAINMENU_ANIM_TIME, + easing = "easeOutQuint", + } ) end self:setVisible( true ) @@ -69,14 +72,20 @@ function mainmenu:close() opacity = 0 -- y = love.graphics.getHeight() -- scale = 0 - }, MAINMENU_ANIM_TIME, "easeOutQuint", function() - self:setVisible( false ) - self:setOpacity( 1 ) - - self.closing = nil - end ) + }, { + duration = MAINMENU_ANIM_TIME, + easing = "easeOutQuint", + complete = function() + self:setVisible( false ) + self:setOpacity( 1 ) + + self.closing = nil + end + } ) - game.call( "client", "onMainMenuClose" ) + if ( game ) then + game.call( "client", "onMainMenuClose" ) + end end function mainmenu:createButtons() @@ -95,7 +104,7 @@ function mainmenu:createButtons() end table.insert( self.buttons, self.joinLeaveServer ) - table.insert( self.buttons, gui.mainmenubutton( self ) ) + table.insert( self.buttons, gui.mainmenubutton( self, "" ) ) local options = gui.mainmenubutton( self, "Options" ) options.onClick = function() @@ -129,7 +138,7 @@ function mainmenu:enableServerConnections() end function mainmenu:invalidateLayout() - self:setSize( love.graphics.getWidth(), love.graphics.getHeight() ) + self:setDimensions( love.graphics.getWidth(), love.graphics.getHeight() ) local margin = gui.scale( 96 ) local y = margin @@ -175,12 +184,6 @@ function mainmenu:draw() gui.panel.draw( self ) end -function mainmenu:drawTranslucency() - if ( gui._translucencyFramebuffer ) then - gui._translucencyFramebuffer:draw() - end -end - function mainmenu:drawLogo() local logo = self.logo local height = self:getHeight() @@ -231,7 +234,7 @@ function mainmenu:remove() end function mainmenu:update( dt ) - if ( gui._translucencyFramebuffer and self:isVisible() ) then + if ( gui._translucencyCanvas and self:isVisible() ) then self:invalidate() end diff --git a/game/client/gui/mainmenubutton.lua b/game/client/gui/mainmenubutton.lua index 610074b6..51788779 100644 --- a/game/client/gui/mainmenubutton.lua +++ b/game/client/gui/mainmenubutton.lua @@ -14,7 +14,7 @@ function mainmenubutton:mainmenubutton( parent, text ) self:setBorderWidth( 0 ) local font = self:getScheme( "mainmenuFont" ) - self.text = gui.text( self, name .. " Text Node", text or "" ) + self.text:set( text ) self.text:setFont( font ) self.height = font:getHeight() @@ -22,11 +22,12 @@ end function mainmenubutton:draw() local textColor = "mainmenubutton.dark.textColor" + local mouseover = ( self.mouseover or self:isChildMousedOver() ) if ( self:isDisabled() ) then textColor = "mainmenubutton.dark.disabled.textColor" - elseif ( self.mousedown and ( self.mouseover or self:isChildMousedOver() ) ) then + elseif ( self.mousedown and mouseover ) then textColor = "mainmenubutton.dark.mousedown.textColor" - elseif ( self.mousedown or ( self.mouseover or self:isChildMousedOver() ) or self.focus ) then + elseif ( self.mousedown or mouseover or self.focus ) then textColor = "mainmenubutton.dark.mouseover.textColor" end diff --git a/game/client/gui/mainmenuclosebutton.lua b/game/client/gui/mainmenuclosebutton.lua index 49df2282..8f3a3fea 100644 --- a/game/client/gui/mainmenuclosebutton.lua +++ b/game/client/gui/mainmenuclosebutton.lua @@ -4,9 +4,8 @@ -- --==========================================================================-- -local gui = gui -local love = love -local unpack = unpack +local gui = gui +local love = love class "gui.mainmenuclosebutton" ( "gui.closebutton" ) diff --git a/game/client/gui/optionsitem.lua b/game/client/gui/optionsitem.lua index 1ebece98..c6c4bdf2 100644 --- a/game/client/gui/optionsitem.lua +++ b/game/client/gui/optionsitem.lua @@ -8,9 +8,9 @@ class "gui.optionsitem" ( "gui.dropdownlistitem" ) local optionsitem = gui.optionsitem -function optionsitem:optionsitem( name, text ) - gui.dropdownlistitem.dropdownlistitem( self, name, text .. " " ) - self.entityText = gui.text( self, name .. " Text Node", "" ) +function optionsitem:optionsitem( parent, name, text ) + gui.dropdownlistitem.dropdownlistitem( self, parent, name, text .. " " ) + self.entityText = gui.text( self, "" ) self.entityText:setFont( self:getScheme( "fontBold" ) ) end @@ -29,12 +29,13 @@ function optionsitem:drawText() self.entityText:setColor( color ) local entity = self:getEntity() + local text = "" if ( type( entity ) == "string" ) then text = entity else text = entity and entity:getName() or "" end - self.entityText:setText( text ) + self.entityText:set( text ) end function optionsitem:setDefault( default ) diff --git a/game/client/gui/optionsitemgroup.lua b/game/client/gui/optionsitemgroup.lua index b61be9de..52c88720 100644 --- a/game/client/gui/optionsitemgroup.lua +++ b/game/client/gui/optionsitemgroup.lua @@ -32,12 +32,13 @@ end function optionsitemgroup:invalidateLayout() self:updatePos() + gui.panel.invalidateLayout( self ) end function optionsitemgroup:updatePos() local parent = self:getParent() local x, y = self:getPos() - local width, height = self:getSize() + local width, height = self:getDimensions() local windowPadding = 4 if ( x + width > parent:getWidth() ) then x = x - width diff --git a/game/client/init.lua b/game/client/init.lua index df607137..61130e9c 100644 --- a/game/client/init.lua +++ b/game/client/init.lua @@ -46,6 +46,10 @@ function createDefaultPanels() -- Initialize dialogue -- local dialogue = gui.huddialogue( _G.g_Viewport ) -- _G.g_Dialogue = dialogue + + -- Initialize profiler + local profiler = gui.hudprofiler( _G.g_Viewport ) + _G.g_Profiler = profiler end function draw() @@ -81,7 +85,7 @@ end function onReloadSound( filename ) end -function onTick( timestep ) +function tick( timestep ) end function quit() diff --git a/game/server/init.lua b/game/server/init.lua index 57c39ff1..19ea015d 100644 --- a/game/server/init.lua +++ b/game/server/init.lua @@ -56,7 +56,7 @@ if ( _VADVENTURE ) then end end -function onTick( timestep ) +function tick( timestep ) end function quit() diff --git a/game/shared/entities/item_apple.lua b/game/shared/entities/item_apple.lua index 56679ba8..f6eb0985 100644 --- a/game/shared/entities/item_apple.lua +++ b/game/shared/entities/item_apple.lua @@ -50,6 +50,11 @@ function item_apple:spawn() local max = vector( tileSize, -tileSize ) self:initializePhysics( "dynamic" ) self:setCollisionBounds( min, max ) + + local body = self:getBody() + if ( body ) then + body:setMass( 0.1496855 ) + end end function item_apple:useItem( activator, value ) diff --git a/game/shared/entities/prop_worldgate_spawn.lua b/game/shared/entities/prop_worldgate_spawn.lua index 1ee79762..43349564 100644 --- a/game/shared/entities/prop_worldgate_spawn.lua +++ b/game/shared/entities/prop_worldgate_spawn.lua @@ -56,7 +56,7 @@ function prop_worldgate_spawn:spawn() self:setCollisionBounds( min, max ) end -function prop_worldgate_spawn:update( dt ) +function prop_worldgate_spawn:tick( timestep ) local position = self:getPosition() local players = player.getAll() for _, player in ipairs( players ) do diff --git a/game/shared/entities/vaplayer.lua b/game/shared/entities/vaplayer.lua index d91a9c9f..6d7ff5cc 100644 --- a/game/shared/entities/vaplayer.lua +++ b/game/shared/entities/vaplayer.lua @@ -193,6 +193,16 @@ if ( _CLIENT ) then payload.setHandler( onPlayerRemovedItem, "playerRemovedItem" ) end +function vaplayer:onNetworkVarChanged( networkvar ) + entity.onNetworkVarChanged( self, networkvar ) + + if ( networkvar:getName() == "health" ) then + if ( _CLIENT and g_HudHealth ) then + g_HudHealth:invalidateLayout() + end + end +end + local function moveTo( position ) return function( character, next ) character:moveTo( position, next ) diff --git a/game/shared/entities/weapon_bow.lua b/game/shared/entities/weapon_bow.lua index 41c72fe6..e9052dbe 100644 --- a/game/shared/entities/weapon_bow.lua +++ b/game/shared/entities/weapon_bow.lua @@ -40,6 +40,11 @@ function weapon_bow:spawn() local max = vector( tileSize, -tileSize ) self:initializePhysics( "dynamic" ) self:setCollisionBounds( min, max ) + + local body = self:getBody() + if ( body ) then + body:setMass( 1.81437 ) + end end entities.linkToClassname( weapon_bow, "weapon_bow" ) diff --git a/images/player.lua b/images/player.lua index c0183f00..9b134385 100644 --- a/images/player.lua +++ b/images/player.lua @@ -2,7 +2,7 @@ return { image = "images/player.png", width = 16, height = 32, - frametime = 0.2, + frametime = 0.25, animations = { idlenorth = { from = 1, @@ -20,6 +20,26 @@ return { from = 4, to = 4 }, + -- idlenorth + idlenortheast = { + from = 1, + to = 1, + }, + -- idlesouth + idlesoutheast = { + from = 3, + to = 3, + }, + -- idlesouth + idlesouthwest = { + from = 3, + to = 3, + }, + -- idlenorth + idlenorthwest = { + from = 1, + to = 1, + }, walknorth = { from = 33, to = 36 diff --git a/schemes/Chat.lua b/schemes/Chat.lua index 9a075a93..1a118407 100644 --- a/schemes/Chat.lua +++ b/schemes/Chat.lua @@ -1,4 +1,4 @@ ---=========== Copyright © 2018, Planimeter, All rights reserved. ===========-- +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- -- -- Purpose: Chat HUD scheme -- @@ -22,4 +22,13 @@ chat.textbox = { } } -chat.font = love.graphics.newFont( "fonts/SourceSansPro-Regular.otf", 14 ) +-- NOTE: The following arguments to `newFont` are undocumented. +local dpiscale = love.graphics.getDPIScale() +local r_window_highdpi = convar.getConvar( "r_window_highdpi" ) +if ( r_window_highdpi:getNumber() == 2 ) then + dpiscale = 1 +end + +chat.font = love.graphics.newFont( + "fonts/SourceSansPro-Regular.otf", 14, "normal", dpiscale +) diff --git a/schemes/Console.lua b/schemes/Console.lua index 153c8f88..a40b6bfb 100644 --- a/schemes/Console.lua +++ b/schemes/Console.lua @@ -22,4 +22,13 @@ console.textbox = { } } -console.font = love.graphics.newFont( "fonts/SourceCodePro-Light.otf", 12 ) +-- NOTE: The following arguments to `newFont` are undocumented. +local dpiscale = love.graphics.getDPIScale() +local r_window_highdpi = convar.getConvar( "r_window_highdpi" ) +if ( r_window_highdpi:getNumber() == 2 ) then + dpiscale = 1 +end + +console.font = love.graphics.newFont( + "fonts/SourceCodePro-Light.otf", 12, "normal", dpiscale +) diff --git a/schemes/Default.lua b/schemes/Default.lua index 92df0e15..26077dad 100644 --- a/schemes/Default.lua +++ b/schemes/Default.lua @@ -1,4 +1,4 @@ ---=========== Copyright © 2018, Planimeter, All rights reserved. ===========-- +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- -- -- Purpose: Default scheme -- @@ -147,6 +147,10 @@ t.hudmoveindicator = { indicatorColor = t.colors.gold } +t.hudprofiler = { + textColor = t.colors.white +} + t.itembutton = { textColor = t.colors.gold } @@ -156,7 +160,7 @@ t.label = { } t.mainmenu = { - backgroundColor = color( t.colors.black, 0.27 * 255 ), + backgroundColor = color( t.colors.black, 0.70 * 255 ), logo = love.graphics.newImage( "images/gui/logo.png" ), logoSmall = love.graphics.newImage( "images/gui/logo_small.png" ) } @@ -292,12 +296,37 @@ t.bindlistheader = { borderColor = color( 15, 15, 15, 255 ), } -t.mainmenuFont = love.graphics.newFont( "fonts/SourceSansPro-Regular.otf", 24 ) -t.titleFont = love.graphics.newFont( "fonts/SourceSansPro-Bold.otf", 18 ) -t.font = love.graphics.newFont( "fonts/SourceSansPro-Regular.otf", 14 ) -t.fontBold = love.graphics.newFont( "fonts/SourceSansPro-Bold.otf", 14 ) -t.fontSmall = love.graphics.newFont( "fonts/SourceSansPro-Regular.otf", 12 ) -t.consoleFont = love.graphics.newFont( "fonts/SourceCodePro-Light.otf", 12 ) -t.chatFont = love.graphics.newFont( "fonts/SourceCodePro-Light.otf", 14 ) -t.entityFont = love.graphics.newFont( "fonts/SourceSansPro-Regular.otf", 24 ) -t.itemCountFont = love.graphics.newFont( "fonts/SourceSansPro-Regular.otf", 12 ) +-- NOTE: The following arguments to `newFont` are undocumented. +local dpiscale = love.graphics.getDPIScale() +local r_window_highdpi = convar.getConvar( "r_window_highdpi" ) +if ( r_window_highdpi:getNumber() == 2 ) then + dpiscale = 1 +end + +t.mainmenuFont = love.graphics.newFont( + "fonts/SourceSansPro-Regular.otf", 24, "normal", dpiscale +) +t.titleFont = love.graphics.newFont( + "fonts/SourceSansPro-Bold.otf", 18, "normal", dpiscale +) +t.font = love.graphics.newFont( + "fonts/SourceSansPro-Regular.otf", 14, "normal", dpiscale +) +t.fontBold = love.graphics.newFont( + "fonts/SourceSansPro-Bold.otf", 14, "normal", dpiscale +) +t.fontSmall = love.graphics.newFont( + "fonts/SourceSansPro-Regular.otf", 12, "normal", dpiscale +) +t.consoleFont = love.graphics.newFont( + "fonts/SourceCodePro-Light.otf", 12, "normal", dpiscale +) +t.chatFont = love.graphics.newFont( + "fonts/SourceCodePro-Light.otf", 14, "normal", dpiscale +) +t.entityFont = love.graphics.newFont( + "fonts/SourceSansPro-Regular.otf", 24, "normal", dpiscale +) +t.itemCountFont = love.graphics.newFont( + "fonts/SourceSansPro-Regular.otf", 12, "normal", dpiscale +) diff --git a/scripts/test/box.lua b/scripts/test/box.lua new file mode 100644 index 00000000..528a8111 --- /dev/null +++ b/scripts/test/box.lua @@ -0,0 +1,62 @@ +--=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- +-- +-- Purpose: Test box +-- +--==========================================================================-- + +local e = gui.createElement + +local name = "Quit Game" + +class "gui.boxtestframe" ( "gui.frame" ) + +local boxtestframe = gui.boxtestframe + +function boxtestframe:boxtestframe( parent, name, title ) + gui.frame.frame( self, parent, name, title ) + + local child = e( "box", { + parent = self, + position = "absolute", + y = 86, + paddingTop = 0, + padding = 36 + }, { + e( "text", { + marginBottom = 9, + text = "Are you sure you want to quit the game?", + -- color = color.white + } ), + e( "box", { + display = "block" + }, { + e( "button", { + display = "inline", + position = "static", + -- width = "nil", + -- height = "nil", + padding = 14, + marginRight = 36, + text = "Quit", + onClick = function() love._shouldQuit = true; love.quit() end + } ), + e( "button", { + display = "inline", + position = "static", + -- width = "nil", + -- height = "nil", + padding = 14, + text = "Cancel", + onClick = function() self:close() end + } ) + } ) + } ) + + self:setWidth( child:getWidth() ) + self:setHeight( 86 + child:getHeight() ) +end + +local frame = gui.boxtestframe( nil, name, name ) +frame:setRemoveOnClose( true ) +frame:moveToCenter() +frame:activate() diff --git a/shaders/coloroverlay.frag b/shaders/coloroverlay.frag index b5f61e6a..a0c52b2d 100644 --- a/shaders/coloroverlay.frag +++ b/shaders/coloroverlay.frag @@ -4,7 +4,7 @@ // //============================================================================// -vec4 effect( vec4 vcolor, Image tex, vec2 texcoord, vec2 pixcoord ) +vec4 effect( vec4 color, Image tex, vec2 texcoord, vec2 pixcoord ) { - return vec4( vcolor.rgb, Texel( tex, texcoord ).a ); + return vec4( color.rgb, Texel( tex, texcoord ).a ); } diff --git a/shaders/gaussianblur.frag b/shaders/gaussianblur.frag new file mode 100644 index 00000000..80a92926 --- /dev/null +++ b/shaders/gaussianblur.frag @@ -0,0 +1,23 @@ +//=========== Copyright © 2019, Planimeter, All rights reserved. =============// +// +// Purpose: Gaussian blur fragment reference shader +// https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch40.html +// +//============================================================================// + +uniform float sigma; // Gaussian sigma +uniform vec2 dir; // horiz=(1.0, 0.0), vert=(0.0, 1.0) +uniform int support; // int(sigma * 3.0) truncation +vec4 effect( vec4 color, Image tex, vec2 texcoord, vec2 pixcoord ) +{ + vec2 loc = texcoord; // center pixel cooordinate + vec4 acc = vec4( 0.0f ); // accumulator + float norm = 0.0f; + for (int i = -support; i <= support; i++) { + float coeff = exp(-0.5 * float(i) * float(i) / (sigma * sigma)); + acc += (Texel(tex, loc + float(i) * dir)) * coeff; + norm += coeff; + } + acc *= 1/norm; // normalize for unity gain + return acc; +} diff --git a/shaders/gaussianblur.lua b/shaders/gaussianblur.lua index 337e9655..39b34257 100644 --- a/shaders/gaussianblur.lua +++ b/shaders/gaussianblur.lua @@ -1,84 +1,35 @@ --=========== Copyright © 2019, Planimeter, All rights reserved. ===========-- -- --- Purpose: Gaussian Blur pixel shader +-- Purpose: Gaussian blur fragment shader -- --==========================================================================-- ---[[ -The MIT License (MIT) - -Copyright (c) 2015 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. - -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. -]]-- - require( "shaders.shader" ) class "gaussianblur" ( "shader" ) --- unroll convolution loop -local function build_shader(sigma) - local support = math.max(1, math.floor(3*sigma + .5)) - local one_by_sigma_sq = sigma > 0 and 1 / (sigma * sigma) or 1 - local norm = 0 - - local code = {[[ - extern vec2 direction; - vec4 effect(vec4 color, Image texture, vec2 tc, vec2 _) - { vec4 c = vec4(0.0f); - ]]} - local blur_line = "c += vec4(%f) * Texel(texture, tc + vec2(%f) * direction);" - - for i = -support,support do - local coeff = math.exp(-.5 * i*i * one_by_sigma_sq) - norm = norm + coeff - code[#code+1] = blur_line:format(coeff, i) - end - - code[#code+1] = ("return c * vec4(%f) * color;}"):format(norm > 0 and 1/norm or 1) - - return love.graphics.newShader(table.concat(code)) -end - -local gaussianblur = gaussianblur or shader._shaders[ "gaussianblur" ] +local gaussianblur = shader._shaders[ "gaussianblur" ] or gaussianblur function gaussianblur:gaussianblur() local width, height = love.graphics.getDimensions() self.scale = 1 / 2 width = width * self.scale height = height * self.scale - self.canvas_h = love.graphics.newCanvas( width, height, { dpiscale = 1 } ) - self.canvas_v = love.graphics.newCanvas( width, height, { dpiscale = 1 } ) - self.shader = build_shader(1) - self.shader:send("direction",{1.0,0.0}) + self.horizontalPass = love.graphics.newCanvas( width, height, { dpiscale = 1 } ) + self.verticalPass = love.graphics.newCanvas( width, height, { dpiscale = 1 } ) + -- local fragmentShader = love.filesystem.read( "shaders/gaussianblur.frag" ) + -- self.shader = love.graphics.newShader( fragmentShader ) end -function gaussianblur:renderTo(func) - local s = love.graphics.getShader() - - love.graphics.setShader(self.shader) +function gaussianblur:renderTo( func ) + local shader = love.graphics.getShader() + love.graphics.setShader( self.shader ) - -- first pass (horizontal blur) - self.shader:send('direction', {1 / love.graphics.getWidth(), 0}) - -- draw scene - -- self.canvas_h:clear() - self.canvas_h:renderTo( function() + local width, height = love.graphics.getDimensions() + width = width * self.scale + height = height * self.scale + self.shader:send( "dir", { 1 / width, 0 } ) + self.horizontalPass:renderTo( function() love.graphics.push() love.graphics.scale( self.scale, self.scale ) func() @@ -86,34 +37,60 @@ function gaussianblur:renderTo(func) end ) local b = love.graphics.getBlendMode() - love.graphics.setBlendMode('alpha', 'premultiplied') - - -- second pass (vertical blur) - self.shader:send('direction', {0, 1 / love.graphics.getHeight()}) - -- self.canvas_v:clear() - self.canvas_v:renderTo(function() love.graphics.clear() love.graphics.draw(self.canvas_h, 0,0) end) + love.graphics.setBlendMode( "alpha", "premultiplied" ) - -- love.graphics.draw(self.canvas_v, 0,0) + self.shader:send( "dir", { 0, 1 / height } ) + self.verticalPass:renderTo( function() + love.graphics.clear() + love.graphics.draw( self.horizontalPass ) + end ) - -- restore blendmode, shader and canvas - love.graphics.setBlendMode(b) - love.graphics.setShader(s) + love.graphics.setBlendMode( b ) + love.graphics.setShader( shader ) end function gaussianblur:draw() love.graphics.setColor( color.white ) - love.graphics.draw( self.canvas_v, 0, 0, 0, 1 / self.scale ) + love.graphics.draw( self.verticalPass, 0, 0, 0, 1 / self.scale ) end -function gaussianblur:set(key, value) - -- Disable blur - -- value = 0 - if key == "sigma" then - self.shader = build_shader(tonumber(value)) - else - error("Unknown property: " .. tostring(key)) +function gaussianblur:set( key, value ) + if ( key == "sigma" ) then + -- self.shader:send( "sigma", value ) + -- self.shader:send( "norm", 1/(math.sqrt(2*math.pi)*value) ) + -- self.shader:send( "support", (value * 3.0) ) + self:generateShader( value, (value * 3.0) ) end + return self end +function gaussianblur:generateShader( sigma, support ) + -- See `shaders/gaussianblur.frag` + -- Loop unroll Gaussian convolution + local norm = 0 + local forLoop = {} + local line = "acc += (Texel(tex, loc + %.1f * dir)) * %f;" + for i = -support, support do + local coeff = math.exp(-0.5 * i * i / (sigma * sigma)); + table.insert( forLoop, ( norm > 0 and "\t" or "" ) .. + string.format( line, i, coeff ) + ) + norm = norm + coeff; + end + table.insert( forLoop, "\tacc *= 1/" .. norm .. ";\r\n" ) + + local fragmentShader = [[ +uniform vec2 dir; +vec4 effect( vec4 color, Image tex, vec2 texcoord, vec2 pixcoord ) +{ + vec2 loc = texcoord; + vec4 acc = vec4( 0.0 ); + ]] .. table.concat( forLoop, "\r\n" ) .. [[ + return acc; +} +]] + self.shader = love.graphics.newShader( fragmentShader ) +end + shader.register( gaussianblur, "gaussianblur" ) diff --git a/shaders/stroke.frag b/shaders/stroke.frag index a86c7ccd..85f97797 100644 --- a/shaders/stroke.frag +++ b/shaders/stroke.frag @@ -10,12 +10,12 @@ uniform float width; const int samples = 20; const float pi = 3.1415926535898f; -vec4 effect( vec4 vcolor, Image tex, vec2 texcoord, vec2 pixcoord ) +vec4 effect( vec4 color, Image tex, vec2 texcoord, vec2 pixcoord ) { // Stroke float alpha = 0.0f; float angle = 0.0f; - for( int i = 0; i < samples; i++ ) + for ( int i = 0; i < samples; i++ ) { angle += 1.0f / ( float( samples ) / 2.0f ) * pi; @@ -28,7 +28,7 @@ vec4 effect( vec4 vcolor, Image tex, vec2 texcoord, vec2 pixcoord ) } // Texture - vec4 FragColor = vcolor * alpha; + vec4 FragColor = color * alpha; vec4 texel = Texel( tex, texcoord ); FragColor = mix( FragColor, texel, texel.a ); return FragColor;