Sfoglia il codice sorgente

Added SoundEffects, Navigation, Chat & SceneReplication samples in script.

Lasse Öörni 12 anni fa
parent
commit
9cbea3b331

+ 1 - 3
Bin/Data/Scripts/12_PhysicsStressTest.as

@@ -70,9 +70,7 @@ void CreateScene()
         floorObject.model = cache.GetResource("Model", "Models/Box.mdl");
         floorObject.material = cache.GetResource("Material", "Materials/StoneTiled.xml");
 
-        // Make the floor physical by adding RigidBody and CollisionShape components. The RigidBody's default
-        // parameters make the object static (zero mass.) Note that a CollisionShape by itself will not participate
-        // in the physics simulation
+        // Make the floor physical by adding RigidBody and CollisionShape components
         RigidBody@ body = floorNode.CreateComponent("RigidBody");
         CollisionShape@ shape = floorNode.CreateComponent("CollisionShape");
         // Set a box shape of size 1 x 1 x 1 for collision. The shape will be scaled with the scene node scale, so the

+ 4 - 3
Bin/Data/Scripts/13_Ragdolls.as

@@ -70,10 +70,11 @@ void CreateScene()
         floorObject.model = cache.GetResource("Model", "Models/Box.mdl");
         floorObject.material = cache.GetResource("Material", "Materials/StoneTiled.xml");
 
-        // Make the floor physical by adding RigidBody and CollisionShape components. The RigidBody's default
-        // parameters make the object static (zero mass.) Note that a CollisionShape by itself will not participate
-        // in the physics simulation
+        // Make the floor physical by adding RigidBody and CollisionShape components
         RigidBody@ body = floorNode.CreateComponent("RigidBody");
+        // We will be spawning spherical objects in this sample. The ground also needs non-zero rolling friction so that
+        // the spheres will eventually come to rest
+        body.rollingFriction = 0.15f;
         CollisionShape@ shape = floorNode.CreateComponent("CollisionShape");
         // Set a box shape of size 1 x 1 x 1 for collision. The shape will be scaled with the scene node scale, so the
         // rendering and physics representation sizes should match (the box model is also 1 x 1 x 1.)

+ 165 - 0
Bin/Data/Scripts/14_SoundEffects.as

@@ -0,0 +1,165 @@
+// Sound effects example
+// This sample demonstrates:
+//     - Playing sound effects and music;
+//     - Controlling sound and music master volume;
+
+#include "Utilities/Sample.as"
+
+Scene@ scene_;
+
+Array<String> soundNames = {
+    "Fist",
+    "Explosion",
+    "Power-up"
+};
+
+Array<String> soundResourceNames = {
+    "Sounds/PlayerFistHit.wav",
+    "Sounds/BigExplosion.wav",
+    "Sounds/Powerup.wav"
+};
+
+void Start()
+{
+    // Execute the common startup for samples
+    SampleStart();
+    
+    // Enable OS cursor
+    input.mouseVisible = true;
+
+    // Create the user interface
+    CreateUI();
+}
+
+void CreateUI()
+{
+    // Create a scene which will not be actually rendered, but is used to hold SoundSource components while they play sounds
+    scene_ = Scene();
+
+    XMLFile@ uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
+    // Set style to the UI root so that elements will inherit it
+    ui.root.defaultStyle = uiStyle;
+    
+    // Create buttons for playing back sounds
+    for (uint i = 0; i < soundNames.length; ++i)
+    {
+        Button@ button = CreateButton(i * 140 + 20, 20, 120, 40, soundNames[i]);
+        // Store the sound effect resource name as a custom variable into the button
+        button.vars["SoundResource"] = soundResourceNames[i];
+        SubscribeToEvent(button, "Pressed", "HandlePlaySound");
+    }
+    
+    // Create buttons for playing/stopping music
+    Button@ button = CreateButton(20, 80, 120, 40, "Play Music");
+    SubscribeToEvent(button, "Released", "HandlePlayMusic");
+    
+    button = CreateButton(160, 80, 120, 40, "Stop Music");
+    SubscribeToEvent(button, "Released", "HandleStopMusic");
+
+    // Create sliders for controlling sound and music master volume
+    Slider@ slider = CreateSlider(20, 140, 200, 20, "Sound Volume");
+    slider.value = audio.masterGain[SOUND_EFFECT];
+    SubscribeToEvent(slider, "SliderChanged", "HandleSoundVolume");
+    
+    slider = CreateSlider(20, 200, 200, 20, "Music Volume");
+    slider.value = audio.masterGain[SOUND_MUSIC];
+    SubscribeToEvent(slider, "SliderChanged", "HandleMusicVolume");
+}
+
+Button@ CreateButton(int x, int y, int xSize, int ySize, const String&in text)
+{
+    Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");
+    
+    // Create the button and center the text onto it
+    Button@ button = ui.root.CreateChild("Button");
+    button.SetStyleAuto();
+    button.SetPosition(x, y);
+    button.SetSize(xSize, ySize);
+    
+    Text@ buttonText = button.CreateChild("Text");
+    buttonText.SetAlignment(HA_CENTER, VA_CENTER);
+    buttonText.SetFont(font, 12);
+    buttonText.text = text;
+    
+    return button;
+}
+
+Slider@ CreateSlider(int x, int y, int xSize, int ySize, const String& text)
+{
+    Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");
+
+    // Create text and slider below it
+    Text@ sliderText = ui.root.CreateChild("Text");
+    sliderText.SetPosition(x, y);
+    sliderText.SetFont(font, 12);
+    sliderText.text = text;
+    
+    Slider@ slider = ui.root.CreateChild("Slider");
+    slider.SetStyleAuto();
+    slider.SetPosition(x, y + 20);
+    slider.SetSize(xSize, ySize);
+    // Use 0-1 range for controlling sound/music master volume
+    slider.range = 1.0f;
+    
+    return slider;
+}
+
+void HandlePlaySound(StringHash eventType, VariantMap& eventData)
+{
+    Button@ button = GetEventSender();
+    String soundResourceName = button.vars["SoundResource"].GetString();
+    
+    // Get the sound resource
+    Sound@ sound = cache.GetResource("Sound", soundResourceName);
+
+    if (sound !is null)
+    {
+        // Create a scene node with a SoundSource component for playing the sound. The SoundSource component plays
+        // non-positional audio, so its 3D position in the scene does not matter. For positional sounds the
+        // SoundSource3D component would be used instead
+        Node@ soundNode = scene_.CreateChild("Sound");
+        SoundSource@ soundSource = soundNode.CreateComponent("SoundSource");
+        soundSource.Play(sound);
+        // In case we also play music, set the sound volume below maximum so that we don't clip the output
+        soundSource.gain = 0.7f;
+        // Set the sound component to automatically remove its scene node from the scene when the sound is done playing
+        soundSource.autoRemove = true;
+    }
+}
+
+void HandlePlayMusic(StringHash eventType, VariantMap& eventData)
+{
+    // Check if the music player node/component already exist
+    if (scene_.GetChild("Music") !is null)
+        return;
+
+    Sound@ music = cache.GetResource("Sound", "Music/Ninja Gods.ogg");
+    // Set the song to loop
+    music.looped = true;
+    
+    // Create a scene node and a sound source for the music
+    Node@ musicNode = scene_.CreateChild("Music");
+    SoundSource@ musicSource = musicNode.CreateComponent("SoundSource");
+    // Set the sound type to music so that master volume control works correctly
+    musicSource.soundType = SOUND_MUSIC;
+    musicSource.Play(music);
+}
+
+void HandleStopMusic(StringHash eventType, VariantMap& eventData)
+{
+    // Remove the music player node from the scene
+    scene_.RemoveChild(scene_.GetChild("Music"));
+}
+
+void HandleSoundVolume(StringHash eventType, VariantMap& eventData)
+{
+    float newVolume = eventData["Value"].GetFloat();
+    audio.masterGain[SOUND_EFFECT] = newVolume;
+}
+
+void HandleMusicVolume(StringHash eventType, VariantMap& eventData)
+{
+    float newVolume = eventData["Value"].GetFloat();
+    audio.masterGain[SOUND_MUSIC] = newVolume;
+}
+

