Skip to content

TAS Movie Script Tutorial

Eddio0141 edited this page Apr 10, 2023 · 16 revisions

Prelude

The movie scripts accepted for UniTAS are lua 5.2 scripts, and this tutorial won't cover how lua works, but it should be simple to understand how it works with other tutorials on lua

A simple TAS

To start off, we'll make a simple TAS that just presses the space bar every frame for 3 seconds

MOVIE_CONFIG = {
    fps = 60
}

for i = 1, 30 * 3 do
    key.hold("Space")
    movie.frame_advance()
    key.release("Space")
    movie.frame_advance()
end

Let's break this down

MOVIE_CONFIG = {
    fps = 60
}

This is the configuration for the movie, and it's required to be defined before the first movie.frame_advance call

You can check out what's possible to configure in the Movie config page


key.hold("Space")
movie.frame_advance()
key.release("Space")
movie.frame_advance()

This is the main part of the script, and in this case, key.hold and key.release are the methods that are used to hold and release a keyboard key

and movie.frame_advance is the method that is used to advance the frame in the movie


Because lua allows you to bind a function to a variable, you can set up your own shortcuts for the methods

adv = movie.frame_advance
hold = key.hold

-- those are now valid!
hold("Space")
adv(100)

If your script looks like this

-- first frame
movie.frame_advance()
-- second frame

The first frame is ran BEFORE the movie.frame_advance() call, and the second frame is ran AFTER the movie.frame_advance() call, in total, the script will run for 2 frames

If you want to check out what other methods are available, you can check out the TAS Movie API page

Rendering a TAS as a video

Before we continue, let's render our TAS as a video

To render a TAS, you need to have ffmpeg installed, and you need to add it to your PATH or place the ffmpeg executable in the same folder as the game executable

Now, to render a TAS, you can call movie.start_capture() and movie.stop_capture() to start and stop the capture

MOVIE_CONFIG = {
    fps = 60
}

movie.start_capture()
-- your TAS here
movie.stop_capture()

This will render the TAS as a video, and the output video by default is placed in the game folder

You don't need to call movie.stop_capture() if you want to stop the capture at the end of the script like this

MOVIE_CONFIG = {
    fps = 60
}

movie.start_capture()
-- your TAS here

You can also configure the render settings by using named arguments

movie.start_capture{
    fps = 60,
    width = 1920,
    height = 1080,
    path = "hl3 full TAS.mp4"
}

You can check out what's possible to configure in the API page

A TAS with concurrent method

Now let's make a TAS that will hold space if the fps goes below 30, and release space if the fps goes above 30

MOVIE_CONFIG = {
    fps = 60
}

function concurrent_method()
    if env.fps < 30 then
        key.hold("Space")
    else
        key.release("Space")
    end
end

concurrent.register(concurrent_method)

movie.frame_advance(30)
env.fps = 10
movie.frame_advance(30)

Let's break this down

function concurrent_method()
    if env.fps < 30 then
        key.hold("Space")
    else
        key.release("Space")
    end
end

This is the concurrent method, and it will be ran concurrently with the main script coroutine

This itself wouldn't do anything, so we need to register it


concurrent.register(concurrent_method)

This will register the concurrent method, and it will be ran before the main movie.frame_advance() call

Registering the method will run it over and over again every env() call, unless manually stopped or the script is stopped

By default, registering a method will immidiately run it on that line, but you can change the behaviour by passing a second argument


movie.frame_advance(30)
env.fps = 10
movie.frame_advance(30)

To finish off our code, here we're frame advancing 30 frames, and setting the fps to 10, and frame advancing 30 frames again

This makes it so the concurrent method will hold space at the second movie.frame_advance() call

A concurrent method will run indefinitely every env(), unless manually stopped or the script is stopped


Purpose of the concurrent method is to automate inputs in the background of the TAS

As a real world example, you can have a concurrent method that will press the jump button if the player lands on the ground, making it so the player will jump automatically


You can also register a method that will run after the main movie.frame_advance() call

concurrent.register(concurrent_method, true)

Unlike the default behavior, registering a method with the second argument being true will not immidiately run it on that line


What if you want the concurrent method to run only once?

In that case, you can use concurrent.register_once, which has the same arguments as before

concurrent.register_once(concurrent_method)

Advanced stuff

Main scope

The main scope is a coroutine in disguise, where the movie.frame_advance() call is the coroutine.yield() call, and normally you can't yield if its not in a coroutine

However the main scope is secretly wrapped in a method as a coroutine like this

-- main script
key.hold("Space")
movie.frame_advance()
key.release("Space")
movie.frame_advance()

Into this

return function()
    -- main script
    key.hold("Space")
    movie.frame_advance()
    key.release("Space")
    movie.frame_advance()
end

You can change this behavior by setting MOVIE_CONFIG.is_global_script to true

MOVIE_CONFIG = {
    is_global_script = true
}

This will make the main script not wrapped in a coroutine, and you must instead return a function that will be ran as the main script like this

return function()
    -- main script
    key.hold("Space")
    movie.frame_advance()
    key.release("Space")
    movie.frame_advance()
end

Concurrent methods

Concurrent methods are also coroutines, and you can yield in them

If you have a method like so registered as a concurrent method on pre-advancing

function concurrent_method() end

You can think of the script running like this

concurrent_method()
movie.frame_advance()
concurrent_method()
movie.frame_advance()

However if you have a method like so registered as a concurrent method on post-advancing

function concurrent_method() end

concurrent.register(concurrent_method, true)

You can think of the script running like this

movie.frame_advance()
concurrent_method()
movie.frame_advance()
concurrent_method()