Forums Gallery LOGIN |REGISTER

Community Archive is read-only - Here you can view content recorded up until February 2017. To join the latest discussions visit our new forums


[Lua Tutorial] Smoothly rotating one entity to face another.

Help others by sharing your tutorials here.


 

User avatar jethroj
Just getting started
Just getting started
 
Posts: 21
Member since: 07.02.2013, 16:02
Location: the Netherlands
Likes: 13

[Lua Tutorial] Smoothly rotating one entity to face another.

Postby jethroj » 21.01.2014, 18:35

After following the CryDev documentation and creating my first entity I was lost… there were still many things that I needed to know before getting started with my grand plans. How to rotate entities for instance, or how to make different Lua scripts communicate together. Eventually I learned this stuff through trial and error and now I want to share it with you.

I am still a pretty novice Lua scripter and do not have much experience with the CryEditor. This is my very first tutorial and therefore it would be great to hear your thoughts on it! I’ll take any feedback to heart and adapt it in my future tutorials.

What are we going to create?
We are going to create an entity that, when input with the name of another entity, rotates toward it smoothly in real-time. This can be useful for, for instance, moving a camera around the player or as part of a path following system.

If well received this tutorial will be the first part of a series that will eventually teach you how to create a Train that can follow tracks.

What will we learn during this tutorial?

- Basic Lua scripting for CryEngine
- How to rotate entities
- Parameters and arguments
- Communicating between different Lua scripts
- Some advanced Lua maths: atan2, modulo[%] and Lerp()

What do we need to know beforehand?
Basic scripting knowledge will help a lot. I will, however, be as detailed as possible in this tutorial.
We will start off with the following code:

MyEntity.lua
Code: Select all
MyEntity = {
    Properties = {
        object_Model = "GameSDK/objects/props/industrial/trainyard/trains/locos/l_diesel/l_diesel.cgf",
    },
    Editor = { Icon = "Checkpoint.bmp", },
};

function MyEntity:OnInit() -- use OnSpawn if you want to spawn this entity on the fly
    self:OnReset();
end

function MyEntity:OnPropertyChange() --makes for realtime updating when in the editor
    self:OnReset();
end

function MyEntity:OnReset()
   
    if (self.Properties.object_Model ~= "") then --makes sure a model is specified
            self:LoadObject(0, self.Properties.object_Model); -- loads the model
    else Log("Error: Modelname not set!"); end

end


If you do not understand this I highly recommend following this tutorial in advance: http://www.crydev.net/wiki/index.php/Creating_a_New_Entity

Got it? Let’s go!
Part 1 – Setting up the functions and storing other entities.

First we are going to add a couple a variables: We will be needing a place to store the name of the entity we want to rotate towards, we want the unique ID of the named entity which we can use and we want a place where we can store the current angles (rotation) of MyEntity. Later on we will also need the position (pos) and speed at which it rotates (fRotSpeed).

Code: Select all
entName = "", -- place this within the Properties table
fRotSpeed = 3, -- place this within the Properties table
entID = "", -- place this within the MyEntity table
angles = 0, -- place this within the MyEntity table
pos = 0, -- place this within the MyEntity table


As of now, these variables are empty, so let’s fill them the moment the game starts:

Code: Select all
function MyEntity:OnReset()
    if (self.Properties.object_Model ~= "") then --makes sure a model is specified
        self:LoadObject(0, self.Properties.object_Model); -- loads the model
       
        -- **NEW**
        if (self.Properties.entName ~= "") then --makes sure entName is specified
            self.entID = System.GetEntityByName(self.Properties.entName); -- get entID by name: entName
            self.angles = self:GetAngles(); --gets the current angles of MyEntity
            self.pos = self:GetPos(); --gets the current position of MyEntity
        else Log("Error: Target entity not set!"); end
        -- **END**

    else Log("Error: Modelname not set!"); end
end


Just like with the model, we make sure an entity has been set before continuing. If this is the case the code looks for an entity in your editor project with the name of entName and stores its ID in entID. From now on we are able to access the functions and variables of that entity from this script. The next two lines are simple: They get the current angles + position from MyEntity and store them in angles and pos.

