VozDuh
59 subscribers
8 photos
26 videos
11 links
Это канал по разработке игр, вдохновлённый другим ТГ каналом, в этом канале я буду показывать как разрабатываю игру с открытым кодом
Download Telegram
VozDuh pinned «Ссылки: GitHub, Itch.io. Путь проекта: Новость №0 - Знакомство Новость №1 - Мир | Видео Новость №2 - Физика | Видео Новость №3 - Авто-тайлы | Видео Новость №4 - Персонаж | Видео Новость №5 - GUI | Видео Новость №6 - Инвентарь | Видео Новость №7 Кирка | Видео…»
Вот и первый пост о игре, стартовая точка.
Создал карту, плитки и чанки.

Для начала я создал 2 интерфейса:

public interface ITile
{
string Name { get; }
string Description { get; }
byte Hardness { get; }
void Changed(IMap map, int x, int y, TileData data);
void Update(IMap map, int x, int y, TileData data);
void Start(IMap map, int x, int y, TileData data);
void Draw(IMap map, int x, int y, Vector2 drawPosition, TileData data);
byte[] GetData();
}

И интерфейс IMap:
    public interface IMap
{
bool TryPlaceTile(ITile tile, int x, int y);
bool TryPlaceTile(ITile tile, (int x, int y) position);
bool PlaceTile(ITile tile, int x, int y);
bool PlaceTile(ITile tile, (int x, int y) position);
bool TrySetTile(ITile tile, int x, int y);
bool TrySetTile(ITile tile, (int x, int y) position);
void SetTile(ITile tile, int x, int y);
void SetTile(ITile tile, (int x, int y) position);
TileData GetTile(int x, int y);
TileData GetTile((int x, int y) position);
(int x, int y) World2Cell(float x, float y);
(int x, int y) World2Cell(Vector2 position);
Vector2 Cell2World(int x, int y);
Vector2 Cell2World((int x, int y) position);
}

В этом интерфейсе есть:
TryPlaceTile - ставит плитку если вокруг есть хотя бы одна плитка и место куда ставиться пусто.
PlaceTile - ставит плитку если вокруг есть хотя бы одна плитка.
TrySetTile - ставит плитку если место куда ставиться пусто.
SetTile - ставит плитку.
GetTile - получает данные о плитке.
World2Cell - конвертирует из мировых координат в координаты клетки.
Cell2World - конвертирует из координат клетки в мировые координаты.

После чего была создана структура:
    public struct TileData
{
public byte Health { get; init; }
public byte[] StateData { get; init; }
public ITile Tile { get; init; }

public TileData()
{
Health = 0;
StateData = null;
Tile = null;
}

public TileData(byte health, ITile tile) : this()
{
Health = health;
StateData = tile.GetData();
Tile = tile;
}
}

В ней находятся данные для обработки ITile'ом.

Немного настроил проект добавив более приятный инпут с мыши и клавиатуры.
Так-же добавил камеру которую сделал на самом деле уже давно а передвижение сделано при помощи строчек:
if (Mouse.RightPressed)
Start = camera.Position + Mouse.GUIPosition;
if (Mouse.RightDown)
camera.Position = Start - Mouse.GUIPosition;


Теперь в игре можно рисовать плитками которые я оптимизировал как смог!
🔥2
Как физический движок я решил использовать Aether.Physics2D, порыскав в документации я нашёл как создать менеджер и добавил его в код с помощью строк:
private World world;

protected override void Initialize()
{
...
world = new World(new FVector2(0, 90));
world.Add(aabb);
...
}
protected override void Update(GameTime gameTime)
{
...
world.Step(Time.Delta);
...
}

Потом добавил просто коллайдер который будет падать:
aabb = new Body();
aabb.BodyType = BodyType.Dynamic;
var r = aabb.CreateRectangle(13, 20, 0, FVector2.Zero);
r.Restitution = 0.2f;


В чанках добавил поле:
private Fixture[] fixtures;

