Skip to content

Tutorial

rech edited this page Jun 15, 2026 · 1 revision

ArcCreate Scenecontrol Tutorial

Welcome to Scenecontrol Tutorial Documentation!
This document is a tutorial designed to guide newcomers through the ArcCreate scenecontrol API. This will not cover all features, but will prepare you with enough knowledge to explore the rest on your own. Please check the Reference documentation for more information.

As usual, scripting requires knowledge of Lua, and so this guide will assume some familiarity with it. If you're new or would like a refresher on the language, please refer to the official tutorial.

What is Scenecontrol?

The scenecontrol system is built around the concept of Channels. This only requires a single execution of Lua code, and the result can be exported to other formats that do not require Lua.

With this in mind, here are reasons to prefer the channel-based system:

  1. Completely deterministic: Channels define values as a function of time. The script runs once to build the channel graph, then the engine evaluates it during gameplay.
  2. Transparent: You have complete control over the system, which allows for faster scripting.
  3. Collaboration: It's easier to use and combine other people's scripts!

Getting started

Tools you will need:

  • ArcCreate editor (desktop, Windows/macOS/Linux)
  • A sufficiently good IDE / text editor such as VSCode is recommended

Part 1. Built-in commands

Let's start from the very basic, and use the built-in commands. We'll first familiarize ourselves with the new Scenecontrol editing window.

The aff command syntax

The basic syntax for scenecontrol commands in the .aff file:

scenecontrol(timing, scenecontrolType, argument0, argument1, argument2, ...);

For example:

scenecontrol(1000, trackdisplay, 1000, 127);
  • timing (generally) signifies when the effect takes place.
  • scenecontrolType defines the type of this command.
  • argument0, argument1,... define all the arguments, which essentially dictate exactly how the command will operate.

It's also possible to nest scenecontrol commands inside a timinggroup, which may or may not make a difference depending on the command type.

Using the built-in commands

ArcCreate supports these command types out of the box:

scenecontrol(timing, trackdisplay, duration, changeToAlpha);
scenecontrol(timing, hidegroup, duration, changeToAlpha);
scenecontrol(timing, enwidenlane, duration, changeToState);
scenecontrol(timing, enwidencamera, duration, changeToState);
  • trackdisplay changes the transparency (or alpha) of the main track.
  • hidegroup changes the transparency of all notes within the same timing group of the scenecontrol command.
  • enwidenlane adds 2 more lanes to both sides of the main track (used in Pentiment, Arcana Eden and Testify).
  • enwidencamera moves the camera to support enwidenlane.

Both timing and duration arguments are in milliseconds. changeToAlpha takes in a value from 0 to 255. changeToState takes in either 0 (disable) or 1 (enable).

The editing window

Open the chart in ArcCreate, then click the star icon on the right side. You'll be greeted with the scenecontrol editing window.

On the top, you have:

  • The Scenecontrol Type dropdown. Select the type to work with here.
  • The Switch Field button. This toggles between separated fields for each argument and one combined field.
  • The Refresh button. This reloads the script and refreshes the list.

In the middle is the list of events. Right now it's probably empty because your chart does not contain any event corresponding to the currently selected scenecontrol type. Add one by pressing the "+" button on any row.

From here you can change the arguments to your liking. Note that the window only shows scenecontrol events included in the currently active timing group.

That's it for the basics! Now we'll start diving into the scripting part.

Part 2. Scripting

2.1. Project structure

To define your own scenecontrol types, start by navigating to your project folder (the one that includes the .arcproj file, the Audio folder, and your music/jacket files). Create a folder named Scenecontrol (case sensitive), and within it create a file named init.lua.

Scenecontrol/init.lua is the script that's run by default upon loading your chart file. The Scenecontrol folder is also where you'll put all of your asset files (images and such) to be read by the script(s).

For advanced users: you can also use require to split your Lua code into multiple script files. The file names are not important (except for the default init.lua of course), as long as they are in the Scenecontrol folder.

Let's open the newly created lua file and start coding! However, for now we won't actually be defining our own scenecontrol type just yet, and focus on a concept much more fundamental: Controllers and Channels.

2.2. Controllers & Channels - Level 1

Level 1 of Controllers and Channels will prime you to take anything that already exists within the scene and manipulate it to your liking!

Controllers are where your custom Lua code will interact with in order to control objects in the scene. For example, you'll interact with a track controller to control the track, a sprite controller to control a 2D image sprite object, and so on.