As we want to rotate MyEntity in real-time we will need to use a function that is continually called: OnUpdate(). OnUpdate()is called every frame. This function has a drawback though: Someone playing your project on 30 FPS will have half the number of calls than someone playing on 60 FPS resulting in everything moving half the pace. We can solve this by calculating the time between frames and using that to modify the rotation values. This time is frameTime and is automatically passed on to OnUpdate().

So let’s create this OnUpdate function after OnReset():

Code: Select all
function MyEntity:OnUpdate(frameTime)
    self:FaceAt(self.EntID, frameTime); --runs the FaceAt function with self.EntID and frameTime as arguments
end


From within OnUpdate() we call the function that will do the rotating: FaceAt(). This will keep our code clean when we keep adding things to OnUpdate()later. Let’s create the FaceAt function:

Code: Select all
function MyEntity:FaceAt(ID, fT)
    Log("ID: " .. tostring(ID)); --ID needs to be converted to a string for the Log() function
end


In this example the contents of self.EntID and frameTime are passed from OnUpdate()to FaceAt() as ID and fT so that they can be used there.

The names of the parameters (ID & fT) can be anything. It is the position within the parenthesis that is of import. The first argument passed by the line: self:FaceAt(first , second); will be stored in the first parameter of the called function MyEntity:FaceAt(first , second).

Now to make this code work we must activate OnUpdate() as it is disabled by default:

Code: Select all
self:Activate(1); --1 = on, 0 = off


Try it out for yourself! You should now be getting an output like ID: table: 000xxx0 continually being printed to the console. If this is not the case, scroll down a bit to find what your script should look like.

Assignment: If you got this working I would say it is time to do some thinking for yourself. It is a bit messy that OnUpdate() is always on, even when it doesn’t need to. CryTek disabled it by default for a reason right?

Make it so that in the editor we can enable and disable OnUpdate() by switching between self:Activate(1) and (0).

Done? Then you should have this:
Code: Select all
MyEntity = {
    Properties = {
        object_Model = "GameSDK/objects/props/industrial/trainyard/trains/locos/l_diesel/l_diesel.cgf",
        fRotSpeed = 3, --[0.1, 20, 0.1, "Speed of rotation"]
        entName = "",
        bActive = 0, -- **NEW**
    },
    Editor = { Icon = "Checkpoint.bmp", },
    entID = "",
    angles = 0,
    pos = 0,
};

function MyEntity:OnInit() -- use OnSpawn if you want to spawn this entity on the fly
    self:OnReset();
end

function MyEntity:OnPropertyChange() --makes for realtime updating when in the editor
    self:OnReset();
end

function MyEntity:OnReset()
   
    if (self.Properties.object_Model ~= "") then --makes sure a model is specified
        self:LoadObject(0, self.Properties.object_Model); -- loads the model so that it is visible
       
        if (self.Properties.entName ~= "") then --makes sure a target entity is specified
            self.entID = System.GetEntityByName(self.Properties.entName); -- get entID by name: entName
            self.angles = self:GetAngles(); --gets the current angles of MyEntity
            self.pos = self:GetPos(); --gets the current position of MyEntity
            self:Activate(self.Properties.bActive); --set OnUpdate() on/off **NEW**

       else Log("Error: Target entity not set!"); end
    else Log("Error: Modelname not set!"); end
end

function MyEntity:OnUpdate(frameTime)
    self:FaceAt(self.entID, frameTime); --runs the FaceAt function with EntID and frameTime as arguments
end

function MyEntity:FaceAt(ID, fT)
    Log("ID: " .. tostring(ID)); --ID needs to be converted to string for the Log() function
end
Last edited by jethroj on 21.01.2014, 18:49, edited 3 times in total.
User avatar jethroj
Just getting started
Just getting started
 
Posts: 21
Member since: 07.02.2013, 16:02
Location: the Netherlands
Likes: 13

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby jethroj » 21.01.2014, 18:37

Part 2 – Rotating towards our entity

