000176. Выделение памяти

Всем привет! В этом уроке мы ознакомимся с функцией выделения памяти и рассмотрим подробный пример работы этой фишки.

Иногда при проектировании больших скриптов возникают проблемы с хранением каких-либо значений в CLEO-скриптах:

  • Количество локальных переменных мало;

  • Глобальные переменные не рекомендовано использовать в CLEO;

  • Глобальные CLEO-переменные могут привести к конфликтам между скриптами;

Проблемы очевидны и решить их можно несколькими способами. Сегодня мы рассмотрим способ с выделением динамической памяти.

Допустим, у нас стоит задача: создать 100 актёров. Их нужно где-то хранить, и буфер является очень привлекательным вариантом. Рассмотрим опкоды, для работы с ними:

0AC8: 0@ = allocate_memory_size 120
0AC9: free_allocated_memory 0@

Первый опкод выделяет память под буфер, где 120 — это размер (в байтах), а 0@ — переменная, которая будет хранить указатель на начало области памяти (он же совпадает с началом буфера). Второй опкод освобождает выделенную память.

Возникает вопрос: как узнать размер выделяемой памяти, чтобы нам хватило места для записи этих актёров? Поскольку переменные для создания/записи актёров являются ссылками, то размер одного сегмента этой памяти будут равняться 4 байтам, а общий размер выделяемой памяти — 100 × 4 = 400 байт.

Для того, чтобы получить 1 элемент этого буфера, легче всего сделать несколько SCM-функций, которые делали бы навигацию по этой области памяти. Первый участок кода будет выделять память под актёров, а второй — очистка выделенной памяти:

goto @SKIP_FUNCTIONS

:CREATE_ARRAY
0A90: 2@ = 4 * 0@ // 4 * size
0AC8: 1@ = allocate_memory_size 2@
0AB2: ret 1 1@

:DESTROY_ARRAY
0AC9: free_allocated_memory 0@
0AB2: ret 0

:SKIP_FUNCTIONS

Я назвал SCM-функции "создать массив" и "удалить массив" согласно принципам, по которым они работают. Теперь добавим возможность читать/записывать в ячейку памяти нужное значение по индексу. Индекс будет определятся как Номер × размер, размер ссылки естественно 4 байта:

:SET_ARRAY_ITEM
0A90: 3@ = 4 * 1@ // 4 * size
0A8E: 4@ = 0@ + 3@ // Buffer + offset
0A8C: write_memory 4@ size 4 value 2@ virtual_protect 0
0AB2: ret 0

:GET_ARRAY_ITEM
0A90: 3@ = 4 * 1@ // 4 * size
0A8E: 4@ = 0@ + 3@ // Buffer + offset
0A8D: 2@ = read_memory 4@ size 4 virtual_protect 0
0AB2: ret 1 2@

Поскольку у нас идёт работа с памятью, мы обязательно применяем опкоды 0A8C и 0A8D для чтения и записи её участка. Адрес чтения мы получили за формулой: Начало буфера + ( размер ссылки × индекс ). Поэтому наши SCM-функции будут обязательно принимать ссылку на выделенный буфер:

0AB1: call_scm_func @CREATE_ARRAY 1 num_items 100 store_to 0@
0AB1: call_scm_func @DESTROY_ARRAY 1 array 0@
0AB1: call_scm_func @SET_ARRAY_ITEM 3 array 0@ index 0 value 0@
0AB1: call_scm_func @GET_ARRAY_ITEM 2 array 0@ index 0 value 0@

Итак, запишем в наш массив 100 случайных актёра, разместив их у Гроув Стрит:

{$CLEO}
0000:

goto @SKIP_FUNCTIONS

:CREATE_ARRAY
0A90: 2@ = 4 * 0@ // 4 * size
0AC8: 1@ = allocate_memory_size 2@
0AB2: ret 1 1@

:DESTROY_ARRAY
0AC9: free_allocated_memory 0@
0AB2: ret 0

