Как вы бы реализовали хранение вокселей «а-ля Minecraft» на Rust?
- Каждый воксель имеет тип, который задаёт его поведение (начиная от построения меша для рендеринга и заканчивая реакцией на действия игрока и т. д.)
- Помимо типа у каждого вокселя может быть некоторое состояние. Структура состояния зависит от типа (не всем типам вокселей вообще оно нужно). Реализация каждого поведения от типа должна иметь доступ к состоянию конкретного вокселя.
- Воксели хранятся в массиве, поэтому структура вокселя должна иметь константный размер (ну и да, мы не хотим дёргать аллокатор отдельно на каждый из миллионов вокселей). Так что звучит разумно ограничить размер состояния некоей небольшой величиной, а сколько реально байт в нём будут использоваться пусть решает конкретный тип.
- У небольшого количества вокселей может быть большое состояние, которое не влезет в фиксированный размер. В этом случае в состоянии вокселя помещается указатель на динамически выделяемую структуру и нужно в какой-то момент (либо при уничтожении мира, либо при смене типа этого вокселя) освободить память.
На Си это могло бы выглядеть как-то так:
typedef struct voxel_type voxel_type;
struct voxel {
voxel_type *type;
char data[16]; // Мы уверены, что на целевой платформе сюда влезет хотя бы один указатель
};
struct voxel_type {
void (*init)(voxel_type *type, voxel *v);
void (*destroy)(voxel_type *type, voxel *v);
void (*to_string)(voxel_type *type, voxel *v, char *buffer, size_t buffer_size);
texture_t *(*get_texture)(voxel_type *type, voxel *v);
void (*build_vertex_data)(voxel_type *type, voxel *v, char *buffer, size_t buffer_size);
void (*on_click)(voxel_type *type, voxel *v);
...
};
void set_voxel_type(voxel *v, voxel_type *t) {
v->type->destroy(v->type, v); // Подразумевается, что изначально все воксели инициализированы каким-нибудь "пустым" типом при создании их массива
v->type = t;
t->init(t, v);
}
void voxel_to_string(voxel *v, char *buffer, size_t buffer_size) {
v->type->to_string(v->type, v, buffer, size);
}
Соответственно, каждая функция из структуры voxel_type внутри себя кастует data в конкретную для данного типа вокселя структуру состояния (которая обязана быть не больше 16 байт размером, однако может содержать указатель на что-то большее), при этом init производит начальную инициализацию и не должен делать никаких допущений о том, что лежало в data до него (там может быть неинициализированная память или данные от предыдущего типа вокселя), а destroy может либо ничего не делать, либо освобождать память (если мы клали в data указатель).
Это похоже на класс с vtable из C++ (который создаётся через placement new на буфере), однако является более гибким механизмом: своё состояние имеет не только сам воксель, но и его тип (то есть можно в рантайме инстанцировать несколько типов вокселей с одинаковым поведением, но разными параметрами типа, например, текстурой, при этом каждый воксель не будет нести в себе эти параметры, а только указатель на «шаблон»).
Как это можно уложить на синтаксис и парадигму Rust? Разумеется, жалательно поменьше unsafe и побольше compile-time гарантий (например, было бы неплохо, если бы реализации типов вокселей сразу получали скастованное в нужный тип состояние, возможно, с помощью какой-нибудь магии на макросах).
Первая мысль - тип вокселя должен быть трейтом (соответственно, в нём оказываются все нужные методы, но без реализации), а конкретный воксель имеет в себе поле вроде kind: &'static dyn VoxelType. Преобразования типов можно сделать на макросах (чтобы был макрос для описания типа вокселя, который под капотом перенаправляет вызовы методов, принимающих &Voxel, в методы, принимающие конкретный тип состояния, а также берёт на себя реализацию методов создания и удаления состояния). Однако встаёт вопрос собственно инициализации и деинициализации состояния. Rust гораздо стороже относится к этому вопросу.