-
Notifications
You must be signed in to change notification settings - Fork 2
TAS Movie Script Tutorial
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
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
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
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)
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 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()