From 3c865ed22310a2c0b0c65c039b869df46e209b4b Mon Sep 17 00:00:00 2001 From: dbjdbj Date: Sun, 21 Jan 2024 18:15:56 +0100 Subject: [PATCH] added --- sampling/minimaze | 1 - sampling/minimaze/Fluff.cs | 14 ++ sampling/minimaze/Program.cs | 277 ++++++++++++++++++++++++++++++++++ sampling/minimaze/README.md | 5 + sampling/minimaze/Win32.cs | 119 +++++++++++++++ sampling/minimaze/maze.csproj | 9 ++ 6 files changed, 424 insertions(+), 1 deletion(-) delete mode 160000 sampling/minimaze create mode 100644 sampling/minimaze/Fluff.cs create mode 100644 sampling/minimaze/Program.cs create mode 100644 sampling/minimaze/README.md create mode 100644 sampling/minimaze/Win32.cs create mode 100644 sampling/minimaze/maze.csproj diff --git a/sampling/minimaze b/sampling/minimaze deleted file mode 160000 index 8138410..0000000 --- a/sampling/minimaze +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 81384108994501e9b9461f0931c2d778b689361c diff --git a/sampling/minimaze/Fluff.cs b/sampling/minimaze/Fluff.cs new file mode 100644 index 0000000..c19b1cf --- /dev/null +++ b/sampling/minimaze/Fluff.cs @@ -0,0 +1,14 @@ +#if BFLAT +using System.Runtime.InteropServices; +class Math +{ + [DllImport("*", EntryPoint = "sin", CallingConvention = CallingConvention.Cdecl)] + public static extern double Sin(double x); + + [DllImport("*", EntryPoint = "cos", CallingConvention = CallingConvention.Cdecl)] + public static extern double Cos(double x); + + [DllImport("*", EntryPoint = "fabs", CallingConvention = CallingConvention.Cdecl)] + public static extern double Abs(double x); +} +#endif diff --git a/sampling/minimaze/Program.cs b/sampling/minimaze/Program.cs new file mode 100644 index 0000000..b19da72 --- /dev/null +++ b/sampling/minimaze/Program.cs @@ -0,0 +1,277 @@ +using System; +using System.Runtime.CompilerServices; + +using static Win32; + +unsafe class Program +{ + const int Width = 640; + const int Height = 480; + + const int MapWidth = 24; + const int MapHeight = 24; + + static ReadOnlySpan WorldMap => [ + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,2,2,2,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1, + 1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,3,0,0,0,3,0,0,0,1, + 1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,2,2,0,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,4,0,0,0,0,5,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,4,0,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]; + + private static void RenderEffect(uint tick, byte* framebuf) + { + new Span(framebuf, Width * Height / 2).Fill(0x110000); + new Span((int*)framebuf + Width * Height / 2, Width * Height / 2).Fill(0x333333); + + for (int x = 0; x < Width; x++) + { + //calculate ray position and direction + double cameraX = 2 * x / (double)Width - 1; //x-coordinate in camera space + double rayDirX = dirX + planeX * cameraX; + double rayDirY = dirY + planeY * cameraX; + //which box of the map we're in + int mapX = (int)posX; + int mapY = (int)posY; + + //length of ray from current position to next x or y-side + double sideDistX; + double sideDistY; + + //length of ray from one x or y-side to next x or y-side + //these are derived as: + //deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX)) + //deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY)) + //which can be simplified to abs(|rayDir| / rayDirX) and abs(|rayDir| / rayDirY) + //where |rayDir| is the length of the vector (rayDirX, rayDirY). Its length, + //unlike (dirX, dirY) is not 1, however this does not matter, only the + //ratio between deltaDistX and deltaDistY matters, due to the way the DDA + //stepping further below works. So the values can be computed as below. + // Division through zero is prevented, even though technically that's not + // needed in C++ with IEEE 754 floating point values. + double deltaDistX = (rayDirX == 0) ? 1e30 : Math.Abs(1 / rayDirX); + double deltaDistY = (rayDirY == 0) ? 1e30 : Math.Abs(1 / rayDirY); + + double perpWallDist; + + //what direction to step in x or y-direction (either +1 or -1) + int stepX; + int stepY; + + int hit = 0; //was there a wall hit? + int side = 0; //was a NS or a EW wall hit? + //calculate step and initial sideDist + if (rayDirX < 0) + { + stepX = -1; + sideDistX = (posX - mapX) * deltaDistX; + } + else + { + stepX = 1; + sideDistX = (mapX + 1.0 - posX) * deltaDistX; + } + if (rayDirY < 0) + { + stepY = -1; + sideDistY = (posY - mapY) * deltaDistY; + } + else + { + stepY = 1; + sideDistY = (mapY + 1.0 - posY) * deltaDistY; + } + //perform DDA + while (hit == 0) + { + //jump to next map square, either in x-direction, or in y-direction + if (sideDistX < sideDistY) + { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } + else + { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + //Check if ray has hit a wall + if (WorldMap[mapX * MapHeight + mapY] > 0) hit = 1; + } + //Calculate distance projected on camera direction. This is the shortest distance from the point where the wall is + //hit to the camera plane. Euclidean to center camera point would give fisheye effect! + //This can be computed as (mapX - posX + (1 - stepX) / 2) / rayDirX for side == 0, or same formula with Y + //for size == 1, but can be simplified to the code below thanks to how sideDist and deltaDist are computed: + //because they were left scaled to |rayDir|. sideDist is the entire length of the ray above after the multiple + //steps, but we subtract deltaDist once because one step more into the wall was taken above. + if (side == 0) perpWallDist = (sideDistX - deltaDistX); + else perpWallDist = (sideDistY - deltaDistY); + + //Calculate height of line to draw on screen + int lineHeight = (int)(Height / perpWallDist); + + //calculate lowest and highest pixel to fill in current stripe + int drawStart = -lineHeight / 2 + Height / 2; + if (drawStart < 0) drawStart = 0; + int drawEnd = lineHeight / 2 + Height / 2; + if (drawEnd >= Height) drawEnd = Height - 1; + + //choose wall color + int color = WorldMap[mapX * MapHeight + mapY] switch + { + 1 => 0xFF, + 2 => 0xFF00, + 3 => 0xFF0000, + 4 => 0xFFFFFF, + _ => 0xFFFF00, + }; + + if (side == 1) + color &= ~0x808080; + + //draw the pixels of the stripe as a vertical line + for (int y = drawStart; y < drawEnd; y++) + *(int*)(framebuf + (x + y * Width) * 4) = color; + + double frameTime = (tick - time) / 1000.0; //frameTime is the time this frame has taken, in seconds + + //speed modifiers + double moveSpeed = frameTime * 5.0; //the constant value is in squares/second + double rotSpeed = frameTime * 3.0; //the constant value is in radians/second + + //move forward or backwards if no wall + if ((keyState & (KeyState.Up | KeyState.Down)) != 0) + { + if ((keyState & KeyState.Down) != 0) + moveSpeed = -moveSpeed; + if (WorldMap[(int)(posX + dirX * moveSpeed) * MapHeight + (int)posY] == 0) posX += dirX * moveSpeed; + if (WorldMap[(int)posX * MapHeight + (int)(posY + dirY * moveSpeed)] == 0) posY += dirY * moveSpeed; + } + ////rotate to the right or left + if ((keyState & (KeyState.Right | KeyState.Left)) != 0) + { + if ((keyState & KeyState.Right) != 0) + rotSpeed = -rotSpeed; + //both camera direction and camera plane must be rotated + double oldDirX = dirX; + dirX = dirX * Math.Cos(rotSpeed) - dirY * Math.Sin(rotSpeed); + dirY = oldDirX * Math.Sin(rotSpeed) + dirY * Math.Cos(rotSpeed); + double oldPlaneX = planeX; + planeX = planeX * Math.Cos(rotSpeed) - planeY * Math.Sin(rotSpeed); + planeY = oldPlaneX * Math.Sin(rotSpeed) + planeY * Math.Cos(rotSpeed); + } + + time = tick; + } + } + + static double posX = 22, posY = 12; //x and y start position + static double dirX = -1, dirY = 0; //initial direction vector + static double planeX = 0, planeY = 0.66; //the 2d raycaster version of camera plane + static KeyState keyState; + static uint time; + + enum KeyState + { + Left = 1, + Up = 2, + Right = 4, + Down = 8, + } + + class Screen + { + internal static ScreenBuffer s_buffer; + } + + class BitmapInfo + { + internal static BITMAPINFO bmi = new BITMAPINFO + { + bmiHeader = new BITMAPINFOHEADER + { + biSize = (uint)sizeof(BITMAPINFOHEADER), + biWidth = Width, + biHeight = -Height, + biPlanes = 1, + biBitCount = 32, + biCompression = BI.RGB, + biSizeImage = 0, + biXPelsPerMeter = 0, + biYPelsPerMeter = 0, + biClrUsed = 0, + biClrImportant = 0, + }, + bmiColors = default + }; + } + + static void Main() + { + long className = 'e' | 'd' << 8 | 'i' << 16 | 't' << 24; + //long className = 's' | 't' << 8 | 'a' << 16 | 't' << 24 | 'i' << 32 | 'c' << 40; + + IntPtr hwnd = CreateWindowExA(WS_EX_APPWINDOW | WS_EX_WINDOWEDGE, (byte*)&className, null, + WS_VISIBLE | WS_CAPTION | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, + 0, 0, Width, Height, 0, 0, 0, 0); + + IntPtr hdc = GetDC(hwnd); + + bool done = false; + while (!done) + { + MSG msg; + while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE) != BOOL.FALSE) + { + if (msg.message == WM_KEYDOWN) + keyState |= AsKeyState(msg.wParam); + if (msg.message == WM_KEYUP) + keyState &= ~AsKeyState(msg.wParam); + + done |= GetAsyncKeyState(VK_ESCAPE) != 0; + + DispatchMessageA(&msg); + + static KeyState AsKeyState(nint lParam) => (KeyState)(lParam >= 0x25 && lParam <= 0x28 ? 1 << (int)(lParam - 0x25) : 0); + } + + fixed (BITMAPINFO* pBmi = &BitmapInfo.bmi) + fixed (ScreenBuffer* pBuffer = &Screen.s_buffer) + { + RenderEffect(GetTickCount(), (byte*)pBuffer); + StretchDIBits(hdc, 0, 0, Width, Height, 0, 0, Width, Height, pBuffer, pBmi, DIB_RGB_COLORS, SRCCOPY); + } + //Sleep(1); // give some time to other processes + } + + //ReleaseDC(hwnd, hdc); + //DestroyWindow(hwnd); + + //ExitProcess(0); + } + + struct ScreenBuffer + { + fixed byte _pixel[Width * Height * 4]; + } +} \ No newline at end of file diff --git a/sampling/minimaze/README.md b/sampling/minimaze/README.md new file mode 100644 index 0000000..8b0b85e --- /dev/null +++ b/sampling/minimaze/README.md @@ -0,0 +1,5 @@ +# minimaze + +> A small "game" in C# that builds into a 1936-byte executable with no .NET runtime needed. Article: https://migeel.sk/blog/2024/01/02/building-a-self-contained-game-in-csharp-under-2-kilobytes/ + +Purpose here is to sample dbj_core diff --git a/sampling/minimaze/Win32.cs b/sampling/minimaze/Win32.cs new file mode 100644 index 0000000..333d109 --- /dev/null +++ b/sampling/minimaze/Win32.cs @@ -0,0 +1,119 @@ +using System; +using System.Runtime.InteropServices; + +internal unsafe class Win32 +{ + public const int WS_VISIBLE = 0x10000000; + public const int WS_CAPTION = 0x00C00000; + public const int WS_CLIPSIBLINGS = 0x04000000; + public const int WS_CLIPCHILDREN = 0x02000000; + + public const int WS_EX_APPWINDOW = 0x00040000; + public const int WS_EX_WINDOWEDGE = 0x00000100; + + public const int PM_REMOVE = 0x0001; + + public const int DIB_RGB_COLORS = 0; + + public const uint SRCCOPY = (uint)(0x00CC0020); + + public const int WM_KEYDOWN = 0x0100; + public const int WM_KEYUP = 0x0101; + + public const int VK_ESCAPE = 0x1B; + + public enum BOOL : int { FALSE = 0 } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MSG + { + public IntPtr hwnd; + public uint message; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public POINT pt; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RGBQUAD + { + public byte rgbBlue; + public byte rgbGreen; + public byte rgbRed; + public byte rgbReserved; + } + + public enum BI + { + RGB = 0, + RLE8 = 1, + RLE4 = 2, + BITFIELDS = 3, + JPEG = 4, + PNG = 5, + } + + [StructLayout(LayoutKind.Sequential)] + public struct BITMAPINFOHEADER + { + public uint biSize; + public int biWidth; + public int biHeight; + public ushort biPlanes; + public ushort biBitCount; + public BI biCompression; + public uint biSizeImage; + public int biXPelsPerMeter; + public int biYPelsPerMeter; + public uint biClrUsed; + public uint biClrImportant; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BITMAPINFO + { + public BITMAPINFOHEADER bmiHeader; + public RGBQUAD bmiColors; + } + + [DllImport("user32")] + public static extern IntPtr CreateWindowExA( + int dwExStyle, + byte* lpClassName, + byte* lpWindowName, + int dwStyle, + int X, + int Y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInst, + IntPtr lParam); + + [DllImport("user32")] + public static extern IntPtr GetDC(IntPtr hWnd); + + [DllImport("kernel32")] + public static extern uint GetTickCount(); + + [DllImport("user32")] + public static extern BOOL PeekMessageA(MSG* lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); + + [DllImport("user32")] + public static extern IntPtr DispatchMessageA(MSG* msg); + + [DllImport("gdi32")] + public static extern int StretchDIBits(IntPtr hdc, int xDest, int yDest, int DestWidth, int DestHeight, int xSrc, int ySrc, int SrcWidth, int SrcHeight, void* lpBits, BITMAPINFO* lpbmi, uint iUsage, uint rop); + + [DllImport("user32")] + public static extern short GetAsyncKeyState(int vKey); +} diff --git a/sampling/minimaze/maze.csproj b/sampling/minimaze/maze.csproj new file mode 100644 index 0000000..42548c1 --- /dev/null +++ b/sampling/minimaze/maze.csproj @@ -0,0 +1,9 @@ + + + + Exe + net7.0 + true + + +