VozDuh
59 subscribers
8 photos
26 videos
11 links
Это канал по разработке игр, вдохновлённый другим ТГ каналом, в этом канале я буду показывать как разрабатываю игру с открытым кодом
Download Telegram
Редкие кадры дебага воды... Моему ПК плохо от всех этих цифр... 😔
А редкие они ибо фпс сильно падает и итогом я дебаг воды не пользуюсь.
😨4🐳1
Media is too big
VIEW IN TELEGRAM
Графическое обновление!

В основном тут поменялись спрайты но как вы видите на видео вода сделана явно не спрайтами.
Это треугольники, я не смогу показать как я сделал её ибо код разошёлся на 1000 строк в основном его занимает массив правил тип которого сам по себе может ввести в ступор неподготовленного человека:
(bool lu, bool u, bool ru, bool l, bool r, bool ld, bool d, bool rd, (FVector2 a, FVector2 b, FVector2 c, byte at, byte bt, byte ct, FVector2 ac, FVector2 bc, FVector2 cc)[])[]
👍3
Воу! У на же ещё нету противников! Что за бред!?
Нужно срочно исправлять, просто попробуйте представить террарию без противников!

Ладно, для противников я решил сделать компонентную систему, для начала нужно создать менеджер компонентов, самого противника:
public class Enemy : Entity
{
public readonly EnemyData data;
private readonly IEnemyComponent[] components;
private readonly float viewRadius;
private readonly FVector2 size;
private readonly bool seeAnytime;
public readonly float maxHealth;
public readonly BodyTransform transform;

public FVector2 lastPlayerPosition;

public byte state = 0;
public bool baseState = false;
public bool checkPosition = false;
public float health;

public Enemy(float health, float viewRadius, FVector2 size, params IEnemyComponent[] components)
{
this.components = components;
this.viewRadius = viewRadius * Map.tileSize;
this.size = size;
seeAnytime = false;
maxHealth = health;
this.health = health;
}

public Enemy(float health, float viewRadius, FVector2 size, bool seeAnytime, params IEnemyComponent[] components)
{
this.components = components;
this.viewRadius = viewRadius * Map.tileSize;
this.size = size;
this.seeAnytime = seeAnytime;
maxHealth = health;
this.health = health;
}

private Enemy(FVector2 position, IEnemyComponent[] components, float health, float viewRadius, FVector2 size, bool seeAnytime = false)
{
Body body = new Body();
Fixture fixture = body.CreateSmoothRectangle(1, size.X, size.Y, 1, FVector2.Zero);
fixture.Friction = 0;
fixture.CollisionCategories = Category.Cat2;
fixture.CollidesWith = Category.Cat1;

body.BodyType = BodyType.Dynamic;
body.FixedRotation = true;
body.Tag = this;
body.SleepingAllowed = false;
body.Position = position;
Core.world.Add(body);
transform = new BodyTransform(body);
data = new EnemyData();
this.components = components;
this.viewRadius = viewRadius;
this.size = size;
this.seeAnytime = seeAnytime;
maxHealth = health;
this.health = health;
}

public Enemy Spawn(FVector2 position)
{
var clone = new Enemy(position, components, health, viewRadius, size, seeAnytime);
Core.AddEntity(clone.Draw, clone.Update);
return clone;
}

public override void Draw()
{
foreach (IEnemyComponent component in components)
{
data.current = component.GetType().Name;
component.Draw(this, data);
}
}

public override void Update()
{
if (seeAnytime) baseState = true;
else if (FVector2.Distance(Core.player.transform.Position, transform.Position) <= viewRadius)
{
bool see = true;
Core.world.RayCast((fixture, point, normal, fraction) =>
{
if (fixture.Body.Tag is IMap)
{
see = false;
return 0;
}
return -1;
},
transform.Position,
Core.player.transform.Position);
if (baseState = see)
{
checkPosition = true;
lastPlayerPosition = Core.player.transform.Position;
}
}
else baseState = false;

if (baseState)
AgressState();
else if (!checkPosition)
IdleState();
else
CheckState();
}

public virtual void CheckState()
{
foreach (IEnemyComponent component in components)
{
data.current = component.GetType().Name;
component.CheckState(this, data);
}
if (FVector2.Distance(transform.Position, lastPlayerPosition) < MathF.Max(size.X, size.Y))
checkPosition = false;
}
👍2


public virtual void IdleState()
{
foreach (IEnemyComponent component in components)
{
data.current = component.GetType().Name;
component.IdleState(this, data);
}
}

public virtual void AgressState()
{
foreach (IEnemyComponent component in components)
{
data.current = component.GetType().Name;
component.AgressState(this, data);
}
}

public virtual void Hit(float damage)
{
health -= damage;
foreach (IEnemyComponent component in components)
{
data.current = component.GetType().Name;
component.OnHit(damage, this, data);
}
if (health <= 0)
{
Die();
}
}

public virtual void Die()
{
transform.body.World.Remove(transform.body);
Remove();
foreach (IEnemyComponent component in components)
{
data.current = component.GetType().Name;
component.OnDie(this, data);
}
}
}
👍1
Тут можно увидеть, что в коде противника есть такие класс как IEnemyComponent и EnemyData.
Эти классы выглядят вот так:
public interface IEnemyComponent
{
void Draw(Enemy enemy, EnemyData data);
void CheckState(Enemy enemy, EnemyData data);
void IdleState(Enemy enemy, EnemyData data);
void AgressState(Enemy enemy, EnemyData data);
void OnHit(float damage, Enemy enemy, EnemyData data);
void OnDie(Enemy enemy, EnemyData data);
}

