17_SceneReplication.lua 17 KB

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