The second and final part of this tutorial will explain how we will turn MyEntity towards the specified entity.

The very first thing we will need to do is get the position of our entity and the one we want to rotate to. To do that we need to make a couple of variables that we will only need to use within FaceAt().

Code: Select all
    local a = self.pos; --self.pos is a vec 3 table, meaning it contains an x, y, and z variable
    local b = ID:GetPos(); --GetPos also returns a vec3


You can access all functions and non-local variables of another entity. You just need to replace self with an entity ID, in our case that is ID.

Now we have both locations we can use maths to calculate the rotation.

1. Image
2. Image

1. We want to calculate (a). (a) is the rotation of (A) when it is facing (B).
2. From our younger years in high-school we recall that we need to use the arc tangent: (a) = atan(y/x)

We end up with: (a) = ~59°

However, x and y can at some point become negative and we know that -3/5 == 3/-5. This can become a problem as our result will be the same, even though the position of B is vastly different in these situations. This is why Lua has a function that keeps the x and the y separate and can thus calculate the coordinates correctly. atan2(y,x):

Code: Select all
  local newAngle = math.atan2 (b.y-a.y, b.x-a.x);     -- calculates angle at which b is from a


Hopefully you found this part not too hard to follow, because now we are going to do something a bit more complicated. Instead of snapping the rotation to (B), we want to make it rotate towards (B) nice and smooth.

To do that we are going to linearly interpolate between the currentAngle of (A) to the newAngle (a) that we just calculated.

Code: Select all
    self.angles.z = Lerp(self.angles.z, newAngle, (self.Properties.fRotSpeed*fT));


You’ll understand Lerp in no time if you watch this explanation: http://funprogramming.org/64-Animate-objects-that-slow-down-and-stop-using-lerp.html.

Our fRotspeed, multiplied by our frameTime is the value by which it interpolates. The higher fRotSpeed is, the faster MyEntity will turn.

Image

But! What happens when our entity is at 10° and we want it to rotate to 350° ? The shortest way would be turning clockwise for 20°, pass through 360° and end up at 350°. Lerp, however, just interpolates one number towards the other linearly and cannot pass 360°. This means it would take the long way round. We could create some interesting if statements that compare rotations and make a different calculation depending on the results, but we can also create one funky formula that can solve this issue a bit more efficiently:

Code: Select all
  local difference =((((newAngle - self.angles.z) % 360) + 540) % 360) - 180;


difference is the shortest angle between currentAngle (self.angles.z) and newAngle. So our example above would result in: -20°.

Modulo[%] works like this: 9 % 4 = 1. It checks how many times 4 fits in 9, which is 2, then it subtracts this sum (2*4= 8) resulting in 1. So to (overly) simplify: In this example it takes out all the 4’s and calculates what is left.

For a more in depth explanation I found this source to be great: http://www.i-programmer.info/babbages-bag/481-the-mod-function.html

Got it? Now let’s find out what it does for us in the above formula! First off, we should break it up so it becomes easier to understand:

Code: Select all
difference =((newAngle - self.angles.z) % 360);
difference = (difference + 540) % 360);
difference = difference - 180;


Keep the above diagram and example in mind as we will be using those values.

- Line 1 calculates the difference between the two angles and modulates it. This means we will end up with a value between the -359 and +359. (Remember: 360 will become 0, and 370 will become 10)

In our example difference = 340.

- Line 2 adds +360 and +180 (540). Then modulates by 360.

The +360 makes sure we cannot get any negative numbers. It moves our range of values from (-359 and +359) to (+1 and +719). (As we modulate this by 360 at the end: 719 == 359.) You can see this as transforming our range to (+0 to +359).

The +180 makes sure that when difference is higher than 180 (this is when we want our entity to turn the other way round, as it’s shorter) will end up being higher than 359 and thus, being modulated again.

After this modulation all differences with a value higher than 180 will range from (+1 to +180).
When adding 180 to differences below 180, the results will range from (+180 to +360) and will not be modulated.

In our example difference becomes = 160

- Line 3 subtracts 180 from these results making every difference that is less than 180 a negative value, and every difference that is more than 180 will become less than 180.

