├── LICENSE ├── README.md ├── game ├── CharacterStates.lua ├── Constants.lua ├── InputSystem.lua ├── MatchStates.lua ├── MatchSystem.lua ├── Network.lua ├── PlayerObject.lua ├── PlayerState.lua ├── RunOverride.lua ├── StateTimelines.lua ├── Util.lua ├── World.lua ├── assets │ ├── player1 │ │ ├── attack_00.png │ │ ├── attack_01.png │ │ ├── attack_02.png │ │ └── idle_00.png │ ├── player2 │ │ ├── attack_00.png │ │ ├── attack_01.png │ │ ├── attack_02.png │ │ └── idle_00.png │ └── sounds │ │ ├── hit.wav │ │ ├── jump.wav │ │ └── whiff.wav ├── conf.lua └── main.lua └── screenshot.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chase LaCas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo Fighter With Netcode 2 | An example fighting game demonstrating how to implement rollback based netcode. 3 | 4 | ![Alt text](/screenshot.png?raw=true "") 5 | 6 | ## Debug Commands 7 | * F1 Show Debug Information 8 | * F2: Frame Step (Once paused) 9 | * F3: Pause Game 10 | * F4: Toggle Hitbox Display 11 | * F5: Reset Game 12 | * F7: Store State 13 | * F8: Load State 14 | * F9: Connect to the server 15 | * F11: Host a server and wait for a client. 16 | 17 | ## Install & run 18 | * Get [LÖVE](https://love2d.org/) 19 | * Clone repository to your machine. 20 | 21 | -------------------------------------------------------------------------------- /game/CharacterStates.lua: -------------------------------------------------------------------------------- 1 | require("PlayerState") 2 | 3 | -- Default gravity acceleration 4 | local GRAVITY = -2 5 | 6 | -- Default table of states 7 | CharacterStates = {} 8 | 9 | CharacterStates.Standing = PlayerState:New() 10 | 11 | function CharacterStates.Standing:Begin(player) 12 | player:PlayTimeline("stand") 13 | end 14 | 15 | function CharacterStates.Standing:Update(player) 16 | 17 | if player:GetInputState().attack_pressed then 18 | return CharacterStates.Attack 19 | elseif player:GetInputState().up then 20 | return CharacterStates.Jump 21 | elseif player:GetInputState().right then 22 | player.physics.xVel = 3 23 | elseif player:GetInputState().left then 24 | player.physics.xVel = -3 25 | else 26 | player.physics.xVel = 0 27 | end 28 | end 29 | 30 | function CharacterStates.Standing:End(player) 31 | end 32 | 33 | CharacterStates.Walk = PlayerState:New() 34 | 35 | CharacterStates.Backup = PlayerState:New() 36 | 37 | -- Neutral Jump 38 | CharacterStates.Jump = PlayerState:New() 39 | 40 | function CharacterStates.Jump:Begin(player) 41 | player:PlayTimeline("stand") 42 | 43 | -- Returing to this state while in the air doesn't not add any vertical velocity. 44 | if player.physics.y <= 0 then 45 | player.physics.yVel = 30; 46 | end 47 | 48 | player.physics.yAcc = GRAVITY 49 | 50 | player.jumpSound:play() 51 | end 52 | 53 | function CharacterStates.Jump:Update(player) 54 | if player.events.GroundCollision then 55 | player.physics.yVel = 0 56 | player.physics.yAcc = 0 57 | return CharacterStates.Standing 58 | elseif player:GetInputState().attack then 59 | return CharacterStates.Attack 60 | end 61 | end 62 | 63 | function CharacterStates.Jump:End(player) 64 | end 65 | 66 | 67 | 68 | CharacterStates.JumpBack = PlayerState:New() 69 | 70 | CharacterStates.JumpForward = PlayerState:New() 71 | 72 | 73 | -- Ground damage reaction 74 | CharacterStates.GroundDamage = PlayerState:New() 75 | function CharacterStates.GroundDamage:Begin(player) 76 | player:PlayTimeline("stand") 77 | player.hitSound:play() 78 | end 79 | 80 | function CharacterStates.GroundDamage:Update(player) 81 | -- When hitstun is over the player can return to controlling the character. 82 | if player.hitstunTimer <= 0 then 83 | return CharacterStates.Standing 84 | end 85 | end 86 | 87 | 88 | 89 | CharacterStates.Attack = PlayerState:New() 90 | CharacterStates.Attack.attack = true -- Indicates this state is an attack 91 | 92 | 93 | function CharacterStates.Attack:Begin(player) 94 | player:PlayTimeline("attack") 95 | player.whiffSound:play() 96 | end 97 | 98 | function CharacterStates.Attack:Update(player) 99 | if player.currentFrame == 8 then 100 | player.attackCanHit = true 101 | else 102 | player.attackCanHit = false 103 | end 104 | 105 | if player.events.AnimEnd then 106 | return CharacterStates.Standing 107 | end 108 | end 109 | 110 | CharacterStates.JumpAttack = PlayerState:New() 111 | 112 | function CharacterStates.JumpAttack:Begin(player) 113 | player:PlayTimeline("attack") 114 | end 115 | 116 | function CharacterStates.JumpAttack:Update(player) 117 | if player.events.AnimEnd then 118 | if player.physics.y > 0 then 119 | return CharacterStates.Jump 120 | else 121 | return CharacterStates.Standing 122 | end 123 | end 124 | end -------------------------------------------------------------------------------- /game/Constants.lua: -------------------------------------------------------------------------------- 1 | -- Constants 2 | SCREEN_WIDTH = 1024 3 | SCREEN_HEIGHT = 768 4 | STAGE_WIDTH = 1000 -- Stage width in screen coordinates. 5 | STAGE_RADIUS = STAGE_WIDTH / 2 -- Half of the stage width 6 | GROUND_HEIGHT = 200 -- Stage ground height in screen coordinates. 7 | DEFAULT_HP = 10000 -- Default Max HP of players. 8 | 9 | -- Global Variables 10 | SHOW_HITBOXES = false -- Debugg settings for displaying hitboxes 11 | SHOW_DEBUG_INFO = false -- Prints debug information on screen when enabled. 12 | SKIP_MATCH_INTRO = true -- Set to skip the match intro. Must be the same value on both clients or a desync will occur. 13 | GRAPH_UNIT_SCALE = 5 -- The height scaled use for drawing stat graphs 14 | 15 | 16 | -- Network Settings 17 | SERVER_IP = "localhost" -- The network address of the other player to connect to. 18 | 19 | SERVER_PORT = 12345 -- The network port the server is running on. 20 | NET_INPUT_DELAY = 3 -- Amount of input delay to use by default during online matches. Should always be > 0 21 | NET_ROLLBACK_MAX_FRAMES = 10 -- The maximum number of frames we allow the game run forward without a confirmed frame from the opponent. 22 | NET_DETECT_DESYNCS = true -- Whether or not desyncs are detected and terminates a network session. 23 | 24 | NET_INPUT_HISTORY_SIZE = 60 -- The size of the input history buffer. Must be atleast 1. 25 | NET_SEND_HISTORY_SIZE = 5 -- The number of inputs we send from the input history buffer. Must be atleast 1. 26 | NET_SEND_DELAY_FRAMES = 0 -- Delay sending packets when this value is great than 0. Set on both clients to not have one ended latency. 27 | 28 | -- Rollback test settings 29 | ROLLBACK_TEST_ENABLED = false 30 | ROLLBACK_TEST_FRAMES = 10 -- Number of frames to rollback for tests. -------------------------------------------------------------------------------- /game/InputSystem.lua: -------------------------------------------------------------------------------- 1 | require("Util") 2 | 3 | -- The input system is an abstraction layer between system input and commands used to control player objects. 4 | InputSystem = 5 | { 6 | MAX_INPUT_FRAMES = 60, -- The maximum number of input commands stored in the player controller ring buff. 7 | 8 | localPlayerIndex = 1, -- The player index for the player on the local client. 9 | remotePlayerIndex = 2, -- The player index for the player on the remote client. 10 | 11 | keyboardState = {}, -- System keyboard state. This is updated in love callbacks love.keypressed and love.keyreleased. 12 | 13 | remotePlayerState = {}, -- Store the input state for the remote player. 14 | 15 | polledInput = {{}, {}}, -- Latest polled inputs 16 | 17 | playerCommandBuffer = {{}, {}}, -- A ring buffer. Stores the on/off state for each basic input command. 18 | 19 | inputDelay = 0, -- Specify how many frames the player's inputs will be delayed by. Used in networking. Increase this value to test delay! 20 | 21 | joysticks = {}, -- Available joysticks 22 | 23 | } 24 | 25 | function InputSystem:InputIndex(offset) 26 | local tick = self.game.tick 27 | if offset then 28 | tick = tick + offset 29 | end 30 | 31 | return 1 + ((InputSystem.MAX_INPUT_FRAMES + tick) % InputSystem.MAX_INPUT_FRAMES) 32 | end 33 | 34 | 35 | -- Used in the rollback system to make a copy of the input system state 36 | function InputSystem:CopyState() 37 | local state = {} 38 | state.playerCommandBuffer = table.deep_copy(self.playerCommandBuffer) 39 | return state 40 | end 41 | 42 | -- Used in the rollback system to restore the old state of the input system 43 | function InputSystem:SetState(state) 44 | self.playerCommandBuffer = table.deep_copy(state.playerCommandBuffer) 45 | end 46 | 47 | 48 | -- Get the entire input state for the current from a player's input command buffer. 49 | function InputSystem:GetInputState(bufferIndex, tick) 50 | -- The 1 appearing here is because lua arrays used 1 based and not 0 based indexes. 51 | local inputFrame = 1 + ((InputSystem.MAX_INPUT_FRAMES + tick ) % InputSystem.MAX_INPUT_FRAMES) 52 | 53 | local state = self.playerCommandBuffer[bufferIndex][inputFrame] 54 | if not state then 55 | return {} 56 | end 57 | return state 58 | end 59 | 60 | function InputSystem:GetLatestInput(bufferIndex) 61 | return self.polledInput[bufferIndex] 62 | end 63 | 64 | -- Get the current input state for a player 65 | function InputSystem:CurrentInputState(bufferIndex) 66 | return self:GetInputState(bufferIndex, self.game.tick) 67 | end 68 | 69 | -- Directly set the input state or the player. This is used for a online match. 70 | function InputSystem:SetInputState(playerIndex, state) 71 | local stateCopy = table.copy(state) 72 | self.playerCommandBuffer[playerIndex][self:InputIndex()] = stateCopy 73 | end 74 | 75 | -- Initialize the player input command ring buffer. 76 | function InputSystem:InitializeBuffer(bufferIndex) 77 | for i=1,InputSystem.MAX_INPUT_FRAMES do 78 | self.playerCommandBuffer[bufferIndex][i] = { up = false, down = false, left = false, right = false, attack = false} 79 | end 80 | end 81 | 82 | -- Record inputs the player pressed this frame. 83 | function InputSystem:UpdateInputChanges() 84 | local inputIndex = self:InputIndex() 85 | local previousInputIndex = self:InputIndex(-1) 86 | 87 | 88 | for i=1,2 do 89 | local state = self.playerCommandBuffer[i][inputIndex] 90 | local previousState = self.playerCommandBuffer[i][previousInputIndex] 91 | 92 | state.up_pressed = state.up and not previousState.up 93 | state.down_pressed = state.down and not previousState.down 94 | state.left_pressed = state.left and not previousState.left 95 | state.right_pressed = state.right and not previousState.right 96 | state.attack_pressed = state.attack and not previousState.attack 97 | end 98 | end 99 | 100 | function InputSystem:PollInputs(updateBuffers) 101 | 102 | -- Input polling from the system can be disabled for setting inputs from a buffer. Used in testing rollbacks. 103 | -- Update the local player's command buffer for the current frame. 104 | self.polledInput[self.localPlayerIndex] = table.copy(self.keyboardState) 105 | 106 | -- Update the remote player's command buffer. 107 | --self.playerCommandBuffer[self.remotePlayerIndex][delayedIndex] = table.copy(self.remotePlayerState) 108 | 109 | -- Get buttons from first joysticks 110 | for index, joystick in pairs(self.joysticks) do 111 | if self.joysticks[1] and (not self.game.network.enabled or (self.localPlayerIndex == index) ) then 112 | 113 | local commandBuffer = self.polledInput[index] 114 | local axisX = joystick:getAxis(1) 115 | local axisY = joystick:getAxis(2) 116 | 117 | -- Reset the direction state for this frame. 118 | commandBuffer.left = false 119 | commandBuffer.right = false 120 | commandBuffer.up = false 121 | commandBuffer.down = false 122 | commandBuffer.attack = false 123 | 124 | 125 | -- Indicates the neutral zone of the joystick 126 | local axisGap = 0.5 127 | 128 | if axisX > axisGap then 129 | commandBuffer.right = true 130 | elseif axisX < -axisGap then 131 | commandBuffer.left = true 132 | end 133 | 134 | if axisY > axisGap then 135 | commandBuffer.down = true 136 | elseif axisY < -axisGap then 137 | commandBuffer.up = true 138 | end 139 | 140 | if joystick:isDown(1) then 141 | commandBuffer.attack = true 142 | end 143 | 144 | end 145 | end 146 | 147 | -- Updated the player input buffers from the polled inputs. Set to false in network mode. 148 | if updateBuffers then 149 | local bufferIndex = self:InputIndex() 150 | for i=1,2 do 151 | self.playerCommandBuffer[bufferIndex] = self.polledInput[bufferIndex] 152 | end 153 | end 154 | 155 | end 156 | 157 | -- The update method syncs the keyboard and joystick input with the internal player input state. It also handles syncing the remote player's inputs. 158 | function InputSystem:Update() 159 | -- Update input changes 160 | InputSystem:UpdateInputChanges() 161 | end 162 | 163 | -- Set the internal keyboard state input to true on pressed. 164 | function love.keypressed(key, scancode, isrepeat) 165 | 166 | if key == 'w' then 167 | InputSystem.keyboardState.up = true 168 | elseif key == 's' then 169 | InputSystem.keyboardState.down = true 170 | elseif key == 'a' then 171 | InputSystem.keyboardState.left = true 172 | elseif key == 'd' then 173 | InputSystem.keyboardState.right = true 174 | elseif key == 'g' then 175 | InputSystem.keyboardState.attack = true 176 | end 177 | 178 | if key == 'f5' then 179 | InputSystem.game:Reset() 180 | elseif key == 'f4' then 181 | SHOW_HITBOXES = not SHOW_HITBOXES 182 | elseif key == 'f3' then 183 | InputSystem.game.paused = not InputSystem.game.paused 184 | elseif key == 'f2' then 185 | InputSystem.game.frameStep = true 186 | elseif key == 'f1' then 187 | SHOW_DEBUG_INFO = not SHOW_DEBUG_INFO 188 | elseif key == "space" then 189 | InputSystem.game.forcePause = true; 190 | -- Test controls for storing/restoring state. 191 | elseif key == 'f7' then 192 | InputSystem.game:StoreState() 193 | elseif key == 'f8' then 194 | InputSystem.game:RestoreState() 195 | elseif key == 'f9' then 196 | InputSystem.game.network:StartConnection() 197 | InputSystem.localPlayerIndex = 2 -- Right now the client is always player 2. 198 | InputSystem.remotePlayerIndex = 1 -- Right now the server is always players 1. 199 | elseif key == 'f11' then 200 | InputSystem.game.network:StartServer() 201 | InputSystem.localPlayerIndex = 1 -- Right now the server is always players 1. 202 | InputSystem.remotePlayerIndex = 2 -- Right now the client is always player 2. 203 | end 204 | end 205 | 206 | -- Set the internal keyboard state input to false on release. 207 | function love.keyreleased(key, scancode, isrepeat) 208 | 209 | if key == 'w' then 210 | InputSystem.keyboardState.up = false 211 | elseif key == 's' then 212 | InputSystem.keyboardState.down = false 213 | elseif key == 'a' then 214 | InputSystem.keyboardState.left = false 215 | elseif key == 'd' then 216 | InputSystem.keyboardState.right = false 217 | elseif key == 'g' then 218 | InputSystem.keyboardState.attack = false 219 | elseif key == "space" then 220 | InputSystem.game.forcePause = false; 221 | end 222 | end -------------------------------------------------------------------------------- /game/MatchStates.lua: -------------------------------------------------------------------------------- 1 | require("Constants") 2 | 3 | -- Base table for match states. 4 | MatchState = {} 5 | 6 | 7 | -- Boiler plate for making inheritance and instancing possible 8 | function MatchState:New(o) 9 | o = o or {} 10 | setmetatable(o, self) 11 | self.__index = self 12 | return o 13 | end 14 | 15 | 16 | -- Called when transitioning into this state. 17 | function MatchState:Begin(match) 18 | 19 | end 20 | 21 | -- Called every frame. 22 | function MatchState:Update(match) 23 | end 24 | 25 | -- Called when transitioning out of this state. 26 | function MatchState:End(match) 27 | end 28 | 29 | -- Handles drawing for the state 30 | function MatchState:Draw(match) 31 | end 32 | 33 | Match = {} 34 | 35 | -- Initial state for the match 36 | Match.Start = MatchState:New() 37 | 38 | function Match.Start:Begin(match) 39 | -- Entry functions for the players starting a match 40 | match.players[1]:Begin() 41 | match.players[2]:Begin() 42 | 43 | match.players[2].facing = true 44 | 45 | -- Initial Player Positions. 46 | match.players[1].physics.x = -200 47 | match.players[1].physics.y = 0 48 | 49 | match.players[2].physics.x = 200 50 | match.players[2].physics.y = 0 51 | end 52 | 53 | function Match.Start:Update(match) 54 | -- to test networking going straight into the match 55 | if SKIP_MATCH_INTRO then 56 | return Match.Run 57 | end 58 | 59 | if match.timer > 60 * 2 then 60 | return Match.Go 61 | end 62 | end 63 | 64 | function Match.Start:Draw(match) 65 | love.graphics.push() 66 | love.graphics.setColor(1, 1, 1) 67 | love.graphics.print("Match Start", 10, 80, 0, 4, 4, -85, -50) 68 | love.graphics.pop() 69 | end 70 | 71 | -- Handles the Go! Message at that appears before the match begins. 72 | Match.Go = MatchState:New() 73 | 74 | function Match.Go:Update(match) 75 | 76 | if match.timer > 60 * 1 then 77 | return Match.Run 78 | end 79 | end 80 | 81 | function Match.Go:Draw(match) 82 | love.graphics.push() 83 | love.graphics.setColor(1, 1, 1) 84 | love.graphics.print("Go!", 10, 80, 0, 4, 4, -110, -50) 85 | love.graphics.pop() 86 | end 87 | 88 | -- Running state for the match 89 | Match.Run = MatchState:New() 90 | 91 | function Match.Run:Begin(match) 92 | match.players[1].inputEnabled = true 93 | match.players[2].inputEnabled = true 94 | end 95 | function Match.Run:Update(match) 96 | match.players[1].inputEnabled = true 97 | -- Check match end condition 98 | if match.players[1].hp <= 0 or match.players[2].hp <= 0 then 99 | return Match.End 100 | end 101 | end 102 | 103 | 104 | -- Runs when atleast one player runs out of HP 105 | Match.End = MatchState:New() 106 | 107 | function Match.End:Begin(match) 108 | -- Disable world updates 109 | match.world.stop = true 110 | 111 | -- Disable player input at the end of a match 112 | match.players[1].inputEnabled = false 113 | match.players[2].inputEnabled = false 114 | end 115 | 116 | function Match.End:Update(match) 117 | if match.timer > 60 * 2 then 118 | return Match.EndWait 119 | end 120 | end 121 | 122 | function Match.End:End(match) 123 | -- Enable world updates 124 | match.world.stop = false 125 | end 126 | 127 | function Match.End:Draw(match) 128 | love.graphics.push() 129 | love.graphics.setColor(1, 1, 1) 130 | love.graphics.print("KO!", 10, 80, 0, 4, 4, -110, -50) 131 | love.graphics.pop() 132 | end 133 | 134 | -- Final state for the match 135 | Match.EndWait = MatchState:New() 136 | 137 | function Match.EndWait:Update(match) 138 | if match.timer > 60 * 2 then 139 | match:Reset() 140 | end 141 | end -------------------------------------------------------------------------------- /game/MatchSystem.lua: -------------------------------------------------------------------------------- 1 | require("MatchStates") 2 | MatchSystem = 3 | { 4 | currentState = MatchState:New(), -- Current match state. 5 | timer = 0, -- Total time the current state has been running (in frames) 6 | players = {}, -- player list 7 | } 8 | 9 | -- Resets the match 10 | function MatchSystem:Reset() 11 | 12 | self.players[1]:Reset() 13 | self.players[2]:Reset() 14 | 15 | self.timer = 0 16 | self:Begin() 17 | end 18 | 19 | -- Used in the rollback system to make a copy of the match system state 20 | function MatchSystem:CopyState() 21 | local state = {} 22 | state.currentState = self.currentState 23 | state.timer = self.timer 24 | return state 25 | end 26 | 27 | -- Used in the rollback system to restore the old state of the match system 28 | function MatchSystem:SetState(state) 29 | self.currentState = state.currentState 30 | self.timer = state.timer 31 | end 32 | 33 | -- Starts the match 34 | function MatchSystem:Begin() 35 | self.currentState = Match.Start:New() 36 | self.currentState:Begin(self) 37 | end 38 | 39 | function MatchSystem:Update() 40 | -- Update the current state and then execute the relevant callbacks when transition occurs. 41 | local nextState = self.currentState:Update(self) 42 | if nextState then 43 | self.timer = -1 44 | self.currentState:End(self) 45 | self.currentState = nextState:New() 46 | self.currentState:Begin(self) 47 | end 48 | 49 | self.timer = self.timer + 1 50 | end 51 | 52 | function MatchSystem:Draw() 53 | self.currentState:Draw(self) 54 | end 55 | -------------------------------------------------------------------------------- /game/Network.lua: -------------------------------------------------------------------------------- 1 | require("Constants") 2 | require("Util") 3 | local socket = require("socket") 4 | 5 | local netlogName = 'netlog-'.. os.time(os.date("!*t")) ..'.txt' 6 | local packetLogName = 'packetLog-'.. os.time(os.date("!*t")) ..'.txt' 7 | 8 | 9 | -- Create net log file 10 | love.filesystem.write(netlogName, 'Network log start\r\n') 11 | love.filesystem.write(packetLogName, 'Packet log start\r\n') 12 | 13 | function NetLog(data) 14 | love.filesystem.append(netlogName, data .. '\r\n') 15 | -- print(data) 16 | end 17 | 18 | function PacketLog(data) 19 | love.filesystem.append(packetLogName, data .. '\r\n') 20 | -- print(data) 21 | end 22 | 23 | -- Network code indicating the type of message. 24 | local MsgCode = 25 | { 26 | Handshake = 1, -- Used when sending the hand shake. 27 | PlayerInput = 2, -- Sends part of the player's input buffer. 28 | Ping = 3, -- Used to tracking packet round trip time. Expect a "Pong" back. 29 | Pong = 4, -- Sent in reply to a Ping message for testing round trip time. 30 | Sync = 5, -- Used to pass sync data 31 | } 32 | 33 | -- Bit flags used to convert input state to a form suitable for network transmission. 34 | local InputCode = 35 | { 36 | Up = bit.lshift(1, 0), 37 | Down = bit.lshift(1, 1), 38 | Left = bit.lshift(1, 2), 39 | Right = bit.lshift(1, 3), 40 | Attack = bit.lshift(1, 4), 41 | } 42 | 43 | -- Generates a string which is used to pack/unpack the data in a player input packet. 44 | -- This format string is used by the love.data.pack() and love.data.unpack() functions. 45 | local INPUT_FORMAT_STRING = string.format('Bjj%.' .. NET_SEND_HISTORY_SIZE .. 's', 'BBBBBBBBBBBBBBBB') 46 | 47 | -- Packing string for sync data 48 | local SYNC_DATA_FORMAT_STRING = 'Bjs8' 49 | 50 | -- This object will handle all network related functionality 51 | Network = 52 | { 53 | enabled = false, -- Set to true when the network is running. 54 | connectedToClient = false, -- Indicates whether or not the game is connected to another client 55 | isServer = false, -- Indicates whether or not this game is the server. 56 | 57 | clientIP = "", -- Detected network address for the non-server client 58 | clientPort = -1, -- Detected port for the non-server client 59 | 60 | confirmedTick = 0, -- The confirmed tick indicates up to what game frame we have the inputs for. 61 | inputState = nil, -- Current input state sent over the network 62 | inputDelay = NET_INPUT_DELAY, -- This must be set to a value of 1 more higher. 63 | 64 | inputHistory = {}, -- The input history for the local player. Stored as bit flag encoded input states. 65 | remoteInputHistory = {}, -- The input history for the local player. Stored as bit flag encoded input states. 66 | 67 | inputHistoryIndex = 0, -- Current index in history buffer. 68 | 69 | syncDataHistoryLocal = {}, -- Keeps track of the sync data for the local client 70 | syncDataHistoryRemote = {}, -- Keeps track of the sync data for the remote client 71 | 72 | syncDataTicks = {}, -- Keeps track of the tick for each sync data index 73 | 74 | latency = 0, -- Keeps track of the latency. 75 | 76 | toSendPackets = {}, -- Packets that have been queued for sending later. Used to test network latency. 77 | 78 | lastSyncedTick =-1, -- Indicates the last game tick that was confirmed to be in sync. 79 | 80 | localTickDelta = 0, -- Stores the difference between the last local tick and the remote confirmed tick. Remote client. 81 | remoteTickDelta = 0, -- Stores the difference between the last local tick and the remote confirmed tick sent from the remote client. 82 | 83 | tickOffset = 0.0, -- Current difference between remote and local ticks 84 | tickSyncing = false, -- Indicates whether or not the game is currently in time syncing mode. 85 | 86 | desyncCheckRate = 20, -- The rate at which we check for state desyncs. 87 | localSyncData = nil, -- Latest local data for state desync checking. 88 | remoteSyncData = nil, -- Latest remote data for state desync checking. 89 | localSyncDataTick = -1, -- Tick for the latest local desync data. 90 | remoteSyncDataTick = -1, -- Tick for the latest remote desync data. 91 | 92 | isStateDesynced = false, -- Set to true once a game state desync is detected. 93 | 94 | } 95 | 96 | -- Initialize History Buffer 97 | function Network:InitializeInputHistoryBuffer() 98 | -- local emptySyncData = love.data.pack("string", "nn", 0, 0) 99 | for i=1,NET_INPUT_HISTORY_SIZE do 100 | self.inputHistory[i] = 0 101 | self.remoteInputHistory[i] = 0 102 | self.syncDataHistoryLocal[i] = nil 103 | self.syncDataHistoryRemote[i] = nil 104 | self.syncDataTicks[i] = nil 105 | end 106 | end 107 | 108 | -- Probably will move this call to some initialization function. 109 | Network:InitializeInputHistoryBuffer() 110 | 111 | -- Setup a network connection at connect to the server. 112 | function Network:StartConnection() 113 | print("Starting Network") 114 | NetLog("Starting Client") 115 | 116 | -- the address and port of the server 117 | local address, port = SERVER_IP, SERVER_PORT 118 | self.clientIP = address 119 | self.clientPort = port 120 | 121 | self.enabled = true 122 | self.isServer = false 123 | 124 | self.udp = socket.udp() 125 | 126 | -- Since there isn't a seperate network thread we need non-blocking sockets. 127 | self.udp:settimeout(0) 128 | 129 | -- The client can bind to any port since the server will wait on a handshake message and record it later. 130 | self.udp:setsockname('*', 0) 131 | 132 | -- Start the connection with the server 133 | self:ConnectToServer() 134 | end 135 | 136 | -- Setup a network connection as the server then wait for a client to connect. 137 | function Network:StartServer() 138 | 139 | print("Starting Server") 140 | NetLog("Starting Server") 141 | 142 | self.enabled = true 143 | self.isServer = true 144 | 145 | self.udp = socket.udp() 146 | 147 | -- Since there isn't a seperate network thread we need non-blocking sockets. 148 | self.udp:settimeout(0) 149 | 150 | -- Bind to a specific port since the client needs to know where to send its handshake message. 151 | self.udp:setsockname('*', SERVER_PORT) 152 | 153 | end 154 | 155 | -- Get input from the remote player for the passed in game tick. 156 | function Network:GetRemoteInputState(tick) 157 | if tick > self.confirmedTick then 158 | -- Repeat the last confirmed input when we don't have a confirmed tick 159 | tick = self.confirmedTick 160 | end 161 | return self:DecodeInput(self.remoteInputHistory[1+((NET_INPUT_HISTORY_SIZE + tick) % NET_INPUT_HISTORY_SIZE)]) -- First index is 1 not 0. 162 | end 163 | 164 | -- Get input state for the local client 165 | function Network:GetLocalInputState(tick) 166 | return self:DecodeInput(self.inputHistory[1+((NET_INPUT_HISTORY_SIZE + tick) % NET_INPUT_HISTORY_SIZE)]) -- First index is 1 not 0. 167 | end 168 | 169 | function Network:GetLocalInputEncoded(tick) 170 | return self.inputHistory[1+((NET_INPUT_HISTORY_SIZE + tick) % NET_INPUT_HISTORY_SIZE)] -- First index is 1 not 0. 171 | end 172 | 173 | 174 | -- Get the sync data which is used to check for game state desync between the clients. 175 | function Network:GetSyncDataLocal(tick) 176 | local index = 1+( (NET_INPUT_HISTORY_SIZE + tick) % NET_INPUT_HISTORY_SIZE) 177 | return self.syncDataHistoryLocal[index] -- First index is 1 not 0. 178 | end 179 | 180 | -- Get sync data from the remote client. 181 | function Network:GetSyncDataRemote(tick) 182 | local index = 1+( (NET_INPUT_HISTORY_SIZE + tick) % NET_INPUT_HISTORY_SIZE) 183 | 184 | return self.syncDataHistoryRemote[index] -- First index is 1 not 0. 185 | end 186 | 187 | -- Set sync data for a game tick 188 | function Network:SetLocalSyncData(tick, syncData) 189 | if not self.isStateDesynced then 190 | self.localSyncData = syncData 191 | self.localSyncDataTick = tick 192 | end 193 | end 194 | 195 | -- Check for a desync. 196 | function Network:DesyncCheck() 197 | if self.localSyncDataTick < 0 then 198 | return 199 | end 200 | 201 | -- When the local sync data does not match the remote data indicate a desync has occurred. 202 | if self.isStateDesynced or self.localSyncDataTick == self.remoteSyncDataTick then 203 | -- print("Desync Check at: " .. self.localSyncDataTick) 204 | 205 | if self.localSyncData ~= self.remoteSyncData then 206 | self.isStateDesynced = true 207 | return true, self.localSyncDataTick 208 | end 209 | end 210 | 211 | return false 212 | end 213 | 214 | -- Connects to the other player who is hosting as the server.d 215 | function Network:ConnectToServer() 216 | -- This most be called to connect with the server. 217 | self:SendPacket(self:MakeHandshakePacket(), 5) 218 | end 219 | 220 | -- Send the inputState for the local player to the remote player for the given game tick. 221 | function Network:SendInputData(tick) 222 | 223 | -- Don't send input data when not connect to another player's game client. 224 | if not (self.enabled and self.connectedToClient) then 225 | return 226 | end 227 | 228 | self:SendPacket(self:MakeInputPacket(tick, syncData), 1) 229 | end 230 | 231 | function Network:SetLocalInput(inputState, tick) 232 | local encodedInput = self:EncodeInput(inputState) 233 | self.inputHistory[1+((NET_INPUT_HISTORY_SIZE + tick) % NET_INPUT_HISTORY_SIZE)] = encodedInput -- 1 base indexing. 234 | end 235 | 236 | function Network:SetRemoteEncodedInput(encodedInput, tick) 237 | self.remoteInputHistory[1+((NET_INPUT_HISTORY_SIZE + tick) % NET_INPUT_HISTORY_SIZE)] = encodedInput -- 1 base indexing. 238 | end 239 | 240 | -- Handles sending packets to the other client. Set duplicates to something > 0 to send more than once. 241 | function Network:SendPacket(packet, duplicates) 242 | if not duplicates then 243 | duplicates = 1 244 | end 245 | 246 | for i=1,duplicates do 247 | if NET_SEND_DELAY_FRAMES > 0 then 248 | self:SendPacketWithDelay(packet) 249 | else 250 | self:SendPacketRaw(packet) 251 | end 252 | end 253 | end 254 | 255 | -- Queues a packet to be sent later 256 | function Network:SendPacketWithDelay(packet) 257 | local delayedPacket = {packet=packet, time=love.timer.getTime()} 258 | table.insert(self.toSendPackets, delayedPacket) 259 | end 260 | 261 | -- Send all packets which have been queued and who's delay time as elapsed. 262 | function Network:ProcessDelayedPackets() 263 | local newPacketList = {} -- List of packets that haven't been sent yet. 264 | local timeInterval = (NET_SEND_DELAY_FRAMES/60) -- How much time must pass (converting from frames into seconds) 265 | 266 | for index,data in pairs(self.toSendPackets) do 267 | if (love.timer.getTime() - data.time) > timeInterval then 268 | self:SendPacketRaw(data.packet) -- Send packet when enough time as passed. 269 | else 270 | table.insert(newPacketList, data) -- Keep the packet if the not enough time as passed. 271 | end 272 | end 273 | self.toSendPackets = newPacketList 274 | end 275 | 276 | -- Send a packet immediately 277 | function Network:SendPacketRaw(packet) 278 | self.udp:sendto(packet, self.clientIP, self.clientPort) 279 | end 280 | 281 | -- Handles receiving packets from the other client. 282 | function Network:ReceivePacket(packet) 283 | local data = nil 284 | local msg = nil 285 | local ip_or_msg = nil 286 | local port = nil 287 | 288 | data, ip_or_msg, port = self.udp:receivefrom() 289 | 290 | if not data then 291 | msg = ip_or_msg 292 | end 293 | 294 | return data, msg, ip_or_msg, port 295 | end 296 | 297 | 298 | -- Checks the queue for any incoming packets and process them. 299 | function Network:ReceiveData() 300 | if not self.enabled then 301 | return 302 | end 303 | 304 | -- For now we'll process all packets every frame. 305 | repeat 306 | local data,msg,ip,port = self:ReceivePacket() 307 | 308 | if data then 309 | local code = love.data.unpack("B", data, 1) 310 | 311 | -- Handshake code must be received by both game instances before a match can begin. 312 | if code == MsgCode.Handshake then 313 | if not self.connectedToClient then 314 | self.connectedToClient = true 315 | 316 | -- The server needs to remember the address and port in order to send data to the other cilent. 317 | if true then 318 | -- Server needs to the other the client address and ip to know where to send data. 319 | if self.isServer then 320 | self.clientIP = ip 321 | self.clientPort = port 322 | end 323 | print("Received Handshake. Address: " .. self.clientIP .. ". Port: " .. self.clientPort) 324 | -- Send handshake to client. 325 | self:SendPacket(self:MakeHandshakePacket(), 5) 326 | end 327 | end 328 | 329 | elseif code == MsgCode.PlayerInput then 330 | -- Break apart the packet into its parts. 331 | local results = { love.data.unpack(INPUT_FORMAT_STRING, data, 1) } -- Final parameter is the start position 332 | 333 | local tickDelta = results[2] 334 | local receivedTick = results[3] 335 | 336 | -- We only care about the latest tick delta, so make sure the confirmed frame is atleast the same or newer. 337 | -- This would work better if we added a packet count. 338 | if receivedTick >= self.confirmedTick then 339 | self.remoteTickDelta = tickDelta 340 | end 341 | 342 | if receivedTick > self.confirmedTick then 343 | if receivedTick - self.confirmedTick > self.inputDelay then 344 | NetLog("Received packet with a tick too far ahead. Last: " .. self.confirmedTick .. " Current: " .. receivedTick ) 345 | end 346 | 347 | self.confirmedTick = receivedTick 348 | 349 | -- PacketLog("Received Input: " .. results[3+NET_SEND_HISTORY_SIZE] .. " @ " .. receivedTick) 350 | 351 | for offset=0, NET_SEND_HISTORY_SIZE-1 do 352 | -- Save the input history sent in the packet. 353 | self:SetRemoteEncodedInput(results[3+NET_SEND_HISTORY_SIZE-offset] , receivedTick-offset) 354 | end 355 | 356 | end 357 | 358 | -- NetLog("Received Tick: " .. receivedTick .. ", Input: " .. self.remoteInputHistory[(self.confirmedTick % NET_INPUT_HISTORY_SIZE)+1]) 359 | elseif code == MsgCode.Ping then 360 | local pingTime = love.data.unpack("n", data, 2) 361 | self:SendPacket(self:MakePongPacket(pingTime)) 362 | elseif code == MsgCode.Pong then 363 | local pongTime = love.data.unpack("n", data, 2) 364 | self.latency = love.timer.getTime() - pongTime 365 | --print("Got pong message: " .. self.latency) 366 | elseif code == MsgCode.Sync then 367 | local code, tick, syncData = love.data.unpack(SYNC_DATA_FORMAT_STRING, data, 1) 368 | -- Ignore any tick that isn't more recent than the last sync data 369 | if not self.isStateDesynced and tick > self.remoteSyncDataTick then 370 | self.remoteSyncDataTick = tick 371 | self.remoteSyncData = syncData 372 | 373 | -- Check for a desync 374 | self:DesyncCheck() 375 | end 376 | 377 | end 378 | elseif msg and msg ~= 'timeout' then 379 | error("Network error: "..tostring(msg)) 380 | end 381 | -- When we no longer have data we're done processing packets for this frame. 382 | until data == nil 383 | end 384 | 385 | 386 | 387 | -- Generate a packet containing information about player input. 388 | function Network:MakeInputPacket(tick) 389 | 390 | local historyIndexStart = tick - NET_SEND_HISTORY_SIZE + 1 391 | local history = {} 392 | for i=0, NET_SEND_HISTORY_SIZE-1 do 393 | history[i+1] = self.inputHistory[((NET_INPUT_HISTORY_SIZE + historyIndexStart + i) % NET_INPUT_HISTORY_SIZE) + 1] -- +1 here because lua indices start at 1 and not 0. 394 | end 395 | 396 | --NetLog('[Packet] tick: ' .. tick .. ' input: ' .. history[NET_SEND_HISTORY_SIZE]) 397 | local data = love.data.pack("string", INPUT_FORMAT_STRING, MsgCode.PlayerInput, self.localTickDelta, tick, unpack(history)) 398 | return data 399 | end 400 | 401 | -- Send a ping message in order to test network latency 402 | function Network:SendPingMessage() 403 | self:SendPacket(self:MakePingPacket(love.timer.getTime())) 404 | end 405 | 406 | -- Make a ping packet 407 | function Network:MakePingPacket(time) 408 | return love.data.pack("string", "Bn", MsgCode.Ping, time) 409 | end 410 | 411 | -- Make pong packet 412 | function Network:MakePongPacket(time) 413 | return love.data.pack("string", "Bn", MsgCode.Pong, time) 414 | end 415 | 416 | -- Sends sync data 417 | function Network:SendSyncData() 418 | self:SendPacket(self:MakeSyncDataPacket(self.localSyncDataTick, self.localSyncData), 5) 419 | end 420 | 421 | -- Make a sync data packet 422 | function Network:MakeSyncDataPacket(tick, syncData) 423 | return love.data.pack("string", SYNC_DATA_FORMAT_STRING, MsgCode.Sync, tick, syncData) 424 | end 425 | 426 | -- Generate handshake packet for connecting with another client. 427 | function Network:MakeHandshakePacket() 428 | return love.data.pack("string", "B", MsgCode.Handshake) 429 | end 430 | 431 | -- Encodes the player input state into a compact form for network transmission. 432 | function Network:EncodeInput(state) 433 | local data = 0 434 | 435 | if state.up then 436 | data = bit.bor( data, InputCode.Up) 437 | end 438 | 439 | if state.down then 440 | data = bit.bor( data, InputCode.Down) 441 | end 442 | 443 | if state.left then 444 | data = bit.bor( data, InputCode.Left) 445 | end 446 | 447 | if state.right then 448 | data = bit.bor( data, InputCode.Right) 449 | end 450 | 451 | if state.attack then 452 | data = bit.bor( data, InputCode.Attack) 453 | end 454 | 455 | return data 456 | end 457 | 458 | 459 | -- Decodes the input from a packet generated by EncodeInput(). 460 | function Network:DecodeInput(data) 461 | local state = {} 462 | 463 | state.up = bit.band( data, InputCode.Up) > 0 464 | state.down = bit.band( data, InputCode.Down) > 0 465 | state.left = bit.band( data, InputCode.Left) > 0 466 | state.right = bit.band( data, InputCode.Right) > 0 467 | state.attack = bit.band( data, InputCode.Attack) > 0 468 | 469 | return state 470 | end 471 | -------------------------------------------------------------------------------- /game/PlayerObject.lua: -------------------------------------------------------------------------------- 1 | require("Constants") 2 | require("CharacterStates") 3 | require("StateTimelines") 4 | 5 | 6 | -- System for managing the physics of a player. 7 | 8 | -- Boiler late for making it object inheritance possible 9 | local PhysicsSystem = Object:New() 10 | 11 | -- Once a frame update of the physics system for the player. 12 | function PhysicsSystem:Update(player) 13 | -- For this example project we are using a fixed frame step update, so simple integration will work just fine. 14 | self.x = self.x + self.xVel 15 | self.y = self.y + self.yVel 16 | self.xVel = self.xVel + self.xAcc 17 | 18 | -- Apply friction on the ground 19 | if self.y <= 0 then 20 | self.xVel = self.xVel * 0.8 21 | end 22 | 23 | self.yVel = self.yVel + self.yAcc 24 | 25 | ---------------- 26 | -- Contraints -- 27 | ---------------- 28 | 29 | -- Never allow the horizontal position of the player to move beyond the stage walls. 30 | if self.x < -STAGE_RADIUS then 31 | self.x = -STAGE_RADIUS 32 | elseif self.x > STAGE_RADIUS then 33 | self.x = STAGE_RADIUS 34 | end 35 | 36 | -- Never allow the vertical position of the player fall below the ground. 37 | if self.yVel < 0 and self.y <= 0 then 38 | self.y = 0 39 | player.events.GroundCollision = true 40 | end 41 | end 42 | 43 | -- Used in the rollback system to make a copy of the state of the physics system. 44 | function PhysicsSystem:CopyState() 45 | local state = {} 46 | state.x = self.x 47 | state.y = self.y 48 | state.xVel = self.xVel 49 | state.yVel = self.yVel 50 | state.xAcc = self.xAcc 51 | state.yAcc = self.yAcc 52 | state.facing = self.facing 53 | 54 | return state 55 | end 56 | 57 | -- Used in the rollback system to restore the old state of the physics system. 58 | function PhysicsSystem:SetState(state) 59 | self.x = state.x 60 | self.y = state.y 61 | self.xVel = state.xVel 62 | self.yVel = state.yVel 63 | self.xAcc = state.xAcc 64 | self.yAcc = state.yAcc 65 | self.facing = state.facing 66 | end 67 | 68 | -- Physics system factor 69 | function MakePhysicsSystem() 70 | -- Have to create a unique table or it will copy the base one. 71 | return PhysicsSystem:New({ 72 | x = 0, -- world x coordinate of the player. 73 | y = 0, -- world y coordinate of the player. 74 | xVel = 0, -- Absolute x velocity of the player. 75 | yVel = 0, -- Absolute y velocity of the player. 76 | 77 | xAcc = 0, -- Absolute x acceleration of the player. 78 | yAcc = 0, -- Absolute y acceleration of the player. 79 | 80 | facing = false -- Whether or not the facing direction is flipped. 81 | }) 82 | end 83 | 84 | -- Define the player object we'll use to manage the player characters in this game. 85 | PlayerObject = Object:New() 86 | 87 | 88 | 89 | local PlayerColors = 90 | { 91 | {213 / 255, 94 / 255, 0}, 92 | {86 / 255, 180 / 255, 233 / 255} 93 | } 94 | 95 | -- Amount of hit shake to apply on hit. 96 | local shakeAmount = 2 97 | 98 | -- Draw the player object 99 | function PlayerObject:Draw() 100 | local xShake = 0 101 | 102 | if self.hitstopTimer > 0 and self.hitstunTimer > 0 then 103 | local shakeTick = self.hitstopTimer % 4 104 | if shakeTick < 2 then 105 | xShake = -shakeAmount 106 | else 107 | xShake = shakeAmount 108 | end 109 | end 110 | love.graphics.push() 111 | 112 | love.graphics.translate(self.physics.x, -self.physics.y) 113 | love.graphics.translate(xShake, 0) 114 | love.graphics.setColor(1,1,1) 115 | 116 | local xScale = 1 117 | 118 | if self.facing then 119 | xScale = -1 120 | end 121 | 122 | love.graphics.scale(xScale, 1) 123 | 124 | -- Draw the image referenced in the current timeline 125 | if self.currentTimeline then 126 | local currentImage = self:GetImageFromTimeline(self.currentTimeline, self.currentFrame) 127 | love.graphics.draw(currentImage.image, currentImage.x, currentImage.y) 128 | end 129 | 130 | love.graphics.pop() 131 | 132 | if SHOW_HITBOXES then 133 | local damageBoxList = self:GetDamageBoxFromTimeline(self.currentTimeline, self.currentFrame) 134 | 135 | -- Draw damage collision boxes 136 | for index, box in pairs(damageBoxList) do 137 | box = TranslateBox(box, self.physics.x, self.physics.y, self.facing) 138 | love.graphics.setColor(0,0,1, 0.5) 139 | love.graphics.rectangle('fill', box.x, -box.y, box.r - box.x, box.y - box.b) 140 | end 141 | 142 | local attackBoxList = self:GetAttackBoxFromTimeline(self.currentTimeline, self.currentFrame) 143 | 144 | -- Draw attack collision boxes 145 | for index, box in pairs(attackBoxList) do 146 | box = TranslateBox(box, self.physics.x, self.physics.y, self.facing) 147 | love.graphics.setColor(1,0,0, 0.5) 148 | love.graphics.rectangle('fill', box.x, -box.y, box.r - box.x, box.y - box.b) 149 | end 150 | 151 | love.graphics.setColor(1,1,1) 152 | end 153 | end 154 | 155 | 156 | function PlayerObject:Begin() 157 | self.currentState = CharacterStates.Standing:New() 158 | self.currentState:Begin(self) 159 | end 160 | 161 | -- Handle any changes that must happen before Update() is called. 162 | function PlayerObject:PreUpdate() 163 | -- Create new event list. 164 | self.events = {} 165 | end 166 | 167 | -- Called once every frame 168 | function PlayerObject:Update() 169 | 170 | -- The player is paused on the frame the their attack collision occurred 171 | if self.events.HitEnemyThisFrame then 172 | self.hitstopTimer = self.events.hitstop 173 | return; 174 | end 175 | 176 | -- While counting down hitstop, don't update the player's state. 177 | if self.hitstopTimer > 0 then 178 | self.hitstopTimer = self.hitstopTimer - 1 179 | return 180 | end 181 | 182 | -- Updating the physics first so that the state system can respond to it later. 183 | self.physics:Update(self) 184 | 185 | if self.currentTimeline then 186 | self:UpdateTimeline() 187 | end 188 | 189 | -- Update the current state and then execute the relevant callbacks when transition occurs. 190 | local nextState = self.currentState:Update(self) 191 | if nextState then 192 | self:ChangeState(nextState) 193 | end 194 | end 195 | 196 | -- Handle the player being hit and update hitstun. 197 | function PlayerObject:HandleHitReaction() 198 | -- Transitions into a hit reaction if hit by an attack 199 | if self.events.AttackedThisFrame then 200 | self.events.HitEnemyThisFrame = false 201 | self:ChangeState(CharacterStates.GroundDamage:New()) 202 | self.hitstunTimer = self.events.hitstun -- Get hitstun that was passed in during the collision from the opponent's attack 203 | self.hitstopTimer = self.events.hitstop 204 | else 205 | if self.hitstunTimer > 0 then 206 | self.hitstunTimer = self.hitstunTimer - 1 207 | end 208 | end 209 | end 210 | 211 | function PlayerObject:ChangeState(state) 212 | self.attackHit = false 213 | self.attackCanHit = false 214 | self.currentState:End(self) 215 | self.currentState = state:New() 216 | self.currentState:Begin(self) 217 | end 218 | 219 | function PlayerObject:UpdateTimeline() 220 | -- Update frame count 221 | if self.currentFrame + 1 >= self.currentTimeline.duration then 222 | -- Indicate the animation has ended for other systems to use. 223 | self.events.AnimEnd = true 224 | 225 | -- Loop if this is a looping move. 226 | if self.currentTimeline.looping then 227 | self.currentFrame = 0 228 | end 229 | else 230 | self.currentFrame = self.currentFrame + 1 231 | end 232 | end 233 | 234 | function PlayerObject:PlayTimeline(timeline) 235 | self.currentTimeline = self.timelines[timeline] 236 | self.currentFrame = 0 237 | self.events.AnimEnd = false 238 | end 239 | 240 | -- Search for the currently displayed image in a timeline. 241 | function PlayerObject:GetImageFromTimeline(timeline, frame) 242 | local offset = 0 243 | local lastImage = nil 244 | for index, imageDescription in pairs(timeline.images) do 245 | lastImage = self.imageSequences[imageDescription.sequence][imageDescription.index] 246 | 247 | if (imageDescription.duration + offset) > frame then 248 | return lastImage 249 | end 250 | offset = offset + imageDescription.duration 251 | end 252 | return lastImage 253 | end 254 | 255 | function PlayerObject:GetAttackBoxFromTimeline(timeline, frame) 256 | local boxList = {} 257 | for index, box in pairs(timeline.attackBoxes) do 258 | if frame >= box.start and frame < box.last then 259 | table.insert(boxList, box) 260 | end 261 | end 262 | return boxList 263 | end 264 | 265 | function PlayerObject:GetDamageBoxFromTimeline(timeline, frame) 266 | local boxList = {} 267 | for index, box in pairs(timeline.damageBoxes) do 268 | if frame >= box.start and frame < box.last then 269 | table.insert(boxList, box) 270 | end 271 | end 272 | return boxList 273 | end 274 | 275 | 276 | -- Check to see if 2 boxes are colliding 277 | function CheckIfBoxesCollide(box1, box2) 278 | return not (box1.x > box2.r or box2.x > box1.r or box1.y < box2.b or box2.y < box1.b) 279 | end 280 | 281 | function TranslateBox(box, x, y, flipped) 282 | if flipped then 283 | return { x = x - box.r, y = box.y + y, r = x - box.x, b = box.b + y} 284 | end 285 | 286 | return { x = box.x + x, y = box.y + y, r = box.r + x, b = box.b + y} 287 | end 288 | function PlayerObject:IsAttacking() 289 | if self.currentState and self.currentState.attack and self.attackCanHit then 290 | return true 291 | end 292 | end 293 | 294 | -- Check if colliding with one of the enemy's attack boxes. 295 | function PlayerObject:CheckIfHit(enemy) 296 | local attackBoxList = self:GetAttackBoxFromTimeline(enemy.currentTimeline, enemy.currentFrame) 297 | local damageBoxList = self:GetDamageBoxFromTimeline(self.currentTimeline, self.currentFrame) 298 | 299 | 300 | 301 | for index, box1 in pairs(attackBoxList) do 302 | -- Check against all damage boxes 303 | for index2, box2 in pairs(damageBoxList) do 304 | if CheckIfBoxesCollide(TranslateBox(box1, enemy.physics.x, enemy.physics.y, enemy.facing), TranslateBox(box2, self.physics.x, self.physics.y, self.facing)) then 305 | return true 306 | end 307 | end 308 | end 309 | return false 310 | end 311 | 312 | -- Get the current attack properties if they exist. 313 | function PlayerObject:GetAttackProperties() 314 | if self.currentTimeline then 315 | return self.currentTimeline.attackProperties 316 | end 317 | end 318 | 319 | -- Apply all the hit properties to the defending player 320 | function PlayerObject:ApplyHitProperties(attackProperties) 321 | self.events.hitstop = attackProperties.hitStop 322 | self.events.hitstun = attackProperties.hitStun 323 | self.hp = self.hp - attackProperties.damage 324 | if self.hp < 0 then 325 | self.hp = 0 326 | end 327 | end 328 | 329 | -- Used this table when inputs for the player are disabled. 330 | local emptyInputState = {} 331 | -- Get State State 332 | function PlayerObject:GetInputState() 333 | if self.inputEnabled then 334 | return self.input:CurrentInputState(self.playerIndex) 335 | end 336 | return emptyInputState 337 | end 338 | 339 | 340 | function PlayerObject:Reset() 341 | self.currentState = CharacterStates.Standing:New() 342 | self.physics = MakePhysicsSystem() -- May add a reset function to the physics system later. 343 | self.currentTimeline = nil 344 | self.currentFrame = 0 345 | self.hitstunTimer = 0 346 | self.hitstopTimer = 0 347 | self.attackCanHit = false 348 | self.attackHit = false 349 | self.hp = DEFAULT_HP 350 | self.inputEnabled = false 351 | end 352 | 353 | -- Used in the rollback system to make a copy of the state of the player. 354 | function PlayerObject:CopyState() 355 | local state = {} 356 | state.physics = self.physics:CopyState() -- The physics system must be rolled back at well. 357 | 358 | state.currentState = self.currentState 359 | state.currentTimeline = self.currentTimeline 360 | state.currentFrame = self.currentFrame 361 | state.hitstunTimer = self.hitstunTimer 362 | state.hitstopTimer = self.hitstopTimer 363 | state.attackCanHit = self.attackCanHit 364 | state.attackHit = self.attackHit 365 | state.hp = self.hp 366 | state.inputEnabled = self.inputEnabled 367 | 368 | return state 369 | end 370 | 371 | -- Used in the rollback system to restore the old state of the player. 372 | function PlayerObject:SetState(state) 373 | self.physics:SetState(state.physics) -- The physics system must be rolled back at well. 374 | 375 | self.currentState = state.currentState 376 | self.currentTimeline = state.currentTimeline 377 | self.currentFrame = state.currentFrame 378 | self.hitstunTimer = state.hitstunTimer 379 | self.hitstopTimer = state.hitstopTimer 380 | self.attackCanHit = state.attackCanHit 381 | self.attackHit = state.attackHit 382 | self.hp = state.hp 383 | self.inputEnabled = state.inputEnabled 384 | end 385 | 386 | -- Player Object Factory 387 | function MakePlayerObject() 388 | return PlayerObject:New( 389 | { 390 | playerIndex = 1, -- Index that references the player. 391 | states = CharacterStates, -- List of all the player character states 392 | currentState = CharacterStates.Standing:New(), -- Current executing state 393 | physics = MakePhysicsSystem(), -- Physics system for the player character 394 | timelines = Timelines, -- List of timelines used by this player character 395 | currentTimeline = nil, -- Currently playing timeline 396 | currentFrame = 0, -- Current frame in the timeline. 397 | 398 | hitstunTimer = 0, -- Timer used to down down the frames until a damage reaction is over. 399 | hitstopTimer = 0, -- Timer used while in hitstop 400 | 401 | attackCanHit = false, -- Whether or not the attack is currently able to hit 402 | attackHit = false, -- Whether or not the current attack already hit. 403 | 404 | hpMax = DEFAULT_HP, -- Maximum amount of life the player can have 405 | hp = DEFAULT_HP, -- Current amount of life the player has. 406 | 407 | events = {}, -- Events that get cleared at the start of every frame. 408 | 409 | inputEnabled = false, -- Indicates whether or not the character responds to player inputs. 410 | } 411 | ) 412 | end 413 | 414 | -- Loads images reference in ImageSequences 415 | function LoadPlayerImageSequences(playerIndex) 416 | 417 | local imageSequences = table.deep_copy(ImageSequences) 418 | 419 | -- Load images 420 | for sequenceName,sequence in pairs(imageSequences) do 421 | for index, imageDescription in pairs(sequence) do 422 | imageDescription.image = love.graphics.newImage('assets/player' .. playerIndex .. "/" .. imageDescription.source .. '.png') 423 | end 424 | end 425 | 426 | return imageSequences 427 | end 428 | 429 | 430 | -------------------------------------------------------------------------------- /game/PlayerState.lua: -------------------------------------------------------------------------------- 1 | require("Util") 2 | -- Base table for all tables that are inheritable. 3 | Object = {} 4 | 5 | -- Boiler plate for making object inheritance and instancing possible 6 | function Object:New(o) 7 | o = o or {} 8 | setmetatable(o, self) 9 | self.__index = self 10 | return o 11 | end 12 | 13 | PlayerState = {} 14 | 15 | -- Base table for player states. 16 | PlayerState = Object:New() 17 | 18 | -- Called when transitioning into this state. 19 | function PlayerState:Begin(player) 20 | end 21 | 22 | -- Called every frame. 23 | function PlayerState:Update(player) 24 | end 25 | 26 | -- Called when transitioning out of this state. 27 | function PlayerState:End(player) 28 | end 29 | 30 | -------------------------------------------------------------------------------- /game/RunOverride.lua: -------------------------------------------------------------------------------- 1 | 2 | --[[ 3 | Original Author: https://github.com/Leandros 4 | Updated Author: https://github.com/jakebesworth 5 | MIT License 6 | Copyright (c) 2018 Jake Besworth 7 | 8 | Original Gist: https://gist.github.com/Leandros/98624b9b9d9d26df18c4 9 | Love.run 11.X: https://love2d.org/wiki/love.run 10 | Original Article, 4th algorithm: https://gafferongames.com/post/fix_your_timestep/ 11 | Forum Discussion: https://love2d.org/forums/viewtopic.php?f=3&t=85166&start=10 12 | 13 | Add this code to bottom of main.lua to override love.run() for Love2D 11.X 14 | Tickrate is how many frames your simulation happens per second (Timestep) 15 | Max Frame Skip is how many frames to allow skipped due to lag of simulation outpacing (on slow PCs) tickrate 16 | ---]] 17 | 18 | -- 1 / Ticks Per Second 19 | local TICK_RATE = 1 / 60 20 | 21 | -- How many Frames are allowed to be skipped at once due to lag (no "spiral of death") 22 | local MAX_FRAME_SKIP = 25 23 | 24 | -- No configurable framerate cap currently, either max frames CPU can handle (up to 1000), or vsync'd if conf.lua 25 | 26 | function love.run() 27 | if love.load then love.load(love.arg.parseGameArguments(arg), arg) end 28 | 29 | -- We don't want the first frame's dt to include time taken by love.load. 30 | if love.timer then love.timer.step() end 31 | 32 | local lag = 0.0 33 | 34 | -- Main loop time. 35 | return function() 36 | -- Process events. 37 | if love.event then 38 | love.event.pump() 39 | for name, a,b,c,d,e,f in love.event.poll() do 40 | if name == "quit" then 41 | if not love.quit or not love.quit() then 42 | return a or 0 43 | end 44 | end 45 | love.handlers[name](a,b,c,d,e,f) 46 | end 47 | end 48 | 49 | -- Cap number of Frames that can be skipped so lag doesn't accumulate 50 | if love.timer then lag = math.min(lag + love.timer.step(), TICK_RATE * MAX_FRAME_SKIP) end 51 | 52 | while lag >= TICK_RATE do 53 | if love.update then love.update(TICK_RATE) end 54 | lag = lag - TICK_RATE 55 | end 56 | 57 | if love.graphics and love.graphics.isActive() then 58 | love.graphics.origin() 59 | love.graphics.clear(love.graphics.getBackgroundColor()) 60 | 61 | if love.draw then love.draw() end 62 | love.graphics.present() 63 | end 64 | 65 | -- Even though we limit tick rate and not frame rate, we might want to cap framerate at 1000 frame rate as mentioned https://love2d.org/forums/viewtopic.php?f=4&t=76998&p=198629&hilit=love.timer.sleep#p160881 66 | if love.timer then love.timer.sleep(0.001) end 67 | end 68 | end -------------------------------------------------------------------------------- /game/StateTimelines.lua: -------------------------------------------------------------------------------- 1 | -- Image sequence description. 2 | -- source: Image name without the extension. 3 | -- x: X coordinate alignment offset. 4 | -- y: Y coordinate alignment offset. 5 | ImageSequences = 6 | { 7 | stand = {{source = "idle_00", x = -32, y = -152}}, 8 | attack = 9 | { 10 | {source = "attack_00", x = -108, y = -131}, 11 | {source = "attack_01", x = -103, y = -146}, 12 | {source = "attack_02", x = -32, y = -63}, 13 | } 14 | } 15 | 16 | -- Stores timing associated data used in player states. Animation and collision boxes are described here. 17 | Timelines = 18 | { 19 | stand = 20 | { 21 | duration = 5, 22 | 23 | images = 24 | { 25 | { sequence = "stand", index = 1, duration = 5 } 26 | }, 27 | 28 | damageBoxes = 29 | { 30 | {start = 0, last = 5, x = -40, y = 80, r = 40, b = 0} 31 | }, 32 | attackBoxes = {} 33 | 34 | }, 35 | 36 | attack = 37 | { 38 | duration = 26, 39 | 40 | images = 41 | { 42 | { sequence = "attack", index = 1, duration = 8 }, 43 | { sequence = "attack", index = 2, duration = 3 }, 44 | { sequence = "attack", index = 3, duration = 15 }, 45 | }, 46 | 47 | damageBoxes = 48 | { 49 | {start = 0, last = 26, x = -40, y = 80, r = 40, b = 0} 50 | }, 51 | 52 | attackBoxes = 53 | { 54 | {start = 8, last = 11, x = 32, y = 80, r = 120, b = -20} 55 | }, 56 | 57 | attackProperties = 58 | { 59 | damage = 1300, 60 | hitStun = 15, 61 | hitStop = 8, 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /game/Util.lua: -------------------------------------------------------------------------------- 1 | -- Makes a shallow copy of a table. Deep copies of child tables will not be duplicate. 2 | function table.copy(t) 3 | if t == nil then return nil end 4 | local t2 = {} 5 | for k,v in pairs(t) do 6 | t2[k] = v 7 | end 8 | return t2 9 | end 10 | 11 | function table.array_copy(t) 12 | if not t then return nil end 13 | local t2 = {} 14 | for i,v in ipairs(t) do 15 | if type(v) == "table" then 16 | t2[i] = table.deep_copy(v) 17 | else 18 | t2[i] = v 19 | end 20 | end 21 | return t2 22 | end 23 | 24 | -- Recursively makes a deep copy of a table. Assumes there are no cycles. 25 | function table.deep_copy(t) 26 | if not t then return nil end 27 | local t2 = {} 28 | for k,v in pairs(t) do 29 | if type(v) == "table" then 30 | t2[k] = table.deep_copy(v) 31 | else 32 | t2[k] = v 33 | end 34 | end 35 | return t2 36 | end 37 | 38 | -- Print the table 39 | function table.print(t) 40 | if not t then 41 | print("Table is nil") 42 | return nil 43 | end 44 | for k,v in pairs(t) do 45 | print(k .. " : ", v) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /game/World.lua: -------------------------------------------------------------------------------- 1 | -- The world stores manages state that effect all in game objects. 2 | World = 3 | { 4 | stop = false, -- Pauses the entire world when true 5 | } 6 | 7 | -- Used in the rollback system to make a copy of the world state 8 | function World:CopyState() 9 | local state = {} 10 | state.stop = World.stop 11 | return state 12 | end 13 | 14 | -- Used in the rollback system to restore the old state of the world 15 | function World:SetState(state) 16 | World.stop = state.stop 17 | end -------------------------------------------------------------------------------- /game/assets/player1/attack_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player1/attack_00.png -------------------------------------------------------------------------------- /game/assets/player1/attack_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player1/attack_01.png -------------------------------------------------------------------------------- /game/assets/player1/attack_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player1/attack_02.png -------------------------------------------------------------------------------- /game/assets/player1/idle_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player1/idle_00.png -------------------------------------------------------------------------------- /game/assets/player2/attack_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player2/attack_00.png -------------------------------------------------------------------------------- /game/assets/player2/attack_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player2/attack_01.png -------------------------------------------------------------------------------- /game/assets/player2/attack_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player2/attack_02.png -------------------------------------------------------------------------------- /game/assets/player2/idle_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/player2/idle_00.png -------------------------------------------------------------------------------- /game/assets/sounds/hit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/sounds/hit.wav -------------------------------------------------------------------------------- /game/assets/sounds/jump.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/sounds/jump.wav -------------------------------------------------------------------------------- /game/assets/sounds/whiff.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/game/assets/sounds/whiff.wav -------------------------------------------------------------------------------- /game/conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.window.title = "Simple Fighting Game with Networking Example" 3 | t.window.width = 1024 4 | t.window.height = 768 5 | 6 | t.modules.physics = false -- disable the physics module 7 | 8 | t.console = true -- print to the console 9 | end 10 | -------------------------------------------------------------------------------- /game/main.lua: -------------------------------------------------------------------------------- 1 | require("InputSystem") -- Manages player inputs. 2 | require("World") -- World object 3 | require("PlayerObject") -- Player object handling. 4 | require("RunOverride") -- Includes an overrided love.run function for handling fixed time step. 5 | require("MatchSystem") -- Manages match state 6 | require("Network") -- Handles networking 7 | 8 | 9 | 10 | -- This table stores time sync data that will be used for drawing the sync graph. 11 | local timeSyncGraphTable = {} 12 | 13 | for i=0,60-1 do 14 | timeSyncGraphTable[1+i*2] = i*10 15 | timeSyncGraphTable[1+(i*2 + 1)] = 0 16 | end 17 | 18 | -- Table for storing graph data for monitoring the number of rollbacked frames 19 | local rollbackGraphTable = {} 20 | 21 | for i=0,60-1 do 22 | rollbackGraphTable[1+i*2] = i*10 23 | rollbackGraphTable[1+(i*2 + 1)] = 0 24 | end 25 | 26 | -- Manages the game state 27 | local Game = 28 | { 29 | -- Enabled when the game is paused 30 | paused = false, 31 | 32 | -- Enabled when game needs to update for a single frame. 33 | frameStep = false, 34 | 35 | -- Number of ticks since the start of the game. 36 | tick = 0, 37 | 38 | -- The confirmed tick checked the last frame 39 | lastConfirmedTick = -1, 40 | 41 | -- Indicates that sync occurred last update 42 | syncedLastUpdate = false, 43 | 44 | -- Used to force dropped frames to test network syncing code 45 | forcePause = false 46 | 47 | } 48 | 49 | -- Resets the game. 50 | function Game:Reset() 51 | Game.tick = 0 52 | MatchSystem:Reset() 53 | end 54 | 55 | -- Stores the state of all rollbackable objects and systems in the game. 56 | function Game:StoreState() 57 | self.storedState = {} 58 | 59 | -- All rollbackable objects and systems will have a CopyState() method. 60 | self.storedState.world = World:CopyState() 61 | self.storedState.inputSystem = InputSystem:CopyState() 62 | self.storedState.matchSystem = MatchSystem:CopyState() 63 | self.storedState.players = {self.players[1]:CopyState(), self.players[2]:CopyState()} 64 | 65 | self.storedState.tick = self.tick 66 | end 67 | 68 | -- Restores the state of all rollbackable objects and systems in the game. 69 | function Game:RestoreState() 70 | -- Can't restore the state if has not been saved yet. 71 | if not self.storedState then 72 | return 73 | end 74 | 75 | -- All rollbackable objects and systems will have a SetState() method. 76 | World:SetState(self.storedState.world) 77 | InputSystem:SetState(self.storedState.inputSystem) 78 | MatchSystem:SetState(self.storedState.matchSystem) 79 | self.players[1]:SetState(self.storedState.players[1]) 80 | self.players[2]:SetState(self.storedState.players[2]) 81 | 82 | self.tick = self.storedState.tick 83 | end 84 | 85 | 86 | -- Top level update for the game state. 87 | function Game:Update() 88 | 89 | -- Pause and frame step control 90 | if Game.paused then 91 | if Game.frameStep then 92 | Game.frameStep = false 93 | else 94 | -- Do not update the game when paused. 95 | return 96 | end 97 | end 98 | 99 | -- Update the input system 100 | InputSystem:Update() 101 | 102 | -- When the world state is paused, don't update any of the players 103 | if not World.stop then 104 | -- Run the preupdate 105 | Game.players[1]:PreUpdate() 106 | Game.players[2]:PreUpdate() 107 | 108 | for playerIndex1, attacker in pairs(Game.players) do 109 | -- Handle collisions. 110 | if not attacker.attackHit and attacker:IsAttacking() then 111 | for playerIndex2, defender in pairs(Game.players) do 112 | if playerIndex1 ~= playerIndex2 and defender:CheckIfHit(attacker) then 113 | 114 | 115 | local attackProperties = attacker:GetAttackProperties() 116 | 117 | -- When there are no attack properties, the collision will be ignored. 118 | if attackProperties then 119 | 120 | -- These events are only valid until the end of the frame. 121 | defender.events.AttackedThisFrame = true 122 | attacker.events.HitEnemyThisFrame = true 123 | attacker.events.hitstop = attackProperties.hitStop 124 | attacker.attackHit = true 125 | 126 | -- Apply the hit properties. I'll probably make an event and delay until the Update() call later. 127 | defender:ApplyHitProperties(attackProperties) 128 | end 129 | end 130 | end 131 | end 132 | end 133 | 134 | -- Transition to hit reaction and handle other damaged state. 135 | Game.players[1]:HandleHitReaction() 136 | Game.players[2]:HandleHitReaction() 137 | 138 | -- Update the player objects. 139 | Game.players[1]:Update() 140 | Game.players[2]:Update() 141 | end 142 | 143 | MatchSystem:Update() 144 | end 145 | 146 | 147 | local lifeBarXOffset = 56 -- Position from the side of the screen of the life bars. 148 | local lifeBarYOffset = 40 -- Position from the top of the screen of the life bars. 149 | 150 | local lifeBarWidth = 386 -- Lifebar width. 151 | local lifeBarHeight = 22 -- Lifebar height. 152 | 153 | local lifeBarColor = {0, 193 / 255, 0} -- Color indicating the current amount of HP. 154 | local lifeBarBGColor = {0.3, 0.3, 0.3} -- Color behind the lifebar when HP is depleated. 155 | 156 | function DrawLifeBar(hpRate) 157 | love.graphics.setColor(lifeBarBGColor) 158 | love.graphics.rectangle('fill', 0, 0, lifeBarWidth, lifeBarHeight) 159 | 160 | love.graphics.setColor(lifeBarColor) 161 | love.graphics.rectangle('fill', 0, 0, lifeBarWidth*hpRate, lifeBarHeight) 162 | end 163 | 164 | -- Draw lifebars and other information that will be displayed to the player. 165 | function DrawHUD() 166 | 167 | -- Draw player 1's life bar. 168 | love.graphics.push() 169 | love.graphics.translate(lifeBarXOffset, lifeBarYOffset) 170 | DrawLifeBar(Game.players[1].hp / Game.players[1].hpMax) 171 | love.graphics.pop() 172 | 173 | 174 | -- Draw player 2's life bar. 175 | love.graphics.push() 176 | love.graphics.translate(SCREEN_WIDTH-lifeBarXOffset, lifeBarYOffset) 177 | love.graphics.scale(-1, 1) 178 | DrawLifeBar(Game.players[2].hp / Game.players[2].hpMax) 179 | love.graphics.pop() 180 | 181 | end 182 | 183 | 184 | 185 | -- Top level drawing function 186 | function Game:Draw() 187 | -- Draw the ground. 188 | love.graphics.rectangle('fill', 0, 768 - GROUND_HEIGHT, 1024, GROUND_HEIGHT) 189 | 190 | love.graphics.push() 191 | 192 | -- Move draw everything in world coordinates 193 | love.graphics.translate(1024 / 2, 768 - GROUND_HEIGHT) 194 | 195 | -- Create drawing priority list. 196 | local drawList = {Game.players[1], Game.players[2]} 197 | 198 | -- Comparison function 199 | local comparePlayers = function(a, b) 200 | if a.currentState.attack then 201 | return false 202 | end 203 | return true 204 | end 205 | 206 | -- Sort based on priority 207 | table.sort(drawList, comparePlayers) 208 | 209 | -- Draw players from the sorted list 210 | for index, player in pairs(drawList) do 211 | player:Draw() 212 | end 213 | 214 | love.graphics.pop() 215 | 216 | DrawHUD() 217 | 218 | MatchSystem:Draw() 219 | 220 | 221 | if SHOW_DEBUG_INFO then 222 | --- Draw debug information ontop of everything else. 223 | love.graphics.setColor(1,1,1) 224 | love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10) 225 | 226 | love.graphics.print("Hitstun: (".. Game.players[1].hitstunTimer .. ", " .. Game.players[2].hitstunTimer .. ")", 10, 20) 227 | love.graphics.print("Hitstop: (".. Game.players[1].hitstopTimer .. ", " .. Game.players[2].hitstopTimer .. ")", 10, 30) 228 | love.graphics.print("P1.x: ".. Game.players[1].physics.x .. ", P2.x" .. Game.players[2].physics.x, 10, 40) 229 | love.graphics.print("Tick Offset: " .. Network.tickOffset, 10, 60) 230 | if World.stop == true then 231 | love.graphics.print("World Stop", 10, 40) 232 | end 233 | 234 | love.graphics.print("Tick: " .. Game.tick, 10, 50) 235 | 236 | love.graphics.push() 237 | love.graphics.translate(0, 200) 238 | 239 | -- Draw sync graph 240 | love.graphics.setColor(1, 1, 1) 241 | 242 | love.graphics.line(0, 0, 10*60, 0) 243 | 244 | love.graphics.setColor(0, 1, 0) 245 | love.graphics.line(timeSyncGraphTable) 246 | 247 | love.graphics.setColor(1, 0, 0) 248 | love.graphics.line(rollbackGraphTable) 249 | 250 | love.graphics.pop() 251 | end 252 | 253 | 254 | 255 | -- Shown while the server is running but not connected to a client. 256 | if Network.isServer and not Network.connectedToClient then 257 | love.graphics.setColor(1,0,0) 258 | love.graphics.print("Network: Waiting on client to connect", 10, 40) 259 | end 260 | 261 | -- Stage ground color 262 | love.graphics.setColor(1,1,1) 263 | end 264 | 265 | function love.load() 266 | 267 | InputSystem.joysticks = love.joystick.getJoysticks() 268 | for index, stick in pairs(InputSystem.joysticks) do 269 | print("Found Gamepad: " .. stick:getName()) 270 | end 271 | 272 | Game.players = { MakePlayerObject(), MakePlayerObject() } 273 | 274 | -- Load all images needed for the player character animations 275 | Game.players[1].imageSequences = LoadPlayerImageSequences(1) 276 | Game.players[2].imageSequences = LoadPlayerImageSequences(2) 277 | 278 | -- Load sounds 279 | for index, player in pairs(Game.players) do 280 | player.jumpSound = love.audio.newSource("assets/sounds/jump.wav", "static") 281 | player.hitSound = love.audio.newSource("assets/sounds/hit.wav", "static") 282 | player.whiffSound = love.audio.newSource("assets/sounds/whiff.wav", "static") 283 | end 284 | 285 | love.keyboard.setKeyRepeat(false) 286 | 287 | InputSystem.game = Game 288 | 289 | -- Initialize player input command buffers 290 | InputSystem:InitializeBuffer(1) 291 | InputSystem:InitializeBuffer(2) 292 | 293 | -- Initialize refence to command buffers for each player 294 | Game.players[1].input = InputSystem 295 | Game.players[1].playerIndex = 1 296 | 297 | Game.players[2].input = InputSystem 298 | Game.players[2].playerIndex = 2 299 | 300 | 301 | -- Initialize the match system 302 | MatchSystem.world = World 303 | MatchSystem.players = Game.players 304 | MatchSystem.game = Game 305 | 306 | Game.network = Network 307 | 308 | Game:Reset() 309 | 310 | -- Store game state before the first update 311 | Game:StoreState() 312 | end 313 | 314 | -- Gets the sync data to confirm the client game states are in sync 315 | function Game:GetSyncData() 316 | -- For now we will just compare the x coordinates of the both players 317 | return love.data.pack("string", "nn", self.players[1].physics.x, self.players[2].physics.x) 318 | end 319 | 320 | -- Checks whether or not a game state desync has occurred between the local and remote clients. 321 | function Game:SyncCheck() 322 | 323 | if not NET_DETECT_DESYNCS then 324 | return 325 | end 326 | 327 | if Network.lastSyncedTick < 0 then 328 | return 329 | end 330 | 331 | -- Check desyncs at a fixed rate. 332 | if (Network.lastSyncedTick % Network.desyncCheckRate) ~= 0 then 333 | return 334 | end 335 | 336 | -- Generate the data we'll send to the other player for testing that their game state is in sync. 337 | Network:SetLocalSyncData(Network.lastSyncedTick, Game:GetSyncData()) 338 | 339 | -- Send sync data everytime we've applied from the remote player to a game frame. 340 | Network:SendSyncData() 341 | 342 | 343 | local desynced, desyncFrame = Network:DesyncCheck() 344 | if not desynced then 345 | return 346 | end 347 | 348 | 349 | -- Detect when the sync data doesn't match then halt the game 350 | NetLog("Desync detected at tick: " .. desyncFrame) 351 | 352 | love.window.showMessageBox( "Alert", "Desync detected", "info", true ) 353 | -- the log afterward is pretty useless so exiting here. It also helps to know when a desync occurred. 354 | love.event.quit(0) 355 | end 356 | 357 | -- Rollback if needed. 358 | function HandleRollbacks() 359 | local lastGameTick = Game.tick - 1 360 | -- The input needed to resync state is available so rollback. 361 | -- Network.lastSyncedTick keeps track of the lastest synced game tick. 362 | -- When the tick count for the inputs we have is more than the number of synced ticks it's possible to rerun those game updates 363 | -- with a rollback. 364 | 365 | -- The number of frames that's elasped since the game has been out of sync. 366 | -- Rerun rollbackFrames number of updates. 367 | rollbackFrames = lastGameTick - Network.lastSyncedTick 368 | 369 | -- Update the graph indicating the number of rollback frames 370 | rollbackGraphTable[ 1 + (lastGameTick % 60) * 2 + 1 ] = -1 * rollbackFrames * GRAPH_UNIT_SCALE 371 | 372 | if lastGameTick >= 0 and lastGameTick > (Network.lastSyncedTick + 1) and Network.confirmedTick > Network.lastSyncedTick then 373 | 374 | -- Must revert back to the last known synced game frame. 375 | Game:RestoreState() 376 | 377 | for i=1,rollbackFrames do 378 | -- Get input from the input history buffer. The network system will predict input after the last confirmed tick (for the remote player). 379 | InputSystem:SetInputState(InputSystem.localPlayerIndex, Network:GetLocalInputState(Game.tick)) -- Offset of 1 ensure it's used for the next game update. 380 | InputSystem:SetInputState(InputSystem.remotePlayerIndex, Network:GetRemoteInputState(Game.tick)) 381 | 382 | local lastRolledBackGameTick = Game.tick 383 | Game:Update() 384 | Game.tick = Game.tick + 1 385 | 386 | -- Confirm that we are indeed still synced 387 | if lastRolledBackGameTick <= Network.confirmedTick then 388 | -- Store the state since we know it's synced. We really only need to call this on the last synced frame. 389 | -- Leaving in for demonstration purposes. 390 | Game:StoreState() 391 | Network.lastSyncedTick = lastRolledBackGameTick 392 | 393 | -- Confirm the game clients are in sync 394 | Game:SyncCheck() 395 | end 396 | end 397 | end 398 | 399 | end 400 | 401 | -- Handles testing rollbacks offline. 402 | function TestRollbacks() 403 | if ROLLBACK_TEST_ENABLED then 404 | if Game.tick >= ROLLBACK_TEST_FRAMES then 405 | 406 | -- Get sync data that we'll test after the rollback 407 | local syncData = love.data.pack("string", "nn", Game.players[1].physics.y, Game.players[2].physics.y) 408 | 409 | Game:RestoreState() 410 | 411 | -- Prevent polling for input since we set it directly from the input history. 412 | for i=1,ROLLBACK_TEST_FRAMES do 413 | -- Get input from a input history buffer that we update below 414 | InputSystem:SetInputState(InputSystem.localPlayerIndex, Network:GetLocalInputState(Game.tick)) 415 | InputSystem:SetInputState(InputSystem.remotePlayerIndex, Network:GetRemoteInputState(Game.tick)) 416 | 417 | 418 | Game.tick = Game.tick + 1 419 | Game:Update() 420 | 421 | -- Store only the first updated state 422 | if i == 1 then 423 | Game:StoreState() 424 | end 425 | end 426 | 427 | -- Get the sync data after a rollback and check to see if it matches the data before the rollback. 428 | local postSyncData = love.data.pack("string", "nn", Game.players[1].physics.y, Game.players[2].physics.y) 429 | 430 | if syncData ~= postSyncData then 431 | love.window.showMessageBox( "Alert", "Rollback Desync Detected", "info", true ) 432 | love.event.quit(0) 433 | end 434 | end 435 | end 436 | end 437 | -- Used for testing performance. 438 | local lastTime = love.timer.getTime() 439 | function love.update(dt) 440 | 441 | local lastGameTick = Game.tick 442 | 443 | local updateGame = false 444 | 445 | if ROLLBACK_TEST_ENABLED then 446 | updateGame = true 447 | end 448 | 449 | -- The network is update first 450 | if Network.enabled then 451 | local x = love.timer.getTime() 452 | 453 | -- Setup the local input delay to match the network input delay. 454 | -- If this isn't done, the two game clients will be out of sync with each other as the local player's input will be applied on the current frame, 455 | -- while the opponent's will be applied to a frame inputDelay frames in the input buffer. 456 | InputSystem.inputDelay = Network.inputDelay 457 | 458 | -- First get any data that has been sent from the other client 459 | Network:ReceiveData() 460 | 461 | -- Send any packets that have been queued 462 | Network:ProcessDelayedPackets() 463 | 464 | if Network.connectedToClient then 465 | 466 | -- First we assume that the game can be updated, sync checks below can halt updates 467 | updateGame = true 468 | 469 | if Game.forcePause then 470 | updateGame = false 471 | end 472 | 473 | -- Run any rollbacks that can be processed before the next game update 474 | HandleRollbacks() 475 | 476 | 477 | -- Calculate the difference between remote game tick and the local. This will be used for syncing. 478 | -- We don't use the latest local tick, but the tick for the latest input sent to the remote client. 479 | Network.localTickDelta = lastGameTick - Network.confirmedTick 480 | 481 | timeSyncGraphTable[ 1 + (lastGameTick % 60) * 2 + 1 ] = -1 * (Network.localTickDelta - Network.remoteTickDelta) * GRAPH_UNIT_SCALE 482 | 483 | -- Only do time sync check when the previous confirmed tick from the remote client hasn't been used yet. 484 | if Network.confirmedTick > Game.lastConfirmedTick then 485 | 486 | Game.lastConfirmedTick = Network.confirmedTick 487 | 488 | -- Prevent updating the game when the tick difference is greater on this end. 489 | -- This allows the game deltas to be off by 2 frames. Our timing is only accurate to one frame so any slight increase in network latency 490 | -- would cause the game to constantly hold. You could increase this tolerance, but this would increase the advantage for one player over the other. 491 | 492 | -- Only calculate time sync frames when we are not currently time syncing. 493 | if Network.tickSyncing == false then 494 | -- Calculate tick offset using the clock synchronization algorithm. 495 | -- See https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm 496 | Network.tickOffset = (Network.localTickDelta - Network.remoteTickDelta) / 2.0 497 | 498 | -- Only sync when the tick difference is more than one frame. 499 | if Network.tickOffset >= 1 then 500 | Network.tickSyncing = true 501 | end 502 | end 503 | 504 | if Network.tickSyncing and Game.syncedLastUpdate == false then 505 | updateGame = false 506 | Game.syncedLastUpdate = true 507 | 508 | Network.tickOffset = Network.tickOffset - 1 509 | 510 | -- Stop time syncing when the tick difference is less than 1 so we don't overshoot 511 | if Network.tickOffset < 1 then 512 | Network.tickSyncing = false 513 | end 514 | else 515 | Game.syncedLastUpdate = false 516 | end 517 | 518 | end 519 | 520 | -- Only halt the game update based on exceeding the rollback window when the game updated hasn't previously been stopped by time sync code 521 | if updateGame then 522 | -- We allow the game to run for NET_ROLLBACK_MAX_FRAMES updates without having input for the current frame. 523 | -- Once the game can no longer update, it will wait until the other player's client can catch up. 524 | if lastGameTick <= (Network.confirmedTick + NET_ROLLBACK_MAX_FRAMES) then 525 | updateGame = true 526 | else 527 | updateGame = false 528 | end 529 | end 530 | end 531 | 532 | end 533 | 534 | if updateGame then 535 | -- Test rollbacks 536 | TestRollbacks() 537 | 538 | -- Poll inputs for this frame. In network mode the network manager will handle updating player command buffers. 539 | local updateCommandBuffers = not Network.enabled 540 | InputSystem:PollInputs(updateCommandBuffers) 541 | 542 | -- Network manager will handle updating inputs. 543 | if Network.enabled then 544 | -- Update local input history 545 | local sendInput = InputSystem:GetLatestInput(InputSystem.localPlayerIndex) 546 | Network:SetLocalInput(sendInput, lastGameTick+Network.inputDelay) 547 | 548 | -- Set the input state fo[r the current tick for the remote player's character. 549 | InputSystem:SetInputState(InputSystem.localPlayerIndex, Network:GetLocalInputState(lastGameTick)) 550 | InputSystem:SetInputState(InputSystem.remotePlayerIndex, Network:GetRemoteInputState(lastGameTick)) 551 | 552 | end 553 | 554 | -- Increment the tick count only when the game actually updates. 555 | Game:Update() 556 | 557 | Game.tick = Game.tick + 1 558 | 559 | -- Save stage after an update if testing rollbacks 560 | if ROLLBACK_TEST_ENABLED then 561 | -- Save local input history for this game tick 562 | Network:SetLocalInput(InputSystem:GetLatestInput(InputSystem.localPlayerIndex), lastGameTick) 563 | end 564 | 565 | 566 | if Network.enabled then 567 | -- Check whether or not the game state is confirmed to be in sync. 568 | -- Since we previously rolled back, it's safe to set the lastSyncedTick here since we know any previous frames will be synced. 569 | if (Network.lastSyncedTick + 1) == lastGameTick and lastGameTick <= Network.confirmedTick then 570 | 571 | -- Increment the synced tick number if we have inputs 572 | Network.lastSyncedTick = lastGameTick 573 | 574 | -- Applied the remote player's input, so this game frame should synced. 575 | Game:StoreState() 576 | 577 | -- Confirm the game clients are in sync 578 | Game:SyncCheck() 579 | 580 | end 581 | 582 | end 583 | end 584 | 585 | 586 | -- Since our input is update in Game:Update() we want to send the input as soon as possible. 587 | -- Previously this as happening before the Game:Update() and adding uneeded latency. 588 | if Network.enabled and Network.connectedToClient then 589 | 590 | -- if updateGame then 591 | -- PacketLog("Sending Input: " .. Network:GetLocalInputEncoded(lastGameTick + Network.inputDelay) .. ' @ ' .. lastGameTick + Network.inputDelay ) 592 | -- end 593 | 594 | -- Send this player's input state. We when Network.inputDelay frames ahead. 595 | -- Note: This input comes from the last game update, so we subtract 1 to set the correct tick. 596 | Network:SendInputData(Game.tick - 1 + Network.inputDelay) 597 | 598 | 599 | -- Send ping so we can test network latency. 600 | Network:SendPingMessage() 601 | 602 | end 603 | end 604 | 605 | function love.draw() 606 | Game:Draw() 607 | end 608 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcmagic/DemoFighterWithNetcode/154b03af9b35e3cef970f96fb7858a212bf756a8/screenshot.png --------------------------------------------------------------------------------