However, instead of modifying the properties directly, you'll be doing it indirectly through Channels.

All channels have one job in common: they answer the question:

"It's now X milliseconds into the song. What's the value right now?"

Each property of a controller will have a channel attached to it, which will determine how the property will change at any point in time.

Let's look at a concrete example. We'll begin by defining a controller. Let's grab an internal controller by using Scene:

-- init.lua
local controller = Scene.track -- Grabs the track controller

Set the controller's position to somewhere else:

-- init.lua
...
controller.translationX = 1
controller.translationY = 2
controller.translationZ = 3

Click the Refresh button on the scenecontrol edit window. This will reload your script, which told ArcCreate to move the track to the new position located at (1, 2, 3).

Here's a detailed explanation of what just happened:

  1. number 1, 2, 3 automatically casts into Channel.constant().
  2. Channel.constant(value) defines a channel that will always return the value you passed in, regardless of time.
  3. You took 3 of those channels and assigned them to 3 properties of a controller.
  4. Now the controller will update its values based on the set channels, which we defined to be constant values of 1, 2 and 3.

But what if we want the object to move over time? For that, Channel.constant() simply won't do. Entering: Keyframe channels.

A keyframe channel is defined by a series of Keys. If you have ever animated anything before then this concept will be familiar to you. A key is a point in time with a specific value, and the channel interpolates between them.

-- init.lua
local channel = Channel.keyframe()
channel.addKey(0, 0)
channel.addKey(1000, 1)
channel.addKey(2000, 0)
controller.translationX = channel

You could also write it more concisely:

controller.translationX = Channel.keyframe().addKey(0, 0).addKey(1000, 1).addKey(2000, 0)

You can reuse channels! The same channel can be assigned to multiple properties, and changing one channel will change all properties reading from it.

local channel = Channel.keyframe().addKey(0, 0).addKey(1000, 1).addKey(2000, 0)
controller.translationX = channel
controller.translationY = channel
controller.translationZ = channel
-- even completely unrelated property is fine
controller.colorR = channel

Refresh and see the result. The track should move linearly to another position from 0ms to 1000ms, then back from 1000ms to 2000ms.

If you don't want it to move linearly, use easings! Simply pass an additional string:

local channel = Channel.keyframe()
channel.addKey(0, 0, 'so')
channel.addKey(1000, 1, 'so')
channel.addKey(2000, 0, 'so')

You can also change the default easing when creating the channel:

local channel = Channel.keyframe().setDefaultEasing('so')
channel.addKey(0, 0)
channel.addKey(1000, 1)
channel.addKey(2000, 0)

Here's the list of all the easings supported. Each easing type can be written multiple ways.

Name Aliases
linear l
inconstant inconst, cnsti
outconstant outconst, cnsto
inoutconstant inoutconst, cnstb
insine si
outsine so
inoutsine b
inquadratic inquad, 2i
outquadratic outquad, 2o
inoutquadratic inoutquad, 2b
incubic incube, 3i
outcubic outcube, 3o
inoutcubic inoutcube, 3b
inquartic inquart, 4i
outquartic outquart, 4o
inoutquartic inoutquart, 4b
inquintic inquint, 5i
outquintic outquint, 5o
inoutquintic inoutquint, 5b
inexponential inexpo, exi
outexponential outexpo, exo
inoutexponential inoutexpo, exb
incircle incirc, ci
outcircle outcirc, co
inoutcircle inoutcirc, cb
inback bki
outback bko
inoutback bkb
inelastic eli
outelastic elo
inoutelastic elb
inbounce bni
outbounce bno
inoutbounce bnb

For fun! You can also turn extrapolation of a channel on. Extrapolation continues the curve beyond the keyframes you have added.

local channel = Channel.keyframe()
    .setDefaultEasing('so')
    .setIntroExtrapolation(true)
    .setOuttroExtrapolation(true)

2.3 Adding a scenecontrol type

You can totally ignore the whole scenecontrol type concept, and just key everything in init.lua. But for reusability, defining types is recommended.

Let's make a simple scenecontrol type that reads in 3 numbers, and moves the track from its current position to a new one specified by those 3 numbers.

local track = Scene.track
track.translationX = Channel.keyframe()
track.translationY = Channel.keyframe()
track.translationZ = Channel.keyframe()

