17_SceneReplication.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. -- Scene network replication example.
  2. -- This sample demonstrates:
  3. -- - Creating a scene in which network clients can join
  4. -- - Giving each client an object to control and sending the controls from the clients to the server,
  5. -- where the authoritative simulation happens
  6. -- - Controlling a physics object's movement by applying forces
  7. require "LuaScripts/Utilities/Sample"
  8. -- UDP port we will use
  9. local SERVER_PORT = 2345
  10. -- Control bits we define
  11. local CTRL_FORWARD = 1
  12. local CTRL_BACK = 2
  13. local CTRL_LEFT = 4
  14. local CTRL_RIGHT = 8
  15. local scene_ = nil
  16. local cameraNode = nil
  17. local instructionsText = nil
  18. local buttonContainer = nil
  19. local textEdit = nil
  20. local connectButton = nil
  21. local disconnectButton = nil
  22. local startServerButton = nil
  23. local clients = {}
  24. local yaw = 0.0
  25. local pitch = 1.0
  26. local clientObjectID = 0
  27. function Start()
  28. -- Execute the common startup for samples
  29. SampleStart()
  30. -- Create the scene content
  31. CreateScene()
  32. -- Create the UI content
  33. CreateUI()
  34. -- Setup the viewport for displaying the scene
  35. SetupViewport()
  36. -- Hook up to necessary events
  37. SubscribeToEvents()
  38. end
  39. function CreateScene()
  40. scene_ = Scene()
  41. -- Create octree and physics world with default settings. Create them as local so that they are not needlessly replicated
  42. -- when a client connects
  43. scene_:CreateComponent("Octree", LOCAL)
  44. scene_:CreateComponent("PhysicsWorld", LOCAL)
  45. -- All static scene content and the camera are also created as local, so that they are unaffected by scene replication and are
  46. -- not removed from the client upon connection. Create a Zone component first for ambient lighting & fog control.
  47. local zoneNode = scene_:CreateChild("Zone", LOCAL)
  48. local zone = zoneNode:CreateComponent("Zone")
  49. zone.boundingBox = BoundingBox(-1000.0, 1000.0)
  50. zone.ambientColor = Color(0.1, 0.1, 0.1)
  51. zone.fogStart = 100.0
  52. zone.fogEnd = 300.0
  53. -- Create a directional light without shadows
  54. local lightNode = scene_:CreateChild("DirectionalLight", LOCAL)
  55. lightNode.direction = Vector3(0.5, -1.0, 0.5)
  56. local light = lightNode:CreateComponent("Light")
  57. light.lightType = LIGHT_DIRECTIONAL
  58. light.color = Color(0.2, 0.2, 0.2)
  59. light.specularIntensity = 1.0
  60. -- Create a "floor" consisting of several tiles. Make the tiles physical but leave small cracks between them
  61. for y = -20, 20 do
  62. for x = -20, 20 do
  63. local floorNode = scene_:CreateChild("FloorTile", LOCAL)
  64. floorNode.position = Vector3(x * 20.2, -0.5, y * 20.2)
  65. floorNode.scale = Vector3(20.0, 1.0, 20.0)
  66. local floorObject = floorNode:CreateComponent("StaticModel")
  67. floorObject.model = cache:GetResource("Model", "Models/Box.mdl")
  68. floorObject.material = cache:GetResource("Material", "Materials/Stone.xml")
  69. local body = floorNode:CreateComponent("RigidBody")
  70. body.friction = 1.0
  71. local shape = floorNode:CreateComponent("CollisionShape")
  72. shape:SetBox(Vector3(1.0, 1.0, 1.0))
  73. end
  74. end
  75. -- Create the camera. Limit far clip distance to match the fog
  76. cameraNode = scene_:CreateChild("Camera", LOCAL)
  77. local camera = cameraNode:CreateComponent("Camera")
  78. camera.farClip = 300.0
  79. -- Set an initial position for the camera scene node above the plane
  80. cameraNode.position = Vector3(0.0, 5.0, 0.0)
  81. end
  82. function CreateUI()
  83. local uiStyle = cache:GetResource("XMLFile", "UI/DefaultStyle.xml")
  84. -- Set style to the UI root so that elements will inherit it
  85. ui.root.defaultStyle = uiStyle
  86. -- Create a Cursor UI element because we want to be able to hide and show it at will. When hidden, the mouse cursor will
  87. -- control the camera, and when visible, it will point the raycast target
  88. local cursor = ui.root:CreateChild("Cursor")
  89. cursor:SetStyleAuto(uiStyle)
  90. ui.cursor = cursor
  91. -- Set starting position of the cursor at the rendering window center
  92. cursor:SetPosition(graphics.width / 2, graphics.height / 2)
  93. -- Construct the instructions text element
  94. instructionsText = ui.root:CreateChild("Text")
  95. instructionsText:SetText("Use WASD keys to move and RMB to rotate view")
  96. instructionsText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
  97. -- Position the text relative to the screen center
  98. instructionsText.horizontalAlignment = HA_CENTER
  99. instructionsText.verticalAlignment = VA_CENTER
  100. instructionsText:SetPosition(0, graphics.height / 4)
  101. -- Hide until connected
  102. instructionsText.visible = false
  103. buttonContainer = ui.root:CreateChild("UIElement")
  104. buttonContainer:SetFixedSize(500, 20)
  105. buttonContainer:SetPosition(20, 20)
  106. buttonContainer.layoutMode = LM_HORIZONTAL
  107. textEdit = buttonContainer:CreateChild("LineEdit")
  108. textEdit:SetStyleAuto()
  109. connectButton = CreateButton("Connect", 90)
  110. disconnectButton = CreateButton("Disconnect", 100)
  111. startServerButton = CreateButton("Start Server", 110)
  112. UpdateButtons()
  113. end
  114. function SetupViewport()
  115. -- Set up a viewport to the Renderer subsystem so that the 3D scene can be seen
  116. local viewport = Viewport:new(scene_, cameraNode:GetComponent("Camera"))
  117. renderer:SetViewport(0, viewport)
  118. end
  119. function SubscribeToEvents()
  120. -- Subscribe to fixed timestep physics updates for setting or applying controls
  121. SubscribeToEvent("PhysicsPreStep", "HandlePhysicsPreStep")
  122. -- Subscribe HandlePostUpdate() method for processing update events. Subscribe to PostUpdate instead
  123. -- of the usual Update so that physics simulation has already proceeded for the frame, and can
  124. -- accurately follow the object with the camera
  125. SubscribeToEvent("PostUpdate", "HandlePostUpdate")
  126. -- Subscribe to button actions
  127. SubscribeToEvent(connectButton, "Released", "HandleConnect")
  128. SubscribeToEvent(disconnectButton, "Released", "HandleDisconnect")
  129. SubscribeToEvent(startServerButton, "Released", "HandleStartServer")
  130. -- Subscribe to network events
  131. SubscribeToEvent("ServerConnected", "HandleConnectionStatus")
  132. SubscribeToEvent("ServerDisconnected", "HandleConnectionStatus")
  133. SubscribeToEvent("ConnectFailed", "HandleConnectionStatus")
  134. SubscribeToEvent("ClientConnected", "HandleClientConnected")
  135. SubscribeToEvent("ClientDisconnected", "HandleClientDisconnected")
  136. -- This is a custom event, sent from the server to the client. It tells the node ID of the object the client should control
  137. SubscribeToEvent("ClientObjectID", "HandleClientObjectID")
  138. end
  139. function CreateButton(text, width)
  140. local font = cache:GetResource("Font", "Fonts/Anonymous Pro.ttf")
  141. local button = buttonContainer:CreateChild("Button")
  142. button:SetStyleAuto()
  143. button:SetFixedWidth(width)
  144. local buttonText = button:CreateChild("Text")
  145. buttonText:SetFont(font, 12)
  146. buttonText:SetAlignment(HA_CENTER, VA_CENTER)
  147. buttonText:SetText(text)
  148. return button
  149. end
  150. function UpdateButtons()
  151. local serverConnection = network:GetServerConnection()
  152. local serverRunning = network.serverRunning
  153. -- Show and hide buttons so that eg. Connect and Disconnect are never shown at the same time
  154. connectButton.visible = serverConnection == nil and not serverRunning
  155. disconnectButton.visible = serverConnection ~= nil or serverRunning
  156. startServerButton.visible = serverConnection == nil and not serverRunning
  157. textEdit.visible = serverConnection == nil and not serverRunning
  158. end
  159. function CreateControllableObject()
  160. -- Create the scene node & visual representation. This will be a replicated object
  161. local ballNode = scene_:CreateChild("Ball")
  162. ballNode.position = Vector3(Random(40.0) - 20.0, 5.0, Random(40.0) - 20.0)
  163. ballNode:SetScale(0.5)
  164. local ballObject = ballNode:CreateComponent("StaticModel")
  165. ballObject.model = cache:GetResource("Model", "Models/Sphere.mdl")
  166. ballObject.material = cache:GetResource("Material", "Materials/StoneSmall.xml")
  167. -- Create the physics components
  168. local body = ballNode:CreateComponent("RigidBody")
  169. body.mass = 1.0
  170. body.friction = 1.0
  171. -- In addition to friction, use motion damping so that the ball can not accelerate limitlessly
  172. body.linearDamping = 0.5
  173. body.angularDamping = 0.5
  174. local shape = ballNode:CreateComponent("CollisionShape")
  175. shape:SetSphere(1.0)
  176. -- Create a random colored point light at the ball so that can see better where is going
  177. local light = ballNode:CreateComponent("Light")
  178. light.range = 3.0
  179. light.color = Color(0.5 + RandomInt(2) * 0.5, 0.5 + RandomInt(2) * 0.5, 0.5 + RandomInt(2) * 0.5)
  180. return ballNode
  181. end
  182. function MoveCamera()
  183. -- Right mouse button controls mouse cursor visibility: hide when pressed
  184. ui.cursor.visible = not input:GetMouseButtonDown(MOUSEB_RIGHT)
  185. -- Mouse sensitivity as degrees per pixel
  186. local MOUSE_SENSITIVITY = 0.1
  187. -- Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch and only move the camera
  188. -- when the cursor is hidden
  189. if not ui.cursor.visible then
  190. local mouseMove = input.mouseMove
  191. yaw = yaw + MOUSE_SENSITIVITY * mouseMove.x
  192. pitch = pitch + MOUSE_SENSITIVITY * mouseMove.y
  193. pitch = Clamp(pitch, 1.0, 90.0)
  194. end
  195. -- Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
  196. cameraNode.rotation = Quaternion(pitch, yaw, 0.0)
  197. -- Only move the camera / show instructions if we have a controllable object
  198. local showInstructions = false
  199. if clientObjectID ~= 0 then
  200. local ballNode = scene_:GetNode(clientObjectID)
  201. if ballNode ~= nil then
  202. local CAMERA_DISTANCE = 5.0
  203. -- Move camera some distance away from the ball
  204. cameraNode.position = ballNode.position + cameraNode.rotation * Vector3(0.0, 0.0, -1.0) * CAMERA_DISTANCE
  205. showInstructions = true
  206. end
  207. end
  208. instructionsText.visible = showInstructions
  209. end
  210. function HandlePostUpdate(eventType, eventData)
  211. -- We only rotate the camera according to mouse movement since last frame, so do not need the time step
  212. MoveCamera()
  213. end
  214. function HandlePhysicsPreStep(eventType, eventData)
  215. -- This function is different on the client and server. The client collects controls (WASD controls + yaw angle)
  216. -- and sets them to its server connection object, so that they will be sent to the server automatically at a
  217. -- fixed rate, by default 30 FPS. The server will actually apply the controls (authoritative simulation.)
  218. local serverConnection = network:GetServerConnection()
  219. -- Client: collect controls
  220. if serverConnection ~= nil then
  221. local controls = Controls()
  222. -- Copy mouse yaw
  223. controls.yaw = yaw
  224. -- Only apply WASD controls if there is no focused UI element
  225. if ui.focusElement == nil then
  226. controls:Set(CTRL_FORWARD, input:GetKeyDown(KEY_W))
  227. controls:Set(CTRL_BACK, input:GetKeyDown(KEY_S))
  228. controls:Set(CTRL_LEFT, input:GetKeyDown(KEY_A))
  229. controls:Set(CTRL_RIGHT, input:GetKeyDown(KEY_D))
  230. end
  231. serverConnection.controls = controls
  232. -- In case the server wants to do position-based interest management using the NetworkPriority components, we should also
  233. -- tell it our observer (camera) position. In this sample it is not in use, but eg. the NinjaSnowWar game uses it
  234. serverConnection.position = cameraNode.position
  235. -- Server: apply controls to client objects
  236. elseif network.serverRunning then
  237. for i, v in ipairs(clients) do
  238. local connection = v.connection
  239. -- Get the object this connection is controlling
  240. local ballNode = v.object
  241. local body = ballNode:GetComponent("RigidBody")
  242. -- Torque is relative to the forward vector
  243. local rotation = Quaternion(0.0, connection.controls.yaw, 0.0)
  244. local MOVE_TORQUE = 3.0
  245. -- Movement torque is applied before each simulation step, which happen at 60 FPS. This makes the simulation
  246. -- independent from rendering framerate. We could also apply forces (which would enable in-air control),
  247. -- but want to emphasize that it's a ball which should only control its motion by rolling along the ground
  248. if connection.controls:IsDown(CTRL_FORWARD) then
  249. body:ApplyTorque(rotation * Vector3(1.0, 0.0, 0.0) * MOVE_TORQUE)
  250. end
  251. if connection.controls:IsDown(CTRL_BACK) then
  252. body:ApplyTorque(rotation * Vector3(-1.0, 0.0, 0.0) * MOVE_TORQUE)
  253. end
  254. if connection.controls:IsDown(CTRL_LEFT) then
  255. body:ApplyTorque(rotation * Vector3(0.0, 0.0, 1.0) * MOVE_TORQUE)
  256. end
  257. if connection.controls:IsDown(CTRL_RIGHT) then
  258. body:ApplyTorque(rotation * Vector3(0.0, 0.0, -1.0) * MOVE_TORQUE)
  259. end
  260. end
  261. end
  262. end
  263. function HandleConnect(eventType, eventData)
  264. local address = textEdit.text
  265. if address == "" then
  266. address = "localhost" -- Use localhost to connect if nothing else specified
  267. end
  268. -- Connect to server, specify scene to use as a client for replication
  269. clientObjectID = 0 -- Reset own object ID from possible previous connection
  270. network:Connect(address, SERVER_PORT, scene_)
  271. UpdateButtons()
  272. end
  273. function HandleDisconnect(eventType, eventData)
  274. local serverConnection = network.serverConnection
  275. -- If we were connected to server, disconnect. Or if we were running a server, stop it. In both cases clear the
  276. -- scene of all replicated content, but let the local nodes & components (the static world + camera) stay
  277. if serverConnection ~= nil then
  278. serverConnection:Disconnect()
  279. scene_:Clear(true, false)
  280. clientObjectID = 0
  281. -- Or if we were running a server, stop it
  282. elseif network.serverRunning then
  283. network:StopServer()
  284. scene_:Clear(true, false)
  285. end
  286. UpdateButtons()
  287. end
  288. function HandleStartServer(eventType, eventData)
  289. network:StartServer(SERVER_PORT)
  290. UpdateButtons()
  291. end
  292. function HandleConnectionStatus(eventType, eventData)
  293. UpdateButtons()
  294. end
  295. function HandleClientConnected(eventType, eventData)
  296. -- When a client connects, assign to scene to begin scene replication
  297. local newConnection = eventData:GetPtr("Connection", "Connection")
  298. newConnection.scene = scene_
  299. -- Then create a controllable object for that client
  300. local newObject = CreateControllableObject()
  301. local newClient = {}
  302. newClient.connection = newConnection
  303. newClient.object = newObject
  304. table.insert(clients, newClient)
  305. -- Finally send the object's node ID using a remote event
  306. local remoteEventData = VariantMap()
  307. remoteEventData:SetInt("ID", newObject.ID)
  308. newConnection:SendRemoteEvent("ClientObjectID", true, remoteEventData)
  309. end
  310. function HandleClientDisconnected(eventType, eventData)
  311. -- When a client disconnects, remove the controlled object
  312. local connection = eventData:GetPtr("Connection", "Connection")
  313. for i, v in ipairs(clients) do
  314. if v.connection == connection then
  315. v.object:Remove()
  316. table.remove(clients, i)
  317. break
  318. end
  319. end
  320. end
  321. function HandleClientObjectID(eventType, eventData)
  322. clientObjectID = eventData:GetUInt("ID")
  323. end