17_SceneReplication.as 17 KB

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