VozDuh
59 subscribers
8 photos
26 videos
11 links
Это канал по разработке игр, вдохновлённый другим ТГ каналом, в этом канале я буду показывать как разрабатываю игру с открытым кодом
Download Telegram
Я вернулся, сделал кирку и лук а так-же воду, но разделю это всё на отдельные посты.
А ещё я не буду вам всё объяснять дотошно а то я иногда меняю кучу всяких вещей в разных класса но итогом изменений практически нету...

Давайте начнём с кирки, тут всё просто:
Для начала я сделал класс для оружий ближнего боя, он в принципе не сложный.
При нажатии мыши ждать 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.
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 - обновление каждой клетки
    ...
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
Редкие кадры дебага воды... Моему ПК плохо от всех этих цифр... 😔
А редкие они ибо фпс сильно падает и итогом я дебаг воды не пользуюсь.
😨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