A 3D first-person arena shooter that ships as three separate builds of the same game:
- Web edition —
html/game.html— a single self-contained file running on Three.js in any browser. No build step; open and play. - Windows desktop edition —
windows/— a native port on .NET 9 + raylib (Raylib-cs), with Windows Raw Input for independent pointing devices in split-screen. - macOS desktop edition —
mac/— a native .NET 9 + raylib desktop build packaged asSwordAndGun.app, sharing the same gameplay source as the Windows build.
All editions share the design: six primary weapons plus a fists fallback, a drivable tank, climbable multi-tier buildings, health and weapon pickups, AI waves, and 1v1 duels.
Web (html/game.html) |
Windows (windows/) |
macOS (mac/) |
|
|---|---|---|---|
| Runtime | Browser + Three.js (CDN) | .NET 9 + raylib | .NET 9 + raylib |
| Single-player waves | Yes | Yes | Yes |
| Local split-screen 2P | No | Yes (two pointing devices) | Yes (keyboard/mouse fallback) |
| LAN duel | WebRTC manual pairing | UDP broadcast discovery | UDP broadcast discovery |
| Drivable tank | friendly tank | one per player (2P) / player + AI (single) | one per player (2P) / player + AI (single) |
| Renderer | WebGL, Three.js Lambert + fog | raylib + directional "sun" shader, 4x MSAA | raylib + directional "sun" shader, 4x MSAA |
The Windows build requires the .NET 9 SDK.
dotnet run --project windows/SwordAndGun.csproj # Windows
The macOS build requires the .NET 9 SDK and publishes a self-contained .app by default:
sh mac/build.sh
open mac/build/SwordAndGun.app
The desktop window opens in borderless full-screen (toggle with F11). The first screen asks for a name and a mode.
- 單人波次 — Single — eight escalating waves of AI (shooters, brawlers, AI-driven tanks). You also get your own drivable tank.
- 同機雙人分割對戰 — Local split-screen — 1v1 on one PC using the shared keyboard and two pointing devices. You enter both names and choose the win condition on the setup screen.
- 建立 / 搜尋區網房間 — LAN host / join — 1v1 over the local network with automatic UDP-broadcast discovery; full bidirectional state sync (players, tanks, hits).
| Action | Player 1 / Single / LAN | Player 2 (split-screen) |
|---|---|---|
| Move | W A S D |
Arrow keys |
| Sprint | Left Shift |
Right Shift |
| Jump | Space |
Right Ctrl |
| Aim | Mouse / pointing device 1 | Pointing device 2, or I J K L fallback |
| Fire | Left mouse / tap | Device-2 click / tap, or Enter fallback |
| Aim / zoom (sniper scope) | Right mouse | Device-2 right button, or PageDown fallback |
| Select weapon | 1–6 |
7 8 9 0 - = |
| Cycle weapon | Q |
End |
| Reload | R |
Backspace |
| Enter / exit tank | F |
Right Alt |
| Pause | P or Esc |
— |
| Restart match | F2 |
|
| Main menu | M |
|
| Toggle language | F10 (anytime) |
|
| Quit | Ctrl+Q (or Esc at the menu) |
Player 2's weapon keys avoid the numpad so the scheme works on laptops. The in-game HUD shows each player's bindings.
The whole UI is bilingual — 繁體中文 (default) / English. Toggle with F10 anytime (or the button on the web build); the choice is persisted across sessions (%AppData%\SwordAndGun.lang on desktop, localStorage on the web).
The start screen shows a persisted top-10 high-score board from single-player runs (name, score, and the wave reached or 勝利/Victory). Scores are saved to %AppData%\SwordAndGun.scores (desktop) / localStorage (web).
Choosing split-screen opens a setup screen:
- Names — enter a name for P1 and P2 (
Tabswitches fields,Ctrl+Vpastes). Each player's name is shown in their HUD and floats above their avatar in the world. P1 is blue, P2 is orange. - Win condition (
←/→to switch,↑/↓to adjust):- 先到 N 殺 — first to N kills (1–50, default 10), or
- 限時 M 分鐘 — timed match (0.5–15 min); most kills when the clock hits zero wins. A live countdown shows at the top; ties are declared 平手.
Split-screen on Windows needs two independent pointers. The Windows desktop build uses Raw Input to tell devices apart:
- Any external mouse is its own controller.
- A precision touchpad is also supported — its HID digitizer reports are parsed into relative aim, and a physical click-pad press or a quick tap acts as the fire/assign button.
- The setup screen shows a live device list (
#index [mouse/touchpad] move / clicks). Wiggle each device to confirm they register separately, then click/tap to assign P1 then P2.
On macOS and any desktop environment without Raw Input, split-screen starts immediately with the keyboard/mouse fallback: P1 uses the normal mouse, while P2 uses arrow keys to move, I/J/K/L to aim, Enter to fire, and PageDown for aim/zoom.
The Windows split-screen mode is built for two independent Raw Input pointing devices. On the assignment screen, wiggle or click each device and confirm two different device rows move before assigning P1 and P2. If one mouse appears to control both players, return to the menu and reassign using two distinct listed devices; some wireless receivers expose multiple handles, so the game groups related handles before assigning them. Platforms without Raw Input use the P2 keyboard aiming fallback instead.
Each half of the split-screen has its own camera, crosshair, HUD, and screen-space shot tracer. The firing player sees a short tracer from their weapon toward their own crosshair, while the other player can still see the 3D tracer in the world. Player avatars, visible body bounds, and hitscan boxes use the same dimensions, so a centered shot on the visible opponent body should reduce HP and briefly flash the target white.
The Windows desktop build loads a Windows CJK font at startup and expands the glyph atlas as new Traditional Chinese text appears. It prefers Microsoft JhengHei / YaHei style fonts when available and falls back to other installed CJK fonts. If Chinese text shows as ????, install a Windows Traditional Chinese supplemental font or make sure C:\Windows\Fonts\msjh.ttc / msjhbd.ttc is present.
The macOS desktop build loads installed macOS CJK fonts such as PingFang and STHeiti before falling back to the default raylib font.
Desktop LAN discovery uses UDP broadcast on port 45678; gameplay packets use UDP port 45679. Both PCs must be on the same LAN/subnet and the OS firewall must allow the game or these UDP ports. The browser/WebRTC build does not use these UDP ports because browsers cannot send raw LAN broadcast packets.
You start with the pistol and fists; the other five primary weapons are picked up from crates scattered around the map (see the shared sections below). Each player has a drivable tank (600 HP) parked near spawn — press the interact key within 5 m to get in. It fires a high-explosive shell, can smash destructible cover by ramming, and ejects you (−25 HP) if destroyed while you're inside. In single-player the AI also fields tanks.
- A weapon bar shows the six numbered primary slots plus fists fallback: owned weapons bright, not-yet-found dimmed, out-of-ammo in red, the equipped one boxed; owned firearms also show their current ammo.
- Player and tank HP bars (coloured, value overlaid; tank bars also float above each tank in the world).
- Damage-direction arrows point toward incoming fire; enemy HP bars float over each enemy in single-player.
- Avatars are built from legs/torso/head and visibly hold the current weapon; gunfire shows a muzzle flash, melee shows a sword swing, and a first-person view-model is drawn for the local player.
- A fixed directional sun: every face of every box is shaded by its normal, so the six sides differ in tone.
- 4× MSAA smooths edges and the ground grid.
- Borderless full-screen at the monitor's resolution; split-screen renders each half to its own render target so the crosshair and the shot direction line up exactly.
The desktop editions use the same sword/pistol/shield artwork:
mac/icon.pngis the 1024px source artwork for macOS.mac/build.shconverts that source intoAppIcon.icnsduring packaging.windows/icon.pngis the 256px window icon.windows/icon.icois the Windows executable icon, with 16/32/48/64/128/256px entries.
A single-file build that runs entirely in the browser. No installation — open html/game.html and play.
- Download
html/game.html. - Double-click to open it in your browser.
- Enter a player name, then click the screen — pointer lock engages and the round begins.
Internet is required on first load to fetch Three.js from the CDN; afterwards the browser cache serves it offline.
The browser version pairs two computers through WebRTC manual signalling (no STUN/TURN; same-LAN only):
- Host clicks 建立區網房間 and sends the generated pairing code to the other computer.
- The joiner pastes it, clicks 加入房間, and sends its reply code back.
- The host pastes the reply and clicks 套用對方回覆碼.
- Once connected, enter names and start — a 1v1 duel, first to 10 kills.
Browsers can't do raw UDP broadcast, so automatic IP discovery isn't available in the web edition (the desktop edition does it over UDP).
| Action | Key / Input |
|---|---|
| Move | W A S D |
| Sprint | Shift |
| Aim | Mouse |
| Attack | Left mouse button |
| Aim / zoom (sniper scope) | Right mouse button |
| Switch weapon | 1–6 |
| Cycle weapon | Q |
| Reload | R |
| Enter / exit friendly tank | F (within 5 m) |
| Pause | P or Esc; click to resume |
The game truly pauses when pointer lock is released — enemies stop, bullets freeze, cooldowns hold. Input state resets on resume so a held key doesn't carry over.
- Three.js via an import map from a CDN; pure ES module + vanilla DOM, no bundler.
- Pointer Lock API for mouse capture; WebGLRenderer with
antialias: true. - Local top-10 ranking board via
localStorage. - Every model is a
BoxGeometry/SphereGeometrycomposition — the whole game is one ~40 KB HTML file.
You start with the pistol and fists; the other primary weapons are found in crates around the arena.
| # | Name | Mag | Damage | Cooldown | Range | Notes |
|---|---|---|---|---|---|---|
| 1 | 手槍 Pistol | 16 | 24 | 0.22 s | 75 | Accurate, semi-auto |
| 2 | 衝鋒槍 SMG | 40 | 13 | 0.055 s | 58 | Full-auto, some spread |
| 3 | 機關槍 Machine Gun | 90 | 18 | 0.085 s | 85 | Full-auto |
| 4 | 火箭筒 RPG | 5 | 115 | 0.9 s | 100 | Projectile, ~7.5 m blast |
| 5 | 刀劍 Sword | — | 280 | 0.35 s | 4 | Melee, one-shots most |
| 6 | 狙擊槍 Sniper | 5 | 180 | 1.0 s | 220 | Long range, high damage |
| — | 拳頭 Fists | — | 30 | 0.4 s | 2.2 | Always owned fallback, auto-selected when firearms run dry |
- Shooter (bright red) — keeps its distance, strafes, fires bolts.
- Brawler (orange) — charges into melee.
- Tank (crimson, ~400+ HP) — large, slow, fires heavy shells; explodes on death.
Enemies are vividly coloured, carry a HP bar above them, and steer around obstacles (probing a fan of headings) rather than grinding into cover. A 第 N 波 banner announces each wave; cleared waves trigger a 3-second countdown before the next. Per wave n: shooters 3 + n, brawlers n, tanks ⌊(n−1)/2⌋. Clearing wave 8 wins the run.
In the 2-player modes (desktop split-screen and both editions' LAN duel) players respawn at random, obstacle-free positions rather than fixed corners. Single-player has no respawn — death ends the run.
- Health packs — white-and-green crosses; walk over one to regain 55 HP (cap 180), or +120 tank HP if you're driving a damaged tank. Respawns after ~18 s.
- Weapon crates — gold boxes that grant the weapon and a full magazine (and auto-equip it the first time). Respawn after ~22 s. Ammo is per-magazine: when a mag runs dry you must reload (
R) — there's no auto-reload.
A large square arena bounded by solid walls, with procedurally placed cover each round: tall pillars, low cover walls, and climbable multi-tier buildings (step up the stairs, jump between tiers). Spawn points are always cleared of cover. Destructible cover can be smashed by tanks.
MIT.