17_SceneReplication.lua 16 KB

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