Adding Scripting To My Game
I am writing a game in C# that allows player to navigate a marble through a world of 3D levels. This brief page describes how I am using external Typescript files to make this game more dynamic.To define the logic for what flows in a game level, I wanted to incorporate a scripting system that uses external files that can define everything that can happen in a game level, without the need to rebuild a thing. The scripting system I choose to accomplish this is Typescript. My C# code neatly generates a set of scriptable interfaces that can be used by an external script file to make dynamic decisions about what happens in any game level. Here is a very simple example of what the scripting looks like.
Note My scripting system supports animated changes to most properties, and will be extended as I add more components and other types.
Video: Marbles Script
The Script
Here is the Typescript for the gameplay you see above. It is an external file that attaches to the game at runtime. It starts by adding a looping animation altering the rotation and y position of the airhorn doodad. A button that the ball can roll over toggles selects a new random color for the airhorn using an animated tint change. A different button resets the airhorn to its original settings, and toggles the airhorn blowing state.function randomColor(): string {
const channel = () => Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
return "#" + channel() + channel() + channel();
}
function isButton(obj: Component | null): obj is Button {
return (obj != null) && obj.kind === "button";
}
function isAirHorn(obj: Component | null): obj is AirHorn {
return (obj != null) && obj.kind === "airhorn";
}
const red = "#FF0000";
const blue = "#0000FF";
let colorButton: Button;
let blowButton: Button;
let airhorn: AirHorn;
let tint: string;
let rotation: number;
let height: number;
function colorButtonPressed(b: Button) {
if (airhorn)
airhorn.animate(`tint(${randomColor()}, 0.25, sine-inout)`);
b.tint = blue;
}
function boot() {
game.log("booting test script");
let obj = game.find("color");
if (isButton(obj)) {
colorButton = obj;
colorButton.autoRelease = true;
colorButton.tint = red;
colorButton.intensity = 0;
colorButton.animate("intensity(1, 1, sine-inout, repeat-flip)");
colorButton.onPressed = colorButtonPressed;
colorButton.onReleased = (b) => b.tint = red;
}
obj = game.find("blow");
if (isButton(obj)) {
let time = game.time;
obj.autoRelease = true;
obj.onPressed = () => {
if (airhorn == null)
return;
if (game.time - time < 2)
return;
time = game.time;
airhorn.animate("none");
airhorn.animate(`rotate-y(${rotation}, 0.5, sine-inout)`);
airhorn.animate(`translate-y(${height}, 0.5, sine-inout)`);
airhorn.animate(`tint(${tint}, 0.5, sine-inout)`);
airhorn.blowing = !airhorn.blowing;
};
}
for (const obj of game.items) {
if (isAirHorn(obj) && airhorn == null) {
airhorn = obj;
tint = airhorn.tint;
rotation = airhorn.rotation.y;
height = airhorn.position.y;
airhorn.animate(`rotate-y(${rotation + 360}, 4, linear, repeat`));
airhorn.animate(`translate-y(${height + 0.5}, 2, linear, repeat-flip)`);
}
}
}
Definitely Typed
Below is thegame.ts.d
file that describe what is currently provide to Typescript.interface Vector3 {
x: number;
y: number;
z: number;
}
interface AnimationHandle {
then(spec: string): AnimationHandle;
run(callback: () => void, seconds?: number): AnimationHandle;
reverse(delay?: number): AnimationHandle;
repeat(delay?: number, n?: number): AnimationHandle;
delay(seconds: number): AnimationHandle;
}
type ComponentKind = "component" | "mesh" | "doodad" | "button" | "crate" | "gear" | "spinner" | "catapult" | "inlaylight" | "airhorn" | "decal";
type EasingName =
| "linear"
| "sine-in" | "sine-out" | "sine-inout" | "sine-outin"
| "quad-in" | "quad-out" | "quad-inout" | "quad-outin"
| "cubic-in" | "cubic-out" | "cubic-inout" | "cubic-outin"
| "quart-in" | "quart-out" | "quart-inout" | "quart-outin"
| "quint-in" | "quint-out" | "quint-inout" | "quint-outin"
| "expo-in" | "expo-out" | "expo-inout" | "expo-outin"
| "circ-in" | "circ-out" | "circ-inout" | "circ-outin"
| "elastic-in" | "elastic-out" | "elastic-inout" | "elastic-outin"
| "back-in" | "back-out" | "back-inout" | "back-outin"
| "bounce-in" | "bounce-out" | "bounce-inout" | "bounce-outin"
| "spring-in" | "spring-out" | "spring-inout" | "spring-outin";
interface Component {
readonly kind: ComponentKind;
readonly name: string;
parent: Component | null;
readonly center: Vector3;
position: Vector3;
size: Vector3;
rotation: Vector3;
normal: Vector3;
scale: Vector3;
visible: boolean;
animate(spec: string): AnimationHandle;
delay(seconds: number): AnimationHandle;
onAnimationComplete: ((obj: Component, spec: string) => void) | null;
}
interface Mesh extends Component {
readonly kind: "mesh";
}
interface Doodad extends Component {
tint: string;
}
interface Button extends Doodad {
readonly kind: "button";
intensity: number;
autoRelease: boolean;
onPressed: ((button: Button) => void) | null;
onReleased: ((button: Button) => void) | null;
}
interface Crate extends Doodad { readonly kind: "crate"; }
interface Gear extends Doodad { readonly kind: "gear"; }
interface Spinner extends Doodad {
readonly kind: "spinner";
image: string;
foilImage: string;
onSpin: ((spinner: Spinner) => void) | null;
}
interface Catapult extends Doodad {
readonly kind: "catapult";
onBallLoaded: ((catapult: Catapult) => void) | null;
}
type InlayShape = "circle" | "triangle" | "arrow" | "box" | "star" | "hexagon" | "plus";
interface InlayLight extends Doodad {
readonly kind: "inlaylight";
on: boolean;
shape: InlayShape;
intensity: number;
stroke: number;
fillet: number;
angle: number;
pulseDuration: number;
}
interface AirHorn extends Doodad {
readonly kind: "airhorn";
raised: boolean;
blowing: boolean;
blowStrength: number;
blowCone: number;
}
interface Decal extends Doodad {
readonly kind: "decal";
image: string;
readonly imageWidth: number;
readonly imageHeight: number;
opacity: number;
transition: number;
resize(width: number): void;
}
interface DoodadKindMap {
button: Button;
crate: Crate;
gear: Gear;
spinner: Spinner;
catapult: Catapult;
inlaylight: InlayLight;
airhorn: AirHorn;
decal: Decal;
}
declare const game: {
readonly totalTime: number;
readonly time: number;
readonly delta: number;
log(message: string): void;
setTimeout(callback: () => void, milliseconds: number): number;
setInterval(callback: () => void, milliseconds: number): number;
clearTimeout(id: number): void;
clearInterval(id: number): void;
find(name: string): Component | null;
readonly items: Component[];
createNullPoint(): Component;
createDoodad(kind: K): DoodadKindMap[K];
spawnLocation: Vector3;
};