Привет! А я тут чуть ли не помер от мысли об этом посте...
Так уж вышло, что инвентарь - не простая штука, а именно:
- Добавляя инвентарь ты добавляешь GUI который плотно связан с игровым процессом.
- Добавляешь предметы, а предметы в моей игре могут находиться в мире как объекты.
И как вы понимаете это немного, но! У Monogame есть проблема:
Шрифты которые он нам предлагает - говно... Да вот так кратко.
Так что я добавил SpriteFontPlus для нормального текста в игре.
Ну, теперь давайте о игре поговорим уже...
Для начала я резделил игру на гейм стейты и добавил копание на правую клавишу, но это не интересно, показывать не буду.
И давайте о том, что же я сделал и что я буду показывать:
Немного поменялся интерфейс предмета:
Тут всё для новых механик которые будут в игре.
Так уж вышло, что инвентарь - не простая штука, а именно:
- Добавляя инвентарь ты добавляешь GUI который плотно связан с игровым процессом.
- Добавляешь предметы, а предметы в моей игре могут находиться в мире как объекты.
И как вы понимаете это немного, но! У Monogame есть проблема:
Шрифты которые он нам предлагает - говно... Да вот так кратко.
Так что я добавил SpriteFontPlus для нормального текста в игре.
Ну, теперь давайте о игре поговорим уже...
Для начала я резделил игру на гейм стейты и добавил копание на правую клавишу, но это не интересно, показывать не буду.
И давайте о том, что же я сделал и что я буду показывать:
Немного поменялся интерфейс предмета:
public interface IItem
{
string Name { get; }
string Description { get; }
int MaxCount { get; }
Sprite ItemSprite { get; }
void Use(ITransform transform, ref int count);
void With(ITransform transform, ref byte armsState, ref float armsRotation);
}
Тут всё для новых механик которые будут в игре.
👍1
Теперь о предметах:
Это предмет, он падает и подбирается игроком но вы могли увидеть тут совсем нам не знакомый класс Core.
public class Item : Entity
{
public const float getItemDistance = Map.tileSize * 4;
public (IItem, int) item;
private FVector2 velocity;
private FVector2 position;
private readonly Player player;
public Item((IItem, int) item, FVector2 position) : base()
{
this.item = item;
this.position = position;
velocity = new FVector2(Random.Float(-10, 10), Random.Float(-55, -30));
player = Core.GetEntity<Player>();
}
public override void Draw() => SDraw.Rect(item.Item1.ItemSprite, position);
public override void Update()
{
bool collided = false;
Core.world.RayCast(
(fixture, point, normal, fraction) =>
{
if (fixture.Body.Tag is IMap)
{
position = point + normal * 0.1f;
velocity.X = 0;
if (normal.Y != 0)
velocity.Y = 0;
collided = true;
return 0;
}
return -1;
}, position, position + velocity * Time.Delta);
if (FVector2.Distance(player.transform.Position, position) < getItemDistance)
{
int count = player.inventory.Add(item.Item1, item.Item2);
if (count != 0)
item.Item2 = count;
else
Remove();
}
if (!collided) position += velocity * Time.Delta;
velocity.Y += InGameState.gravity * Time.Delta;
}
}
Это предмет, он падает и подбирается игроком но вы могли увидеть тут совсем нам не знакомый класс Core.
👍1
Не волнуйтесь, этот класс это просто контейнер с переменными и некоторыми функциями для менеджмента существ:
public static class Core
{
public static Sprite[] icons;
public static Sprite OnInventoryIcon => icons[1];
public static Sprite OffInventoryIcon => icons[0];
public static Button.Style buttonStyle;
public static Window.Style windowStyle;
public static DynamicSpriteFontScaled font;
public static (Action draw, Action update) entities = (() => { }, () => { });
public static World world;
public static IMap map;
public static Camera camera;
public static Action criticalGuiDraw = () => { };
public static T GetEntity<T>()
where T : Entity
{
foreach (var entity in entities.update.GetInvocationList())
{
if (entity.Target is T t) return t;
}
return null;
}
public static void AddEntity(Action draw, Action update)
{
entities.update += update;
entities.draw += draw;
}
public static void RemoveEntity(Action draw, Action update)
{
entities.update -= update;
entities.draw -= draw;
}
}
👍1
Теперь о GUI, в нём всё тоже самое но только поменялись эти функции а с ними и конструктор:
public FVector2 GetAnchoredPosition(FVector2 point, FRectangle rectangle) => rectangle.Location + (rectangle.Size - this.rectangle.Size) * Anchor + point;
public void Draw() => BaseDraw(new FRectangle(FVector2.Zero, Camera.WorldViewport));
public void BaseDraw(FRectangle rectangle)
{
FRectangle rect = this.rectangle.Size == FVector2.Zero ? rectangle : new FRectangle(GetAnchoredPosition(this.rectangle.Location, rectangle), this.rectangle.Size);
Draw(rect);
DrawAction(rect);
}
public virtual void Draw(FRectangle rectangle)
{
}
public void Update() => BaseUpdate(new FRectangle(FVector2.Zero, Camera.WorldViewport));
public void BaseUpdate(FRectangle rectangle)
{
FRectangle rect = this.rectangle.Size == FVector2.Zero ? rectangle : new FRectangle(GetAnchoredPosition(this.rectangle.Location, rectangle), this.rectangle.Size);
Update(rect);
if (MouseOn)
{
Delegate[] list = UpdateAction.GetInvocationList();
bool breaked = false;
for (int i = list.Length - 1; i >= 0; i--)
if (list[i].Target is GUIElement target)
{
target.MouseOn = false;
if (breaked) continue;
list[i].DynamicInvoke(rect);
if (target.MouseOn) breaked = true;
}
}
}
public virtual void Update(FRectangle rectangle)
{
if (MouseOn = (Parent?.MouseOn ?? true) && rectangle.Contains(Mouse.GUIPosition))
Mouse.OnGUI = true;
}
👍1
Раз уж со всеми проблемами разобрались то давайте переходить к инвентарю:
public class InventoryContainer : IInventory
{
public (IItem item, int count)[,] items = new (IItem item, int count)[5, 5];
const int slotSize = 10;
GUIElement on, off, window;
public InventoryContainer(GUIElement GUI)
{
// Код вырезан по причине того, что обьясняется отдельно
}
public void DrawSelected()
{
if (Selected.item == null) return;
SDraw.Rect(Selected.item.ItemSprite, Mouse.GUIPosition);
SDraw.Text(Core.font, $"{Selected.count}개", Mouse.GUIPosition);
}
public (IItem item, int count) Selected { get; private set; }
public void Get(int x, int y)
{
var item = items[x, y];
if (Selected.item != item.item)
{
items[x, y] = Selected;
Selected = item;
}
else
{
int count = item.count += Selected.count;
if (count > item.item.MaxCount) Selected = (Selected.item, count - item.item.MaxCount);
else Selected = default;
}
}
public int Add(IItem item, int count)
{
int _x, _y;
for (_x = 0; _x < items.GetLength(0); _x++)
for (_y = 0; _y < items.GetLength(1); _y++)
if (items[_x, _y].item == item)
{
int i = items[_x, _y].count += count;
if (i > item.MaxCount)
{
count = i - item.MaxCount;
items[_x, _y].count = item.MaxCount;
}
else return 0;
}
for (_x = 0; _x < items.GetLength(0); _x++)
for (_y = 0; _y < items.GetLength(1); _y++)
{
if (items[_x, _y].item == null)
{
if (count > item.MaxCount)
{
items[_x, _y] = (item, item.MaxCount);
count -= item.MaxCount;
}
else
{
items[_x, _y] = (item, count);
return 0;
}
}
}
return count;
}
public void Remove(IItem item, int count)
{
for (int x = 0; x < items.GetLength(0); x++)
for (int y = 0; y < items.GetLength(1); y++)
if (items[x, y].item == item)
{
count -= items[x, y].count;
if (count < 0)
items[x, y].count = -count;
else
items[x, y] = default;
}
}
public bool Contains(IItem item, int count)
{
int counter = 0;
for (int x = 0; x < items.GetLength(0); x++)
for (int y = 0; y < items.GetLength(1); y++)
if (items[x, y].item == item)
counter += items[x, y].count;
return counter >= count;
}
}
Простая реализация IInventory с интерфейсом который создаётся этими строками:
Тут создаётся кнопка нажимая на которую у нас открывается инвентарь а сама кнопка исчезает
Дальше:
Создаём окно с кнопками инвентаря, тут так-же видно, что я использую корейский символ в тексте, шрифт от Monogame не поддерживает такое из коробки и приходится идти путями окольными, благо кто-то добрый вывел нас из темени этих дебрей светом от великолепного SpriteFontPlus!
on = new Button(null, new FVector2(0, 0), new FRectangle(0, 0, slotSize, slotSize), () =>
{
on.Remove();
window.Remove();
GUI.Add(off);
Core.criticalGuiDraw -= DrawSelected;
}, Core.buttonStyle, Core.OnInventoryIcon);
Тут создаётся кнопка нажимая на которую у нас открывается инвентарь а сама кнопка исчезает
Дальше:
window = new Window(null, new FVector2(0, 0), new FRectangle(slotSize + 1, 0, items.GetLength(0) * (slotSize + 1) + 3, items.GetLength(1) * (slotSize + 1) + 3), Core.windowStyle);
for (int x = 0; x < items.GetLength(0); x++)
for (int y = 0; y < items.GetLength(1); y++)
{
int _x = x, _y = y;
new Button(window, new FVector2(0, 0), new FRectangle(
x * (slotSize + 1) + 2,
y * (slotSize + 1) + 2,
slotSize, slotSize),
() => Get(_x, _y), Core.buttonStyle,
(rectangle) =>
{
if (items[_x, _y].item == null) return;
SDraw.Rect(items[_x, _y].item.ItemSprite, rectangle.Center);
SDraw.Text(Core.font, $"{items[_x, _y].count}개", rectangle.Center);
});
}
Создаём окно с кнопками инвентаря, тут так-же видно, что я использую корейский символ в тексте, шрифт от Monogame не поддерживает такое из коробки и приходится идти путями окольными, благо кто-то добрый вывел нас из темени этих дебрей светом от великолепного SpriteFontPlus!
👍1
Кхм, что-то меня занесло, продолжаем:
Тут создаётся кнопка для удаления инвентаря и добавления кнопки.
Так-же в игрока были добавлены строки:
Поля:
В конструкторе:
При рисовании:
И в обновлении:
И теперь у нас есть инвентарь, предметы и копание.
ВВЕРХ
off = new Button(GUI, new FVector2(0, 0), new FRectangle(0, 0, slotSize, slotSize), () =>
{
off.Remove();
GUI.Add(on);
GUI.Add(window);
Core.criticalGuiDraw += DrawSelected;
if (Selected.item != null) Add(Selected.item, Selected.count);
Selected = default;
}, Core.buttonStyle, Core.OffInventoryIcon);
Тут создаётся кнопка для удаления инвентаря и добавления кнопки.
Так-же в игрока были добавлены строки:
Поля:
public readonly InventoryContainer inventory;
private float armsRotation;
private byte inArm;
private byte armsState;
В конструкторе:
inventory = new InventoryContainer(GUI);
При рисовании:
inventory.items[inArm, 0].item?.With(transform, ref armsState, ref armsRotation);
И в обновлении:
(IItem item, int count) = inventory.items[inArm, 0];
item?.Use(transform, ref count);
if (count <= 0)
inventory.items[inArm, 0] = default;
else
inventory.items[inArm, 0] = (item, count);
Core.camera.Position = transform.Position;
if (Keyboard.IsPressed(Keys.D1)) inArm = 0;
if (Keyboard.IsPressed(Keys.D2)) inArm = 1;
if (Keyboard.IsPressed(Keys.D3)) inArm = 2;
if (Keyboard.IsPressed(Keys.D4)) inArm = 3;
if (Keyboard.IsPressed(Keys.D5)) inArm = 4;
if (Keyboard.IsPressed(Keys.Q))
{
new Item(inventory.items[inArm, 0], transform.Position);
inventory.items[inArm, 0] = (null, 0);
}
И теперь у нас есть инвентарь, предметы и копание.
ВВЕРХ
👍1
Ну, я заболел, мне хреново, ввожу в курс дела чтобы потом не было вопросов, мне прям очень плохо, даже говорить больно не говоря о мыслительных процессах.
Я вернулся, сделал кирку и лук а так-же воду, но разделю это всё на отдельные посты.
А ещё я не буду вам всё объяснять дотошно а то я иногда меняю кучу всяких вещей в разных класса но итогом изменений практически нету...
Давайте начнём с кирки, тут всё просто:
Для начала я сделал класс для оружий ближнего боя, он в принципе не сложный.
При нажатии мыши ждать X время и менять угол замаха на противоположный после удара.
А ещё я не буду вам всё объяснять дотошно а то я иногда меняю кучу всяких вещей в разных класса но итогом изменений практически нету...
Давайте начнём с кирки, тут всё просто:
Для начала я сделал класс для оружий ближнего боя, он в принципе не сложный.
При нажатии мыши ждать X время и менять угол замаха на противоположный после удара.
public class MeleeWeapon : IItem
{
public string Name => throw new NotImplementedException();
public string Description => throw new NotImplementedException();
public int MaxCount => 1;
public Sprite ItemSprite { get; init; }
public bool Flip => false;
public bool Animated => true;
private readonly float swingAngle;
private readonly float swingPerSecond;
private readonly byte state;
private readonly float offset;
private readonly float attackOffset;
private readonly Sprite sprite;
private static bool side;
public MeleeWeapon(float swingAngle, float swingPerSecond, byte state, float offset, float attackOffset, Sprite sprite, Sprite item)
{
this.swingAngle = swingAngle;
this.swingPerSecond = swingPerSecond;
this.state = state;
this.offset = offset;
this.attackOffset = attackOffset;
this.sprite = sprite;
ItemSprite = item;
}
public void Use(ITransform transform, ref byte armsState, ref float armLRotation, ref float armRRotation, ref int count, ref float timer, ArmData armData)
{
armsState = state;
FVector2 basePosition = transform.Local2World(new FVector2(0, -4));
FVector2 lposition = basePosition - Mouse.Position;
lposition.Normalize();
float baseAngle = MathHelper.ToDegrees(MathF.Atan2(lposition.Y, lposition.X));
float angle = baseAngle + (side ? swingAngle : -swingAngle) + 90;
FVector2 armPosition = basePosition + FVector2.UpOf(angle) * offset;
FVector2 p = transform.Local2World(new FVector2(2, -4)) - armPosition;
armLRotation = MathHelper.ToDegrees(MathF.Atan2(p.Y, p.X)) + 90;
p = transform.Local2World(new FVector2(-2, -4)) - armPosition;
armRRotation = MathHelper.ToDegrees(MathF.Atan2(p.Y, p.X)) + 90;
if (Mouse.LeftDown)
{
timer += Time.Delta * swingPerSecond;
while (timer >= 1)
{
FVector2 attackPosition = basePosition - lposition * attackOffset;
Attack(attackPosition);
Effects.slashMedium.Spawn(attackPosition, baseAngle + 180);
side = !side;
timer--;
}
}
else
{
timer = 0;
}
armData.Set(
("angle", angle),
("armPosition", armPosition)
);
}
public void With(ITransform transform, byte armsState, float armLRotation, float armRRotation, ArmData armData)
{
armData.Get(out FVector2 position, "armPosition");
armData.Get(out float angle, "angle");
SDraw.Rect(sprite, position, angle + 90, 1, 0, Origin.Zero);
}
public virtual void Attack(FVector2 point)
{
}
}
Теперь о самой кирке:
Просто используем класс оружия ближнего боя но во время удара копаем.
Просто используем класс оружия ближнего боя но во время удара копаем.
public class Pickaxe : MeleeWeapon
{
private readonly float power;
private readonly float radius;
public Pickaxe(float power, float radius, float swingAngle, float swingPerSecond, byte state, float offset, float attackOffset, Sprite sprite, Sprite item) : base(swingAngle, swingPerSecond, state, offset, attackOffset, sprite, item)
{
this.power = power;
this.radius = radius;
}
public override void Attack(FVector2 point)
{
Core.map.MineTile(Core.map.World2Cell(point), power, radius);
}
}
А вот и лук, тут уже всё сложнее:
Если при отжатой левой клавишей мыши анимация находиться на кадре больше нулевого - запускай стрелу.
Если при отжатой левой клавишей мыши анимация находиться на кадре больше нулевого - запускай стрелу.
public class Bow : IItem
{
public string Name => throw new NotImplementedException();
public string Description => throw new NotImplementedException();
public int MaxCount => 1;
public Sprite ItemSprite { get; init; }
public bool Flip => false;
public bool Animated => true;
private readonly Func<Projectile> projectile;
private readonly (float min, float max) arrowOffset;
private readonly float offset;
private readonly float framerateScale;
private readonly float power;
private readonly Sprite[] sprites;
public Bow(Func<Projectile> projectile, (float min, float max) arrowOffset, float offset, float power, Sprite[] sprites, Sprite item, float framerateScale = 1)
{
this.projectile = projectile;
this.arrowOffset = arrowOffset;
this.offset = offset;
this.framerateScale = framerateScale;
this.power = power;
this.sprites = sprites;
ItemSprite = item;
}
public void Use(ITransform transform, ref byte armsState, ref float armLRotation, ref float armRRotation, ref int count, ref float timer, ArmData armData)
{
int frame = 0;
FVector2 basePosition = transform.Local2World(new FVector2(2, -4));
FVector2 position = basePosition - Mouse.Position;
armLRotation = MathHelper.ToDegrees(MathF.Atan2(position.Y, position.X)) + 90;
position.Normalize();
sprites.AnimationEnd(out frame, SpriteHelpers.frameRate * framerateScale, ref timer);
if (Mouse.LeftUp)
{
if (frame > 0)
projectile().Spawn(basePosition, armLRotation + 90, power * timer);
frame = 0;
timer = 0;
}
FVector2 rp = transform.Local2World(new FVector2(-2, -4)) - (basePosition - position * arrowOffset.max);
position = basePosition - position * MathHelper.Lerp(arrowOffset.max, arrowOffset.min, frame / (float)(sprites.Length-1));
armRRotation = MathHelper.ToDegrees(MathF.Atan2(rp.Y, rp.X)) + 90;
armsState = Player.GetState(frame, 1);
armData.Set(
("frame", frame),
("position", position)
);
}
public void With(ITransform transform, byte armsState, float armLRotation, float armRRotation, ArmData armData)
{
armData.Get(out int frame, "frame");
armData.Get(out FVector2 position, "position");
FVector2 offset = FVector2.UpOf(armLRotation) * this.offset;
SDraw.Rect(sprites[frame], transform.Local2World(new FVector2(2, -4)) + offset, armLRotation + 90);
SDraw.Rect(projectile().sprite, position + offset, armLRotation + 90, 1, 0, Origin.Zero);
}
}
This media is not supported in your browser
VIEW IN TELEGRAM
Тут стрельба из лука и копание киркой.
👍1
А вот пост о воде.
Для начала рассмотрим конструктор и поля:
cells - клетки воды, значение 0 - пусто, -1 - стенка.
render - клетки воды но сглаженные меж кадрами.
liquid - спрайты для авто-тайла воды.
timer - таймер для обновления мира.
Не думаю, что width и height требуют объяснений.
Дальше идёт конструктор который собирает мир с помощью лямбды генератора, потом буду заменять на отельный класс WordGenerator.
Дальше идут функции для проверок:
CanFlow - проверка на то можно ли в клетку плыть. (в данном случае именно плыть ибо это либо просто движение либо подводное передвижение)
CanPush - проверка на то можно ли в клетку выкинуть часть себя.
Move - движение в клетку, возвращает ту часть которая не смогла переместиться.
Для начала рассмотрим конструктор и поля:
cells - клетки воды, значение 0 - пусто, -1 - стенка.
render - клетки воды но сглаженные меж кадрами.
liquid - спрайты для авто-тайла воды.
timer - таймер для обновления мира.
Не думаю, что width и height требуют объяснений.
Дальше идёт конструктор который собирает мир с помощью лямбды генератора, потом буду заменять на отельный класс WordGenerator.
public class WaterWorld
{
public float[,] cells;
public readonly float[,] render;
private readonly Sprite[] liquid;
private readonly int width;
private readonly int height;
private float timer = 0;
private const float time = .01f;
public WaterWorld(Sprite sprite, int width, int height, Func<int, int, bool> generator)
{
cells = new float[width, height];
render = new float[width, height];
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
render[i, j] = cells[i, j] = generator(i, j) ? 0 : -1;
}
}
liquid = sprite.Split(4, 4, 1);
this.width = width;
this.height = height;
}
...
Дальше идут функции для проверок:
CanFlow - проверка на то можно ли в клетку плыть. (в данном случае именно плыть ибо это либо просто движение либо подводное передвижение)
CanPush - проверка на то можно ли в клетку выкинуть часть себя.
Move - движение в клетку, возвращает ту часть которая не смогла переместиться.
...
public bool CanFlow(float other)
{
if (other == -1) return false;
if (other >= 1) return false;
return true;
}
public bool CanPush(float other)
{
if (other == -1) return false;
if (other >= .5) return false;
return true;
}
public float Move(float pop, float r, ref float other)
{
float sum = r + pop;
if (sum > 1)
{
other += 1 - r;
return sum - 1;
}
other += pop;
return 0;
}
...
Дальше идёт две функции обновления:
Update - обновляет таймер и если время таймера больше чем возможное время то пока оно не будет меньше вызывать Tick.
Сделано чтобы вода на всех устройствах лилась одинаково.
Tick - обновление каждой клетки
Update - обновляет таймер и если время таймера больше чем возможное время то пока оно не будет меньше вызывать Tick.
Сделано чтобы вода на всех устройствах лилась одинаково.
Tick - обновление каждой клетки
...
public void Update()
{
timer += Time.Delta;
while (timer >= time)
{
Tick();
timer -= time;
}
}
public void Tick()
{
float[,] mask = new float[width, height];
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
float self = cells[i, j];
if (self == -1)
{
mask[i, j] = -1;
continue;
}
if (self == 0) continue;
bool
b_l = false,
b_r = false,
b_d = false,
b_u = false;
float l = i == 0 ? -1 : cells[i-1, j];
float r = i == width-1 ? -1 : cells[i+1, j];
float d = j == height-1 ? -1 : cells[i, j+1];
float u = j == 0 ? -1 : cells[i, j-1];
b_d = CanFlow(d);
if (!CanPush(d))
{
b_l = l < self && CanFlow(l);
b_r = r < self && CanFlow(r);
if (b_l && b_r && r != l)
{
if (l > r)
b_l = false;
else
b_r = false;
}
}
if (self > .5)
{
b_u = CanPush(u);
}
int c = 1;
if (b_l) c++;
if (b_r) c++;
if (b_d) c++;
if (b_u) c++;
float sum = self / c;
if (c == 2 && b_d)
{
self = Move(self, d, ref mask[i, j + 1]);
}
else
{
self = sum;
if (b_l) self += Move(sum, l, ref mask[i - 1, j]);
if (b_r) self += Move(sum, r, ref mask[i + 1, j]);
if (b_d) self += Move(sum, d, ref mask[i, j + 1]);
if (b_u) self += Move(sum, u, ref mask[i, j - 1]);
}
mask[i, j] += self;
}
}
for (int j = 0; j < height; j++)
for (int i = 0; i < width; i++)
{
if (mask[i, j] == -1) render[i, j] = -1;
else render[i, j] = MathHelper.Lerp(render[i, j], mask[i, j], .1f);
}
cells = mask;
}
...
И наконец функция Draw рисует каждую клетку которая может быть видна а код авто-тайлинга взят из класса AutoTile:
public void Draw()
{
for (int i = 0; i < width; i++)
for (int j = 0; j < height; j++)
{
if (render[i, j] <= 0.1) continue;
float l = i == 0 ? -1 : render[i - 1, j];
float r = i == width - 1 ? -1 : render[i + 1, j];
float d = j == height - 1 ? -1 : render[i, j + 1];
float u = j == 0 ? -1 : render[i, j - 1];
byte res = 5;
bool left = l != -1 && l <= 0.1, right = r != -1 && r <= 0.1,
down = u != -1 && u <= 0.1, up = d != -1 && d <= 0.1;
bool lr = left && right,
du = down && up;
if (up) res = 9;
if (down) res = 1;
if (right) res = 6;
if (left) res = 4;
if (left && up) res = 8;
if (right && up) res = 10;
if (left && down) res = 0;
if (right && down) res = 2;
if (lr) res = 7;
if (du) res = 13;
if (lr && up) res = 11;
if (lr && down) res = 3;
if (du && left) res = 12;
if (du && right) res = 14;
if (lr && du) res = 15;
SDraw.Rect(liquid[res], new Color(1f, 1, 1, render[i, j]), new FVector2(i, j) * Map.tileSize, 0, 1, 0, Origin.Zero, Origin.Zero);
}
}
}
👍1