Add networked Fighter3D minigame

This commit is contained in:
ud18010
2026-08-10 16:47:16 +08:00
parent bff617091d
commit ef7f24738e
53 changed files with 5458 additions and 19 deletions
@@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using XWorld.Framework;
using XWorld.Framework.Protocol;
namespace Fighter3D.Core
{
public sealed class FighterLogic : IGameLogic<FighterState, FighterInput, FighterEvent>
{
public const int TickRate = 60;
public const float FixedDelta = 1f / TickRate;
public const float StageHalfWidth = 4f;
public const float RoundSeconds = 60f;
private const float MoveSpeed = 2.6f;
private const int MaxHp = 1000;
public FighterState CreateInitial(RoomConfig config, IRandom random)
{
var state = new FighterState
{
Phase = FighterPhase.Running,
WinnerSeat = -1
};
InitFighter(state.Fighters[0], 0, -1f, 1);
InitFighter(state.Fighters[1], 1, 1f, -1);
return state;
}
public StepResult<FighterState, FighterEvent> Step(FighterState state, FighterInput input, IRandom random)
{
var events = new List<FighterEvent>();
if (state == null)
state = CreateInitial(null, random);
if (input.Kind == FighterInputKind.Controls)
{
if (input.Seat >= 0 && input.Seat < state.Fighters.Length)
{
state.Fighters[input.Seat].ControlAxis = input.Axis < 0 ? -1 : input.Axis > 0 ? 1 : 0;
state.Fighters[input.Seat].HeldButtons = input.Buttons;
}
return new StepResult<FighterState, FighterEvent>(state, events);
}
if (state.Phase == FighterPhase.Finished)
return new StepResult<FighterState, FighterEvent>(state, events);
state.TickAccumulator += Math.Max(0f, input.Dt);
while (state.TickAccumulator + 0.000001f >= FixedDelta && state.Phase == FighterPhase.Running)
{
state.TickAccumulator -= FixedDelta;
AdvanceFixedTick(state, events);
}
return new StepResult<FighterState, FighterEvent>(state, events);
}
public byte[] Encode(FighterState state)
{
var writer = new PacketWriter(96);
writer.WriteByte((byte)state.Phase);
writer.WriteVarInt(state.Tick);
writer.WriteSingle(state.TickAccumulator);
writer.WriteSingle(state.ElapsedSeconds);
writer.WriteVarInt(state.WinnerSeat);
for (int i = 0; i < 2; i++)
WriteFighter(writer, state.Fighters[i]);
return writer.ToArray();
}
public FighterState Decode(byte[] data)
{
var reader = new PacketReader(data);
var state = new FighterState
{
Phase = (FighterPhase)reader.ReadByte(),
Tick = reader.ReadVarInt(),
TickAccumulator = reader.ReadSingle(),
ElapsedSeconds = reader.ReadSingle(),
WinnerSeat = reader.ReadVarInt()
};
for (int i = 0; i < 2; i++)
ReadFighter(reader, state.Fighters[i]);
return state;
}
private static void InitFighter(FighterUnitState fighter, int seat, float x, int facing)
{
fighter.Seat = seat;
fighter.X = x;
fighter.Hp = MaxHp;
fighter.Facing = facing;
fighter.Action = FighterAction.Idle;
fighter.ActionFrame = 0;
fighter.StunTicks = 0;
fighter.ControlAxis = 0;
fighter.HeldButtons = FighterButtons.None;
fighter.HasHit = false;
}
private static void AdvanceFixedTick(FighterState state, List<FighterEvent> events)
{
state.Tick++;
state.ElapsedSeconds += FixedDelta;
UpdateFacing(state);
for (int i = 0; i < 2; i++)
AdvanceFighter(state, i, events);
ResolveTimeout(state, events);
}
private static void UpdateFacing(FighterState state)
{
var a = state.Fighters[0];
var b = state.Fighters[1];
if (Math.Abs(a.X - b.X) < 0.0001f)
return;
a.Facing = a.X < b.X ? 1 : -1;
b.Facing = b.X < a.X ? 1 : -1;
}
private static void AdvanceFighter(FighterState state, int seat, List<FighterEvent> events)
{
var fighter = state.Fighters[seat];
if (fighter.Action == FighterAction.Knockout)
return;
if (fighter.Action == FighterAction.HitStun || fighter.Action == FighterAction.BlockStun)
{
fighter.ActionFrame++;
fighter.StunTicks--;
if (fighter.StunTicks <= 0)
EnterAction(fighter, FighterAction.Idle);
return;
}
if (fighter.Action == FighterAction.LightAttack || fighter.Action == FighterAction.HeavyAttack)
{
fighter.ActionFrame++;
TryResolveAttack(state, fighter, events);
if (fighter.ActionFrame >= TotalFrames(fighter.Action))
EnterAction(fighter, FighterAction.Idle);
return;
}
if ((fighter.HeldButtons & FighterButtons.Heavy) != 0)
{
EnterAction(fighter, FighterAction.HeavyAttack);
return;
}
if ((fighter.HeldButtons & FighterButtons.Light) != 0)
{
EnterAction(fighter, FighterAction.LightAttack);
return;
}
if (fighter.ControlAxis != 0)
{
fighter.Action = FighterAction.Moving;
fighter.X = Clamp(fighter.X + fighter.ControlAxis * MoveSpeed * FixedDelta, -StageHalfWidth, StageHalfWidth);
}
else
{
fighter.Action = FighterAction.Idle;
}
}
private static void TryResolveAttack(FighterState state, FighterUnitState attacker, List<FighterEvent> events)
{
if (attacker.HasHit || !IsActive(attacker.Action, attacker.ActionFrame))
return;
var defender = state.Fighters[1 - attacker.Seat];
if (defender.Action == FighterAction.Knockout)
return;
float distance = Math.Abs(defender.X - attacker.X);
if (distance > Range(attacker.Action))
return;
if (Math.Sign(defender.X - attacker.X) != attacker.Facing)
return;
attacker.HasHit = true;
if (IsGuardingAway(defender))
{
EnterStun(defender, FighterAction.BlockStun, BlockStun(attacker.Action));
Push(defender, attacker.Facing, BlockPush(attacker.Action));
events.Add(new FighterEvent { Kind = FighterEventKind.Block, AttackerSeat = attacker.Seat, DefenderSeat = defender.Seat });
return;
}
defender.Hp = Math.Max(0, defender.Hp - Damage(attacker.Action));
Push(defender, attacker.Facing, HitPush(attacker.Action));
if (defender.Hp <= 0)
{
EnterAction(defender, FighterAction.Knockout);
state.Phase = FighterPhase.Finished;
state.WinnerSeat = attacker.Seat;
events.Add(new FighterEvent { Kind = FighterEventKind.Finished, AttackerSeat = attacker.Seat, DefenderSeat = defender.Seat, WinnerSeat = attacker.Seat });
}
else
{
EnterStun(defender, FighterAction.HitStun, HitStun(attacker.Action));
events.Add(new FighterEvent { Kind = FighterEventKind.Hit, AttackerSeat = attacker.Seat, DefenderSeat = defender.Seat });
}
}
private static bool IsGuardingAway(FighterUnitState defender)
{
return (defender.HeldButtons & FighterButtons.Guard) != 0
|| defender.ControlAxis == -defender.Facing;
}
private static void EnterAction(FighterUnitState fighter, FighterAction action)
{
fighter.Action = action;
fighter.ActionFrame = 0;
fighter.StunTicks = 0;
fighter.HasHit = false;
}
private static void EnterStun(FighterUnitState fighter, FighterAction action, int stunTicks)
{
fighter.Action = action;
fighter.ActionFrame = 0;
fighter.StunTicks = stunTicks;
fighter.HasHit = false;
}
private static void Push(FighterUnitState fighter, int direction, float distance)
{
fighter.X = Clamp(fighter.X + direction * distance, -StageHalfWidth, StageHalfWidth);
}
private static void ResolveTimeout(FighterState state, List<FighterEvent> events)
{
float battleSeconds = state.Tick * FixedDelta;
if (state.Phase == FighterPhase.Finished || battleSeconds + 0.0001f < RoundSeconds)
return;
int hp0 = state.Fighters[0].Hp;
int hp1 = state.Fighters[1].Hp;
state.Phase = FighterPhase.Finished;
state.WinnerSeat = hp0 == hp1 ? 2 : hp0 > hp1 ? 0 : 1;
events.Add(new FighterEvent { Kind = FighterEventKind.Finished, WinnerSeat = state.WinnerSeat });
}
private static bool IsActive(FighterAction action, int frame)
{
return frame >= Startup(action) && frame < Startup(action) + Active(action);
}
private static int Startup(FighterAction action) => action == FighterAction.HeavyAttack ? 7 : 4;
private static int Active(FighterAction action) => action == FighterAction.HeavyAttack ? 4 : 3;
private static int TotalFrames(FighterAction action) => action == FighterAction.HeavyAttack ? 29 : 15;
private static int Damage(FighterAction action) => action == FighterAction.HeavyAttack ? 250 : 60;
private static int HitStun(FighterAction action) => action == FighterAction.HeavyAttack ? 18 : 12;
private static int BlockStun(FighterAction action) => action == FighterAction.HeavyAttack ? 14 : 8;
private static float Range(FighterAction action) => action == FighterAction.HeavyAttack ? 2.2f : 1.15f;
private static float HitPush(FighterAction action) => action == FighterAction.HeavyAttack ? 0.04f : 0.02f;
private static float BlockPush(FighterAction action) => action == FighterAction.HeavyAttack ? 0.03f : 0.015f;
private static float Clamp(float value, float min, float max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
private static void WriteFighter(PacketWriter writer, FighterUnitState fighter)
{
writer.WriteVarInt(fighter.Seat);
writer.WriteSingle(fighter.X);
writer.WriteVarInt(fighter.Hp);
writer.WriteVarInt(fighter.Facing);
writer.WriteByte((byte)fighter.Action);
writer.WriteVarInt(fighter.ActionFrame);
writer.WriteVarInt(fighter.StunTicks);
writer.WriteVarInt(fighter.ControlAxis);
writer.WriteByte((byte)fighter.HeldButtons);
writer.WriteBool(fighter.HasHit);
}
private static void ReadFighter(PacketReader reader, FighterUnitState fighter)
{
fighter.Seat = reader.ReadVarInt();
fighter.X = reader.ReadSingle();
fighter.Hp = reader.ReadVarInt();
fighter.Facing = reader.ReadVarInt();
fighter.Action = (FighterAction)reader.ReadByte();
fighter.ActionFrame = reader.ReadVarInt();
fighter.StunTicks = reader.ReadVarInt();
fighter.ControlAxis = reader.ReadVarInt();
fighter.HeldButtons = (FighterButtons)reader.ReadByte();
fighter.HasHit = reader.ReadBool();
}
}
}
@@ -0,0 +1,104 @@
using System;
namespace Fighter3D.Core
{
public enum FighterPhase : byte
{
Running = 0,
Finished = 1
}
public enum FighterAction : byte
{
Idle = 0,
Moving = 1,
LightAttack = 2,
HeavyAttack = 3,
HitStun = 4,
BlockStun = 5,
Knockout = 6
}
[Flags]
public enum FighterButtons : byte
{
None = 0,
Guard = 1,
Light = 2,
Heavy = 4
}
public enum FighterInputKind : byte
{
Tick = 0,
Controls = 1
}
public enum FighterEventKind : byte
{
Hit = 0,
Block = 1,
Finished = 2
}
public sealed class FighterUnitState
{
public int Seat;
public float X;
public int Hp;
public int Facing;
public FighterAction Action;
public int ActionFrame;
public int StunTicks;
public int ControlAxis;
public FighterButtons HeldButtons;
public bool HasHit;
}
public sealed class FighterState
{
public int Tick;
public float TickAccumulator;
public float ElapsedSeconds;
public FighterPhase Phase;
public int WinnerSeat = -1;
public FighterUnitState[] Fighters =
{
new FighterUnitState { Seat = 0 },
new FighterUnitState { Seat = 1 }
};
}
public struct FighterInput
{
public FighterInputKind Kind;
public int Seat;
public int Axis;
public FighterButtons Buttons;
public float Dt;
public static FighterInput Tick(float dt)
{
return new FighterInput { Kind = FighterInputKind.Tick, Dt = dt };
}
public static FighterInput Controls(int seat, int axis, FighterButtons buttons)
{
return new FighterInput
{
Kind = FighterInputKind.Controls,
Seat = seat,
Axis = axis < 0 ? -1 : axis > 0 ? 1 : 0,
Buttons = buttons
};
}
}
public struct FighterEvent
{
public FighterEventKind Kind;
public int AttackerSeat;
public int DefenderSeat;
public int WinnerSeat;
}
}