И конечно же данные противника:
public class EnemyData
{
public string current;
private readonly Dictionary<string, object> values = new Dictionary<string, object>();

public T Get<T>(string name) => values.TryGetValue(current + name, out object obj) ? (T)obj : default;
public void Get<T>(out T to, string name) => to = values.TryGetValue(current + name, out object obj) ? (T)obj : default;
public void Set(string name, object value) => values[current + name] = value;
public void Set(params (string name, object value)[] values)
{
foreach (var (name, value) in values)
this.values[current + name] = value;
}
}

Как вы видите второй это просто контейнер для динамичных данных компонентов.
👍1
Теперь можно делать компоненты для противников, сначала хотелось бы сделать лут дроп, самый важный компонент:
public class LootDropComponent : IEnemyComponent
{
private readonly float chance;
private readonly Range range;
private readonly Func<IItem> item;

public LootDropComponent(float chance, Range range, Func<IItem> item)
{
this.chance = chance;
this.range = range;
this.item = item;
}

public void Draw(Enemy enemy, EnemyData data) { }

public void CheckState(Enemy enemy, EnemyData data) { }

public void IdleState(Enemy enemy, EnemyData data) { }

public void AgressState(Enemy enemy, EnemyData data) { }

public void OnDie(Enemy enemy, EnemyData data)
{
if (URandom.Float(100) <= chance) new Item((item(), URandom.Int(range.Start.Value, range.End.Value)), enemy.transform.Position);
}

public void OnHit(float damage, Enemy enemy, EnemyData data) { }
}
А теперь насчёт противников которые ходят по земле, их тоже буду делать компонентом:
public class GroundComponent : IEnemyComponent
{
private readonly Sprite sprite;
private readonly float acceleration;
private readonly float maxSpeed;
private readonly float drag;

public GroundComponent(Sprite sprite, float acceleration, float maxSpeed, float drag)
{
this.sprite = sprite;
this.acceleration = acceleration;
this.maxSpeed = maxSpeed;
this.drag = drag;
}

public void Draw(Enemy enemy, EnemyData data)
{
SDraw.Rect(sprite, enemy.transform.Position);
SDraw.Text(Core.font, $"{(enemy.baseState ? "Agress" : (enemy.checkPosition ? "Check" : "Idle"))} | Health: {enemy.health}", enemy.transform.Position);
}

public void CheckState(Enemy enemy, EnemyData data)
{
Move(enemy, data);
}

public void IdleState(Enemy enemy, EnemyData data)
{
data.Get(out float speed, "speed");

if (speed != 0)
if (speed < 0)
{
speed += drag * Time.Delta;
if (speed > 0) speed = 0;
}
else
{
speed -= drag * Time.Delta;
if (speed < 0) speed = 0;
}
enemy.transform.body.LinearVelocity = new FVector2(speed, enemy.transform.body.LinearVelocity.Y);

data.Set("speed", speed);
}

public void AgressState(Enemy enemy, EnemyData data)
{
Move(enemy, data);
}

public void Move(Enemy enemy, EnemyData data)
{
data.Get(out float speed, "speed");

if (enemy.transform.Position.X < enemy.lastPlayerPosition.X)
{
if (speed < 0)
{
speed += drag * Time.Delta;
if (speed > 0) speed = 0;
}
speed += acceleration * Time.Delta;
if (speed > maxSpeed) speed = maxSpeed;
}
else
{
if (speed > 0)
{
speed -= drag * Time.Delta;
if (speed < 0) speed = 0;
}
speed -= acceleration * Time.Delta;
if (speed < -maxSpeed) speed = -maxSpeed;
}
enemy.transform.body.LinearVelocity = new FVector2(speed, enemy.transform.body.LinearVelocity.Y);

data.Set("speed", speed);
}

public void OnHit(float damage, Enemy enemy, EnemyData data) { }

public void OnDie(Enemy enemy, EnemyData data) { }
}

