17_SceneReplication.lua 16 KB

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