И написал функцию которая использует принцип квадро-дерева для создания коллизии.
public void UpdateColision(Body body, int x, int y)
{
Queue<Rectangle> rectangles = new Queue<Rectangle>();
rectangles.Enqueue(new Rectangle(0, 0, chunkSize, chunkSize));
Queue<Rectangle> result = new Queue<Rectangle>();

int j, i;
while (rectangles.Count != 0)
{
Rectangle rectangle = rectangles.Dequeue();
for (i = rectangle.X; i < rectangle.X + rectangle.Width; i++)
{
for (j = rectangle.Y; j < rectangle.Y + rectangle.Height; j++)
{
if (tiles[i, j].Tile == null)
{
if (rectangle.Width > 1)
{
int w = rectangle.Width / 2,
h = rectangle.Height / 2;
rectangles.Enqueue(new Rectangle(rectangle.X, rectangle.Y, w, h));
rectangles.Enqueue(new Rectangle(rectangle.X + w, rectangle.Y, w, h));
rectangles.Enqueue(new Rectangle(rectangle.X, rectangle.Y + h, w, h));
rectangles.Enqueue(new Rectangle(rectangle.X + w, rectangle.Y + h, w, h));
}
goto SKIP;
}
}
}
result.Enqueue(rectangle);
SKIP:;
}

fixtures = new Fixture[result.Count];
while (result.Count != 0)
{
Rectangle rectangle = result.Dequeue();
fixtures[result.Count] = body.CreateRectangle(
rectangle.Width * tileSize, rectangle.Height * tileSize,
1,
new FVector2(
x * chunkSize + rectangle.X + rectangle.Width / 2f,
y * chunkSize + rectangle.Y + rectangle.Height / 2f
) * tileSize);
}
}


Добавил обновление чанков в SetTile:
c.UpdateColision(body, chunk.x, chunk.y);


Ну и для удобства сделал структуру FVector2 которая на автомате конвертируется в Vector2 из физ. движка в Vector2 из XNA и наоборот.

Теперь у нас есть коллизия и бедный человечек который на видео падает головой вниз!
🔥1
Демонстрация квадро-древесной коллизии.
Места с не объединяющимися плитками - стыки чанков.
🔥3
Благодаря предыдущим подготовкам я создал автотайлы очень быстро:
public class AutoTile : ITile
{
private readonly Sprite[] sprites;

public AutoTile(byte health, Sprite sprite)
{
sprites = sprite.Split(4, 4, 1);
this.health = health;
}

public string Name => throw new System.NotImplementedException();

public string Description => throw new System.NotImplementedException();

public byte Health => health;
public byte health;

public byte Hardness => throw new System.NotImplementedException();

public void Changed(IMap map, int x, int y, TileData data)
{
data[0] = UpdateTile(map, x, y);
}

public void Draw(IMap map, int x, int y, FVector2 drawPosition, TileData data)
{
SDraw.Rect(sprites[data[0]], drawPosition, 0, 1, 0, SpriteEffects.None, SDraw.Origin.Zero, SDraw.Origin.Zero);
}

public byte[] GetData() => new byte[] { 0 };

public void Start(IMap map, int x, int y, TileData data)
{
data[0] = UpdateTile(map, x, y);
}

// ВНИМАНИЕ!!! Ужасный иф-о код который спасёт вам пару кадров, но всё равно простите за такой ужас
public byte UpdateTile(IMap map, int x, int y)
{
byte res = 5;
bool left = map.GetTile(x-1, y).Tile == null, right = map.GetTile(x+1, y).Tile == null,
down = map.GetTile(x, y-1).Tile == null, up = map.GetTile(x, y+1).Tile == null;
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;

return res;
}

public void Update(IMap map, int x, int y, TileData data)
{
}

public void Use()
{
throw new System.NotImplementedException();
}

public byte With()
{
throw new System.NotImplementedException();
}
}

А так-же я создал IItem для будущего инвентаря:
public interface IItem
{
string Name { get; }
string Description { get; }
void Use();
byte With();
}

В нём есть функции:
Use - использование предмета, зелье будет выпито а меч создаст атаку.
With - с помощью него будут рисоваться предметы в руках возвращает стейт рук.
Потом так-же будет передаваться ITransform а возвращаемый byte станет ref значением.

ITransform - ещё не готов но он будет содержать функции:
Vector2 World2Local(Vector2 position);
Vector2 Local2World(Vector2 position);
float World2Local(float degrees);
float Local2World(float degrees);