Difference becomes = -20

That’s the answer we were looking for! Don’t worry if you don’t get it straight away! Try this formula with a couple of different values and eventually the penny will drop.
The final thing we need to do is add our difference to our original angle:

Code: Select all
  newAngle = (self.angles.z + difference);


Make sure these calculations are done before interpolating towards your newAngle and we are set. Or are we?

Nope. If you would run your script now, you would be in for a nasty surprise: The entity would rotate totally erratic and will not face its target. How is this possible?

Because CryEngine calculates in radians; not in degrees. So let’s quickly adapt our formula: (360° == 2*π rad)

Code: Select all
  local difference =((((newAngle - self.angles.z) % (2 * math.pi)) + (3 * math.pi)) % (2 * math.pi)) - math.pi;


Now that we correctly calculate the turning angle, the final thing that remains is actually setting our entity to this calculated angle.

Add the following line after the lerp:
Code: Select all
    self:SetAngles(self.angles);


Now when you go into the editor, reload your script and add the name of an entity in your scene to the properties of MyEntity and you will see MyEntity rotate towards the assigned entity. (Don’t forget to check Active too!)

Now move the target entity around and see how great it works! Congratulations! The hardest part is done.

The final code we have is:
Code: Select all
MyEntity = {
    Properties = {
        object_Model = "GameSDK/objects/props/industrial/trainyard/trains/locos/l_diesel/l_diesel.cgf",
        fRotSpeed = 3, --[0.1, 20, 0.1, "Speed of rotation"]
        entName = "",
        bActive = 0,
    },
    Editor = { Icon = "Checkpoint.bmp", },
    entID = "",
    angles = 0,
    pos = 0,
};

function MyEntity:OnInit()
    self:OnReset();
end

function MyEntity:OnPropertyChange()
    self:OnReset();
end

function MyEntity:OnReset()
   
    if (self.Properties.object_Model ~= "") then
        self:LoadObject(0, self.Properties.object_Model);
       
        if (self.Properties.entName ~= "") then
            self.entID = System.GetEntityByName(self.Properties.entName);
            self.angles = self:GetAngles(); --gets the current angles of MyEntity
            self.pos = self:GetPos(); --gets the current position of MyEntity
            self:Activate(self.Properties.bActive); --set OnUpdate() on/off

       else Log("Error: Target entity not set!"); end
    else Log("Error: Modelname not found!"); end
end

function MyEntity:OnUpdate(frameTime)
    self:FaceAt(self.entID, frameTime);
end

function MyEntity:FaceAt(ID, fT)
    local a = self.pos;
    local b = ID:GetPos();
    local newAngle = math.atan2 (b.y-a.y, b.x-a.x);   
   
    local difference =((((newAngle - self.angles.z) % (2 * math.pi)) + (3 * math.pi)) % (2 * math.pi)) - math.pi;
    newAngle = (self.angles.z + difference);
   
    self.angles.z = Lerp(self.angles.z, newAngle, (self.Properties.nRotSpeed*fT)); 
    self:SetAngles(self.angles);

end


Assignment: If you understand this, you should now be able to make the entity not only rotate on its z-axis but also its y-axis.

Good luck!
Last edited by jethroj on 14.10.2014, 13:47, edited 3 times in total.
User avatar Tataru
Regular Dev
Regular Dev
 
Posts: 449
Member since: 19.07.2013, 20:52
Likes: 57

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby Tataru » 21.01.2014, 23:18

Awesome stuff, I would like to see more. You made me curious about your grand plans, any details about them yet?
User avatar jethroj
Just getting started
Just getting started
 
Posts: 21
Member since: 07.02.2013, 16:02
Location: the Netherlands
Likes: 13

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby jethroj » 21.01.2014, 23:40

Thanks! And yes!

Cool that you're interested. I am working on an automated railway system that will have multiple trains, working signs etc.

A detailed plan can be found here: jethrojongeneel_lualine_onepage.pdf

It seems very far from what I have right now but I am positive I'll get there... eventually... I'm really happy with where I've come, as I am starting from scratch.

