Sanny Builder даёт возможность использовать тип данных как переменную класса, но их количество ограниченно и приходилось вручную писать классы и их методы. Генератор же использует все типы данных, и сейчас мы научимся их создавать.
Начну с простых вещей. Типы условно можно разделить на числовые и строковые. Они записываются в переменные, которые могут быть локальными или глобальными. Если в Sanny Builder нам достаточно было использовать символы @ и $ чтобы указать локальная переменная или нет, то в генераторе этот механизм совсем иной. Сначала нужно объявить переменную и только потом записать значение или вызвать команду. Для этого используются команды local и global. Давайте создадим переменную типа Int и запишем в неё число:
[Thread]
public void TEST() {
var myLocalVariable = Int.local( 0 ); // 0@
var myGlobalVariable = Int.global( 2000 ); // $2000
// Не надо так! Иначе перменная "myLocalVariable" станет такой-же, как и "myGlobalVariable":
// myLocalVariable = myGlobalVariable;
myLocalVariable.value = 10;
myGlobalVariable.value = myLocalVariable;
end_thread();
}
На выходе мы получим следующий код:
//------------- MAIN --------------- :TEST 03A4: name_thread 'TEST' 0006: 0@ = 10 // @ = ? (int) 008A: $2000 = 0@ // $ = @ (int) 004E: end_thread
Это напоминает кострукцию "VAR-END" в Sanny Builder, только здесь она обязательна. Обратите внимание, что здесь используется свойство "value", чтобы задать новое значение. Сами значения могут принимать как обычные числа или строки, так и переменные, как в примере выше. Генератор распознает что передано и подставит нужный опкод. Таким образом нам не нужно каждый раз искать опкод для получаения значения одной переменной в другую.
Также Вы заметили, что я передал какие-то числа в методы объявления переменных. Это - индексы ( номера ) переменных. Каждый поток имеет свой набор переменных и, если задать индекс вне диапазона, генератор покажет ошибку.
Мы видим, что в результате в нас нет переменных со строковыми именами. Эту возможность я убрал, так как это сложно контролировать. Поэтому создаётся всё через её индекс. Sanny Builder поймёт нас и так.
Для GTA San Andreas мы можем использовать переменные с маркировками v и s. В этом случае в нас нет привязки к виду кавычек:
[Thread]
public void TEST() {
var myVString = vString.local( 0 ); // 0@v
var mySstring = sString.global( 2000 ); // s$2000
myVString.value = "vString";
mySstring.value = "sString";
end_thread();
}
Генератор поймёт какие кавычки использовать. И в результате будет следующее:
//------------- TEST --------------- :TEST 03A4: name_thread 'TEST' 06D2: 0@v = "vString" // @v = ? (vstring) 05A9: s$2000 = 'sString' // s$ = ? (sstring) 004E: end_thread
Некоторые типы данных поддерживают арифметические операции. Я рекомендую использовать их по очереди, хоть генератор позволяет нам делать сложные конструкции:
[Thread]
public void TEST() {
var myInt = Int.local( 0 ); // 0@
var myFloat = Float.local( 1 ); // 1@
myInt.value = 0;
myFloat.value = 0.0;
// myInt.value = 0.0; // будет вызвана ошибка генерации кода
// myFloat.value = 0; // будет вызвана ошибка генерации кода
comment = "easy construction:";
myInt += 2;
myFloat -= 4.0;
myInt /= 4;
myFloat *= 6.0;
comment = "difficult construction:";
var myInt2 = Int.local( 2 ); // 2@
myInt2.value = 10;
myInt += ( myInt + myInt2 / myInt2 - myInt );
end_thread();
}
Использование сложных конструкций часто приводит к непредсказуемым результатам, поэтому лучше не использовать их вовсе. Вот результат:
//------------- TEST --------------- :TEST 03A4: name_thread 'TEST' 0006: 0@ = 0 // @ = ? (int) 0007: 1@ = 0.0 // @ = ? (float) /* easy construction: */ 000A: 0@ += 2 // @ += ? (int) 000F: 1@ -= 4.0 // @ -= ? (float) 0016: 0@ /= 4 // @ /= ? (int) 0013: 1@ *= 6.0 // @ *= ? (float) /* difficult construction: */ 0006: 2@ = 10 // @ = ? (int) 0072: 2@ /= 2@ // @ /= @ (int) 005A: 0@ += 2@ // @ += @ (int) 0062: 0@ -= 0@ // @ -= @ (int) 005A: 0@ += 0@ // @ += @ (int) 004E: end_thread
Обратите внимание, что дробные числа записываются без каких либо префиксов ( синтаксис C# ). Также генератор чувствителен к типам. Если Sanny Builder мог позволить записать в переменную типа Int дробное значение, то генератор этого не позволит сделать. Это нужно для контроля типов. Другими словами чтобы не допускать логических ошибок, которые приводят к неожиданным результатам уже в игре.
Однако для типа Int мы можем указывать тип bool. В этом случае "истина" будет записана как 1, а "ложь", как 0. Если команда принимает параметр типа "bool", а мы туда передадим целое число, то генератор попытается преобразовать число в булево.
Мы можем объявить две переменные, которые имеют один и тотже индекс. В этом случае нужно использовать их по очереди, так как данные будут перезаписаны уже в игре:
[Thread]
public void TEST() {
var myIntVal = Int.local( 0 ); // 0@
var myFloatVal = Float.local( 0 ); // 0@
comment = "first int:";
myIntVal.value = true;
myIntVal.value = 1000;
wait( myIntVal );
comment = "next float:";
myFloatVal.value = 1.0;
set_gamespeed( myFloatVal );
end_thread();
}
Вот код, который будет сгенерирован в этом случае:
//------------- TEST --------------- :TEST 03A4: name_thread 'TEST' /* first int: */ 0006: 0@ = 1 // @ = ? (int) 0006: 0@ = 1000 // @ = ? (int) 0001: wait 0@ ms /* next float: */ 0007: 0@ = 1.0 // @ = ? (float) 015D: set_gamespeed 0@ 004E: end_thread
Часто возникают ситуации, когда мы используем переменную с одним и темже индексом в разных потоках, но с одной целью. Вместо того, чтобы писать инициализацию для каждого потока, мы можем сделать это только в первом.
// Указываем переменные в области видимости класса, не инициализируя их!
static Int localTimer1, localTimer2;
// Инициализация в области видимости класса приведёт к ошибке.
// static Int index = Int.local( 0 ); // так будет ошибка
[Thread]
public void TEST1() {
localTimer1 = Int.local( 32 );
localTimer2 = Int.local( 33 );
localTimer1.value = 0;
localTimer2.value = 0;
end_thread();
}
[Thread]
public void TEST2() {
localTimer1.value = 0;
localTimer2.value = 0;
end_thread();
}
Обратите внимание, что возле типа нужно указать ключевое слово static. По непонятным мне причинам, без этого слова, переменная будет равна null и работать с ней будет нельзя. В итоге у нас будет такой результат:
//------------- TEST1 --------------- :TEST1 03A4: name_thread 'TEST1' 0006: 32@ = 0 // @ = ? (int) 0006: 33@ = 0 // @ = ? (int) 004E: end_thread //------------- TEST2 --------------- :TEST2 03A4: name_thread 'TEST2' 0006: 32@ = 0 // @ = ? (int) 0006: 33@ = 0 // @ = ? (int) 004E: end_thread
Миссии имеют свой набор переменных и их количество больше. Если индекс у нас больше чем лимит потока, то инициализировать переменные нужно в коде миссии.
Класс "Script" уже имеет в своём составе некоторые переменные, которые уже можно использовать и не инициализировать. Это переменная для игрока, для актёра игрока, для статуса миссий и скриптов, переменная для группы игрока и переменная задержки по-умолчанию. Давайте создадим игрока, дадим одежду и контроль:
[Thread]
public void MAIN() {
fade( false, 0 );
refresh_game_renderer( 2488.562, -1666.865 );
load_scene( 2488.562, -1666.865, 13.3757 ); // Camera.SetAtPos
PlayerChar.create( 2488.562, -1666.865, 12.8757 );
PlayerChar.can_move( false );
PlayerChar.get_actor( PlayerActor );
PlayerChar.get_group( PlayerGroup );
PlayerActor.set_z_angle( 262.0 );
set_camera_behind_player();
PlayerChar.set_clothes( "VEST", "VEST", ClothesBodyPart.TORSO );
PlayerChar.set_clothes( "JEANSDENIM", "JEANS", ClothesBodyPart.LEGS );
PlayerChar.set_clothes( "SNEAKERBINCBLK", "SNEAKER", ClothesBodyPart.SHOES );
PlayerChar.set_clothes( "PLAYER_FACE", "HEAD", 1 );
PlayerChar.rebuild(); // Player.Build
save_player_clothes();
wait( 1000 );
release_weather();
fade( 1, 1000 );
PlayerChar.can_move( 1 );
end_thread();
}
Многие привычные названия команд были переименованы, но обычно они не так часто используются. Для аргументов команд есть альтернативные значения - перечисления, для удобства. В примере выше я использовал перечисление ClothesBodyPart, которое хранит номера частей тела игрока. Таких перечислений много. Обычно они имеют такое же название как и имя аргумента команды. Если мы скомпилируем этот код, то у нас будет такой скрипт:
DEFINE OBJECTS 0 DEFINE MISSIONS 0 DEFINE EXTERNAL_SCRIPTS 0 // Use -1 in order not to compile AAA script DEFINE UNKNOWN_EMPTY_SEGMENT 0 DEFINE UNKNOWN_THREADS_MEMORY 2048 //------------- MAIN --------------- :MAIN 03A4: name_thread 'MAIN' 0180: set_on_mission_flag_to $409 // Note: your missions have to use the variable defined here 0004: $14 = 250 // $ = ? (int) 016A: fade 0 time 0 04E4: refresh_game_renderer_at 2488.562 -1666.865 03CB: set_rendering_origin_at 2488.562 -1666.865 13.3757 0053: $2 = create_player 0 at 2488.562 -1666.865 12.8757 01B4: set_player $2 can_move 0 01F5: $3 = get_player_actor $2 07AF: $11 = player $2 group 0173: set_actor $3 z_angle_to 262.0 0373: set_camera_directly_behind_player 087B: set_player $2 clothes_texture "VEST" model "VEST" body_part 0 087B: set_player $2 clothes_texture "JEANSDENIM" model "JEANS" body_part 2 087B: set_player $2 clothes_texture "SNEAKERBINCBLK" model "SNEAKER" body_part 3 087B: set_player $2 clothes_texture "PLAYER_FACE" model "HEAD" body_part 1 070D: rebuild_player $2 0793: save_player_clothes 0001: wait 1000 ms 01B7: release_weather 016A: fade 1 time 1000 01B4: set_player $2 can_move 1 004E: end_thread
Если скомпилировать в Sanny Builder, то в игре мы уже сможем управлять игроком и делать прочие базовые вещи:

Наличия встроенных глобальных перменных и какие их индексы скорее всего приведут к перезаписи важной перменной. Я рекомендую использовать индексы от 2000 и выше для глобальных переменных.
Напоследок ещё расскажу о механизме вызовов методов. Нам не обязательно каждый раз писать имя переменной перед командой, так как команда возвращает ту переменную, с которой был сделан вызов. Другими словами мы можем написать так:
[Thread]
public void TEST() {
PlayerChar.create( 2488.562, -1666.865, 12.8757 ).get_actor( PlayerActor ).get_group( PlayerGroup );
}
Команды выстраиваются по цепочке и последовательно выполняются. Механизм цепных функций доступен только для типов данных!
P. S. Обратите внимание, что типы данных ВСЕГДА пишутся с большой буквы. "Int" и "int" - это разные вещи.