Кстати, в коде вы можете заметить такие классы как:
SDraw и Sprite.
SDraw - расшифровывается как StaticDraw, это класс для того чтобы не передавать всё время SpriteBatch, содержит функции для рисования спрайтов:
Rect - рисует спрайт.
Text - рисует текст.
RectXLine - рисует спрайт растянутый по оси X от точки А до точки Б.
RectYLine - рисует спрайт растянутый по оси Y от точки А до точки Б.
RectXArrow - рисует спрайт растянутый по оси X от точки А до точки Б с спрайтом на точке Б.
RectYArrow - рисует спрайт растянутый по оси Y от точки А до точки Б с спрайтом на точке Б.
А Sprite это простая структура:
public struct Sprite
{
public Texture2D Texture { get; init; }

public int Width => Texture.Width;
public int Height => Texture.Height;

public Rectangle Rect { get; init; }

public Sprite(Sprite sprite, Rectangle rect)
{
Texture = sprite.Texture;
Rect = new Rectangle(rect.Location + sprite.Rect.Location, rect.Size);
}

public Sprite(Texture2D texture)
{
Texture = texture;
Rect = new Rectangle(0, 0, texture.Width, texture.Height);
}

public Sprite(Texture2D texture, Rectangle rect)
{
Texture = texture;
Rect = rect;
}

public Sprite[] Split(int columns, int rows, int padding = 0, int ignore = 0)
{
int subTextureWidth = (Rect.Width - (columns - 1) * padding) / columns;
int subTextureHeight = (Rect.Height - (rows - 1) * padding) / rows;

Sprite[] subTextures = new Sprite[rows * columns - ignore];

for (int row = 0; row < rows; row++)
{
for (int col = 0; col < columns; col++)
{
if (row * columns + col >= rows * columns - ignore) break;

int x = col * (subTextureWidth + padding),
y = row * (subTextureHeight + padding);
Rectangle subTextureRect = new Rectangle(x, y, subTextureWidth, subTextureHeight);

subTextures[row * columns + col] = new Sprite(this, subTextureRect);
}
}

return subTextures;
}

public void SplitX(int x, out Sprite a, out Sprite b)
{
a = new Sprite(this, new Rectangle(Rect.X, Rect.Y, Rect.Width, x));
b = new Sprite(this, new Rectangle(Rect.X, x, Rect.Width, Rect.Height - x));
}

public void SplitY(int y, out Sprite a, out Sprite b)
{
a = new Sprite(this, new Rectangle(Rect.X, Rect.Y, Rect.Width, y));
b = new Sprite(this, new Rectangle(Rect.X, y, Rect.Width, Rect.Height - y));
}

public static implicit operator Texture2D(Sprite sprite) => sprite.Texture;
}


Ничего от вас не скрываю!
👍2😁1
Проверка комментариев.
Сейчас мы уже начинаем делать что-то похожее на игру, и следующим после плиток я решил добавить персонажа.
Но перед этим нужно опять подготовить для него всё нужное:
public class BodyTransform : ITransform
{
public bool flipX;
public float Rotation
{
get => body.Rotation;
set => body.Rotation = value;
}
public FVector2 Position
{
get => body.Position;
set => body.Position = value;
}

public readonly Body body;

public BodyTransform(Body body)
{
this.body = body;
}

public BodyTransform(Body body, bool flipX)
{
this.body = body;
this.flipX = flipX;
}

public float Local2World(float degrees) => degrees + body.Rotation;
public float World2Local(float degrees) => degrees - body.Rotation;

public FVector2 Local2World(FVector2 point) => FVector2.Transform(point, Matrix);
public FVector2 World2Local(FVector2 point) => FVector2.Transform(point, XMatrix.Invert(Matrix));

private XMatrix Matrix => XMatrix.CreateTranslation(flipX ? -body.Position.X : body.Position.X, body.Position.Y, 0) * XMatrix.CreateRotationZ(Rotation) * XMatrix.CreateScale(1);
}