addScenecontrol("mytypename", 3, function(cmd)
    local timing = cmd.timing
    local x = cmd.args[1] -- lua starts counting from 1
    local y = cmd.args[2]
    local z = cmd.args[3]
    track.translationX.addKey(timing, track.translationX.valueAt(timing))
    track.translationX.addKey(timing + 500, x)
    track.translationY.addKey(timing, track.translationY.valueAt(timing))
    track.translationY.addKey(timing + 500, y)
    track.translationZ.addKey(timing, track.translationZ.valueAt(timing))
    track.translationZ.addKey(timing + 500, z)
end)

What happened?

  1. We first defined the keyframe channels to add our keys into.
  2. We defined a scenecontrol type named "mytypename". It has 3 arguments. This means our aff command will look like this: scenecontrol(timing, mytypename, arg1, arg2, arg3).
  3. We defined the behaviour through a function. This function is run for every scenecontrol event in the chart. Our function reads that command and acts accordingly, adding keyframes to our channels.

Also note the use of channel.valueAt(timing). It's a handy function when working with keyframes.

This example illustrates the basic structure of a scenecontrol script:

  • Setting up the controllers
  • Assigning the channels
  • Then defining scenecontrol types which will fill in the keys

By the way, you can name your arguments:

addScenecontrol("mytypename", {"xpos", "ypos", "zpos"}, function(cmd)
    ...
end)

{"xpos", "ypos", "zpos"} creates a list of strings which will be interpreted as names for arguments. Refresh scripts and check the window — you'll see the column headers updated.

Events with zero arguments also work. The following code below corresponds to this aff command: scenecontrol(timing, mytypename);

addScenecontrol("mytypename", nil, function(cmd) ... end)
-- or --
addScenecontrol("mytypename", {}, function(cmd) ... end)

2.4. Scene

The Scene object serves 2 functions: grabbing internal controllers, and creating new controllers. We have done the former already, but only with one type so far.

To get an idea of what you can do, here's the list of all internal controllers:

Path Type Description
Scene.gameplayCamera CameraController The main camera
Scene.combo TextController The combo text
Scene.score TextController The score text
Scene.scoreTitle TextController The score title text
Scene.predictedGrade TextController The grade text (Predictive score mode)
Scene.predictedGradeBackground ImageController The grade text background
Scene.jacket ImageController The jacket art
Scene.jacketBackground ImageController The jacket background
Scene.title TextController The song title
Scene.composer TextController The composer text
Scene.difficultyText DifficultyController The difficulty text
Scene.difficultyBackground ImageController The difficulty background
Scene.hud CanvasController The HUD canvas
Scene.infoPanel InfoPanelController The info panel
Scene.pauseButton ImageController The pause button
Scene.background ImageController The background image
Scene.videoBackground SpriteController The video background
Scene.track TrackController The main track
Scene.beatlines BeatlinesController The beatlines display
Scene.singleLineL SpriteController The left Memory Archive line
Scene.singleLineR SpriteController The right Memory Archive line
Scene.skyInputLine GlowingSpriteController The sky input line
Scene.skyInputLabel GlowingSpriteController The sky input label
Scene.darken SpriteController The background darkening sprite
Scene.worldCanvas CanvasController The world-space canvas
Scene.screenCanvas CanvasController The screen-space canvas
Scene.cameraCanvas CanvasController The camera-space canvas

The Scene.track controller also exposes children such as Scene.track.divideLine12, Scene.track.criticalLine1, Scene.track.extraL, etc. For a full list, see TrackSubControllers.

Instead of using internal controllers, let's try creating our own controllers. Here are the creation methods available on Scene:

Method Return type Description
Scene.createSprite(imgPath, material, renderLayer, pivot, wrapMode) SpriteController Create a sprite from an image file
Scene.createImage(imgPath, material, renderLayer, pivot, wrapMode) ImageController Create an image from an image file
Scene.createCanvas(worldSpace) CanvasController Create a canvas
Scene.createText(font, fontSize, lineSpacing, alignment, renderLayer) TextController Create a text object
Scene.getNoteGroup(tg) NoteGroupController Get the controller for a timing group

Anytime you see a = in the argument list, it means the argument has a default value and you don't have to specify it.

You can use the material argument to change the blending mode. Here's the list:

  • default
  • add, fastadd
  • colorburn, colordodge
  • darken, fastdarken
  • difference, exclusion
  • fastlighten, fastmultiply, fastscreen
  • hardlight, lighten
  • linearburn, lineardodge, linearlight
  • multiply, overlay, screen
  • softlight, subtract, vividlight