:SET_ARRAY_ITEM
0A90: 3@ = 4 * 1@ // 4 * size
0A8E: 4@ = 0@ + 3@ // Buffer + offset
0A8C: write_memory 4@ size 4 value 2@ virtual_protect 0
0AB2: ret 0

:GET_ARRAY_ITEM
0A90: 3@ = 4 * 1@ // 4 * size
0A8E: 4@ = 0@ + 3@ // Buffer + offset
0A8D: 2@ = read_memory 4@ size 4 virtual_protect 0
0AB2: ret 1 2@

:SKIP_FUNCTIONS

{ !!! НАШ КОД !!! }

0AB1: call_scm_func @CREATE_ARRAY 1 num_items 100 store_to 0@
3@ = 2500.0

for 2@ = 0 to 100 step 1
 0376: 1@ = create_random_actor_at 3@ -1659.0 12.3437
 0AB1: call_scm_func @SET_ARRAY_ITEM 3 array 0@ index 2@ value 1@
 3@ -= 1.0
 wait 0
end

0A93: end_custom_thread

Результат:

Как видим, в скрипте использовалась всего 1 переменная 1@ и мы не потеряли полный доступ ко всем остальным актёрам. Давайте теперь удалим созданных актёров и посмотрим работают ли наши функции удаления и очищения памяти:

{$CLEO}
0000:

goto @SKIP_FUNCTIONS

:CREATE_ARRAY
0A90: 2@ = 4 * 0@ // 4 * size
0AC8: 1@ = allocate_memory_size 2@
0AB2: ret 1 1@

:DESTROY_ARRAY
0AC9: free_allocated_memory 0@
0AB2: ret 0

:SET_ARRAY_ITEM
0A90: 3@ = 4 * 1@ // 4 * size
0A8E: 4@ = 0@ + 3@ // Buffer + offset
0A8C: write_memory 4@ size 4 value 2@ virtual_protect 0
0AB2: ret 0

:GET_ARRAY_ITEM
0A90: 3@ = 4 * 1@ // 4 * size
0A8E: 4@ = 0@ + 3@ // Buffer + offset
0A8D: 2@ = read_memory 4@ size 4 virtual_protect 0
0AB2: ret 1 2@

:SKIP_FUNCTIONS
0AB1: call_scm_func @CREATE_ARRAY 1 num_items 100 store_to 0@
3@ = 2500.0

for 2@ = 0 to 100 step 1
 0376: 1@ = create_random_actor_at 3@ -1659.0 12.3437
 0AB1: call_scm_func @SET_ARRAY_ITEM 3 array 0@ index 2@ value 1@
 3@ -= 1.0
 wait 0
end

{ !!! НАШ НОВЫЙ КОД !!! }

for 2@ = 0 to 100 step 1
 0AB1: call_scm_func @GET_ARRAY_ITEM 2 array 0@ index 2@ value 1@
 if 
 056D: actor 1@ defined
 then
 01C2: remove_references_to_actor 1@ 
 009B: destroy_actor 1@
 end
 wait 0
end 
 
0AB1: call_scm_func @DESTROY_ARRAY 1 array 0@ 

0A93: end_custom_thread

Результат:

Улица снова пуста! Интересно понаблюдать за самим процессом создания/удаления этих актёров: видно сколько примерно времени занимает одна итерация цикла.

Думаю, многие заметили, что в цикл я добавил нетипичную для него задержку в 0 милисекунд. В нашем случае без задержки игра вылетит, что конечно не радует. Надеюсь, что этот пример пригодится вашим проектам, где есть смысл хранить данные именно таким способом.

Чтобы не превышать лимиты, не создавайте слишком много сущностей за один раз!

Не используйте этот способ для хранения данных, которые нужно восстанавливать после перезагрузки игры!

В main.scm этот способ нужно использовать с осторожностью, но там ситуация с переменными более благосклонна к нам.

Кроме актёров, таким вот образом можно записать и другие сущности, а также целые и/или вещественные числа. Также нет привязки к конкретному типу данных: мы можем в разные ячейки записать разные значения. Главное не забыть что и куда было записано :)

На этом, пожалуй, всё. С Вами был wmysterio!

Last updated