C#: свойства в Inspector и префабах
Export, private-поля, Range, сохранение и проверка значений на полном примере.
Запустить пример
Добавьте пять файлов ниже в C#-проект Reckfell 1.0.0 и соберите. Создайте уровень с корнем Entity2D, прикрепите к корню ManagedDataDemo2D и запустите F6. Дочерние объекты создаются кодом. Ожидается боезапас 5; кнопка «Сделать выстрел» меняет его на 4.
ShotSettings.cs
using Reckfell;
[GlobalClass]
public partial class ShotSettings : Resource
{
[Export] public string DisplayName { get; set; } = "Pulse";
[Export(PropertyHint.Range, "1,100,1")] public int Damage { get; set; } = 12;
[Export] public Reckfell.Collections.Array<string> Tags { get; set; } = new() { "weapon" };
public bool TryGetDamage(out int damage)
{
damage = 0;
if (Damage < 1 || Damage > 100) return false;
damage = Damage;
return true;
}
}ExportedWeapon.cs
using Reckfell;
public partial class ExportedWeapon : Entity
{
[Signal] public delegate void FiredEventHandler(int damage);
[Export] public ShotSettings? Settings { get; set; }
[Export(PropertyHint.Range, "0,99,1")] private int _ammo = 5;
public int Ammo => _ammo;
public int TransientValue { get; set; } = 3;
public bool TryFire()
{
if (_ammo <= 0 || !ReckfellObject.IsInstanceValid(Settings) ||
!Settings!.TryGetDamage(out int damage)) return false;
_ammo--;
EmitSignal(SignalName.Fired, damage);
return true;
}
}InventoryLedger.cs
using System;
using System.Collections.Generic;
public sealed class InventoryLedger
{
private readonly Dictionary<string, int> _counts = new(StringComparer.Ordinal);
public int CountOf(string item) => _counts.TryGetValue(item, out int count) ? count : 0;
public bool TryAdd(string item, int amount)
{
if (string.IsNullOrWhiteSpace(item) || amount <= 0) return false;
int current = CountOf(item);
if (current > int.MaxValue - amount) return false;
_counts[item] = current + amount;
return true;
}
public Reckfell.Collections.Dictionary<string, int> Snapshot() => new(_counts);
}VariantCounter.cs
using Reckfell;
public static class VariantCounter
{
public static bool TryRead(Variant value, out int count)
{
count = 0;
if (value.VariantType != Variant.Type.Int) return false;
long raw = value.AsInt64();
if (raw < 0 || raw > 999) return false;
count = (int)raw;
return true;
}
}ManagedDataDemo2D.cs
using Reckfell;
public partial class ManagedDataDemo2D : Entity2D
{
public ExportedWeapon Weapon { get; private set; } = null!;
public InventoryLedger Inventory { get; } = new();
public Label Readout { get; private set; } = null!;
public Button FireButton { get; private set; } = null!;
private bool _ru;
public override void _Ready()
{
_ru = TranslationServer.GetLocale().StartsWith("ru");
var title = new Label { Position = new Vector2(28, 24), Text = _ru ? "C#: настройки и данные" : "C#: settings and data" };
title.AddThemeFontSizeOverride("font_size", 26);
AddChild(title);
AddChild(new Label { Position = new Vector2(28, 74), Text = _ru ? "Resource → свойство → игровая проверка" : "Resource → property → gameplay validation" });
Readout = new Label { Position = new Vector2(28, 126) };
AddChild(Readout);
Weapon = new ExportedWeapon { Settings = new ShotSettings() };
AddChild(Weapon);
Inventory.TryAdd("pulse", 2);
FireButton = new Button { Position = new Vector2(28, 260), Size = new Vector2(230, 48), Text = _ru ? "Сделать выстрел" : "Fire once" };
FireButton.Modulate = new Color(0.35f, 0.55f, 1);
FireButton.Pressed += Fire;
AddChild(FireButton);
AddChild(new Label { Position = new Vector2(28, 336), Text = _ru ? "Range — подсказка. TryFire проверяет данные." : "Range is a hint. TryFire validates the data." });
Refresh();
}
private void Fire()
{
Weapon.TryFire();
Refresh();
}
private void Refresh()
{
Variant sample = 25L;
VariantCounter.TryRead(sample, out int count);
Readout.Text = _ru
? $"Урон: {Weapon.Settings!.Damage}\nБоезапас: {Weapon.Ammo}\nВ инвентаре: {Inventory.CountOf("pulse")}\nVariant.Int: {count}"
: $"Damage: {Weapon.Settings!.Damage}\nAmmo: {Weapon.Ammo}\nInventory: {Inventory.CountOf("pulse")}\nVariant.Int: {count}";
}
}Что сохраняет Export
ExportedWeapon.Settings — ссылка на Resource. Приватное поле _ammo тоже экспортировано: private ограничивает C#-доступ, а не исключает сохранение. При упаковке этого объекта значение _ammo = 9 восстанавливается как 9. Неэкспортированное TransientValue = 42 в новом экземпляре возвращается к инициализатору 3. Доступность свойства через Get/Set сама по себе не означает, что оно сохраняется.
Для сохранения уровня или префаба действуют также правила Owner и PackedEntity. Export не создаёт сохранение игры на диске и не сохраняет изменения запущенного экземпляра обратно в исходный файл автоматически.
Подсказка и проверка — разные операции
Range задаёт метаданные Inspector. Присваивание Damage = 1000 или Set для _ammo со значением −2 не ограничивается указанным диапазоном. TryGetDamage и TryFire явно отклоняют неверное состояние до расхода боезапаса и сигнала. Не полагайтесь на ползунок как на проверку файлов, сетевого ввода или собственного кода.
Экспортируйте совместимые с Variant поля либо читаемые/записываемые свойства экземпляра. List<int> не заменяет Reckfell.Collections.Array<int>. Static, readonly и неподдерживаемый тип дают RF0101, RF0103 и RF0102 соответственно. Resource не может экспортировать ссылку на Entity: RF0107; храните такую ссылку на Entity-компоненте.
Изменили поля — сначала соберите C#-проект. Если Inspector не обновился, проверьте ошибки сборки, прикреплённый скрипт и точное имя класса/файла. Нативное сохранение и метаданные проверены; мышиные действия в Inspector и сохранение из его окна в этой проверке не выполнялись.