17_SceneReplication.as 17 KB

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