When choosing materials for sprites, images and texts, be aware that anything that doesn't start with fast can hurt performance significantly, especially on lower-end hardware.

Let's try adding a custom sprite into our scene:

local sprite = Scene.createSprite("test.png")
sprite.layer = "notes" -- automatically casted into StringChannel.constant("notes")
sprite.order = 1 -- automatically casted into Channel.constant(1)

Feel free to change "test.png" to whatever image file you placed in your Scenecontrol folder.

Let's try it with an image, and change its material:

local image = Scene.createImage("test.png", "fastadd")
image.rectW = Channel.constant(300)
image.rectH = Channel.constant(200)

local canvas = Scene.createCanvas(false)
image.setParent(canvas)
canvas.layer = StringChannel.constant("notes")
canvas.sort = Channel.constant(1)

Notice how you have to specify the width and height of the image this time. That's one major difference between sprites and images. Also, the Add blend mode can make your image hard to see — try other blend modes as well.

Lastly let's try creating some text:

local text = Scene.createText("Forte", 40, 1, "middlecenter", "overlay")
text.text = TextChannel.create().addKey(0, "Hello").addKey(1000, "World")

local canvas = Scene.createCanvas(false)
text.setParent(canvas)
canvas.layer = "notes"
canvas.sort = 1

Text channels can also be keyframed! Here we made it display "Hello" at 0ms and then change to "World" at 1000ms.

You can also use easings on text channels! Try changing it to addKey(0, "Hello", "so") and see what happens.

This covers the most important things to keep in mind when creating controllers. If you want to see the available properties of each type of controller, please refer to the documentation.

2.5. Controllers & Channels - Level 2

Level 2 of Controllers & Channels will teach you how to save tremendous amount of time when working with channels. You'll be learning about different types of channels, and how to combine channels together for various effects.

Let's first consider how we'd tackle a very simple effect: moving an object back and forth, indefinitely, in a sine wave pattern.

It's totally doable to add each keyframe manually:

-- You don't have to replicate this. This is a bad idea
local c = Channel.keyframe()
for timing = 0, Context.songLength, 2000 do
    c.addKey(timing, 0, "so")
    c.addKey(timing + 500, 1, "b")
    c.addKey(timing + 1500, -1, "si")
end

This channel will keep oscillating between -1 and 1 in a sine wave form. It works, but we can do so much better.

In fact, the API provides a bunch of other types of channels. All of the code above can be shortened to:

local c = Channel.sine(2000, -1, 1, 0)
-- also equals to
local c = Channel.sine(Channel.constant(2000), Channel.constant(-1), Channel.constant(1), Channel.constant(0))

You have a range of other channels available to you as well:

Method Type Description
Channel.keyframe() KeyChannel A keyframe channel
Channel.constant(value) ValueChannel A channel with unchanging value
Channel.random(seed, min, max) ValueChannel A channel that returns random value every time
Channel.noise(frequency, min, max, offset, octave) ValueChannel A perlin noise channel
Channel.sine(period, min, max, offset) ValueChannel A sinusoidal wave channel
Channel.cos(period, min, max, offset) ValueChannel A cosine wave channel
Channel.saw(easing, period, min, max, offset) ValueChannel A saw wave with easing
Channel.min(channelA, channelB) ValueChannel Take the minimum of two channels
Channel.max(channelA, channelB) ValueChannel Take the maximum of two channels
Channel.clamp(valueChannel, minChannel, maxChannel) ValueChannel Clamp a channel between two bounds
Channel.condition(control, threshold, ifAbove, ifEqual, ifBelow) ValueChannel Conditional channel
Channel.exp(num, exp) ValueChannel Exponential channel

By default FFT channels operate with 256 frequency bands, but you can change this with Channel.setGlobalFFTResolution(resolution). Resolution must be an integer power of 2 (64, 128, 256, 512, ...)

It doesn't get much better than a single line of code, right? Well, it actually does. You can combine different channels together:

-- Vibrate between -2 and 2
local vibrate = Channel.noise(100, -2, 2, 0, 1)
-- Every 1000ms: change its value from 1 to 0, then jump back
local dampen = Channel.saw("so", 1000, 1, 0, 0)
-- How much it vibrates changes over time!
local combined = vibrate * dampen

By multiplying the vibrate channel with dampen, we limit how much the channel vibrates over time. When dampen returns 1 it vibrates the most; when dampen returns 0, it does not vibrate.

