17_SceneReplication.as 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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. Scene@ scene_;
  22. Node@ cameraNode;
  23. Text@ instructionsText;
  24. UIElement@ buttonContainer;
  25. LineEdit@ textEdit;
  26. Button@ connectButton;
  27. Button@ disconnectButton;
  28. Button@ startServerButton;
  29. Array<Client> clients;
  30. float yaw = 0.0f;
  31. float pitch = 1.0f;
  32. uint clientObjectID = 0;
  33. void Start()
  34. {
  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. }
  46. void CreateScene()
  47. {
  48. scene_ = Scene();
  49. // Create octree and physics world with default settings. Create them as local so that they are not needlessly replicated
  50. // when a client connects
  51. scene_.CreateComponent("Octree", LOCAL);
  52. scene_.CreateComponent("PhysicsWorld", LOCAL);
  53. // All static scene content and the camera are also created as local, so that they are unaffected by scene replication and are
  54. // not removed from the client upon connection. Create a Zone component first for ambient lighting & fog control.
  55. Node@ zoneNode = scene_.CreateChild("Zone", LOCAL);
  56. Zone@ zone = zoneNode.CreateComponent("Zone");
  57. zone.boundingBox = BoundingBox(-1000.0f, 1000.0f);
  58. zone.ambientColor = Color(0.1f, 0.1f, 0.1f);
  59. zone.fogStart = 100.0f;
  60. zone.fogEnd = 300.0f;
  61. // Create a directional light without shadows
  62. Node@ lightNode = scene_.CreateChild("DirectionalLight", LOCAL);
  63. lightNode.direction = Vector3(0.5f, -1.0f, 0.5f);
  64. Light@ light = lightNode.CreateComponent("Light");
  65. light.lightType = LIGHT_DIRECTIONAL;
  66. light.color = Color(0.2f, 0.2f, 0.2f);
  67. light.specularIntensity = 1.0f;
  68. // Create a "floor" consisting of several tiles. Make the tiles physical but leave small cracks between them
  69. for (int y = -20; y <= 20; ++y)
  70. {
  71. for (int x = -20; x <= 20; ++x)
  72. {
  73. Node@ floorNode = scene_.CreateChild("FloorTile", LOCAL);
  74. floorNode.position = Vector3(x * 20.2f, -0.5f, y * 20.2f);
  75. floorNode.scale = Vector3(20.0f, 1.0f, 20.0f);
  76. StaticModel@ floorObject = floorNode.CreateComponent("StaticModel");
  77. floorObject.model = cache.GetResource("Model", "Models/Box.mdl");
  78. floorObject.material = cache.GetResource("Material", "Materials/Stone.xml");
  79. RigidBody@ body = floorNode.CreateComponent("RigidBody");
  80. body.friction = 1.0f;
  81. CollisionShape@ shape = floorNode.CreateComponent("CollisionShape");
  82. shape.SetBox(Vector3(1.0f, 1.0f, 1.0f));
  83. }
  84. }
  85. // Create the camera. Limit far clip distance to match the fog
  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. }
  152. Button@ CreateButton(const String& text, int width)
  153. {
  154. Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");
  155. Button@ button = buttonContainer.CreateChild("Button");
  156. button.SetStyleAuto();
  157. button.SetFixedWidth(width);
  158. Text@ buttonText = button.CreateChild("Text");
  159. buttonText.SetFont(font, 12);
  160. buttonText.SetAlignment(HA_CENTER, VA_CENTER);
  161. buttonText.text = text;
  162. return button;
  163. }
  164. void UpdateButtons()
  165. {
  166. Connection@ serverConnection = network.serverConnection;
  167. bool serverRunning = network.serverRunning;
  168. // Show and hide buttons so that eg. Connect and Disconnect are never shown at the same time
  169. connectButton.visible = serverConnection is null && !serverRunning;
  170. disconnectButton.visible = serverConnection !is null || serverRunning;
  171. startServerButton.visible = serverConnection is null && !serverRunning;
  172. textEdit.visible = serverConnection is null && !serverRunning;
  173. }
  174. Node@ CreateControllableObject()
  175. {
  176. // Create the scene node & visual representation. This will be a replicated object
  177. Node@ ballNode = scene_.CreateChild("Ball");
  178. ballNode.position = Vector3(Random(40.0f) - 20.0f, 5.0f, Random(40.0f) - 20.0f);
  179. ballNode.SetScale(0.5f);
  180. StaticModel@ ballObject = ballNode.CreateComponent("StaticModel");
  181. ballObject.model = cache.GetResource("Model", "Models/Sphere.mdl");
  182. ballObject.material = cache.GetResource("Material", "Materials/StoneSmall.xml");
  183. // Create the physics components
  184. RigidBody@ body = ballNode.CreateComponent("RigidBody");
  185. body.mass = 1.0f;
  186. body.friction = 1.0f;
  187. // In addition to friction, use motion damping so that the ball can not accelerate limitlessly
  188. body.linearDamping = 0.5f;
  189. body.angularDamping = 0.5f;
  190. CollisionShape@ shape = ballNode.CreateComponent("CollisionShape");
  191. shape.SetSphere(1.0f);
  192. // Create a random colored point light at the ball so that can see better where is going
  193. Light@ light = ballNode.CreateComponent("Light");
  194. light.range = 3.0f;
  195. light.color = Color(0.5f + (RandomInt() & 1) * 0.5f, 0.5f + (RandomInt() & 1) * 0.5f, 0.5f + (RandomInt() & 1) * 0.5f);
  196. return ballNode;
  197. }
  198. void MoveCamera()
  199. {
  200. // Right mouse button controls mouse cursor visibility: hide when pressed
  201. ui.cursor.visible = !input.mouseButtonDown[MOUSEB_RIGHT];
  202. // Mouse sensitivity as degrees per pixel
  203. const float MOUSE_SENSITIVITY = 0.1f;
  204. // Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch and only move the camera
  205. // when the cursor is hidden
  206. if (!ui.cursor.visible)
  207. {
  208. IntVector2 mouseMove = input.mouseMove;
  209. yaw += MOUSE_SENSITIVITY * mouseMove.x;
  210. pitch += MOUSE_SENSITIVITY * mouseMove.y;
  211. pitch = Clamp(pitch, 1.0f, 90.0f);
  212. }
  213. // Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
  214. cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);
  215. // Only move the camera / show instructions if we have a controllable object
  216. bool showInstructions = false;
  217. if (clientObjectID != 0)
  218. {
  219. Node@ ballNode = scene_.GetNode(clientObjectID);
  220. if (ballNode !is null)
  221. {
  222. const float CAMERA_DISTANCE = 5.0f;
  223. // Move camera some distance away from the ball
  224. cameraNode.position = ballNode.position + cameraNode.rotation * Vector3(0.0f, 0.0f, -1.0f) * CAMERA_DISTANCE;
  225. showInstructions = true;
  226. }
  227. }
  228. instructionsText.visible = showInstructions;
  229. }
  230. void HandlePostUpdate(StringHash eventType, VariantMap& eventData)
  231. {
  232. // We only rotate the camera according to mouse movement since last frame, so do not need the time step
  233. MoveCamera();
  234. }
  235. void HandlePhysicsPreStep(StringHash eventType, VariantMap& eventData)
  236. {
  237. // This function is different on the client and server. The client collects controls (WASD controls + yaw angle)
  238. // and sets them to its server connection object, so that they will be sent to the server automatically at a
  239. // fixed rate, by default 30 FPS. The server will actually apply the controls (authoritative simulation.)
  240. Connection@ serverConnection = network.serverConnection;
  241. // Client: collect controls
  242. if (serverConnection !is null)
  243. {
  244. Controls controls;
  245. // Copy mouse yaw
  246. controls.yaw = yaw;
  247. // Only apply WASD controls if there is no focused UI element
  248. if (ui.focusElement is null)
  249. {
  250. controls.Set(CTRL_FORWARD, input.keyDown['W']);
  251. controls.Set(CTRL_BACK, input.keyDown['S']);
  252. controls.Set(CTRL_LEFT, input.keyDown['A']);
  253. controls.Set(CTRL_RIGHT, input.keyDown['D']);
  254. }
  255. serverConnection.controls = controls;
  256. // In case the server wants to do position-based interest management using the NetworkPriority components, we should also
  257. // tell it our observer (camera) position. In this sample it is not in use, but eg. the NinjaSnowWar game uses it
  258. serverConnection.position = cameraNode.position;
  259. }
  260. // Server: apply controls to client objects
  261. else if (network.serverRunning)
  262. {
  263. for (uint i = 0; i < clients.length; ++i)
  264. {
  265. Connection@ connection = clients[i].connection;
  266. // Get the object this connection is controlling
  267. Node@ ballNode = clients[i].object;
  268. RigidBody@ body = ballNode.GetComponent("RigidBody");
  269. // Torque is relative to the forward vector
  270. Quaternion rotation(0.0f, connection.controls.yaw, 0.0f);
  271. const float MOVE_TORQUE = 3.0f;
  272. // Movement torque is applied before each simulation step, which happen at 60 FPS. This makes the simulation
  273. // independent from rendering framerate. We could also apply forces (which would enable in-air control),
  274. // but want to emphasize that it's a ball which should only control its motion by rolling along the ground
  275. if (connection.controls.IsDown(CTRL_FORWARD))
  276. body.ApplyTorque(rotation * Vector3(1.0f, 0.0f, 0.0f) * MOVE_TORQUE);
  277. if (connection.controls.IsDown(CTRL_BACK))
  278. body.ApplyTorque(rotation * Vector3(-1.0f, 0.0f, 0.0f) * MOVE_TORQUE);
  279. if (connection.controls.IsDown(CTRL_LEFT))
  280. body.ApplyTorque(rotation * Vector3(0.0f, 0.0f, 1.0f) * MOVE_TORQUE);
  281. if (connection.controls.IsDown(CTRL_RIGHT))
  282. body.ApplyTorque(rotation * Vector3(0.0f, 0.0f, -1.0f) * MOVE_TORQUE);
  283. }
  284. }
  285. }
  286. void HandleConnect(StringHash eventType, VariantMap& eventData)
  287. {
  288. String address = textEdit.text.Trimmed();
  289. if (address.empty)
  290. address = "localhost"; // Use localhost to connect if nothing else specified
  291. // Connect to server, specify scene to use as a client for replication
  292. clientObjectID = 0; // Reset own object ID from possible previous connection
  293. network.Connect(address, SERVER_PORT, scene_);
  294. UpdateButtons();
  295. }
  296. void HandleDisconnect(StringHash eventType, VariantMap& eventData)
  297. {
  298. Connection@ serverConnection = network.serverConnection;
  299. // If we were connected to server, disconnect. Or if we were running a server, stop it. In both cases clear the
  300. // scene of all replicated content, but let the local nodes & components (the static world + camera) stay
  301. if (serverConnection !is null)
  302. {
  303. serverConnection.Disconnect();
  304. scene_.Clear(true, false);
  305. clientObjectID = 0;
  306. }
  307. // Or if we were running a server, stop it
  308. else if (network.serverRunning)
  309. {
  310. network.StopServer();
  311. scene_.Clear(true, false);
  312. }
  313. UpdateButtons();
  314. }
  315. void HandleStartServer(StringHash eventType, VariantMap& eventData)
  316. {
  317. network.StartServer(SERVER_PORT);
  318. UpdateButtons();
  319. }
  320. void HandleConnectionStatus(StringHash eventType, VariantMap& eventData)
  321. {
  322. UpdateButtons();
  323. }
  324. void HandleClientConnected(StringHash eventType, VariantMap& eventData)
  325. {
  326. // When a client connects, assign to scene to begin scene replication
  327. Connection@ newConnection = eventData["Connection"].GetConnection();
  328. newConnection.scene = scene_;
  329. // Then create a controllable object for that client
  330. Node@ newObject = CreateControllableObject();
  331. Client newClient;
  332. newClient.connection = newConnection;
  333. newClient.object = newObject;
  334. clients.Push(newClient);
  335. // Finally send the object's node ID using a remote event
  336. VariantMap remoteEventData;
  337. remoteEventData["ID"] = newObject.id;
  338. newConnection.SendRemoteEvent("ClientObjectID", true, remoteEventData);
  339. }
  340. void HandleClientDisconnected(StringHash eventType, VariantMap& eventData)
  341. {
  342. // When a client disconnects, remove the controlled object
  343. Connection@ connection = eventData["Connection"].GetConnection();
  344. for (uint i = 0; i < clients.length; ++i)
  345. {
  346. if (clients[i].connection is connection)
  347. {
  348. clients[i].object.Remove();
  349. clients.Erase(i);
  350. break;
  351. }
  352. }
  353. }
  354. void HandleClientObjectID(StringHash eventType, VariantMap& eventData)
  355. {
  356. clientObjectID = eventData["ID"].GetUInt();
  357. }