Вот теперь уже можно делать этого красавца:
public class Player
{
public const int armStates = 2;
public const float width = 9, height = 19, baseSpeed = 30, jumpPower = 60;

public readonly BodyTransform transform;
private readonly Camera camera;

private readonly Sprite headSprite;
private readonly Sprite[] armLSprites;
private readonly Sprite[] armRSprites;
private readonly Sprite[] bodySprites;
private readonly Sprite[] legsSprites;
private readonly Sprite[] legsMoveSprites;

private byte armsState;
private bool onFloor;
private bool moving;

public Player(World world, Camera camera, Sprite headSprite, Sprite armLSprite, Sprite armRSprite, Sprite bodySprite, Sprite legsSprite)
{
Body body = new Body();
body.CreateRectangle(width, height, 1, FVector2.Zero).Friction = 0;
body.BodyType = BodyType.Dynamic;
body.FixedRotation = true;
body.Tag = this;
world.Add(body);

transform = new BodyTransform(body);
this.camera = camera;

this.headSprite = headSprite;
armLSprites = armLSprite.Split(armStates, 1, 1);
armRSprites = armRSprite.Split(armStates, 1, 1);
bodySprites = bodySprite.Split(3, 1, 1);
Sprite[] sprites = legsSprite.Split(8, 1, 1);
legsMoveSprites = sprites[2..8];
legsSprites = sprites[0..2];
}

public void Draw()
{
FVector2 offset = FVector2.Zero;
int bodySprite = 0;
int legSprite;
if (onFloor)
{
if (moving)
{
int anim = legsMoveSprites.Animation(.6f);

switch (anim)
{
case 0: offset = new FVector2(1, 1); bodySprite = 1; break;
case 1: offset = new FVector2(1, 0); bodySprite = 1; break;
case 2: offset = new FVector2(1, 0); bodySprite = 2; break;
case 3: offset = new FVector2(1, 1); bodySprite = 1; break;
case 4: offset = new FVector2(1, 0); bodySprite = 1; break;
case 5: offset = new FVector2(1, 0); bodySprite = 2; break;
}


legSprite = anim;
}
else legSprite = 0;
}
else legSprite = 1;

SDraw.SpriteEffects = transform.flipX ? SpriteEffects.FlipHorizontally : SpriteEffects.None;

SDraw.Rect(armLSprites[armsState], transform.Local2World(offset), 0, 1, 0);
if (onFloor && moving)
SDraw.Rect(legsMoveSprites[legSprite], transform.Position + FVector2.UnitY * 3, 0, 1, 0);
else if (onFloor)
SDraw.Rect(legsSprites[0], transform.Position + FVector2.UnitY * 3, 0, 1, 0);
SDraw.Rect(bodySprites[bodySprite], transform.Local2World(new FVector2(0, offset.Y)), 0, 1, 0);
SDraw.Rect(headSprite, transform.Local2World(offset - FVector2.UnitY * 9), 0, 1, 0);
if (!onFloor) SDraw.Rect(legsSprites[1], transform.Position + FVector2.UnitY * 3, 0, 1, 0);
SDraw.Rect(armRSprites[armsState], transform.Local2World(offset), 0, 1, 0);

SDraw.SpriteEffects = SpriteEffects.None;
}

public void Update()
{
onFloor = false;
transform.body.World.RayCast((fixture, point, normal, fraction) =>
{
if (fixture.Body.Tag is IMap)
{
onFloor = true;
return 0;
}
return -1;
}, transform.Position + new FVector2(width / 2 - 0.1f, 0), transform.Position + new FVector2(width / 2 - 0.1f, height / 2 + 0.1f));
transform.body.World.RayCast((fixture, point, normal, fraction) =>
{
if (fixture.Body.Tag is IMap)
{
onFloor = true;
return 0;
}
return -1;
}, transform.Position + new FVector2(-width / 2 + 0.1f, 0), transform.Position + new FVector2(-width / 2 + 0.1f, height / 2f + 0.1f));

float yVel = transform.body.LinearVelocity.Y;

int x = 0;
if (Keyboard.IsDown(Keys.D)) x++;
if (Keyboard.IsDown(Keys.A)) x--;
if (moving = x != 0) transform.flipX = x < 0;

if (onFloor && Keyboard.IsDown(Keys.Space)) yVel = -jumpPower;

transform.body.LinearVelocity = new FVector2(x * baseSpeed, yVel);

camera.Position = transform.Position;
}
}

Это персонаж который собирается буквально по кускам! Ноги двигают тело а руки как и голова прикреплены к телу, а ещё у тела есть 3 кадра:
Покой, движение, движение2.
Второй кадр движения используется для создания эффекта подпрыгивания объектов (цепочки, ткань), это кадр перед кадром с опусканием, кстати, хоть я вам и показал только персонажа мужского пола, но я нарисовал также женского пола, и для подпрыгивания груди у него используется именно этот кадр.
Вы не подумайте, я не сексист какой, просто будем честны, играть за подтянутую, сильную и независимую женщину - приятней чем за мужика, верно?