You can also combine keyframe channels with other channels. This opens up possibility for a very efficient technique. Let's look at enwidenlane:

local track = Scene.track

-- The main channel, which is 0 by default
local enwidenLaneFactor = Channel.keyframe().setDefaultEasing("l").addKey(0, 0)

-- Assign channels as simple transformations of the main channel
track.edgeExtraL.colorA = track.edgeExtraL.colorA + 255 * enwidenLaneFactor
track.edgeExtraR.colorA = track.edgeExtraR.colorA + 255 * enwidenLaneFactor
track.criticalLine0.colorA = track.criticalLine0.colorA + 255 * enwidenLaneFactor
track.divideLine01.colorA = track.divideLine01.colorA + 255 * enwidenLaneFactor
track.divideLine45.colorA = track.divideLine45.colorA + 255 * enwidenLaneFactor
track.criticalLine5.colorA = track.criticalLine5.colorA + 255 * enwidenLaneFactor
track.extraR.colorA = track.extraR.colorA + 255 * enwidenLaneFactor
track.extraL.colorA = track.extraL.colorA + 255 * enwidenLaneFactor

local posY = 100 * enwidenLaneFactor
track.extraL.translationY = Channel.min(track.extraR.translationY + posY, Channel.constant(0))
track.extraR.translationY = Channel.min(track.extraR.translationY + posY, Channel.constant(0))

local alpha = (1 - enwidenLaneFactor)
track.edgeLAlpha = track.edgeLAlpha * alpha
track.edgeRAlpha = track.edgeRAlpha * alpha

-- Choosing a different type name to avoid conflict with the built-in
addScenecontrol("enwidenlanelua", {"duration", "toggle"}, function(cmd)
    local timing = cmd.timing
    local duration = cmd.args[1]
    local toggle = cmd.args[2]
    enwidenLaneFactor.addKey(timing, enwidenLaneFactor.valueAt(timing))
    enwidenLaneFactor.addKey(timing + duration, toggle)
end)

If you're still unclear on what this code is doing, here's a brief explanation:

  • The second argument, "toggle", is a value of either 0 or 1. We write this value directly into the enwidenLaneFactor channel.
  • Every property we need to change is simply some variant of the main channel, calculated directly on the channels themselves instead of individual keys.

Note that we're performing arithmetic between a channel and a number value. Internally the numbers get converted into constant channels, so this is just a convenient shorthand.

Extra tip for debugging:

You can view what a channel is composed of by logging it:

local channel = Channel.saw("si", Channel.constant(1000), Channel.constant(1), Channel.constant(0), Channel.constant(0)) * Channel.noise(Channel.constant(100), Channel.constant(-1), Channel.constant(1), Channel.constant(0), Channel.constant(1))
channel = channel + Channel.keyframe()
channel = channel * 5
log(channel)

You should see the following result when opening the Error Log:

((unnamed@saw(si,1000,0,1,0)) * (unnamed@noise(100,-1,1,0,1)) + unnamed@key(0)) * (5)

This will come in handy to figure out why the effect isn't working as you intended it to.

2.6. Working with note groups

Note groups are a bit odd to work with. The workflow of defining the controllers, then keying them in scenecontrol definition we've been using so far aren't going to work for one simple reason: we don't know which controllers we want ahead of time, because we don't know which timing groups our events will be placed in.

The only way to be sure is to query for the controller for every scenecontrol command:

addScenecontrol("myType", {}, function(cmd)
    local noteGroup = Scene.getNoteGroup(cmd.timingGroup)
end)

That's totally fine but we can't really create a new channel for every scenecontrol command. This will override the old channel for every command:

addScenecontrol("myType", 1, function(cmd)
    local noteGroup = Scene.getNoteGroup(cmd.timingGroup)
    noteGroup.translationX = Channel.keyframe().addKey(0, cmd.args[1])
end)
-- This doesn't work and the note group's properties will only be assigned the last value.

Instead we need a way to find out if we have already created a channel for a note group, and if not, create a new one. Introducing: named channels.

Remember when you were logging your channel and there were a bunch of "unnamed" in there? That's the default name of every channel. You can change your channels' name:

local channel = Channel.named("myChannelName").keyframe()

Then if a channel is combined from multiple channels, you can separate out each one to edit with by using .find(name):

