Skip to content

Manipulating Objects Using Scripts#

Notice

This page is still work in progress and may contain inaccurate information.

This tutorial assumes you've already familiarized yourself with the basics of scripting from following the 'Creating Your First Script' tutorial.

Now that you have a scenario with a basic script attached, let's try and do something a bit more exciting - let's use this scenario to perform some basic object scripting.

Creating an Object#

To get started, add a new 3D primitive object, such as a cuboid. You can do this in Guerilla, but it is generally advised to do so in Sapien.

To add a new object in Sapien, open the Tool Window. Within the Tool Window, you will find two dropdowns side by side: one to select a palette type, and one to select a unique object type. In a new scenario, you will only be able to create 3D primitives - but you can add tags to various palettes as well. This will be covered later.

For now, select the 'primitive' palette in the first dropdown. Then, select 'cuboid' in the second dropdown.

Next, open the Hierarchy View. Select the folder you wish for the object to be placed within. Now, right-click anywhere within the Game Viewport to place a new object. You can then use the transform gizmo to reposition the object to the desired position.

In the Hierarchy View, right-click on the object and provide it a new name. This is what you'll use to access the object at runtime. If the object exists within a subfolder instead of the scenario root, make note of its folder path - as this will also be required to access the object. For this example, we will assume your cube is named SampleCube.

Now, you're ready to manipulate this object from your game scripts.

Accessing the Object#

Within your scenario script, use the GetWorldObject() method to retrieve the world object. If your scenario script is not an instance of BlamScenarioScript, then you can use the BlamAPI::Scenario::GetWorldObject() function. Both the method and function will return a pointer to the BlamWorldObject, or nullptr if the specified object could not be found.

If the object exists within the scenario root, simply provide the object name you specified earlier. If it exists within a subfolder, you can access it like a file path. For instance, if your scenario hierarchy looks like the following:

Root
- 📁 Folder1
    - 📁 Folder2
        - SampleCube

Then for the path parameter of GetWorldObject(), you would use /Folder1/Folder2/SampleCube.

Now, you can use the various methods of the object to change its general properties - including position, rotation, scale, and more. What we'll now do is have this cube rotate on an axis indefinitely over time.

Manipulating the Object#

Within your scenario script, add an override for either the Tick() or Update() method. The Tick() method will always run on a fixed time step of 60 ticks per second (TPS), providing a tick parameter which specifies the current tick number, from 1 to 60. The Update() method will run every frame, providing a delta parameter which specifies the amount of time, in seconds, that has elapsed since the previous frame.

!!! info In general, it is recommended to use the Update() method, as this is often a more standard way of handling object updates and may be more familiar to users accustomed to other game engines, and is less likely to result in unexpected behavior with future engine changes. For physics calculations, Tick() or PhysicsUpdate() should be used instead, as these are both tied directly to the game tick loop.

Let's go over how to rotate the cube by 90 degrees per second, resulting in a complete rotation every 4 seconds.

Rotating using Update#

Within the Rotate() method, you can determine the rotation amount by simply doing 90 * delta. From there, access the cube's rotation by calling GetRotation() on the cube, setting the desired axis value += the rotation amount, then calling SetRotation() to apply the new rotation value. When finished, your method should look something like this:

void ExampleScenarioScript::Update(float delta)
{
    BlamWorldObject* cube = GetWorldObject("SampleCube");
    BlamVector3 rotation = cube->GetRotation();
    rotation.x += (90 * delta);
    cube->SetRotation(rotation);
}

Rotating using Tick#

Within the Tick() method, you can determine the rotation amount by simply doing 90 / 60. From there, access the cube's rotation by calling GetRotation() on the cube, setting the desired axis value += the rotation amount, then calling SetRotation() to apply the new rotation value. When finished, your method should look something like this:

void ExampleScenarioScript::Tick(int tick)
{
    BlamWorldObject* cube = GetWorldObject("SampleCube");
    BlamVector3 rotation = cube->GetRotation();
    rotation.x += (90 / 60); // You can also use rotation.x += (90 / GAME_TICKRATE); here - see info comment below for details
    cube->SetRotation(rotation);
}

!!! info In order to futureproof your code, it is recommended to use the GAME_TICKRATE preprocessor macro instead of using the number 60 directly. This ensures that, should the tickrate be increased or decreased in the future (though we don't plan to), your scripts will continue to work as expected in this regard. If you write code assuming that an action is performed at a certain rate without doing any calculations - such as simply incrementing the rotation amount by 1.5 directly - these scripts would no longer work as expected in the case of an engine tickrate change (though again, this is highly unlikely and would only be performed in a major release).

Now, run and debug your game, load the scenario if required, and you should be able to see your cube rotating - all on its own! Feel free to play around with additional objects, or modifying different object parameters. When you feel comfortable, you may wish to move onto the next step: modifying tag data with scripts.

Manipulating Tag Data#

The last step of this tutorial is to try reading and modifying tag data within a script. For our example, we'll modify the render target clearing color - the color used to clear the screen when there is nothing to render in a given part of the viewport - and have it cycle through colors over time. We'll be showing our example using the Update() method, but you can adapt the calculations to use the Tick() method as well.

Accessing a Tag#

First, you'll need to retrieve the globals tag. To do this, you can call GetTag() within our scenario script, specifying the tag path to your project's globals tag. If you don't have one, you can create one using Guerilla - you don't need to worry about modifying any values. Just create a new tag, and save it wherever you like. For our example, we'll assume you've saved it in a globals subfolder - meaning our tag path would be globals/globals.globals.

Alternatively, or if you need to access the tag outside of a scenario script, you can also call BlamAPI::Tags::GetTag() or BlamAPI::Tags::GetGlobalsTag(). If using GetTag(), you'll need to perform some additional validation and casting - and so to keep things simple, we'll assume you're using GetGlobalsTag().

Updating the Tag#

Now that we have our globals tag, access the render_target_clear_color property. Convert the color to HSV, then increment the H value by 10 * delta. This will increase the hue by 10 degrees per second. Convert the color back to RGB, then apply the new color to the render_target_clear_color property.

In this particular use case, this is all we'll need to do - as this value is read as-is by the game engine. However, many other tags (such as render models, lights, etc) are processed further, reading the tag data at one time, and then no longer read these values. As such, you'll likely want to get into the habit of 'poking' your tag - which is simply informing the game engine that the tag has been changed, and that the engine should re-process it again as need be.

To do this from a scenario script, simply call PokeTag(), providing the tag's path. If you need to do this from outside of a scenario script, you can call BlamAPI::Tags::PokeTag(), providing the tag path. If you accessed the tag using BlamAPI::Tags::GetTag() initially, you can also simply call the Poke() method on the BlamTagData instance that was returned.

When finished, your script should look something like this (omitting the cube rotation code for simplicity):

void ExampleScenarioScript::Update(float delta)
{
    globals* globals_tag = BlamAPI::Tags::GetGlobalsTag("globals/globals.globals");

    BlamColor color = globals_tag->render_target_clear_color;

    color = color.ToHSV();
    color.h += (10 * delta);
    color = color.ToRGB();

    globals_tag->render_target_clear_color = color;
    PokeTag("globals/globals.globals");
}

Now, build and debug your game - and you should see the clear color slowly increasing in hue. Congratulations - you've successfully modified tag data in real-time! Note that these changes are not permanent, and only remain in effect until the tag is unloaded or reloaded. Only changes made from Guerilla or Sapien are written to the tag files themselves.