|
@@ -0,0 +1,403 @@
|
|
|
|
|
+-- Scene network replication example.
|
|
|
|
|
+-- This sample demonstrates:
|
|
|
|
|
+-- - Creating a scene in which network clients can join
|
|
|
|
|
+-- - Giving each client an object to control and sending the controls from the clients to the server,
|
|
|
|
|
+-- where the authoritative simulation happens
|
|
|
|
|
+-- - Controlling a physics object's movement by applying forces
|
|
|
|
|
+
|
|
|
|
|
+require "LuaScripts/Utilities/Sample"
|
|
|
|
|
+
|
|
|
|
|
+-- UDP port we will use
|
|
|
|
|
+local SERVER_PORT = 2345
|
|
|
|
|
+
|
|
|
|
|
+-- Control bits we define
|
|
|
|
|
+local CTRL_FORWARD = 1
|
|
|
|
|
+local CTRL_BACK = 2
|
|
|
|
|
+local CTRL_LEFT = 4
|
|
|
|
|
+local CTRL_RIGHT = 8
|
|
|
|
|
+
|
|
|
|
|
+local scene_
|
|
|
|
|
+local cameraNode
|
|
|
|
|
+local instructionsText
|
|
|
|
|
+local buttonContainer
|
|
|
|
|
+local textEdit
|
|
|
|
|
+local connectButton
|
|
|
|
|
+local disconnectButton
|
|
|
|
|
+local startServerButton
|
|
|
|
|
+local clients = {}
|
|
|
|
|
+local yaw = 0.0
|
|
|
|
|
+local pitch = 1.0
|
|
|
|
|
+local clientObjectID = 0
|
|
|
|
|
+
|
|
|
|
|
+local context = GetContext()
|
|
|
|
|
+
|
|
|
|
|
+local cache = GetCache()
|
|
|
|
|
+local input = GetInput()
|
|
|
|
|
+local graphics = GetGraphics()
|
|
|
|
|
+local network = GetNetwork()
|
|
|
|
|
+local renderer = GetRenderer()
|
|
|
|
|
+local ui = GetUI()
|
|
|
|
|
+
|
|
|
|
|
+function Start()
|
|
|
|
|
+ -- Execute the common startup for samples
|
|
|
|
|
+ SampleStart()
|
|
|
|
|
+
|
|
|
|
|
+ -- Create the scene content
|
|
|
|
|
+ CreateScene()
|
|
|
|
|
+
|
|
|
|
|
+ -- Create the UI content
|
|
|
|
|
+ CreateUI()
|
|
|
|
|
+
|
|
|
|
|
+ -- Setup the viewport for displaying the scene
|
|
|
|
|
+ SetupViewport()
|
|
|
|
|
+
|
|
|
|
|
+ -- Hook up to necessary events
|
|
|
|
|
+ SubscribeToEvents()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function CreateScene()
|
|
|
|
|
+ scene_ = Scene(context)
|
|
|
|
|
+
|
|
|
|
|
+ -- Create octree and physics world with default settings. Create them as local so that they are not needlessly replicated
|
|
|
|
|
+ -- when a client connects
|
|
|
|
|
+ scene_:CreateComponent("Octree", LOCAL)
|
|
|
|
|
+ scene_:CreateComponent("PhysicsWorld", LOCAL)
|
|
|
|
|
+
|
|
|
|
|
+ -- All static scene content and the camera are also created as local, so that they are unaffected by scene replication and are
|
|
|
|
|
+ -- not removed from the client upon connection. Create a Zone component first for ambient lighting & fog control.
|
|
|
|
|
+ local zoneNode = scene_:CreateChild("Zone", LOCAL)
|
|
|
|
|
+ local zone = zoneNode:CreateComponent("Zone")
|
|
|
|
|
+ zone.boundingBox = BoundingBox(-1000.0, 1000.0)
|
|
|
|
|
+ zone.ambientColor = Color(0.1, 0.1, 0.1)
|
|
|
|
|
+ zone.fogStart = 100.0
|
|
|
|
|
+ zone.fogEnd = 300.0
|
|
|
|
|
+
|
|
|
|
|
+ -- Create a directional light without shadows
|
|
|
|
|
+ local lightNode = scene_:CreateChild("DirectionalLight", LOCAL)
|
|
|
|
|
+ lightNode.direction = Vector3(0.5, -1.0, 0.5)
|
|
|
|
|
+ local light = lightNode:CreateComponent("Light")
|
|
|
|
|
+ light.lightType = LIGHT_DIRECTIONAL
|
|
|
|
|
+ light.color = Color(0.2, 0.2, 0.2)
|
|
|
|
|
+ light.specularIntensity = 1.0
|
|
|
|
|
+
|
|
|
|
|
+ -- Create a "floor" consisting of several tiles. Make the tiles physical but leave small cracks between them
|
|
|
|
|
+ for y = -20, 20 do
|
|
|
|
|
+ for x = -20, 20 do
|
|
|
|
|
+ local floorNode = scene_:CreateChild("FloorTile", LOCAL)
|
|
|
|
|
+ floorNode.position = Vector3(x * 20.2, -0.5, y * 20.2)
|
|
|
|
|
+ floorNode.scale = Vector3(20.0, 1.0, 20.0)
|
|
|
|
|
+ local floorObject = floorNode:CreateComponent("StaticModel")
|
|
|
|
|
+ floorObject.model = cache:GetResource("Model", "Models/Box.mdl")
|
|
|
|
|
+ floorObject.material = cache:GetResource("Material", "Materials/Stone.xml")
|
|
|
|
|
+
|
|
|
|
|
+ local body = floorNode:CreateComponent("RigidBody")
|
|
|
|
|
+ body.friction = 1.0
|
|
|
|
|
+ local shape = floorNode:CreateComponent("CollisionShape")
|
|
|
|
|
+ shape:SetBox(Vector3(1.0, 1.0, 1.0))
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ -- Create the camera. Limit far clip distance to match the fog
|
|
|
|
|
+ cameraNode = scene_:CreateChild("Camera", LOCAL)
|
|
|
|
|
+ local camera = cameraNode:CreateComponent("Camera")
|
|
|
|
|
+ camera.farClip = 300.0
|
|
|
|
|
+
|
|
|
|
|
+ -- Set an initial position for the camera scene node above the plane
|
|
|
|
|
+ cameraNode.position = Vector3(0.0, 5.0, 0.0)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function CreateUI()
|
|
|
|
|
+ local uiStyle = cache:GetResource("XMLFile", "UI/DefaultStyle.xml")
|
|
|
|
|
+ -- Set style to the UI root so that elements will inherit it
|
|
|
|
|
+ ui.root.defaultStyle = uiStyle
|
|
|
|
|
+
|
|
|
|
|
+ -- Create a Cursor UI element because we want to be able to hide and show it at will. When hidden, the mouse cursor will
|
|
|
|
|
+ -- control the camera, and when visible, it will point the raycast target
|
|
|
|
|
+ local cursor = ui.root:CreateChild("Cursor")
|
|
|
|
|
+ cursor:SetStyleAuto(uiStyle)
|
|
|
|
|
+ ui.cursor = cursor
|
|
|
|
|
+ -- Set starting position of the cursor at the rendering window center
|
|
|
|
|
+ cursor:SetPosition(graphics.width / 2, graphics.height / 2)
|
|
|
|
|
+
|
|
|
|
|
+ -- Construct the instructions text element
|
|
|
|
|
+ instructionsText = ui.root:CreateChild("Text")
|
|
|
|
|
+ instructionsText:SetText("Use WASD keys to move and RMB to rotate view")
|
|
|
|
|
+ instructionsText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
|
|
|
|
|
+ -- Position the text relative to the screen center
|
|
|
|
|
+ instructionsText.horizontalAlignment = HA_CENTER
|
|
|
|
|
+ instructionsText.verticalAlignment = VA_CENTER
|
|
|
|
|
+ instructionsText:SetPosition(0, graphics.height / 4)
|
|
|
|
|
+ -- Hide until connected
|
|
|
|
|
+ instructionsText.visible = false
|
|
|
|
|
+
|
|
|
|
|
+ buttonContainer = ui.root:CreateChild("UIElement")
|
|
|
|
|
+ buttonContainer:SetFixedSize(500, 20)
|
|
|
|
|
+ buttonContainer:SetPosition(20, 20)
|
|
|
|
|
+ buttonContainer.layoutMode = LM_HORIZONTAL
|
|
|
|
|
+
|
|
|
|
|
+ textEdit = buttonContainer:CreateChild("LineEdit")
|
|
|
|
|
+ textEdit:SetStyleAuto()
|
|
|
|
|
+
|
|
|
|
|
+ connectButton = CreateButton("Connect", 90)
|
|
|
|
|
+ disconnectButton = CreateButton("Disconnect", 100)
|
|
|
|
|
+ startServerButton = CreateButton("Start Server", 110)
|
|
|
|
|
+
|
|
|
|
|
+ UpdateButtons()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function SetupViewport()
|
|
|
|
|
+ -- Set up a viewport to the Renderer subsystem so that the 3D scene can be seen
|
|
|
|
|
+ local viewport = Viewport:new(context, scene_, cameraNode:GetComponent("Camera"))
|
|
|
|
|
+ renderer:SetViewport(0, viewport)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function SubscribeToEvents()
|
|
|
|
|
+ -- Subscribe to fixed timestep physics updates for setting or applying controls
|
|
|
|
|
+ SubscribeToEvent("PhysicsPreStep", "HandlePhysicsPreStep")
|
|
|
|
|
+
|
|
|
|
|
+ -- Subscribe HandlePostUpdate() method for processing update events. Subscribe to PostUpdate instead
|
|
|
|
|
+ -- of the usual Update so that physics simulation has already proceeded for the frame, and can
|
|
|
|
|
+ -- accurately follow the object with the camera
|
|
|
|
|
+ SubscribeToEvent("PostUpdate", "HandlePostUpdate")
|
|
|
|
|
+
|
|
|
|
|
+ -- Subscribe to button actions
|
|
|
|
|
+ SubscribeToEvent(connectButton, "Released", "HandleConnect")
|
|
|
|
|
+ SubscribeToEvent(disconnectButton, "Released", "HandleDisconnect")
|
|
|
|
|
+ SubscribeToEvent(startServerButton, "Released", "HandleStartServer")
|
|
|
|
|
+
|
|
|
|
|
+ -- Subscribe to network events
|
|
|
|
|
+ SubscribeToEvent("ServerConnected", "HandleConnectionStatus")
|
|
|
|
|
+ SubscribeToEvent("ServerDisconnected", "HandleConnectionStatus")
|
|
|
|
|
+ SubscribeToEvent("ConnectFailed", "HandleConnectionStatus")
|
|
|
|
|
+ SubscribeToEvent("ClientConnected", "HandleClientConnected")
|
|
|
|
|
+ SubscribeToEvent("ClientDisconnected", "HandleClientDisconnected")
|
|
|
|
|
+ -- This is a custom event, sent from the server to the client. It tells the node ID of the object the client should control
|
|
|
|
|
+ SubscribeToEvent("ClientObjectID", "HandleClientObjectID")
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function CreateButton(text, width)
|
|
|
|
|
+ local font = cache:GetResource("Font", "Fonts/Anonymous Pro.ttf")
|
|
|
|
|
+
|
|
|
|
|
+ local button = buttonContainer:CreateChild("Button")
|
|
|
|
|
+ button:SetStyleAuto()
|
|
|
|
|
+ button:SetFixedWidth(width)
|
|
|
|
|
+
|
|
|
|
|
+ local buttonText = button:CreateChild("Text")
|
|
|
|
|
+ buttonText:SetFont(font, 12)
|
|
|
|
|
+ buttonText:SetAlignment(HA_CENTER, VA_CENTER)
|
|
|
|
|
+ buttonText:SetText(text)
|
|
|
|
|
+
|
|
|
|
|
+ return button
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function UpdateButtons()
|
|
|
|
|
+ local serverConnection = network:GetServerConnection()
|
|
|
|
|
+ local serverRunning = network.serverRunning
|
|
|
|
|
+
|
|
|
|
|
+ -- Show and hide buttons so that eg. Connect and Disconnect are never shown at the same time
|
|
|
|
|
+ connectButton.visible = serverConnection == nil and not serverRunning
|
|
|
|
|
+ disconnectButton.visible = serverConnection ~= nil or serverRunning
|
|
|
|
|
+ startServerButton.visible = serverConnection == nil and not serverRunning
|
|
|
|
|
+ textEdit.visible = serverConnection == nil and not serverRunning
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function CreateControllableObject()
|
|
|
|
|
+ -- Create the scene node & visual representation. This will be a replicated object
|
|
|
|
|
+ local ballNode = scene_:CreateChild("Ball")
|
|
|
|
|
+ ballNode.position = Vector3(Random(40.0) - 20.0, 5.0, Random(40.0) - 20.0)
|
|
|
|
|
+ ballNode:SetScale(0.5)
|
|
|
|
|
+ local ballObject = ballNode:CreateComponent("StaticModel")
|
|
|
|
|
+ ballObject.model = cache:GetResource("Model", "Models/Sphere.mdl")
|
|
|
|
|
+ ballObject.material = cache:GetResource("Material", "Materials/StoneSmall.xml")
|
|
|
|
|
+
|
|
|
|
|
+ -- Create the physics components
|
|
|
|
|
+ local body = ballNode:CreateComponent("RigidBody")
|
|
|
|
|
+ body.mass = 1.0
|
|
|
|
|
+ body.friction = 1.0
|
|
|
|
|
+ -- In addition to friction, use motion damping so that the ball can not accelerate limitlessly
|
|
|
|
|
+ body.linearDamping = 0.5
|
|
|
|
|
+ body.angularDamping = 0.5
|
|
|
|
|
+ local shape = ballNode:CreateComponent("CollisionShape")
|
|
|
|
|
+ shape:SetSphere(1.0)
|
|
|
|
|
+
|
|
|
|
|
+ -- Create a random colored point light at the ball so that can see better where is going
|
|
|
|
|
+ local light = ballNode:CreateComponent("Light")
|
|
|
|
|
+ light.range = 3.0
|
|
|
|
|
+ light.color = Color(0.5 + RandomInt(2) * 0.5, 0.5 + RandomInt(2) * 0.5, 0.5 + RandomInt(2) * 0.5)
|
|
|
|
|
+
|
|
|
|
|
+ return ballNode
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function MoveCamera()
|
|
|
|
|
+ -- Right mouse button controls mouse cursor visibility: hide when pressed
|
|
|
|
|
+ ui.cursor.visible = not input:GetMouseButtonDown(MOUSEB_RIGHT)
|
|
|
|
|
+
|
|
|
|
|
+ -- Mouse sensitivity as degrees per pixel
|
|
|
|
|
+ local MOUSE_SENSITIVITY = 0.1
|
|
|
|
|
+
|
|
|
|
|
+ -- Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch and only move the camera
|
|
|
|
|
+ -- when the cursor is hidden
|
|
|
|
|
+ if not ui.cursor.visible then
|
|
|
|
|
+ local mouseMove = input.mouseMove
|
|
|
|
|
+ yaw = yaw + MOUSE_SENSITIVITY * mouseMove.x
|
|
|
|
|
+ pitch = pitch + MOUSE_SENSITIVITY * mouseMove.y
|
|
|
|
|
+ pitch = Clamp(pitch, 1.0, 90.0)
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ -- Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
|
|
|
|
|
+ cameraNode.rotation = Quaternion(pitch, yaw, 0.0)
|
|
|
|
|
+
|
|
|
|
|
+ -- Only move the camera / show instructions if we have a controllable object
|
|
|
|
|
+ local showInstructions = false
|
|
|
|
|
+ if clientObjectID ~= 0 then
|
|
|
|
|
+ local ballNode = scene_:GetNode(clientObjectID)
|
|
|
|
|
+ if ballNode ~= nil then
|
|
|
|
|
+ local CAMERA_DISTANCE = 5.0
|
|
|
|
|
+
|
|
|
|
|
+ -- Move camera some distance away from the ball
|
|
|
|
|
+ cameraNode.position = ballNode.position + cameraNode.rotation * Vector3(0.0, 0.0, -1.0) * CAMERA_DISTANCE
|
|
|
|
|
+ showInstructions = true
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ instructionsText.visible = showInstructions
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandlePostUpdate(eventType, eventData)
|
|
|
|
|
+
|
|
|
|
|
+ -- We only rotate the camera according to mouse movement since last frame, so do not need the time step
|
|
|
|
|
+ MoveCamera()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandlePhysicsPreStep(eventType, eventData)
|
|
|
|
|
+
|
|
|
|
|
+ -- This function is different on the client and server. The client collects controls (WASD controls + yaw angle)
|
|
|
|
|
+ -- and sets them to its server connection object, so that they will be sent to the server automatically at a
|
|
|
|
|
+ -- fixed rate, by default 30 FPS. The server will actually apply the controls (authoritative simulation.)
|
|
|
|
|
+ local serverConnection = network:GetServerConnection()
|
|
|
|
|
+
|
|
|
|
|
+ -- Client: collect controls
|
|
|
|
|
+ if serverConnection ~= nil then
|
|
|
|
|
+ local controls = Controls()
|
|
|
|
|
+
|
|
|
|
|
+ -- Copy mouse yaw
|
|
|
|
|
+ controls.yaw = yaw
|
|
|
|
|
+
|
|
|
|
|
+ -- Only apply WASD controls if there is no focused UI element
|
|
|
|
|
+ if ui.focusElement == nil then
|
|
|
|
|
+ controls:Set(CTRL_FORWARD, input:GetKeyDown(KEY_W))
|
|
|
|
|
+ controls:Set(CTRL_BACK, input:GetKeyDown(KEY_S))
|
|
|
|
|
+ controls:Set(CTRL_LEFT, input:GetKeyDown(KEY_A))
|
|
|
|
|
+ controls:Set(CTRL_RIGHT, input:GetKeyDown(KEY_D))
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ serverConnection.controls = controls
|
|
|
|
|
+ -- In case the server wants to do position-based interest management using the NetworkPriority components, we should also
|
|
|
|
|
+ -- tell it our observer (camera) position. In this sample it is not in use, but eg. the NinjaSnowWar game uses it
|
|
|
|
|
+ serverConnection.position = cameraNode.position
|
|
|
|
|
+ -- Server: apply controls to client objects
|
|
|
|
|
+ elseif network.serverRunning then
|
|
|
|
|
+ for i, v in ipairs(clients) do
|
|
|
|
|
+ local connection = v.connection
|
|
|
|
|
+ -- Get the object this connection is controlling
|
|
|
|
|
+ local ballNode = v.object
|
|
|
|
|
+
|
|
|
|
|
+ local body = ballNode:GetComponent("RigidBody")
|
|
|
|
|
+
|
|
|
|
|
+ -- Torque is relative to the forward vector
|
|
|
|
|
+ local rotation = Quaternion(0.0, connection.controls.yaw, 0.0)
|
|
|
|
|
+
|
|
|
|
|
+ local MOVE_TORQUE = 3.0
|
|
|
|
|
+
|
|
|
|
|
+ -- Movement torque is applied before each simulation step, which happen at 60 FPS. This makes the simulation
|
|
|
|
|
+ -- independent from rendering framerate. We could also apply forces (which would enable in-air control),
|
|
|
|
|
+ -- but want to emphasize that it's a ball which should only control its motion by rolling along the ground
|
|
|
|
|
+ if connection.controls:IsDown(CTRL_FORWARD) then
|
|
|
|
|
+ body:ApplyTorque(rotation * Vector3(1.0, 0.0, 0.0) * MOVE_TORQUE)
|
|
|
|
|
+ end
|
|
|
|
|
+ if connection.controls:IsDown(CTRL_BACK) then
|
|
|
|
|
+ body:ApplyTorque(rotation * Vector3(-1.0, 0.0, 0.0) * MOVE_TORQUE)
|
|
|
|
|
+ end
|
|
|
|
|
+ if connection.controls:IsDown(CTRL_LEFT) then
|
|
|
|
|
+ body:ApplyTorque(rotation * Vector3(0.0, 0.0, 1.0) * MOVE_TORQUE)
|
|
|
|
|
+ end
|
|
|
|
|
+ if connection.controls:IsDown(CTRL_RIGHT) then
|
|
|
|
|
+ body:ApplyTorque(rotation * Vector3(0.0, 0.0, -1.0) * MOVE_TORQUE)
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandleConnect(eventType, eventData)
|
|
|
|
|
+ local address = textEdit.text
|
|
|
|
|
+ if address == "" then
|
|
|
|
|
+ address = "localhost" -- Use localhost to connect if nothing else specified
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ -- Connect to server, specify scene to use as a client for replication
|
|
|
|
|
+ clientObjectID = 0 -- Reset own object ID from possible previous connection
|
|
|
|
|
+ network:Connect(address, SERVER_PORT, scene_)
|
|
|
|
|
+
|
|
|
|
|
+ UpdateButtons()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandleDisconnect(eventType, eventData)
|
|
|
|
|
+ local serverConnection = network.serverConnection
|
|
|
|
|
+ -- If we were connected to server, disconnect. Or if we were running a server, stop it. In both cases clear the
|
|
|
|
|
+ -- scene of all replicated content, but let the local nodes & components (the static world + camera) stay
|
|
|
|
|
+ if serverConnection ~= nil then
|
|
|
|
|
+ serverConnection:Disconnect()
|
|
|
|
|
+ scene_:Clear(true, false)
|
|
|
|
|
+ clientObjectID = 0
|
|
|
|
|
+ -- Or if we were running a server, stop it
|
|
|
|
|
+ elseif network.serverRunning then
|
|
|
|
|
+ network:StopServer()
|
|
|
|
|
+ scene_:Clear(true, false)
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ UpdateButtons()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandleStartServer(eventType, eventData)
|
|
|
|
|
+ network:StartServer(SERVER_PORT)
|
|
|
|
|
+
|
|
|
|
|
+ UpdateButtons()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandleConnectionStatus(eventType, eventData)
|
|
|
|
|
+ UpdateButtons()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandleClientConnected(eventType, eventData)
|
|
|
|
|
+ -- When a client connects, assign to scene to begin scene replication
|
|
|
|
|
+ local newConnection = eventData:GetPtr("Connection", "Connection")
|
|
|
|
|
+ newConnection.scene = scene_
|
|
|
|
|
+
|
|
|
|
|
+ -- Then create a controllable object for that client
|
|
|
|
|
+ local newObject = CreateControllableObject()
|
|
|
|
|
+ local newClient = {}
|
|
|
|
|
+ newClient.connection = newConnection
|
|
|
|
|
+ newClient.object = newObject
|
|
|
|
|
+ table.insert(clients, newClient)
|
|
|
|
|
+
|
|
|
|
|
+ -- Finally send the object's node ID using a remote event
|
|
|
|
|
+ local remoteEventData = VariantMap()
|
|
|
|
|
+ remoteEventData:SetInt("ID", newObject.ID)
|
|
|
|
|
+ newConnection:SendRemoteEvent("ClientObjectID", true, remoteEventData)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandleClientDisconnected(eventType, eventData)
|
|
|
|
|
+ -- When a client disconnects, remove the controlled object
|
|
|
|
|
+ local connection = eventData:GetPtr("Connection", "Connection")
|
|
|
|
|
+ for i, v in ipairs(clients) do
|
|
|
|
|
+ if v.connection == connection then
|
|
|
|
|
+ v.object:Remove()
|
|
|
|
|
+ table.remove(clients, i)
|
|
|
|
|
+ break
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function HandleClientObjectID(eventType, eventData)
|
|
|
|
|
+ clientObjectID = eventData:GetUInt("ID")
|
|
|
|
|
+end
|