local myChannel = Channel.named("IWantThisBackLater").keyframe()
myChannel.addKey(0, 0).addKey(1000, 1).addKey(2000, 2)

local otherChannel = Channel.named("OthersCreatedThis").keyframe()
local combinedChannel = myChannel * otherChannel

local myChannelFound = combinedChannel.find("IWantThisBackLater")
-- myChannelFound is the same object as myChannel

What this allows us to do is solve the issue we had earlier with note groups:

addScenecontrol("myType", 1, function(cmd)
    local noteGroup = Scene.getNoteGroup(cmd.timingGroup)

    -- Try finding the channel from the property
    local channel = noteGroup.translationX.find("myType")
    if channel == nil then -- If it's not found
        -- Make a new channel
        channel = Channel.named("myType").keyframe()
        -- Assign it
        noteGroup.translationX = channel
    end

    channel.addKey(0, cmd.args[1])
end)

Always naming your channels is a very good idea, as it helps with debugging. Especially if you intend on sharing your scripts with other people to use.

2.7. Triggers and TriggerChannels

Triggers allow you to react to events during gameplay, such as judgement results or value thresholds being crossed. Triggers by themselves do nothing; they must be combined with a TriggerChannel to produce a value channel.

Judgement triggers

Activate on note judgement events:

local trig = Trigger.judgement()
    .onPerfect()
    .ofTimingGroup(0)
    .dispatch(Channel.constant(1), 500, "so")

local trigChannel = TriggerChannel.loop(trig, Channel.constant(0))

-- Use the trigger channel like any value channel
someSprite.colorA = trigChannel * 255

This trigger fires on Perfect judgements (including early, late, and max) in timing group 0, dispatching a value of 1 with 500ms duration and so easing.

Available judgement filter methods: onMax(), onPerfect(), onPerfectEarly(), onPerfectLate(), onGood(), onGoodEarly(), onGoodLate(), onMiss(), onMissEarly(), onMissLate(). All can be chained to listen to multiple judgement types.

Observe triggers

Activate when a value channel crosses a threshold:

local trig = Trigger.observe(Context.currentCombo)
    .goAbove(100)
    .dispatch(Channel.constant(1), 500, "so")

local trigChannel = TriggerChannel.loop(trig, Channel.constant(0))

This trigger fires when the combo count goes above 100.

TriggerChannels

A TriggerChannel turns triggers into an actual value channel:

Method Description
TriggerChannel.loop(trigger, fallback) Starts at fallback, jumps to the dispatched value when trigger fires, then returns to fallback
TriggerChannel.accumulate(trigger, fallback) Starts at fallback, then adds the dispatched value each time the trigger fires
TriggerChannel.stack(triggers...) Combines multiple triggers into one channel
TriggerChannel.setting(triggers...) Keeps the last dispatched value

2.8. A few extra tips and cautions

These are all small details that you should know but aren't enough to warrant an entire section about.

Cautions
  1. Camera coordinates in aff camera commands and in the scenecontrol API are very different!
  2. All file paths are relative to the Scenecontrol folder, NOT to the running script.

    For example, if within your init.lua you referenced the script folder/other_script.lua, and within other_script.lua you tried to create a sprite with the path image.jpg, the actual file path should be located at Scenecontrol/image.jpg and NOT Scenecontrol/folder/image.jpg.

  3. When choosing materials for sprites, images and texts, be aware that anything that doesn't start with fast can hurt performance significantly, especially on lower-end hardware.

Tips

  1. Alternative to log, notify is also an option, which will display a message on a toast notification. notifyWarn and notifyError are also available.
  2. You can always use log(channel.valueAt(timing)) to check a channel's value at any point in time. This can be useful to figure out the coordinates of objects in the scene.
  3. You can get the list of all valid installed fonts with Context.availableFonts. Only use this for debugging however and don't include this in an actual script.
  4. Always use local unless you do intend on making a variable global.
  5. Context exposes channels for timing data: Context.bpm(tg), Context.beatLength(tg), Context.divisor(tg), Context.floorPosition(tg), as well as gameplay state: Context.currentCombo, Context.currentScore, Context.songLength, Context.screenWidth, Context.screenHeight, Context.laneFrom, Context.laneTo.

What's next?

Congratulations on finishing this tutorial on the ArcCreate scenecontrol API!

You should now be armed with enough knowledge to use the reference documentation effectively, which will detail everything about the API for you to reference while writing your own scripts.

Have fun scripting!

Clone this wiki locally