Now that we understand threads, spawns, and loops, it is time to learn about a more efficient and easy method to handle timed loops. Your game should try to avoid threads and spawns if possible and only work with the built-in ticker system. This tutorial will cover the built-in ticker system which allows any object to be added to an internal game loop and repeatedly execute code after a short delay. We will go over some ways to handle a game loop with the ticker system and also throw in some mouse events and basics of the appearance system.
We will continue from the project we created in the previous tutorials. If you are just now jumping in, go to the previous tutorial and create a project to match the project at the bottom of the page.
Our last tutorial left our project in a bit of a mess, so we will clean it up a bit to start. Open up our previous
FirstProject project, go into the
main code file and remove the
onNew and
onLogin code found under our
Mob/Player type. The below code is the code you should delete.
onNew()
var s = spawn(5000)
this.setLoc(1, 1)
this.setLoc(this.xCoord + 5, this.yCoord + 5)
Event.interruptThread(s)
onLogin()
var mes = this.myFunction(4, 5, false)
alert(mes)
var ob1 = {'a': 1, 'b': 2}
var ob2 = {'a': 2, 'b': 3}
var ob = {'a': ob1, 'b': ob2}
foreach (var o in ob)
alert(o.a + o.b)
Without the need for our custom
myFunction function, we can also delete it. Find the below code in the same file and delete it.
function myFunction(pX, pY, pBool)
if (!pBool || pX > 3)
if (pX !== 3 && pY !== 3)
this.setLoc(pX, pY)
else if (pX <= 3)
this.setLoc(3, 1)
else
this.setLoc(1, 1)
return 'It worked!'
else
return 'It did not work!'
We will start with the ticker system and the basics of game loops. The Vylocity engine has a game loop in the background that always runs, much like a
thread from the previous tutorial. The ticker system executes every 10 milliseconds, so the same as
thread(10). Each iteration of the loop, it loops through ticker objects and executes that Object's
onTick event function and does whatever code is inside of it for that Object. In order for an Object to be added to the ticker, we must call
Event.addTicker with the Object we wish to add. Once added, that Object will have it's
onTick event function executed every 10ms (unless otherwise specified) until we remove it.
So let's replace our current thread that we gave to our monster with the ticker system. In the
main code file find our
Mob/Monster code and replace its
onNew code and add the
onTick event under it like the code below.
Mob
Monster
onNew()
this.dir = 'east'
Event.addTicker(this)
onTick()
if (this.xCoord <= 3)
this.dir = 'east'
else if (this.xCoord >= 23)
this.dir = 'west'
this.stepDir(this.dir, 2)
So what is our code doing. Under the monster's
onNew event we add the monster to the ticker system, then each tick we execute our old thread code which moves the monster east and west. With this system we do not have to create and track a new thread and we also group it with all the other events for the most efficient method possible. There are many more benefits to using the ticker system that we will learn more about below. Build and run the project to see that everything works the same as before.
Our code is doing the exact same thing it was doing with our custom thread (but with simpler and less confusing code), which is good. But now we have more power and control. One of the things that gives us more control is the ticker settings object. The
Event.addTicker function takes in an optional second parameter which allows us to give the ticker for our Object some special instructions such as how often
onTick is called, how often our tracker resets, and if our Object should be removed from the ticker once it reaches its reset. The last one basically allows us to add the Object to the ticker for a specified amount of time before removing it.
So let's make use of this ticker settings object. Change the monster's
onNew function to look like the code below.
Mob
Monster
onNew()
this.dir = 'east'
Event.addTicker(this, {'delay': 100})
The above code adds the
{'delay': 100} object to our
Event.addTicker call. Delay is the amount of ticks to wait before executing the Object's
onTick event. We set it to 100 which means 99 ticks will happen without anything happening to our monster, then on the 100th tick, we execute our code. Because the internal loop runs at 10ms, 100 ticks would be 1000ms (add a zero) or 1 second. So every second our monster makes one step. Build and run to see it in action.
Before we take a look at the other values the ticker settings object can have, we will learn about two more event functions involved with the ticker system. When an Object is added to the ticker it has its
onTickerAdd event function called and when it is removed it has its
onTickerRemove event function called. These events allow us to do special things at the start and end of our action loops. For the sake of testing these event functions, we will learn a variable involved with the appearance system early (we will cover other variables and information on the appearance system later in the tutorial). That variable is the
color variable. Each Diob has a
color variable which allows us to change the color of the icon that belongs to the Diob without making a new icon. Assuming you did not make the color of your monster's icon just the color red, we should be able to tell when we set our monster's color to red and back again as we add and remove it from the ticker.
Add the two following event functions to the
Mob/Monster type.
Mob
Monster
onTickerAdd()
this.color = 'rgb(100, 0, 0)'
onTickerRemove()
this.color = ''
The above code will overlay a solid red color over our monster's icon when it is added to the ticker, then remove the red once it is removed from the ticker. The
color variable can take in rgb (red, green, blue values from 0-255, where 0 is black and 255 is white), hex codes, or a color matrix (this requires its own tutorial).
If we build and run the project now, our monsters will just be a solid red color. In order to see the color change, we will need to remove the monster from the ticker so that it reverts back to normal. So let's change our ticker settings object so that our monster's ticker tracker resets and removes from the ticker. Change our
Event.addTicker call to be
Event.addTicker(this, {'resetAt': 500, 'resetEnd': true}) so that our monster's
onNew function looks like the code below.
Mob
Monster
onNew()
this.dir = 'east'
Event.addTicker(this, {'resetAt': 500, 'resetEnd': true})
This will have the internal ticker counter (each internal tick will increase the counter) reset after 500 ticks. So with each tick being 10ms, after 5 seconds our counter will reset. With our new
resetEnd value set, instead of only resetting, our ticker will end (the monster will be removed from the ticker as if
Event.removeTicker was called). So our monster will be removed after 5 seconds and should stop moving and our new
onTickerRemove event should be called, which removes the red color. Build, run, and see for yourself.
When getting down to the basics of how a game works, a game loop is at the core. Especially for the AI and other automated events that make the game a game. Even the player's actions can be interpreted by the game loop in order for the action to happen. Our current setup only allows us to do a block of code every 10ms (or whatever delay we specify) but games can get more complex. An example would be if our monster had different attacks or actions that happen at varying rates of delay. The best solution to this problem is to have our ticker execute at the lowest possible speed (if we have code that needs executed every 1000ms and then every 5000ms, 1000ms is the smallest amount and covers both cases so our delay can be 100 and the 5000ms block will execute after 5 onTick calls) and then use if statements to check how much time has passed since last reset or last execution of our if statement so that different code executes at different times.
In order to handle our different executions for the same monster, we will need to make use of the argument passed into the
onTick event function that tells us what the ticker tracker count is at. Each time the internal ticker fires and our Object is in the ticker, this value is increased by one. We can use this variable to determine what action we should take. Change our
Mob/Monster's
onTick event function code to look like what is below.
Mob
Monster
onTick(pT)
if (this.xCoord <= 3)
this.dir = 'east'
else if (this.xCoord >= 23)
this.dir = 'west'
this.stepDir(this.dir, 2)
if (!(pT % 150))
this.setTransition({'scale': 2}, -1, 500)
this.setTransition({'scale': 1}, -1, 500, true)
We have three new lines here, and they are pretty complex compared to what we have been working with, so let's break it down as best as possible. Do not worry about understanding
setTransition too much right now, it's a pretty complex function with lots of options which will be covered better in a future tutorial. But the basic idea of the function is it allows you to transition a variable over time from one value to another (in this case, a very smooth animation of scale). There are a few appearance variables that you are allowed to pass into this function in order to transition it from one value to another, but in our case we are making use of
scale, which is basically making our monster go from normal size to double its visual size (the icon) over 500ms. Our second
setTransition returns the scale back to 1 (normal) over another 500ms. The last parameter of our second
setTransition is the queue toggle, which basically means add this transition to a queue to be executed after any current transitions are complete. You might also notice that our
onTick event now has an argument called
pT which holds our ticker counter.
In order for our
setTransition codes to be reached, it has to get past our if statement. So what is happening in
if (!(pT % 150))? Well, the % operation gives you the remainder of division between the two numbers surrounding it. So 390 % 100 would be 90 for example. We divide our monster's ticker count by 150 and then check if the remainder is 0, which means 150 divides into our current counter perfectly, so 150 ticks have happened (1500ms). Breaking everything down, every 1500ms our monster's scale will go to 2 over 500ms then back to 1 over another 500ms (for 500ms it will stay normal before going to 2 again).
There are a couple things to consider with what we have created here. Because we set our
resetAt ticker count reset to 500, our third scale (three 150s) would happen at 450 and our counter would reset at 500 (assuming we didn't have
resetEnd set) so 50ms of tracking would be lost. Also, the first
onTick call will have
pT set to 1, which has a remainder when divided with 150, so the first tick when the ticker starts will not execute anything. So if you want the code to execute when the ticker starts and then every 150ms after, you will need to subtract 1 from
pT so that the first tick is 0 and it makes its way through our if statement. Or you could put the code into a custom function and call it under the if statement as well as in
onTickerAdd, whatever works best for you.
Build and run the project to see our creation in action.
While on the subject of the ticker, let's give our
World a ticker that we can use for general purposes.
We will find out more about this over time, but the
World static object has many useful functions we can call to interact with the world. One of those functions we will need to know about for this next code is the
getDiobs function, which returns an array of every Diob in the world. We will go over it a bit more below.
Find our
World type in the code and give it the events in the code below.
World
onNew()
Event.addTicker(this)
onTick(pT)
if (pT === 1)
foreach (var d in World.getDiobs('Mob', true))
d.text = 'hello'
else if (pT === 50)
foreach (var d in World.getDiobs('Mob', true))
d.text = ''
We are introduced to two new things here (one previously mentioned).
World/getDiobs gives us an array (a list) of all diobs in the world, but because we passed in two parameters, we narrow down what it should return a bit. The first parameter is the type to look for. We put
'Mob' for our type. Normally this would mean only return diobs that have the exact type of 'Mob' but we also put
true for the second parameter, which means include child types as well, so anything that starts with 'Mob' (including our 'Mob/Monster' type) will be returned. Then our
foreach loops each thing in the array returned by our
getDiobs call, referencing each diob with the variable
d, which we use to set the
text of it to equal the 'hello' string. This is all assuming our
pT argument is equal to 1 (the first call). So what does
text do? Well it is another appearance variable which displays text above the diob on the map. In our case, each 'Mob' in the game will have the word 'hello' drawn on it. Our second block of code basically clears the text of every Mob in the game after 500ms of having it set (50 ticks).
Note:
World/getDiobs is pretty expensive because it looks through every diob in the game and your game could have hundreds of thousands of them, or more. Doing this a lot every second could slow down the game. There are most likely better solutions for anything you are trying to implement that is more efficient than using
World/getDiobs. Some of those things will be learned in future tutorials.
The amount of things you can accomplish with the ticker system is infinite. Majority of your automated game logic will probably happen inside of the ticker system.
Most other code will probably happen from built-in engine events like mouse events (for games being played on the computer), among other things. So let's take a look at how to manage mouse events.
There are many mouse event functions. We will list them all here and give a brief description, but we will only make use of a few of them.
Let us start by applying a few new appearance variables to our player as we mouse over it and then return the values back to the defaults after we mouse off of it. Add the following event functions and code to our
Mob/Player type code.
Mob
Player
onMouseEnter()
this.scale = 2
this.alpha = 0.5
this.angle = 0.5
onMouseExit()
this.scale = 1
this.alpha = 1
this.angle = 0
Nothing too complex is happening here, but let's go over it. When a client (you) mouses over a
Mob/Player Diob,
onMouseEnter is executed, which sets the diob's
scale to 2,
alpha to 0.5, and
angle to 0.5. Then when the client moves their cursor off of the Diob,
onMouseExit is executed and
scale returns to 1,
alpha to 1, and
angle to 0.
We introduced two new appearance variables called
alpha and
angle. We can change how transparent our Diob's icon is with the
alpha variable. It is a value between 0 and 1 where 0 is completely transparent (it cannot be visually seen) and 1 (the default) is completely visible. We set it to 0.5 which is 50% visible. Then we can rotate our icon with the
angle variable (which is a value in radians). We set it to 0.5 which is about 28.6 degrees.
Both mouse event functions we just learned have three arguments that gives us additional information to work with. The first argument is a reference to the client that triggered the mouse event (which will be you, since you're the only one playing), the second is the x position over the Diob the mouse interacted at, and the third is the y position over the Diob the mouse interact at.
Most of the mouse events have similar arguments, but let's make use of the ones that belong to
onMouseMove and add the following mouse event function to our
Mob/Player type code.
Mob
Player
onMouseMove(pClient, pX, pY)
this.setAnchor(pX / this.width, pY / this.height)
We are introduced to a new appearance variable here called
anchor, which is our pivot point where all our transformations (scale, angle, so on) and such happen. The default value is 0.5, which means everything happens from the center of our icon. Our code grabs the x and y values passed into the event function (named
pX and
pY respectively) and gets the percentage of the icon that it covers. Origin of everything starts from the top-left corner, so an anchor of 0 means top-left and an achor of 1 means bottom-right. Our x and y values also start from the top-left as well, so mousing over the Diob in the top-left corner would return 0 and mousing over the bottom-right could return 31 (because our size is 32x32 and we start at 0, so 31 is the max).
There are many more mouse event functions left to talk about, but we will only test out one last one called
onMouseClick. Before we do so, here is a list of the rest that were not covered here, and a brief description of what they handle.
Diob/onMouseDblClick() is executed when double clicking the Diob,
Diob/onMouseDown() is executed when the mouse is held down on the Diob,
Diob/onMouseUp() is executed when the mouse is released over the Diob,
Diob/onMouseWheelScrollUp() is executed when the mouse scroll wheel is scrolled upward while over the Diob, and
Diob/onMouseWheelScrollDown() is executed when the mouse scroll wheel is scrolled downward while over the Diob.
So let's add a
onMouseClick mouse event to our
Mob/Player type code that just simply executes code we've seen before.
Mob
Player
onMouseClick()
this.setLoc(1, 1)
With this, any time we click on our player mob it will return to 1,1 coordinates on the map. Feel free to build and run to test it out.
Now that we know about all of the mouse event functions, it is time to learn about the Client versions of them. Every mouse interaction in the game will trigger the proper mouse event for a Client, even if no Diob is involved. The Client and all Diobs have the same mouse event functions, but the Client versions have different arguments.
We wont go over all of the mouse event functions again, but we will add a Client version of
onMouseDown just to see it in action and get another chance to use
setTransition. Anywhere in the
main code file enter the code found below. Just do not interrupt any of our other type code. If you are worried about doing that, the safest bet is to add a new line at the top or bottom of the file and enter it there.
Client
onMouseDown(pD)
if (!pD) return
pD.setTransition({'angle': Math.PI}, -1, 1000)
pD.setTransition({'angle': 0}, -1, 1000, true)
Now any time we mouse down (first part of a click) on a Diob, it will rotate to 180 degrees (
Math.PI is a static variable that is equal to the value of pi, which is ~3.14 radians or half a circle) and then back to 0. The purpose of the first line of code might stick out (along with the fact that you can put a single line of code after something like an if statement). What we do in the first line is check to see if the argument
pD has any value (meaning it is set to something). An if statement can return true if you only check a variable without any expressions and it has a value that is set and not 0, null, or undefined. Because of this fact, we check if
pD is set and if it's not we exit the event function to avoid errors. Our Client mouse events can be triggered even if we aren't interacting with a Diob, which means
pD can be undefined, so we need to make sure it is not undefined before trying to use a function that is unable to be called.
Build and run the project to see the code in action.
There are two more things that go along with the mouse system which we will talk about but not make use of.
Every Diob has a
mouseOpacity variable which determines how the mouse interacts with it. The default value of the variable is 2, which means the mouse interacts with the physical box of the Diob. If this variable was set to 1, it would use the icon as the mouse interaction, but because both our physical box and our visual icon should match up at 32x32, this variable setting does not really matter to us right now. However, if we set it to 0, no mouse events would be triggered on the Diob. This could allow you to have something visual but not take up the mouse interaction when it is above something else.
The last thing that goes with the mouse system is of course the ability to change the mouse cursor. For the purpose of testing, we will throw some code in to make our cursor look like our player. Add the following code to our
Client type code.
Client
onConnect()
this.setMouseCursor(this.mob)
We have a new event function here called
onConnect, which basically calls when a client first connects to the world. We use it for the purpose of setting our cursor as soon as possible, but not before our player has been created and given an icon (using
Client/onNew() would mean we do not have a player yet).
So, we have now learned how to use the ticker system, how to manage mouse events, and learned a bit about some Diob variables associated with the appearance system. The next tutorial will go more into depth on Object types, inheritances, and static objects.
This tutorial is fairly packed, so if you had any issues or need to double check anything, be sure to check out the source below.
If you followed everything in this tutorial, the source of your project should look something like this.
https://vylocity.com/ide/Vylocity/FirstProject5/
References