+ 345 - 0
Bin/Data/Scripts/15_Navigation.as

@@ -0,0 +1,345 @@
+// Navigation example.
+// This sample demonstrates:
+//     - Generating a navigation mesh into the scene;
+//     - Performing path queries to the navigation mesh;
+//     - Rebuilding the navigation mesh partially when adding or removing objects;
+//     - Visualizing custom debug geometry;
+
+#include "Utilities/Sample.as"
+
+Scene@ scene_;
+Node@ cameraNode;
+Vector3 startPos;
+Vector3 endPos;
+Array<Vector3> currentPath;
+float yaw = 0.0f;
+float pitch = 0.0f;
+bool drawDebug = false;
+bool startPosDefined = false;
+bool endPosDefined = false;
+
+void 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 the frame update and render post-update events
+    SubscribeToEvents();
+}
+
+void CreateScene()
+{
+    scene_ = Scene();
+
+    // Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000)
+    // Also create a DebugRenderer component so that we can draw debug geometry
+    scene_.CreateComponent("Octree");
+    scene_.CreateComponent("DebugRenderer");
+    
+    // Create scene node & StaticModel component for showing a static plane
+    Node@ planeNode = scene_.CreateChild("Plane");
+    planeNode.scale = Vector3(100.0f, 1.0f, 100.0f);
+    StaticModel@ planeObject = planeNode.CreateComponent("StaticModel");
+    planeObject.model = cache.GetResource("Model", "Models/Plane.mdl");
+    planeObject.material = cache.GetResource("Material", "Materials/StoneTiled.xml");
+    
+    // Create a Zone component for ambient lighting & fog control
+    Node@ zoneNode = scene_.CreateChild("Zone");
+    Zone@ zone = zoneNode.CreateComponent("Zone");
+    zone.boundingBox = BoundingBox(-1000.0f, 1000.0f);
+    zone.ambientColor = Color(0.15f, 0.15f, 0.15f);
+    zone.fogColor = Color(0.5f, 0.5f, 0.7f);
+    zone.fogStart = 100.0f;
+    zone.fogEnd = 300.0f;
+
+    // Create a directional light to the world. Enable cascaded shadows on it
+    Node@ lightNode = scene_.CreateChild("DirectionalLight");
+    lightNode.direction = Vector3(0.6f, -1.0f, 0.8f);
+    Light@ light = lightNode.CreateComponent("Light");
+    light.lightType = LIGHT_DIRECTIONAL;
+    light.castShadows = true;
+    light.shadowBias = BiasParameters(0.0001f, 0.5f);
+    // Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
+    light.shadowCascade = CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f);
+
+    // Create some mushrooms
+    const uint NUM_MUSHROOMS = 100;
+    for (uint i = 0; i < NUM_MUSHROOMS; ++i)
+        CreateMushroom(Vector3(Random(90.0f) - 45.0f, 0.0f, Random(90.0f) - 45.0f));
+
+    // Create randomly sized boxes. If boxes are big enough, make them occluders. Occluders will be software rasterized before
+    // rendering to a low-resolution depth-only buffer to test the objects in the view frustum for visibility
+    const uint NUM_BOXES = 20;
+    for (uint i = 0; i < NUM_BOXES; ++i)
+    {
+        Node@ boxNode = scene_.CreateChild("Box");
+        float size = 1.0f + Random(10.0f);
+        boxNode.position = Vector3(Random(80.0f) - 40.0f, size * 0.5f, Random(80.0f) - 40.0f);
+        boxNode.SetScale(size);
+        StaticModel@ boxObject = boxNode.CreateComponent("StaticModel");
+        boxObject.model = cache.GetResource("Model", "Models/Box.mdl");
+        boxObject.material = cache.GetResource("Material", "Materials/Stone.xml");
+        boxObject.castShadows = true;
+        if (size >= 3.0f)
+            boxObject.occluder = true;
+    }
+    
+    // Create a NavigationMesh component to the scene root
+    NavigationMesh@ navMesh = scene_.CreateComponent("NavigationMesh");
+    // Create a Navigable component to the scene root. This tags all of the geometry in the scene as being part of the
+    // navigation mesh. By default this is recursive, but the recursion could be turned off from Navigable
+    scene_.CreateComponent("Navigable");
+    // Add padding to the navigation mesh in Y-direction so that we can add objects on top of the tallest boxes
+    // in the scene and still update the mesh correctly
+    navMesh.padding = Vector3(0.0f, 10.0f, 0.0f);
+    // Now build the navigation geometry. This will take some time. Note that the navigation mesh will prefer to use
+    // physics geometry from the scene nodes, as it often is simpler, but if it can not find any (like in this example)
+    // it will use renderable geometry instead
+    navMesh.Build();
+    
+    // Create the camera. Limit far clip distance to match the fog
+    cameraNode = scene_.CreateChild("Camera");
+    Camera@ camera = cameraNode.CreateComponent("Camera");
+    camera.farClip = 300.0f;
+    
+    // Set an initial position for the camera scene node above the plane
+    cameraNode.position = Vector3(0.0f, 5.0f, 0.0f);
+}
+
+void CreateUI()
+{
+    // 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
+    XMLFile@ style = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
+    Cursor@ cursor = Cursor();
+    cursor.SetStyleAuto(style);
+    ui.cursor = cursor;
+    // Set starting position of the cursor at the rendering window center
+    cursor.SetPosition(graphics.width / 2, graphics.height / 2);
+    
+    // Construct new Text object, set string to display and font to use
+    Text@ instructionText = ui.root.CreateChild("Text");
+    instructionText.text =
+        "Use WASD keys to move, RMB to rotate view\n"
+        "Shift+LMB to set path start, LMB to set path end\n"
+        "MMB to add or remove obstacles\n"
+        "Space to toggle debug geometry";
+    instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
+    // The text has multiple rows. Center them in relation to each other
+    instructionText.textAlignment = HA_CENTER;
+
+    // Position the text relative to the screen center
+    instructionText.horizontalAlignment = HA_CENTER;
+    instructionText.verticalAlignment = VA_CENTER;
+    instructionText.SetPosition(0, ui.root.height / 4);
+}
+
+void SetupViewport()
+{
+    // Set up a viewport to the Renderer subsystem so that the 3D scene can be seen
+    Viewport@ viewport = Viewport(scene_, cameraNode.GetComponent("Camera"));
+    renderer.viewports[0] = viewport;
+}
+
+void SubscribeToEvents()
+{
+    // Subscribe HandleUpdate() function for processing update events
+    SubscribeToEvent("Update", "HandleUpdate");
+
+    // Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request
+    // debug geometry
+    SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate");
+}
+
+void MoveCamera(float timeStep)
+{
+    // Right mouse button controls mouse cursor visibility: hide when pressed
+    ui.cursor.visible = !input.mouseButtonDown[MOUSEB_RIGHT];
+
+    // Do not move if the UI has a focused element (the console)
+    if (ui.focusElement !is null)
+        return;
+    
+    // Movement speed as world units per second
+    const float MOVE_SPEED = 20.0f;
+    // Mouse sensitivity as degrees per pixel
+    const float MOUSE_SENSITIVITY = 0.1f;
+    
+    // Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees
+    // Only move the camera when the cursor is hidden
+    if (!ui.cursor.visible)
+    {
+        IntVector2 mouseMove = input.mouseMove;
+        yaw += MOUSE_SENSITIVITY * mouseMove.x;
+        pitch += MOUSE_SENSITIVITY * mouseMove.y;
+        pitch = Clamp(pitch, -90.0f, 90.0f);
+
+        // Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
+        cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);
+    }
+
+    // Read WASD keys and move the camera scene node to the corresponding direction if they are pressed
+    if (input.keyDown['W'])
+        cameraNode.TranslateRelative(Vector3(0.0f, 0.0f, 1.0f) * MOVE_SPEED * timeStep);
+    if (input.keyDown['S'])
+        cameraNode.TranslateRelative(Vector3(0.0f, 0.0f, -1.0f) * MOVE_SPEED * timeStep);
+    if (input.keyDown['A'])
+        cameraNode.TranslateRelative(Vector3(-1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
+    if (input.keyDown['D'])
+        cameraNode.TranslateRelative(Vector3(1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
+
+    // Set route start/endpoint with left mouse button, recalculate route if applicable
+    if (input.mouseButtonPress[MOUSEB_LEFT])
+        SetPathPoint();
+    // Add or remove objects with middle mouse button, then rebuild navigation mesh partially
+    if (input.mouseButtonPress[MOUSEB_MIDDLE])
+        AddOrRemoveObject();
+
+    // Toggle debug geometry with space
+    if (input.keyPress[KEY_SPACE])
+        drawDebug = !drawDebug;
+}
+
+void SetPathPoint()
+{
+    Vector3 hitPos;
+    Drawable@ hitDrawable;
+    
+    if (Raycast(250.0f, hitPos, hitDrawable))
+    {
+        bool setStart = input.qualifierDown[QUAL_SHIFT];
+        if (setStart)
+        {
+            startPos = hitPos;
+            startPosDefined = true;
+        }
+        else
+        {
+            endPos = hitPos;
+            endPosDefined = true;
+        }
+        
+        if (startPosDefined && endPosDefined)
+            RecalculatePath();
+    }
+}
+
+void AddOrRemoveObject()
+{
+    // Raycast and check if we hit a mushroom node. If yes, remove it, if no, create a new one
+    Vector3 hitPos;
+    Drawable@ hitDrawable;
+    
+    if (Raycast(250.0f, hitPos, hitDrawable))
+    {
+        // The part of the navigation mesh we must update, which is the world bounding box of the associated
+        // drawable component
+        BoundingBox updateBox;
+        
+        Node@ hitNode = hitDrawable.node;
+        if (hitNode.name == "Mushroom")
+        {
+            updateBox = hitDrawable.worldBoundingBox;
+            hitNode.Remove();
+        }
+        else
+        {
+            Node@ newNode = CreateMushroom(hitPos);
+            StaticModel@ newObject = newNode.GetComponent("StaticModel");
+            updateBox = newObject.worldBoundingBox;
+        }
+        
+        // Rebuild part of the navigation mesh, then recalculate path if applicable
+        NavigationMesh@ navMesh = scene_.GetComponent("NavigationMesh");
+        navMesh.Build(updateBox);
+        RecalculatePath();
+    }
+}
+
+Node@ CreateMushroom(const Vector3& pos)
+{
+    Node@ mushroomNode = scene_.CreateChild("Mushroom");
+    mushroomNode.position = pos;
+    mushroomNode.rotation = Quaternion(0.0f, Random(360.0f), 0.0f);
+    mushroomNode.SetScale(2.0f + Random(0.5f));
+    StaticModel@ mushroomObject = mushroomNode.CreateComponent("StaticModel");
+    mushroomObject.model = cache.GetResource("Model", "Models/Mushroom.mdl");
+    mushroomObject.material = cache.GetResource("Material", "Materials/Mushroom.xml");
+    mushroomObject.castShadows = true;
+    
+    return mushroomNode;
+}
+
+void RecalculatePath()
+{
+    NavigationMesh@ navMesh = scene_.GetComponent("NavigationMesh");
+    currentPath = navMesh.FindPath(startPos, endPos);
+}
+
+bool Raycast(float maxDistance, Vector3& hitPos, Drawable@& hitDrawable)
+{
+    hitDrawable = null;
+
+    IntVector2 pos = ui.cursorPosition;
+    // Check the cursor is visible and there is no UI element in front of the cursor
+    if (!ui.cursor.visible || ui.GetElementAt(pos, true) !is null)
+        return false;
+
+    Camera@ camera = cameraNode.GetComponent("Camera");
+    Ray cameraRay = camera.GetScreenRay(float(pos.x) / graphics.width, float(pos.y) / graphics.height);
+    // Pick only geometry objects, not eg. zones or lights, only get the first (closest) hit
+    // Note the convenience accessor to scene's Octree component
+    RayQueryResult result = scene_.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, maxDistance, DRAWABLE_GEOMETRY);
+    if (result.drawable !is null)
+    {
+        // Calculate hit position in world space
+        hitPos = cameraRay.origin + cameraRay.direction * result.distance;
+        hitDrawable = result.drawable;
+        return true;
+    }
+
+    return false;
+}
+
+void HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    // Take the frame time step, which is stored as a float
+    float timeStep = eventData["TimeStep"].GetFloat();
+
+    // Move the camera, scale movement with time step
+    MoveCamera(timeStep);
+}
+
+void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
+{
+    // If draw debug mode is enabled, draw navigation mesh debug geometry
+    if (drawDebug)
+    {
+        NavigationMesh@ navMesh = scene_.GetComponent("NavigationMesh");
+        navMesh.DrawDebugGeometry(true);
+    }
+
+    // Visualize the start and end points and the last calculated path
+    // Note the convenience accessor to the DebugRenderer component
+    DebugRenderer@ debug = scene_.debugRenderer;
+    if (startPosDefined)
+        debug.AddBoundingBox(BoundingBox(startPos - Vector3(0.1f, 0.1f, 0.1f), startPos + Vector3(0.1f, 0.1f, 0.1f)), Color(1.0f, 1.0f, 1.0f));
+    if (endPosDefined)
+        debug.AddBoundingBox(BoundingBox(endPos - Vector3(0.1f, 0.1f, 0.1f), endPos + Vector3(0.1f, 0.1f, 0.1f)), Color(1.0f, 1.0f, 1.0f));
+    if (currentPath.length > 1)
+    {
+        // Draw the path with a small upward bias so that it does not clip into the surfaces
+        Vector3 bias(0.0f, 0.05f, 0.0f);
+        for (uint i = 0; i < currentPath.length - 1; ++i)
+            debug.AddLine(currentPath[i] + bias, currentPath[i + 1] + bias, Color(1.0f, 1.0f, 1.0f));
+    }
+}

+ 220 - 0
Bin/Data/Scripts/16_Chat.as

@@ -0,0 +1,220 @@
+// Chat example
+// This sample demonstrates:
+//     - Starting up a network server or connecting to it;
+//     - Implementing simple chat functionality with network messages;
+
+#include "Utilities/Sample.as"
+
+// Identifier for the chat network messages
+const int MSG_CHAT = 32;
+// UDP port we will use
+const uint CHAT_SERVER_PORT = 2345;
+
+Array<String> chatHistory;
+Text@ chatHistoryText;
+UIElement@ buttonContainer;
+LineEdit@ textEdit;
+Button@ sendButton;
+Button@ connectButton;
+Button@ disconnectButton;
+Button@ startServerButton;
+
+void Start()
+{
+    // Execute the common startup for samples
+    SampleStart();
+
+    // Enable OS cursor
+    input.mouseVisible = true;
+
+    // Create the user interface
+    CreateUI();
+    
+    // Subscribe to UI and network events
+    SubscribeToEvents();
+}
+
+void CreateUI()
+{
+    SetLogoVisible(false); // We need the full rendering window
+
+    XMLFile@ uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
+    // Set style to the UI root so that elements will inherit it
+    ui.root.defaultStyle = uiStyle;
+
+    Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");
+    chatHistoryText = ui.root.CreateChild("Text");
+    chatHistoryText.SetFont(font, 12);
+
+    buttonContainer = ui.root.CreateChild("UIElement");
+    buttonContainer.SetFixedSize(graphics.width, 20);
+    buttonContainer.SetPosition(0, graphics.height - 20);
+    buttonContainer.layoutMode = LM_HORIZONTAL;
+    
+    textEdit = buttonContainer.CreateChild("LineEdit");
+    textEdit.SetStyleAuto();
+    
+    sendButton = CreateButton("Send", 70);
+    connectButton = CreateButton("Connect", 90);
+    disconnectButton = CreateButton("Disconnect", 100);
+    startServerButton = CreateButton("Start Server", 110);
+
+    UpdateButtons();
+
+    chatHistory.Resize((graphics.height - 20) / chatHistoryText.rowHeight);
+
+    // No viewports or scene is defined. However, the default zone's fog color controls the fill color
+    renderer.defaultZone.fogColor = Color(0.0f, 0.0f, 0.1f);
+}
+
+void SubscribeToEvents()
+{
+    // Subscribe to UI element events
+    SubscribeToEvent(textEdit, "TextFinished", "HandleSend");
+    SubscribeToEvent(sendButton, "Released", "HandleSend");
+    SubscribeToEvent(connectButton, "Released", "HandleConnect");
+    SubscribeToEvent(disconnectButton, "Released", "HandleDisconnect");
+    SubscribeToEvent(startServerButton, "Released", "HandleStartServer");
+
+    // Subscribe to log messages so that we can pipe them to the chat window
+    SubscribeToEvent("LogMessage", "HandleLogMessage");
+    
+    // Subscribe to network events
+    SubscribeToEvent("NetworkMessage", "HandleNetworkMessage");
+    SubscribeToEvent("ServerConnected", "HandleConnectionStatus");
+    SubscribeToEvent("ServerDisconnected", "HandleConnectionStatus");
+    SubscribeToEvent("ConnectFailed", "HandleConnectionStatus");
+}
+
+Button@ CreateButton(const String&in text, int width)
+{
+    Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");
+    
+    Button@ button = buttonContainer.CreateChild("Button");
+    button.SetStyleAuto();
+    button.SetFixedWidth(width);
+
+    Text@ buttonText = button.CreateChild("Text");
+    buttonText.SetFont(font, 12);
+    buttonText.SetAlignment(HA_CENTER, VA_CENTER);
+    buttonText.text = text;
+    
+    return button;
+}
+
+void ShowChatText(const String& row)
+{
+    chatHistory.Erase(0);
+    chatHistory.Push(row);
+
+    // Concatenate all the rows in history
+    String allRows;
+    for (uint i = 0; i < chatHistory.length; ++i)
+        allRows += chatHistory[i] + "\n";
+    
+    chatHistoryText.text = allRows;
+}
+
+void UpdateButtons()
+{
+    Connection@ serverConnection = network.serverConnection;
+    bool serverRunning = network.serverRunning;
+    
+    // Show and hide buttons so that eg. Connect and Disconnect are never shown at the same time
+    sendButton.visible = serverConnection !is null;
+    connectButton.visible = serverConnection is null && !serverRunning;
+    disconnectButton.visible = serverConnection !is null || serverRunning;
+    startServerButton.visible = serverConnection is null && !serverRunning;
+}
+
+void HandleLogMessage(StringHash eventType, VariantMap& eventData)
+{
+    ShowChatText(eventData["Message"].GetString());
+}
+
+void HandleSend(StringHash eventType, VariantMap& eventData)
+{
+    String text = textEdit.text;
+    if (text.empty)
+        return; // Do not send an empty message
+
+    Connection@ serverConnection = network.serverConnection;
+    
+    if (serverConnection !is null)
+    {
+        // A VectorBuffer object is convenient for constructing a message to send
+        VectorBuffer msg;
+        msg.WriteString(text);
+        // Send the chat message as in-order and reliable
+        serverConnection.SendMessage(MSG_CHAT, true, true, msg);
+        // Empty the text edit after sending
+        textEdit.text = "";
+    }
+}
+
+void HandleConnect(StringHash eventType, VariantMap& eventData)
+{
+    String address = textEdit.text.Trimmed();
+    if (address.empty)
+        address = "localhost"; // Use localhost to connect if nothing else specified
+    // Empty the text edit after reading the address to connect to
+    textEdit.text = "";
+    
+    // Connect to server, do not specify a client scene as we are not using scene replication, just messages.
+    // At connect time we could also send identity parameters (such as username) in a VariantMap, but in this
+    // case we skip it for simplicity
+    network.Connect(address, CHAT_SERVER_PORT, null);
+    
+    UpdateButtons();
+}
+
+void HandleDisconnect(StringHash eventType, VariantMap& eventData)
+{
+    Connection@ serverConnection = network.serverConnection;
+    // If we were connected to server, disconnect
+    if (serverConnection !is null)
+        serverConnection.Disconnect();
+    // Or if we were running a server, stop it
+    else if (network.serverRunning)
+        network.StopServer();
+    
+    UpdateButtons();
+}
+
+void HandleStartServer(StringHash eventType, VariantMap& eventData)
+{
+    network.StartServer(CHAT_SERVER_PORT);
+
+    UpdateButtons();
+}
+
+void HandleNetworkMessage(StringHash eventType, VariantMap& eventData)
+{
+    int msgID = eventData["MessageID"].GetInt();
+    if (msgID == MSG_CHAT)
+    {
+        VectorBuffer msg = eventData["Data"].GetBuffer();
+        String text = msg.ReadString();
+        
+        // If we are the server, prepend the sender's IP address and port and echo to everyone
+        // If we are a client, just display the message
+        if (network.serverRunning)
+        {
+            Connection@ sender = eventData["Connection"].GetConnection();
+            
+            text = sender.ToString() + " " + text;
+            
+            VectorBuffer sendMsg;
+            sendMsg.WriteString(text);
+            // Broadcast as in-order and reliable
+            network.BroadcastMessage(MSG_CHAT, true, true, sendMsg);
+        }
+        
+        ShowChatText(text);
+    }
+}
+
+void HandleConnectionStatus(StringHash eventType, VariantMap& eventData)
+{
+    UpdateButtons();
+}

+ 427 - 0
Bin/Data/Scripts/17_SceneReplication.as

@@ -0,0 +1,427 @@
+// 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;
+
+#include "Utilities/Sample.as"
+
+// UDP port we will use
+const uint SERVER_PORT = 2345;
+
+// Control bits we define
+const uint CTRL_FORWARD = 1;
+const uint CTRL_BACK = 2;
+const uint CTRL_LEFT = 4;
+const uint CTRL_RIGHT = 8;
+
+// Record for each connected client
+class Client
+{
+    Connection@ connection;
+    Node@ object;
+}
+
+Scene@ scene_;
+Node@ cameraNode;
+Text@ instructionsText;
+UIElement@ buttonContainer;
+LineEdit@ textEdit;
+Button@ connectButton;
+Button@ disconnectButton;
+Button@ startServerButton;
+Array<Client> clients;
+float yaw = 0.0f;
+float pitch = 1.0f;
+uint clientObjectID = 0;
+
+void 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();
+}
+
+void 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.
+    Node@ zoneNode = scene_.CreateChild("Zone", LOCAL);
+    Zone@ zone = zoneNode.CreateComponent("Zone");
+    zone.boundingBox = BoundingBox(-1000.0f, 1000.0f);
+    zone.ambientColor = Color(0.1f, 0.1f, 0.1f);
+    zone.fogStart = 100.0f;
+    zone.fogEnd = 300.0f;
+    
+    // Create a directional light without shadows
+    Node@ lightNode = scene_.CreateChild("DirectionalLight", LOCAL);
+    lightNode.direction = Vector3(0.5f, -1.0f, 0.5f);
+    Light@ light = lightNode.CreateComponent("Light");
+    light.lightType = LIGHT_DIRECTIONAL;
+    light.color = Color(0.2f, 0.2f, 0.2f);
+    light.specularIntensity = 1.0f;
+    
+    // Create a "floor" consisting of several tiles. Make the tiles physical but leave small cracks between them
+    for (int y = -20; y <= 20; ++y)
+    {
+        for (int x = -20; x <= 20; ++x)
+        {
+            Node@ floorNode = scene_.CreateChild("FloorTile", LOCAL);
+            floorNode.position = Vector3(x * 20.2f, -0.5f, y * 20.2f);
+            floorNode.scale = Vector3(20.0f, 1.0f, 20.0f);
+            StaticModel@ floorObject = floorNode.CreateComponent("StaticModel");
+            floorObject.model = cache.GetResource("Model", "Models/Box.mdl");
+            floorObject.material = cache.GetResource("Material", "Materials/Stone.xml");
+
+            RigidBody@ body = floorNode.CreateComponent("RigidBody");
+            body.friction = 1.0f;
+            CollisionShape@ shape = floorNode.CreateComponent("CollisionShape");
+            shape.SetBox(Vector3(1.0f, 1.0f, 1.0f));
+        }
+    }
+    
+    // Create the camera. Limit far clip distance to match the fog
+    cameraNode = scene_.CreateChild("Camera", LOCAL);
+    Camera@ camera = cameraNode.CreateComponent("Camera");
+    camera.farClip = 300.0f;
+    
+    // Set an initial position for the camera scene node above the plane
+    cameraNode.position = Vector3(0.0f, 5.0f, 0.0f);
+}
+
+void CreateUI()
+{
+    XMLFile@ 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
+    Cursor@ cursor = 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.text = "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();
+}
+
+void SetupViewport()
+{
+    // Set up a viewport to the Renderer subsystem so that the 3D scene can be seen
+    Viewport@ viewport = Viewport(scene_, cameraNode.GetComponent("Camera"));
+    renderer.viewports[0] = viewport;
+}
+
+void 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");
+}
+
+Button@ CreateButton(const String& text, int width)
+{
+    Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");
+    
+    Button@ button = buttonContainer.CreateChild("Button");
+    button.SetStyleAuto();
+    button.SetFixedWidth(width);
+    
+    Text@ buttonText = button.CreateChild("Text");
+    buttonText.SetFont(font, 12);
+    buttonText.SetAlignment(HA_CENTER, VA_CENTER);
+    buttonText.text = text;
+    
+    return button;
+}
+
+void UpdateButtons()
+{
+    Connection@ serverConnection = network.serverConnection;
+    bool serverRunning = network.serverRunning;
+    
+    // Show and hide buttons so that eg. Connect and Disconnect are never shown at the same time
+    connectButton.visible = serverConnection is null && !serverRunning;
+    disconnectButton.visible = serverConnection !is null || serverRunning;
+    startServerButton.visible = serverConnection is null && !serverRunning;
+    textEdit.visible = serverConnection is null && !serverRunning;
+}
+
+Node@ CreateControllableObject()
+{
+    // Create the scene node & visual representation. This will be a replicated object
+    Node@ ballNode = scene_.CreateChild("Ball");
+    ballNode.position = Vector3(Random(40.0f) - 20.0f, 5.0f, Random(40.0f) - 20.0f);
+    ballNode.SetScale(0.5f);
+    StaticModel@ ballObject = ballNode.CreateComponent("StaticModel");
+    ballObject.model = cache.GetResource("Model", "Models/Sphere.mdl");
+    ballObject.material = cache.GetResource("Material", "Materials/StoneSmall.xml");
+    
+    // Create the physics components
+    RigidBody@ body = ballNode.CreateComponent("RigidBody");
+    body.mass = 1.0f;
+    body.friction = 1.0f;
+    // In addition to friction, use motion damping so that the ball can not accelerate limitlessly
+    body.linearDamping = 0.5f;
+    body.angularDamping = 0.5f;
+    CollisionShape@ shape = ballNode.CreateComponent("CollisionShape");
+    shape.SetSphere(1.0f);
+    
+    // Create a random colored point light at the ball so that can see better where is going
+    Light@ light = ballNode.CreateComponent("Light");
+    light.range = 3.0f;
+    light.color = Color(0.5f + (RandomInt() & 1) * 0.5f, 0.5f + (RandomInt() & 1) * 0.5f, 0.5f + (RandomInt() & 1) * 0.5f);
+    
+    return ballNode;
+}
+
+void MoveCamera()
+{
+    // Right mouse button controls mouse cursor visibility: hide when pressed
+    ui.cursor.visible = !input.mouseButtonDown[MOUSEB_RIGHT];
+    
+    // Mouse sensitivity as degrees per pixel
+    const float MOUSE_SENSITIVITY = 0.1f;
+    
+    // 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 (!ui.cursor.visible)
+    {
+        IntVector2 mouseMove = input.mouseMove;
+        yaw += MOUSE_SENSITIVITY * mouseMove.x;
+        pitch += MOUSE_SENSITIVITY * mouseMove.y;
+        pitch = Clamp(pitch, 1.0f, 90.0f);
+    }
+    
+    // Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);
+
+    // Only move the camera / show instructions if we have a controllable object
+    bool showInstructions = false;
+    if (clientObjectID != 0)
+    {
+        Node@ ballNode = scene_.GetNode(clientObjectID);
+        if (ballNode !is null)
+        {
+            const float CAMERA_DISTANCE = 5.0f;
+            
+            // Move camera some distance away from the ball
+            cameraNode.position = ballNode.position + cameraNode.rotation * Vector3(0.0f, 0.0f, -1.0f) * CAMERA_DISTANCE;
+            showInstructions = true;
+        }
+    }
+    
+    instructionsText.visible = showInstructions;
+}
+
+void HandlePostUpdate(StringHash eventType, VariantMap& eventData)
+{
+    // We only rotate the camera according to mouse movement since last frame, so do not need the time step
+    MoveCamera();
+}
+
+void HandlePhysicsPreStep(StringHash eventType, VariantMap& 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.)
+    Connection@ serverConnection = network.serverConnection;
+    
+    // Client: collect controls
+    if (serverConnection !is null)
+    {
+        Controls controls;
+        
+        // Copy mouse yaw
+        controls.yaw = yaw;
+        
+        // Only apply WASD controls if there is no focused UI element
+        if (ui.focusElement is null)
+        {
+            controls.Set(CTRL_FORWARD, input.keyDown['W']);
+            controls.Set(CTRL_BACK, input.keyDown['S']);
+            controls.Set(CTRL_LEFT, input.keyDown['A']);
+            controls.Set(CTRL_RIGHT, input.keyDown['D']);
+        }
+        
+        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
+    else if (network.serverRunning)
+    {
+        for (uint i = 0; i < clients.length; ++i)
+        {
+            Connection@ connection = clients[i].connection;
+            // Get the object this connection is controlling
+            Node@ ballNode = clients[i].object;
+
+            RigidBody@ body = ballNode.GetComponent("RigidBody");
+
+            // Torque is relative to the forward vector
+            Quaternion rotation(0.0f, connection.controls.yaw, 0.0f);
+            
+            const float MOVE_TORQUE = 3.0f;
+            
+            // 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.buttons & CTRL_FORWARD != 0)
+                body.ApplyTorque(rotation * Vector3(1.0f, 0.0f, 0.0f) * MOVE_TORQUE);
+            if (connection.controls.buttons & CTRL_BACK != 0)
+                body.ApplyTorque(rotation * Vector3(-1.0f, 0.0f, 0.0f) * MOVE_TORQUE);
+            if (connection.controls.buttons & CTRL_LEFT != 0)
+                body.ApplyTorque(rotation * Vector3(0.0f, 0.0f, 1.0f) * MOVE_TORQUE);
+            if (connection.controls.buttons & CTRL_RIGHT != 0)
+                body.ApplyTorque(rotation * Vector3(0.0f, 0.0f, -1.0f) * MOVE_TORQUE);
+        }
+    }
+}
+
+void HandleConnect(StringHash eventType, VariantMap& eventData)
+{
+    String address = textEdit.text.Trimmed();
+    if (address.empty)
+        address = "localhost"; // Use localhost to connect if nothing else specified
+    
+    // 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();
+}
+
+void HandleDisconnect(StringHash eventType, VariantMap& eventData)
+{
+    Connection@ 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 !is null)
+    {
+        serverConnection.Disconnect();
+        scene_.Clear(true, false);
+        clientObjectID = 0;
+    }
+    // Or if we were running a server, stop it
+    else if (network.serverRunning)
+    {
+        network.StopServer();
+        scene_.Clear(true, false);
+    }
+    
+    UpdateButtons();
+}
+
+void HandleStartServer(StringHash eventType, VariantMap& eventData)
+{
+    network.StartServer(SERVER_PORT);
+    
+    UpdateButtons();
+}
+
+void HandleConnectionStatus(StringHash eventType, VariantMap& eventData)
+{
+    UpdateButtons();
+}
+
+void HandleClientConnected(StringHash eventType, VariantMap& eventData)
+{
+    // When a client connects, assign to scene to begin scene replication
+    Connection@ newConnection = eventData["Connection"].GetConnection();
+    newConnection.scene = scene_;
+    
+    // Then create a controllable object for that client
+    Node@ newObject = CreateControllableObject();
+    Client newClient;
+    newClient.connection = newConnection;
+    newClient.object = newObject;
+    clients.Push(newClient);
+
+    // Finally send the object's node ID using a remote event
+    VariantMap remoteEventData;
+    remoteEventData["ID"] = newObject.id;
+    newConnection.SendRemoteEvent("ClientObjectID", true, remoteEventData);
+}
+
+void HandleClientDisconnected(StringHash eventType, VariantMap& eventData)
+{
+    // When a client disconnects, remove the controlled object
+    Connection@ connection = eventData["Connection"].GetConnection();
+    for (uint i = 0; i < clients.length; ++i)
+    {
+        if (clients[i].connection is connection)
+        {
+            clients[i].object.Remove();
+            clients.Erase(i);
+            break;
+        }
+    }
+}
+
+void HandleClientObjectID(StringHash eventType, VariantMap& eventData)
+{
+    clientObjectID = eventData["ID"].GetUInt();
+}

+ 1 - 1
Source/Samples/17_SceneReplication/SceneReplication.cpp

@@ -180,7 +180,7 @@ void SceneReplication::CreateUI()
     // Position the text relative to the screen center
     instructionsText_->SetHorizontalAlignment(HA_CENTER);
     instructionsText_->SetVerticalAlignment(VA_CENTER);
-    instructionsText_->SetPosition(0, ui->GetRoot()->GetHeight() / 4);
+    instructionsText_->SetPosition(0, graphics->GetHeight() / 4);
     // Hide until connected
     instructionsText_->SetVisible(false);