17_SceneReplication.lua 16 KB

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