| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- -- 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 instructionsText = nil
- local buttonContainer = nil
- local textEdit = nil
- local connectButton = nil
- local disconnectButton = nil
- local startServerButton = nil
- local clients = {}
- local clientObjectID = 0
- 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()
- -- 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
- -- The camera needs to be created into a local node so that each client can retain its own camera, that is unaffected by
- -- network messages. Furthermore, because the client removes all replicated scene nodes when connecting to a server scene,
- -- the screen would become blank if the camera node was replicated (as only the locally created camera is assigned to a
- -- viewport in SetupViewports() below)
- 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(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")
- -- Events sent between client & server (remote events) must be explicitly registered or else they are not allowed to be received
- network:RegisterRemoteEvent("ClientObjectID");
- 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
- -- Create XML patch instructions for screen joystick layout specific to this sample app
- function GetScreenJoystickPatchString()
- return
- "<patch>" ..
- " <add sel=\"/element/element[./attribute[@name='Name' and @value='Hat0']]\">" ..
- " <attribute name=\"Is Visible\" value=\"false\" />" ..
- " </add>" ..
- "</patch>"
- end
|