VozDuh
59 subscribers
8 photos
26 videos
11 links
Это канал по разработке игр, вдохновлённый другим ТГ каналом, в этом канале я буду показывать как разрабатываю игру с открытым кодом
Download Telegram
Теперь рассмотрим 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
А теперь о тенях, для неё я создал ту-же реализацию Create/Remove как и в физике но вместо возврата коллайдера идёт возврат индекса который потом используется для следующих методов:
    public void SetPosition(uint lit, float x, float y)
{
lights[lit].x = x;
lights[lit].y = y;
}

public void SetPosition(uint lit, Vec2 position) => SetPosition(lit, position.X, position.Y);

public void SetIntensity(uint lit, float intensity) => lights[lit].intensity = intensity;
public void SetRadius(uint lit, float radius) => lights[lit].radius = radius;

При изменении размера экрана игры нужно создавать "матрицу теней" для нового размера, для этого идёт функция:
    public void Resize()
{
(width, height) = map.World2Cell(camera.WorldViewport);
width += 3; height += 2;
tiles = new Vector3[width, height];
}

И теперь о конструкторе с некоторыми данными:
    private readonly Light[] lights = new Light[255];

private readonly Camera camera;
private readonly IMap map;
private Vector3[,] tiles;
private int x, y, width, height;

private Batch batch = new Batch(SDraw.spriteBatch.GraphicsDevice);

public ShadowMatrix(IMap map, Camera camera)
{
this.map = map;
this.camera = camera;

Resize();
}

Тут мы инициализируем всё что нужно.
Дальше идёт функция рисования:
public void Draw()
{
(this.x, this.y) = map.World2Cell(camera.Position - camera.WorldViewport / 2);
this.x--; this.y--;

int x, y, _x, _y;

for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
tiles[x, y] = Vector3.Zero;

foreach (Light p in lights)
{
if (!p.available) continue;
p.Generate();
for (_x = p.rectangle.X; _x <= p.rectangle.X + p.rectangle.Width; _x++)
for (_y = p.rectangle.Y; _y <= p.rectangle.Y + p.rectangle.Height; _y++)
{
x = _x - this.x;
y = _y - this.y;
if (x < 0 || x >= width || y < 0 || y >= height) continue;

tiles[x, y] += p.color
* (1 - Ray(_x, _y, map.World2Cell(p.x, p.y), p.intensity) / p.intensity) // shadow
* MathF.Max(0, 1 - new Vec2(p.x / Map.tileSize - _x, p.y / Map.tileSize - _y).Length() / p.radius) * p.intensity; // saturation
}
}
batch.Begin(PrimitiveType.TriangleList, width * height * 12, camera.GetViewMatrix());
batch.BlendState = multiplyBlend;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
{
_x = this.x + x;
_y = this.y + y;
Vec2
c = map.Cell2World(_x, _y),
l = map.Cell2World(_x - 1, _y),
r = map.Cell2World(_x + 1, _y),
b = map.Cell2World(_x, _y - 1),
t = map.Cell2World(_x, _y + 1);
Color
rc = new Color(tiles[x == width - 1 ? width - 1 : x + 1, y]),
lc = new Color(tiles[x == 0 ? 0 : x - 1, y]),
bc = new Color(tiles[x, y == 0 ? 0 : y - 1]),
tc = new Color(tiles[x, y == height - 1 ? height - 1 : y + 1]),
cc = new Color(tiles[x, y]);

batch.Color = lc;
batch.Vertex(l);
batch.Color = tc;
batch.Vertex(t);
batch.Color = cc;
batch.Vertex(c);

batch.Color = lc;
batch.Vertex(l);
batch.Color = bc;
batch.Vertex(b);
batch.Color = cc;
batch.Vertex(c);

batch.Color = rc;
batch.Vertex(r);
batch.Color = tc;
batch.Vertex(t);
batch.Color = cc;
batch.Vertex(c);

batch.Color = rc;
batch.Vertex(r);
batch.Color = bc;
batch.Vertex(b);
batch.Color = cc;
batch.Vertex(c);
}
batch.End();
}

Тут мы проходим по всей матрице, делаем её полностью чёрной, рассчитываем свет а потом рисуем треугольники, я использую не 2 треугольника на 1 квадратик а 4 треугольника на ромб, это нужно чтобы освещение было мягким.
👍2
Тут так-же видна функция Ray которая выглядит так:
    public float Ray(int x0, int y0, (int, int) p, float power)
{
var (x1, y1) = p;
float f = 0;

int dx = x1 - x0;
int dy = y1 - y0;

float sd = new Vec2(Math.Abs(dx), Math.Abs(dy)).Length();

float x_incr = dx / sd;
float y_incr = dy / sd;
float x = x0;
float y = y0;
IShadowTile tile;

for (int i = 0; i < sd - 1; i++)
{
y += y_incr;
x += x_incr;
tile = map.GetTile(true, (int)Math.Clamp(MathF.Round(x), 0, map.FullWidth - 1), (int)Math.Clamp(MathF.Round(y), 0, map.FullHeight - 1)).Tile;
if (tile?.ShadowAvailable ?? false)
{
f += tile.ShadowIntensity;
if (f >= power)
{
return power;
}
}
}
return f;
}

В функции я использую алгоритм Брезенхэма чтобы пройтись по линии от одной позиции до другой.

Ещё в коде видно структуру Light, она выглядит так:
public struct Light
{
public bool available;
public float x, y;
public Vector3 color;
public float intensity, radius;
public Rectangle rectangle;
public void Generate()
{
int s = (int)MathF.Ceiling(radius);
int fs = (int)MathF.Ceiling(radius * 2);
rectangle = new Rectangle((int)(x / Map.tileSize) - s - 1, (int)(y / Map.tileSize) - s - 1, fs + 2, fs + 2);
}
}


Так - же плитки теперь реализуют интерфейс для светотени:
public interface IShadowTile
{
bool ShadowAvailable { get; }
float ShadowIntensity { get; }
}


Теперь в игре можно создавать и прозрачные плитки и не очень.

ВВЕРХ
👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Видео прилагается.
👍3
Возможность отключения сглаживания, можно будет менять режимы освещения в настройках.
👍5
Опрос кончится через 2 дня.
Победили:
Особый биом.
Лес.
Болото.
Равнина всё равно будет, её можно сделать как и лес но, без деревьев, их кстати нужно сделать.
👍4