[Lua] Introduction to YieldLongTask()

Come in here to talk about your sky-net style world-destroying super bots!

Moderator: Defcon moderators

User avatar
Montyphy
level5
level5
Posts: 6745
Joined: Tue Apr 19, 2005 2:28 pm
Location: London, England

[Lua] Introduction to YieldLongTask()

Postby Montyphy » Wed Feb 24, 2010 12:16 am

Not sure how much you already understand so I'll just act like you know nothing.

Defcon simulates a game in a server client setup, even when you're playing by yourself. This means the client will frequently need some CPU time in order to synchronise and update the game state. If your bot hogs the CPU for too long the game will fall out of sync and complain of a connection problem.

This is where yield comes in, or more correctly coroutines/generators. Yield allows you to break out of a loop or function to return control to the caller while still preserving the state of the loop or function so that when control is given back to it, it resumes from where it left off. This means during long chunks of code or big loops the bot can give control back to the client, allowing it to sync and update the game state, then resume doing whatever it was doing before.

In order to make sure all of the code gets executed when a yield has been used it is registered with a controller. The job of the controller is maintain a list of active processes/routines (portions of code). When this controller is given control it loops through the registered processes, allowing them to execute until they yield or finish. Whenever a process completes the controller removes it from its list. This is exactly what WorkOnLongTasks() does and by placing it at the beginning of OnTickReal() you ensure it can have frequent control of the CPU in order to do its job.

To register a process with WorkOnLongTasks() you call StartLongTask(), with the code of the process passed as an anonymous function (i.e. the function has no name). This doesn't cause the passed code to be executed, it just registers it with the controller. The code doesn't begin to be executed until WorkOnLongTasks() is called.

Hopefully it should be pretty obvious that the function YieldLongTask() yields control back to WorkOnLongTasks().

One thing to pay attention of is that WorkOnLongTasks() doesn't yield during its looping of active processes. That means if you register a lot of processes you will effectively create a resource hogging loop.

Now how to make use of all this. Well, to start, here's a simple function to print the values 5 to 0 then 0 to 5:

Code: Select all

function OnTickReal()
   --only want to do this once
   if GetGameTick() == 60 then
      local loopsize = 5
      for i=loopsize,0,-1 do
         SendChat(i .. "    loop 1", "public")
      end

      for i=0,loopsize do
         SendChat(i .. "    loop 2", "public")
      end
   end
end


output:

Code: Select all

5    loop 1
4    loop 1
3    loop 1
2    loop 1
1    loop 1
0    loop 1
0    loop 2
1    loop 2
2    loop 2
3    loop 2
4    loop 2
5    loop 2


That's fine since loopsize is nice and small, meaning a small number of loops. However, if you increase it to something like 300 the bot will hog the CPU for too long, thus causing the game to have a connection problem. Time to add in some yields. Your first reaction might be register these two processes:

Code: Select all

function OnTickReal()
   WorkOnLongTasks()
   
   --only want the processes registered once
   if GetGameTick() == 60 then
      local loopsize = 5
      
      StartLongTask(function()
         for i=loopsize,0,-1 do
            SendChat(i, "public")
            YieldLongTask()
         end
      end)

      StartLongTask(function()
         for i=0,loopsize do
            SendChat(i, "public")
            YieldLongTask()
         end
      end)
   end
end


output:

Code: Select all

5    process 1
0    process 2
4    process 1
1    process 2
3    process 1
2    process 2
2    process 1
3    process 2
1    process 1
4    process 2
0    process 1
5    process 2


As you can see, the output is quite different. This is because two processes are registered with the controller, each yielding at the end of each pass through the loop and sequentially being given control.

If you wanted to achieve the same effect as the first code you would need to do:

Code: Select all

function OnTickReal()
   WorkOnLongTasks()
   
   --only want to do this the once
   if GetGameTick() == 60 then
      local loopsize = 5
      
      StartLongTask(function()
         for i=loopsize,0,-1 do
            SendChat(i .. "    loop 1", "public")
            YieldLongTask()
         end

         for i=0,loopsize do
            SendChat(i .. "    loop 2", "public")
            YieldLongTask()
         end
      end)
   end
end


Code: Select all

5    loop 1
4    loop 1
3    loop 1
2    loop 1
1    loop 1
0    loop 1
0    loop 2
1    loop 2
2    loop 2
3    loop 2
4    loop 2
5    loop 2


This will now quite happily deal with huge values of loopsize. However, it's slightly wasteful. The bot is only given control every 100ms and all this function does when given control is output a single value. To speed things up we can do this:

Code: Select all

function OnTickReal()
   WorkOnLongTasks()
   
   --only want to do this the once
   if GetGameTick() == 60 then
      local loopsize = 300
      local yieldfreq = 5
      
      StartLongTask(function()
         for i=loopsize,0,-1 do
            SendChat(i .. "    loop 1", "public")
            
            if (i % yieldfreq == 0) then
               YieldLongTask()
            end
         end

         for i=0,loopsize do
            SendChat(i .. "    loop 2", "public")
            
            if (i % yieldfreq == 0) then
               YieldLongTask()
            end
         end
      end)
   end
end


If you execute this you'll see it completes much faster (approximately 5 times faster). yieldfreq has to be carefully selected otherwise you end up with the original problem of hogging the CPU for too long. You should also make sure that yieldfreq is small enough to be a modulo of i at some point otherwise the yield will never be reached.

As already mentioned, while a single big process can cripple the game you can also cripple the game with loads of small processes. Like so:

Code: Select all

function OnTickReal()
   WorkOnLongTasks()
   
   --only want to do this the once
   if GetGameTick() == 60 then
      local loopsize = 5
      
      -- for each loop create a process
      for j=0,300 do
         StartLongTask(function()
            for i=loopsize,0,-1 do
               SendChat(i .. "    loop " .. j, "public")
               YieldLongTask()
            end
         end)
      end
   end
end


This registers 300 very simple processes that yield frequently and this becomes a problem because the controller WorkOnLongTasks() doesn't yield while looping through each process. Therefore its prevents the client from updating/synchronising. Because of this you have to make sure you don't create too many processes.

Now the problem of what is considered too long or too big. Well, it's pretty much trial and error. Try write your code so you can easily modify and control the yield points and frequency. You could find out a rough idea how much your bot can do in one tick by tweaking the very first piece of code until it causes a connection problem, but it does really depend on the complexity of the instructions.
Uplink help: Check out the Guide or FAQ.
Latest Uplink patch is v1.55.

Return to “AI Bots”

Who is online

Users browsing this forum: No registered users and 4 guests