ВВЕРХ
Ну как вы тут, заскучали небось?
Я пришёл к вам с новостями, новости как всегда: позитивные и негативные одновременно!
Позитивная новость в том, что я возвращаюсь к работе над проектом.
Негативная новость в том, что физика которую я сейчас использую банально не подходит под проект.

Именно по этой причине я написал свою физику!
Давайте начнём с целей физического движка:
- Динамические объекты не врезаются в друг-друга, только с тайлмапом.
- Можно кидать лучи.
- Можно кидать лучи которые коснуться только тайлмапа.
- Тела не должны поворачиваться, проще говоря: они все квадраты!

Теперь можно начинать, сначала мне нужно сделать добавление и удаление тел, я решил, что это можно реализовать простым массивом и очередью:
public class Physics
{
static readonly Queue<int> free = new Queue<int>();
static int length;
static readonly ColliderInfo[] colliders = new ColliderInfo[255];
...
public static Collider Create(float width, float height, float evenness, float elastic)
{
var col = new Collider() { Size = new (width, height), evenness = evenness, elastic = elastic };
if (free.TryDequeue(out int f) && f < length)
{
colliders[f].collider = col;
col.index = f;
}
else
{
colliders[length].collider = col;
col.index = length;
length++;
}
return col;
}

public static void Destroy(Collider collider)
{
colliders[collider.index].collider = null;
colliders[collider.index].last = colliders[collider.index].current = false;
free.Enqueue(collider.index);
return;
}
👍3
Теперь нужно понять, что такое ColliderInfo и Collider?
Первое - информация о коллайдере, она хранит в себе данные для вызова эвентов:
public struct ColliderInfo
{
public readonly Collider collider;
public bool last, current;
}

Второе - само тело, оно устроено сложнее, для начала разберём начало:
public class Collider : IColliderEvents
{
public int index;
public Vec2 halfSize;
public Vec2 Size
{
get => halfSize * 2;
set => halfSize = value / 2;
}

OnCollisionDelegate IColliderEvents.OnCollisionEnter => onCollisionEnter;
OnCollisionDelegate IColliderEvents.OnCollisionExit => onCollisionExit;
OnCollisionDelegate IColliderEvents.OnCollision => onCollision;

public Vec2 velocity, position;
public object tag;
public float evenness, elastic;
public bool sleep;


Тут создаются поля для свойств тела:
velocity - вектор движения.
position - позиция.
tag - тэг, сюда можно засунуть любые данные которые определяют физическое тело.
evenness - гладкость, от 0 до 1, работает так-же как и friction (трение) но наоборот, чем больше гладкость тем более скользящим становится объект.
elastic - эластичность, от 0 до 1, чем больше значение тем более прыгучим будет тело.
sleep - спит ли тело, пока тело спит оно не будет двигаться.
👍1
Теперь продолжим и заглянем в эвенты:
    private OnCollisionDelegate onCollisionEnter = (a, b, c) => { };
public event OnCollisionDelegate OnCollisionEnter { add => onCollisionEnter += value; remove => onCollisionEnter -= value; }

private OnCollisionDelegate onCollisionExit = (a, b, c) => { };
public event OnCollisionDelegate OnCollisionExit { add => onCollisionExit += value; remove => onCollisionExit -= value; }

private OnCollisionDelegate onCollision = (a, b, c) => { };
public event OnCollisionDelegate OnCollision { add => onCollision += value; remove => onCollision -= value; }

Тут есть 3 эвента названия которых говорят сами за себя:
OnCollisionEnter - вызывается при врезании в тайл.
OnCollisionExit - вызывается при отдалении от тайл.
OnCollision - вызывается если ты просто врезался в тайл.
👍1
Так что идём дальше и смотрим на функции:
    public bool Overlap(float mx, float px, float my, float py)
{
return
position.X - halfSize.X <= px &&
position.X + halfSize.X >= mx &&
position.Y - halfSize.Y <= py &&
position.Y + halfSize.Y >= my;
}

public bool Process(Vector4 other, out Vec2 normal, out float pushRange, ref Vec2 velocity)
{
float
mox = other.Z - (position.X - halfSize.X),
pox = position.X + halfSize.X - other.X,
moy = other.W - (position.Y - halfSize.Y),
poy = position.Y + halfSize.Y - other.Y;

normal = -Vec2.UnitY;
pushRange = poy;
if (pushRange > pox)
{
normal = -Vec2.UnitX;
pushRange = pox;
}
if (pushRange > moy)
{
normal = Vec2.UnitY;
pushRange = moy;
}
if (pushRange > mox)
{
normal = Vec2.UnitX;
pushRange = mox;
}

if (mox > 0 && pox > 0 && moy > 0 && poy > 0)
{
velocity = velocity * Vec2.Abs(new Vec2(normal.Y, normal.X)) * evenness - velocity * Vec2.Abs(normal) * elastic;
return true;
}
return false;
}
}

Overlap - пересекается ли тело с абстрактным телом.
Process - процесс коллизии с абстрактным телом.
🔥1
Теперь рассмотрим OnCollisionDelegate и IColliderEvents:
public delegate void OnCollisionDelegate(int x, int y, TileData tile);

public interface IColliderEvents
{
OnCollisionDelegate OnCollisionEnter { get; }
OnCollisionDelegate OnCollisionExit { get; }
OnCollisionDelegate OnCollision { get; }
}

Тут ничего сложного, 2-е вспомогательных типов.
👍2
Переходим дальше по классу Physics, посмотрим на лучи, а именно: на "мировые" лучи:
    ...
public static IMap map;
static readonly RaycastHitInfo[] colliderRays = new RaycastHitInfo[256];
static int hitsCount;
...

public static void Raycast(RaycastDelegate action, Vec2 origin, Vec2 direction, float maxDistance, bool map = false)
{
float rdistance = origin.X + direction.X * maxDistance;
float
mx = Math.Min(origin.X, rdistance),
px = Math.Max(origin.X, rdistance);
rdistance = origin.Y + direction.Y * maxDistance;
float
my = Math.Min(origin.Y, rdistance),
py = Math.Max(origin.Y, rdistance);
hitsCount = 0;
foreach (ColliderInfo info in colliders)
if (info.collider != null && info.collider.Overlap(mx, px, my, py)
&& Raycast(info.collider, origin, direction, maxDistance, out Vec2 point, out Vec2 normal, out float distance))
{
colliderRays[hitsCount] = new RaycastHitInfo(info.collider, point, normal, distance);
hitsCount++;
}
if (map)
{
Vec2 rpoint = Vec2.Zero;
Vec2 rnormal = Vec2.Zero;
rdistance = float.MinValue;

var (mcx, mcy) = Physics.map.World2Cell(new Vec2(mx, my));
var (pcx, pcy) = Physics.map.World2Cell(new Vec2(px, py));
for (int x = mcx; x <= pcx; x++)
for (int y = mcy; y <= pcy; y++)
if (Physics.map.GetTile(x, y).Tile != null
&& RaycastMap(x, y, origin, direction, maxDistance, out Vec2 point, out Vec2 normal, out float distance)
&& distance > rdistance)
{
rdistance = distance;
rnormal = normal;
rpoint = point;
}
if (rdistance != float.MinValue)
{
colliderRays[hitsCount] = new RaycastHitInfo(null, rpoint, rnormal, rdistance);
hitsCount++;
}
}

if (hitsCount == 0) return;

Array.Sort(colliderRays);
for (int i = 0; i < hitsCount; i++)
{
RaycastHitInfo info = colliderRays[i];
if (action(info.collider, info.point, info.normal, info.distance))
break;
}
}

public static void RaycastMap(RaycastMapDelegate action, Vec2 origin, Vec2 direction, float maxDistance)
{
Vec2 rpoint = Vec2.Zero;
Vec2 rnormal = Vec2.Zero;
float rdistance = origin.X + direction.X * maxDistance;
float
mx = Math.Min(origin.X, rdistance),
px = Math.Max(origin.X, rdistance);
rdistance = origin.Y + direction.Y * maxDistance;
float
my = Math.Min(origin.Y, rdistance),
py = Math.Max(origin.Y, rdistance);
rdistance = float.MinValue;

var (mcx, mcy) = map.World2Cell(new Vec2(mx, my));
var (pcx, pcy) = map.World2Cell(new Vec2(px, py));
for (int x = mcx; x <= pcx; x++)
for (int y = mcy; y <= pcy; y++)
if (map.GetTile(x, y).Tile != null
&& RaycastMap(x, y, origin, direction, maxDistance, out Vec2 point, out Vec2 normal, out float distance)
&& distance > rdistance)
{
rdistance = distance;
rnormal = normal;
rpoint = point;
}
if (rdistance != float.MinValue) action(rpoint, rnormal, rdistance);
}
👍1
Raycast - кидает луч в тела и в тайлмап.
RaycastMap - кидает луч в тайлмап.
Объяснять их принцип работы будет очень запарно, но если в общих чертах:
Raycast:
- Проходит по всем телам, те которые попали в луч добавляет в массив.
- Проходит по плиткам, берёт самую ближнюю в которую попал и добавляет в массив.
- Сортирует массив.
- Проходит по массиву и вызывает action, если он вернул true то прерывает цикл.
RaycastMap:
- Проходит по плиткам, берёт самую ближнюю в которую попал.
- Самое ближнее попадание передает в action.
👍2
Дальше идут функции для большего удобства в использовании:
    public static void Raycast(RaycastDelegate action, Vec2 origin, Vec2 direction, bool map = false)
{
float l = direction.Length();
Raycast(action, origin, direction/l, l, map);
}

public static void RaycastMap(RaycastMapDelegate action, Vec2 origin, Vec2 direction)
{
float l = direction.Length();
RaycastMap(action, origin, direction/l, l);
}

public static void Linecast(RaycastDelegate action, Vec2 from, Vec2 to, bool map = false)
{
to -= from;
float len = to.Length();
to /= len;
Raycast(action, from, to, len, map);
}

public static void LinecastMap(RaycastMapDelegate action, Vec2 from, Vec2 to)
{
to -= from;
float len = to.Length();
to /= len;
RaycastMap(action, from, to, len);
}
👍1
Теперь идут функции для "базовой" проверки луча:
...
public static float tileSize;
...
private static bool Raycast(Collider collider, Vec2 origin, Vec2 direction, float maxDistance, out Vec2 point, out Vec2 normal, out float distance)
{
float ap = origin.X - (collider.position.X + collider.halfSize.X);
float bp = origin.X - (collider.position.X - collider.halfSize.X);
float cp = origin.Y - (collider.position.Y + collider.halfSize.Y);
float dp = origin.Y - (collider.position.Y - collider.halfSize.Y);
float a = ap / direction.X;
float b = bp / direction.X;
float c = cp / direction.Y;
float d = dp / direction.Y;

float tMin = Math.Min(Math.Max(a, b), Math.Max(c, d));
float tMax = Math.Max(Math.Min(a, b), Math.Min(c, d));

point = origin - direction * tMin;
distance = tMin;
if (a == tMin) normal = Vec2.UnitX;
else if (b == tMin) normal = -Vec2.UnitX;
else if (c == tMin) normal = Vec2.UnitY;
else normal = -Vec2.UnitY;
return tMax <= 0 && tMin <= maxDistance && tMin >= tMax;
}

private static bool RaycastMap(int x, int y, Vec2 origin, Vec2 direction, float maxDistance, out Vec2 point, out Vec2 normal, out float distance)
{
float ap = origin.X - (x * tileSize + tileSize);
float bp = origin.X - x * tileSize;
float cp = origin.Y - (y * tileSize + tileSize);
float dp = origin.Y - y * tileSize;
float a = ap / direction.X;
float b = bp / direction.X;
float c = cp / direction.Y;
float d = dp / direction.Y;

float tMin = Math.Min(Math.Max(a, b), Math.Max(c, d));
float tMax = Math.Max(Math.Min(a, b), Math.Min(c, d));

point = origin - direction * tMin;
distance = tMin;
if (a == tMin) normal = Vec2.UnitX;
else if (b == tMin) normal = -Vec2.UnitX;
else if (c == tMin) normal = Vec2.UnitY;
else normal = -Vec2.UnitY;
return tMax <= 0 && tMin <= maxDistance && tMin >= tMax;
}

Raycast - кидает луч в тело.
RaycastMap - кидает луч в плитку.
👍2
Продолжаем, самая важная функция в физическом движке:
    public const int VELOCITY_ITERATIONS = 4;
public const float VELOCITY_DIFFERENT = 1f/VELOCITY_ITERATIONS;
public static Vec2 gravity = new Vec2(0, 9.8f);
public static float meter;

public static void Process(float delta)
{
for (int i = 0; i < length; i++)
{
var info = colliders[i];
if (info.collider != null && !info.collider.sleep)
{
IColliderEvents events = info.collider;
info.last = info.current;
info.current = false;
info.collider.velocity += gravity * delta * meter;
for (int j = 0; j < VELOCITY_ITERATIONS; j++)
{
info.current = false;
Vec2 rnormal = Vec2.Zero;
float rpush = float.MinValue;
Vec2 rvel = Vec2.Zero;

info.collider.position += info.collider.velocity * delta * VELOCITY_DIFFERENT;

var (mx, my) = map.World2Cell(info.collider.position - info.collider.halfSize);
var (px, py) = map.World2Cell(info.collider.position + info.collider.halfSize);
for (int x = mx; x <= px; x++)
for (int y = my; y <= py; y++)
{
TileData tile = map.GetTile(x, y);
if (tile.Tile != null)
{
float bx = x * tileSize, by = y * tileSize;
Vec2 vel = info.collider.velocity;
if (info.collider.Process(new Vector4(bx, by, bx + tileSize, by + tileSize), out Vec2 normal, out float push, ref vel) && push > rpush)
{
rpush = push;
rvel = vel;
rnormal = normal;
info.current = true;
if (info.current)
{
events.OnCollision(x, y, tile);
if (!info.last)
events.OnCollisionEnter(x, y, tile);
}
else if (info.last)
events.OnCollisionExit(x, y, tile);
}
}
}
if (info.current)
{
info.collider.position += rnormal * rpush;
info.collider.velocity = rvel;
}
}
}
}
}

Эта функция обновляет физику в игре. 🫥
👍3
Так же я забыл показать вспомогательные типы для лучей:
/// <summary>
/// </summary>
/// <returns>True to break.</returns>
public delegate bool RaycastDelegate(Collider collider, Vec2 point, Vec2 normal, float distance);

public delegate void RaycastMapDelegate(Vec2 point, Vec2 normal, float distance);

public record struct RaycastHitInfo(Collider collider, Vec2 point, Vec2 normal, float distance) : IComparable<RaycastHitInfo>
{
public int CompareTo(RaycastHitInfo o)
{
if (o.distance < distance) return 1;
return 0;
}
}


Дальше идёт реализация, не думаю, что всем интересно будет читать что-то типо такого: вот тут я вот это поменял на это, а тут вот это на это.

А ещё я поменял название FVector2 на Vec2.

ВВЕРХ
👍4