I'll happily continue making tutorials of what I learn during this... adventure?

Stay tuned!
User avatar Tataru
Regular Dev
Regular Dev
 
Posts: 449
Member since: 19.07.2013, 20:52
Likes: 57

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby Tataru » 22.01.2014, 00:27

Nice, by the way cwright already has a working train system made in Lua, if you have questions I'm sure he will be happy to help.
User avatar WetCode
Just getting started
Just getting started
 
Posts: 57
Member since: 15.01.2014, 23:27
Location: Norway
Likes: 9

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby WetCode » 29.01.2014, 23:44

This is great stuff keep it coming mate.
The only thing that was "unclear" was the frameTime veriable.
I assumed it was a global veriable, or was it. ? But again great Tutorial.
Cheers
User avatar CA3r8deP
Junior Dev
Junior Dev
 
Posts: 221
Member since: 23.10.2013, 03:42
Likes: 34

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby CA3r8deP » 03.03.2014, 10:40

Hi, I see you only do Z axis rotation. If you manage to find a way to get it xyz it would so much be appreciated if you show me or us.
thanks for this tutorial no matter.
User avatar jethroj
Just getting started
Just getting started
 
Posts: 21
Member since: 07.02.2013, 16:02
Location: the Netherlands
Likes: 13

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby jethroj » 10.03.2014, 11:31

Thanks for the feedback guys!

WetCode wrote:This is great stuff keep it coming mate.
The only thing that was "unclear" was the frameTime veriable.
I assumed it was a global veriable, or was it. ? But again great Tutorial.
Cheers

frameTime is a variable that contains the time it took the engine to render a frame. This time is always the first parameter of the OnUpdate() function. It's up to us to use this or not but it will always be sent, so yes, it is global.

CA3r8deP wrote:Hi, I see you only do Z axis rotation. If you manage to find a way to get it xyz it would so much be appreciated if you show me or us.
thanks for this tutorial no matter.


The following code will make it rotate on the Y-axis as well. This should be sufficient in making the entity face the target entity, even if it's on a different height(Z).

Code: Select all
function MyEntity:FaceAt(ID, frameTime)
   local a = self.pos;
   local b = ID:GetPos();
   local C = math.sqrt((b.y-a.y)^2 + (b.x-a.x)^2);
   
   -- Calculate Rotation on Z-axis
   local newAngleZ = math.atan2 (b.y-a.y, b.x-a.x);
   local differenceZ =((((newAngleZ - self.angles.z) % (2 * math.pi)) + (3 * math.pi)) % (2 * math.pi)) - math.pi;
   newAngleZ = (self.angles.z + differenceZ);
   
   -- Calculate Rotation on Y-axis
   local newAngleY = math.atan2 (b.z-a.z, C);
   local differenceY =((((newAngleY - self.angles.y) % (2 * math.pi)) + (3 * math.pi)) % (2 * math.pi)) - math.pi;
   newAngleY = (self.angles.y + differenceY)*-1;
   
   -- Set rotation
   self.angles.z = Lerp(self.angles.z, newAngleZ, (self.Properties.fRotSpeed*frameTime));   
   self.angles.y = Lerp(self.angles.y, newAngleY, (self.Properties.fRotSpeed*frameTime));
   self:SetAngles(self.angles);
end


Ask away if anything is unclear!
User avatar cwright
Producer
Producer
 
Posts: 2465
Member since: 11.08.2004, 12:38
Likes: 98

Re: [Lua Tutorial] Smoothly rotating one entity to face anot

Postby cwright » 04.05.2014, 12:23

Hi jethroj,
As Tataru mentioned, I've also developed a drivable train system programmed with Lua, and I've been working with Tataru. The train system has many features, and it includes an editor that can be used to create and edit train tracks. One test track was more than 100 km long.
If you'd like to discuss this and compare notes etc, why not pop around to my Flyable Aircraft thread?
viewtopic.php?f=280&t=25876
Despite its name, the Lua code is for ships, submarines, spacecraft - and trains. I'll be making a public release for the free SDK by the end of June.
Regards,
Chris