VozDuh
59 subscribers
8 photos
26 videos
11 links
Это канал по разработке игр, вдохновлённый другим ТГ каналом, в этом канале я буду показывать как разрабатываю игру с открытым кодом
Download Telegram
Тут можно увидеть, что в коде противника есть такие класс как 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
В игре всё ещё нету одного из самых важных для такого типа игр, структур!
Структуры это очень просто, но, мне хочется сохранять структуры в файле, это значит, что мне нужно сделать:
1. Синтаксис файла структур.
2. Парсер для этого файла.

Для синтаксиса я выбрал простенький вариант:
Переменные: "name: tile".
Сама структура: "[[tile, tile], [tile, tile]]".

А с парсером всё намного сложнее, было бы если бы я не использовал недавно сделанную мною библиотеку Va, эта библиотека позволяет создавать как простые парсеры так и полноценные языки программирования, но второе нас сейчас не нужно.
Название библиотеки произошло от обрезания другого названия Valang (Value Language), интерпретируемый язык программирования, но по факту обёртка для C#.

Ладно, начну уже о структурах:
public class Structure
{
private static ITile[][] tiles;
private static readonly CompileStyle main = null;

static Structure()
{
Solution mainSolution = new Solution();

main = new CompileStyle(
new(
new TokenStyle[]
{
new TokenStyle(TokenType.Keyword),
new TokenStyle(":", TokenType.Special),
new TokenStyle(TokenType.Keyword),
},
(CompileStyleDelegate)
((sln, toks) =>
{
if (!sln.TryAdd(toks[0].Text, new DataStruct(VValueType.String, toks[2].data)))
throw new VaException(toks[1].line, $"Already have same variable.");
})
),
new(
new TokenStyle[]
{
new TokenStyle(TokenType.BoxBracket)
},
(CompileStyleDelegate)
((sln, toks) =>
{
Token[][] rows = toks[0].tokens[0].Split(new TokenStyle(",", TokenType.Special));
tiles = new ITile[rows.Length][];
for (int i = 0; i < rows.Length; i++)
{
Token[][] cols = rows[i][0].tokens[0].Split(new TokenStyle(",", TokenType.Special));
tiles[i] = new ITile[cols.Length];
for (int j = 0; j < cols.Length; j++)
tiles[i][j] = Tiles.Get<ITile>(sln.TryGet(cols[j][0].Text, out DataStruct data) ? data.Str : cols[j][0].Text)();
}
})
)
);
}

Тут я создаю парсер, парсер это просто "стиль компиляции", он создаётся из массива структур которые содержат в себе стиль линии и функцию.
👍1
Продолжу, тут уже идёт не статичная часть структуры:
    private readonly ITile[][] data;

public Structure(string code)
{
Token[][] tokens = Compiler.GetTokens(code);
Compiler.ParseStyle(new Solution(), main, tokens);
data = tiles;
}

public void Spawn(Map map, int x, int y)
{
for (int i = 0; i < data.Length; i++)
for (int j = 0; j < data[i].Length; j++)
map.SetTile(data[i][j], x+j, y+i);
}
}

В конструкторе мы по факту парсим стиль компиляции который создали заранее, дальше идёт единственная функция Spawn, она спавнит структуру на карте.

Теперь можно создавать структуры таким образом:
test = new Structure(@"
i:ignore;
a:air;
s:stone;

[
[i,s,s,s,i],
[s,a,a,a,s],
[s,a,a,a,s],
[s,a,a,a,s],
[i,s,s,s,i]
];
");

Их так-же можно загружать из файлов, но сейчас не об этом, в InGameState.Update я добавляю строки:
if (Keyboard.IsPressed(Keys.T))
{
var (x, y) = map.World2Cell(Mouse.Position);
Structures.test.Spawn(map, x, y);
}

И теперь можно создавать тестовую структуру по нажатию кнопки [T].

Так-же была добавлена новая плитка, плитка камня, она в два раза крепче чем тестовая плитка которая ранее была единственной.

ВВЕРХ
👍1
Рубрика WIP, прикольные баги и другие аспекты которых вы никогда не увидите в отполированной версии игры!
Прошу написать о том, нравится ли вам такое, впервые так делаю.
👍5
Всего день прошёл а я тут как тут, добавил стены, генерацию, и светотень!
На самом деле свет это очень важный аспект игры, если его не добавить то получится, что мы видим всё и всегда.

Но, для начала нужно разобрать, что там с стенами, стены это просто новый слой плиток, для уточнения того, что мы оперируем именно плиткой-стеной я решил использовать bool top.
После чего я добавил это в реализацию карты, сделал возможность правой клавиши у ближне-бойного оружия и добавил проверку на нажатие правой клавиши в кирку.

Теперь можно перейти к структурами, они тоже имеют стены, значит мне нужно поменять их парсер, я решил использовать такой вариант: "name: wall tile".
Вот как-бы и всё о стенах.

Продолжаем, поговорим о бог-, кхм, генерации, давайте подумаем, как же я реализую шум перлина?
Напишу самостоятельно? Нееет...
Украду C++ код с википедии, перепишу его на C# и добавлю возможность сидов? Даа!
Как вы уже поняли я украл код шум перлина с википедии:
public static class Noise
{
private static float Interpolate(float a0, float a1, float w)
{
return (a1 - a0) * w + a0;
}

private static Vec2 RandomGradient(uint seed, int ix, int iy)
{
uint w = 8 * sizeof(uint);
uint s = w / 2;
uint a = (uint)ix, b = (uint)iy;
a *= 3284157443; a += seed; b ^= a << (int)s | a >> (int)w - (int)s;
b *= 1911520717; b += seed; a ^= b << (int)s | b >> (int)w - (int)s;
a *= 2048419325;
float random = a * (3.14159265f / ~(~0u >> 1));
Vec2 v;
v.X = MathF.Cos(random); v.Y = MathF.Sin(random);
return v;
}

private static float DotGridGradient(uint seed, int ix, int iy, float x, float y)
{
Vec2 gradient = RandomGradient(seed, ix, iy);

float dx = x - ix;
float dy = y - iy;

return (dx * gradient.X + dy * gradient.Y);
}

public static float Perlin(uint seed, float x, float y)
{
int x0 = (int)MathF.Floor(x);
int x1 = x0 + 1;
int y0 = (int)MathF.Floor(y);
int y1 = y0 + 1;

float sx = x - x0;
float sy = y - y0;

float n0, n1, ix0, ix1, value;

n0 = DotGridGradient(seed, x0, y0, x, y);
n1 = DotGridGradient(seed, x1, y0, x, y);
ix0 = Interpolate(n0, n1, sx);

n0 = DotGridGradient(seed, x0, y1, x, y);
n1 = DotGridGradient(seed, x1, y1, x, y);
ix1 = Interpolate(n0, n1, sx);

value = Interpolate(ix0, ix1, sy);
return 0.5f + value / 2;
}
}
👍2