Следующим буду делать базовую генерацию мира и потихоньку начну приближаться к инвентарю.
👍2
А вот и GUI, решил использовать своё решение, для начала нужно было создать GUIElement:
public class GUIElement
{
public const float baseWidth = 100, baseHeight = 60;
public static FVector2 BaseSize => new FVector2(baseWidth, baseHeight);

private static IGUICamera Camera { get; set; }
public static FVector2 ScreenSize => Camera.Origin / Camera.Zoom;


public Action<FVector2> DrawAction = (p) => { };
public Action<FVector2> UpdateAction = (p) => { };

public FRectangle rectangle;
public bool MouseOn { get; private set; }

public GUIElement Parent { get; init; }

public GUIElement(GUIElement parent)
{
Parent = parent;
parent.Add(this);
}

public GUIElement(IGUICamera camera)
{
Camera = camera;
rectangle = new FRectangle(0, 0, 0, 0);
}

public void Add(GUIElement gameObject)
{
DrawAction += gameObject.BaseDraw;
UpdateAction += gameObject.BaseUpdate;
}

public void Remove(GUIElement gameObject)
{
DrawAction -= gameObject.BaseDraw;
UpdateAction -= gameObject.BaseUpdate;
}

public void Remove() => Parent.Remove(this);

public void BaseDraw(FVector2 point)
{
FVector2 screenSize = ScreenSize;
Draw(new FRectangle((rectangle.Location + point) / BaseSize * screenSize, rectangle.Size / BaseSize * screenSize));
}

public virtual void Draw(FRectangle rectangle)
{
FVector2 screenSize = ScreenSize;
DrawAction(rectangle.Location * BaseSize / screenSize);
}

public void BaseUpdate(FVector2 point)
{
FVector2 screenSize = ScreenSize;
FRectangle rectangle = new FRectangle((this.rectangle.Location + point) / BaseSize * screenSize, this.rectangle.Size / BaseSize * screenSize);
Update(rectangle);

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)
{
list[i].DynamicInvoke(this.rectangle.Location + point);
if (target.MouseOn) breaked = true;
}
}
}
}

public virtual void Update(FRectangle rectangle)
{
if (rectangle.Size != FVector2.Zero)
{
if (MouseOn = Parent.MouseOn && rectangle.Contains(Mouse.GUIPosition))
Mouse.OnGUI = true;
}
else MouseOn = true;
}
}

Это основа для всего будущего GUI, правда я буду её менять на систему якорей, типо:
xAnchor = Anchor.Less, yAnchor = Anchor.Less, rectangle = new FRectangle(1, 1, 6, 6)

Это будет GUI элемент слева сверху с отступом в 1 по X и Y и размером в 6х6.
После чего я на уже готовой основе сделал класс для кнопки:
public class Button : GUIElement
{
private readonly Style style;
private readonly Sprite icon;
private readonly Action action;

public Button(GUIElement GUI, FRectangle rectangle, Action action, Style style, Sprite icon) : base(GUI)
{
this.rectangle = rectangle;
this.style = style;
this.icon = icon;
this.action = action;
}

public override void Draw(FRectangle rectangle)
{
Sprite[] texture = MouseOn ? Mouse.LeftDown ? style.Down : style.On : style.Idle;

DrawRectWindow(texture, rectangle);

SDraw.Rect(icon, rectangle.Center);

base.Draw(rectangle);
}

public override void Update(FRectangle point)
{
base.Update(point);

if (MouseOn && Mouse.LeftReleased)
action();
}

public class Style
{
public Sprite[] Idle { get; init; }
public Sprite[] On { get; init; }
public Sprite[] Down { get; init; }

public Style(Sprite texture)
{
Sprite[] textures = texture.Split(3, 1, 1);
Sprite idle = textures[0], on = textures[1], down = textures[2];
Idle = idle.Split(3, 3, 1);
Down = down.Split(3, 3, 1);
On = on.Split(3, 3, 1);
}
}
}

В нём вы видите как он работает, не тяжело, но что же это за функция такая DrawRectWindow?
Это специальная функция чтобы рисовать окна из 8-ми спрайтов которые по итогу составят бесшовную текстуру, я не стал её показывать ибо она занимает место в посте и не несёт в себе много информации.
А теперь давайте создадим наш GUI!
Для начала я разделил Game1 класс по файлам сделав его partial.
Теперь я добавляю новый файл для создания GUI ибо оно так-то не маленькое будет:
public partial class Game1
{
GUIElement GUI;

protected void InitGUI()
{
Button.Style buttonBaseStyle = new Button.Style(new Sprite(Content.Load<Texture2D>("button")));

GUI = new GUIElement(camera);

new Button(GUI, new FRectangle(1, 1, 6, 6), () => Exit(), buttonBaseStyle, new Sprite(Content.Load<Texture2D>("icon_exit")));
}
}

Теперь в LoadContent после создания камеры пишу:
InitGUI();

После чего нам нужно в Update после обновления инпута добавить строку:
GUI.BaseUpdate(FVector2.Zero);

И в Draw после отрисовки игры добавить:
SDraw.Matrix = camera.GetGUIMatrix();
SDraw.Apply();

GUI.BaseDraw(FVector2.Zero);
SDraw.End();

Вот, у нас теперь есть хоть и не готовый но всё же GUI, скорее всего следующий пост будет о исправлении GUI и добавлении инвентаря.
👍3
Привет! А я тут чуть ли не помер от мысли об этом посте...
Так уж вышло, что инвентарь - не простая штука, а именно:
- Добавляя инвентарь ты добавляешь 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
Теперь о предметах:
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