Compare commits
7 Commits
83544fe3d2
...
Car
| Author | SHA1 | Date | |
|---|---|---|---|
| 103f8859d4 | |||
| 32becc12f9 | |||
| ec05fb8ddd | |||
| a4792759e6 | |||
| e2fa2ba8a9 | |||
| aa27725c4e | |||
| cf7d73ba08 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -23,6 +23,14 @@ frontend/dist/
|
||||
build/
|
||||
nouveau_build/
|
||||
build_mai/
|
||||
last_build/
|
||||
other_last_build/
|
||||
very_last_build/
|
||||
pretty_build/
|
||||
schema_gen/
|
||||
New folder/
|
||||
game/connectwebgl.zip
|
||||
game/webgl_sharing/
|
||||
|
||||
# Exception: frontend unity-build static assets (committed for deployment)
|
||||
!frontend/public/unity-build/
|
||||
|
||||
159
README.md
Normal file
159
README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>ROLL'D</h1>
|
||||
|
||||
<p><strong>Browser-based marble MMO — multiplayer physics, real-time leaderboards, playable directly in your browser.</strong></p>
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/badge/Unity-6000.0-black?style=for-the-badge&logo=unity&logoColor=white" alt="Unity 6" />
|
||||
<img src="https://img.shields.io/badge/WebGL-build-E34F26?style=for-the-badge&logo=webgl&logoColor=white" alt="WebGL" />
|
||||
<img src="https://img.shields.io/badge/Colyseus-0.17-6C47FF?style=for-the-badge&logo=node.js&logoColor=white" alt="Colyseus" />
|
||||
<img src="https://img.shields.io/badge/React-19-61DAFB?style=for-the-badge&logo=react&logoColor=black" alt="React" />
|
||||
<img src="https://img.shields.io/badge/Vite-5-646CFF?style=for-the-badge&logo=vite&logoColor=white" alt="Vite" />
|
||||
<img src="https://img.shields.io/badge/Tailwind_CSS-3-38BDF8?style=for-the-badge&logo=tailwindcss&logoColor=white" alt="Tailwind" />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://rolld.kerboul.me"><img src="https://img.shields.io/badge/Play_Now-rolld.kerboul.me-22c55e?style=for-the-badge&logo=googlechrome&logoColor=white" alt="Play Now" /></a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## What is ROLL'D?
|
||||
|
||||
ROLL'D is a multiplayer marble game that runs entirely in the browser via Unity WebGL. Players control a physics-based ball in a shared 3D arena, competing for distance, speed, and style. The game features real-time synchronisation at 60 Hz, in-game chat, and persistent leaderboards.
|
||||
|
||||
No install. No account. Just open the page and roll.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time multiplayer** - up to 20 players per room, 60 Hz state sync via Colyseus WebSockets
|
||||
- **Physics-based gameplay** - Unity Rigidbody, jump charge, gel pads (speed boosts), ball-to-ball bumps
|
||||
- **Room lobby** - browse open rooms, create your own, choose your colour and name
|
||||
- **In-game chat** - accessible in-game (T key) and on the dedicated website chat page
|
||||
- **Live leaderboards** - distance, max speed, jumps, bumps, playtime, updated every 30 seconds
|
||||
- **Spectator camera** - orbiting camera while in lobby or after disconnecting
|
||||
- **WebGL-native** - no plugins, no downloads, runs in Chrome/Firefox/Edge
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
rolld/
|
||||
├── game/ # Unity 6 project (WebGL build)
|
||||
│ └── Assets/Scripts/
|
||||
│ ├── Network/ # Colyseus SDK integration, schema, lobby UI
|
||||
│ ├── Stats/ # StatsTracker - periodic HTTP upload
|
||||
│ └── UI/ # IMGUI in-game HUD, chat, keybinds
|
||||
│
|
||||
├── rolld_backend/game/ # Colyseus 0.17 game server (Node.js)
|
||||
│ └── src/
|
||||
│ ├── rooms/ # ArenaRoom - game state machine
|
||||
│ ├── schema/ # Colyseus schema (Player + GameState)
|
||||
│ ├── stats/ # StatsManager - JSON persistence
|
||||
│ └── chat/ # ChatManager - in-memory history
|
||||
│
|
||||
└── frontend/ # React + Vite + Tailwind SPA
|
||||
└── src/
|
||||
├── pages/ # Home, Stats leaderboard, Chat
|
||||
└── components/ # NavBar, GameCanvas (Unity embed)
|
||||
```
|
||||
|
||||
### Network flow
|
||||
|
||||
```
|
||||
Browser
|
||||
└── Unity WebGL (GameCanvas iframe)
|
||||
└── Colyseus SDK (WebSocket wss://)
|
||||
└── ArenaRoom (Node.js)
|
||||
└── Broadcast state @60 Hz
|
||||
|
||||
Browser
|
||||
└── React SPA
|
||||
└── REST API (HTTPS)
|
||||
├── GET /stats/leaderboard/:key
|
||||
├── GET /chat/history?since=
|
||||
└── POST /stats/update (from Unity every 30s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Game engine | Unity 6 LTS, C# |
|
||||
| Multiplayer | Colyseus 0.17 (Node.js + WebSocket) |
|
||||
| Frontend | React 19, Vite 5, Tailwind CSS 3 |
|
||||
| Deployment | Docker, Coolify, nginx |
|
||||
| Self-hosted | Proxmox LXC, Gitea, Traefik reverse proxy |
|
||||
|
||||
---
|
||||
|
||||
## Running locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- Unity 6000.x (for game builds only)
|
||||
|
||||
### Game server
|
||||
|
||||
```bash
|
||||
cd rolld_backend/game
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Server starts on `ws://localhost:2567`.
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:5173`. The frontend points to the production game server by default - edit `src/pages/StatsPage.jsx` and `src/components/GameCanvas.jsx` to switch to localhost.
|
||||
|
||||
### Unity (optional)
|
||||
|
||||
Open `game/` in Unity 6. The server URL is hardcoded in `Assets/Scripts/Network/NetworkManager.cs`. Switch to `wss://game.rolld.kerboul.me` for prod or `ws://localhost:2567` for local testing.
|
||||
|
||||
---
|
||||
|
||||
## Controls
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| WASD / Arrow keys | Move |
|
||||
| Space (hold) | Charge jump |
|
||||
| Space (release) | Jump |
|
||||
| T | Open chat |
|
||||
| Escape | Close chat |
|
||||
| Tab | Show keybindings |
|
||||
| Backtick (`) | Debug network info |
|
||||
|
||||
---
|
||||
|
||||
## Live deployment
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/badge/Frontend-rolld.kerboul.me-22c55e?style=flat-square&logo=nginx&logoColor=white" alt="Frontend" />
|
||||
<img src="https://img.shields.io/badge/Game_server-game.rolld.kerboul.me-6C47FF?style=flat-square&logo=node.js&logoColor=white" alt="Game server" />
|
||||
<img src="https://img.shields.io/badge/Self_hosted-Proxmox_homelab-E57000?style=flat-square&logo=proxmox&logoColor=white" alt="Self-hosted" />
|
||||
</p>
|
||||
|
||||
The stack runs on a self-hosted Proxmox homelab cluster. Coolify handles container orchestration and auto-deployment on git push. Traefik manages HTTPS termination.
|
||||
|
||||
---
|
||||
|
||||
## Licence
|
||||
|
||||
MIT - do whatever you want with it.
|
||||
@@ -1,2 +0,0 @@
|
||||
# Unity WebGL build goes here
|
||||
Place your Unity WebGL build files in a `Build/` subfolder.
|
||||
BIN
frontend/public/unity-build/Build/build_ball.data
Normal file
BIN
frontend/public/unity-build/Build/build_ball.data
Normal file
Binary file not shown.
57
frontend/public/unity-build/Build/build_ball.framework.js
Normal file
57
frontend/public/unity-build/Build/build_ball.framework.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -52,12 +52,12 @@
|
||||
}
|
||||
|
||||
var buildUrl = "Build";
|
||||
var loaderUrl = buildUrl + "/nouveau_build.loader.js";
|
||||
var loaderUrl = buildUrl + "/build_ball.loader.js";
|
||||
var config = {
|
||||
arguments: [],
|
||||
dataUrl: buildUrl + "/nouveau_build.data",
|
||||
frameworkUrl: buildUrl + "/nouveau_build.framework.js",
|
||||
codeUrl: buildUrl + "/nouveau_build.wasm",
|
||||
dataUrl: buildUrl + "/build_ball.data",
|
||||
frameworkUrl: buildUrl + "/build_ball.framework.js",
|
||||
codeUrl: buildUrl + "/build_ball.wasm",
|
||||
streamingAssetsUrl: "StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "BallProject",
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
// Check if Unity build files exist
|
||||
const UNITY_BUILD_PATH = '/unity-build/Build'
|
||||
// Cache-busting version — update this after each Unity build
|
||||
const UNITY_BUILD_VERSION = '20260517b'
|
||||
const BUILD_PREFIX = 'build_mai'
|
||||
const UNITY_BUILD_VERSION = '20260520d'
|
||||
const BUILD_PREFIX = 'build_ball'
|
||||
const LOADER_URL = `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.loader.js?v=${UNITY_BUILD_VERSION}`
|
||||
|
||||
|
||||
|
||||
@@ -4,16 +4,17 @@ import { theme } from '../env'
|
||||
const SERVER = 'https://game.rolld.kerboul.me'
|
||||
|
||||
const TABS = [
|
||||
{ key: 'totalDistance', label: 'Distance', unit: 'm', format: v => Math.round(v).toLocaleString('fr-FR') },
|
||||
{ key: 'maxSpeed', label: 'Vitesse max', unit: 'm/s', format: v => v.toFixed(1) },
|
||||
{ key: 'totalJumps', label: 'Sauts', unit: '', format: v => v.toLocaleString('fr-FR') },
|
||||
{ key: 'bestRaceTime', label: 'Meilleur temps', unit: '', format: v => {
|
||||
const m = Math.floor(v / 60)
|
||||
const s = (v % 60).toFixed(2).padStart(5, '0')
|
||||
return `${m}:${s}`
|
||||
{ key: 'totalDistance', label: 'Distance', unit: 'm', format: v => Math.round(v ?? 0).toLocaleString('fr-FR') },
|
||||
{ key: 'maxSpeed', label: 'Vitesse max', unit: 'm/s', format: v => (v ?? 0).toFixed(1) },
|
||||
{ key: 'totalJumps', label: 'Sauts', unit: '', format: v => (v ?? 0).toLocaleString('fr-FR') },
|
||||
{ key: 'bumpsGiven', label: 'Bumps', unit: '', format: v => (v ?? 0).toLocaleString('fr-FR') },
|
||||
{ key: 'totalPlaytime',label: 'Temps de jeu',unit: '', format: v => {
|
||||
const total = Math.round(v ?? 0)
|
||||
const h = Math.floor(total / 3600)
|
||||
const m = Math.floor((total % 3600) / 60)
|
||||
const s = total % 60
|
||||
return h > 0 ? `${h}h ${m}m` : `${m}m ${s}s`
|
||||
}},
|
||||
{ key: 'racesPlayed', label: 'Courses', unit: '', format: v => v.toLocaleString('fr-FR') },
|
||||
{ key: 'bumpsGiven', label: 'Bumps', unit: '', format: v => v.toLocaleString('fr-FR') },
|
||||
]
|
||||
|
||||
export default function StatsPage() {
|
||||
@@ -38,6 +39,7 @@ export default function StatsPage() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setRows([])
|
||||
fetchLeaderboard(activeTab)
|
||||
const id = setInterval(() => fetchLeaderboard(activeTab), 30_000)
|
||||
return () => clearInterval(id)
|
||||
@@ -124,7 +126,7 @@ export default function StatsPage() {
|
||||
{row.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right font-mono text-sm" style={{ color: theme.accentLight }}>
|
||||
{currentTab.format(row[activeTab])}
|
||||
{currentTab.format(row.value)}
|
||||
{currentTab.unit && <span className="text-rolld-muted ml-1">{currentTab.unit}</span>}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!134 &13400000
|
||||
PhysicsMaterial:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Bouncy
|
||||
serializedVersion: 2
|
||||
m_DynamicFriction: 0.6
|
||||
m_StaticFriction: 0.6
|
||||
m_Bounciness: 0.74
|
||||
m_FrictionCombine: 0
|
||||
m_BounceCombine: 0
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38ed95051af515848a7513429d4f0413
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 13400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,15 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!134 &13400000
|
||||
PhysicsMaterial:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: GelBleu
|
||||
serializedVersion: 2
|
||||
m_DynamicFriction: 0
|
||||
m_StaticFriction: 0
|
||||
m_Bounciness: 1
|
||||
m_FrictionCombine: 1
|
||||
m_BounceCombine: 3
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 458e6466a22c1204cb2e77d378867d7b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 13400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,15 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!134 &13400000
|
||||
PhysicsMaterial:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: GelOrange
|
||||
serializedVersion: 2
|
||||
m_DynamicFriction: 0.6
|
||||
m_StaticFriction: 0.6
|
||||
m_Bounciness: 0.74
|
||||
m_FrictionCombine: 0
|
||||
m_BounceCombine: 0
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61512ca9473715648874e2d1f555c50f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 13400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,15 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!134 &13400000
|
||||
PhysicsMaterial:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: GelViolet
|
||||
serializedVersion: 2
|
||||
m_DynamicFriction: 1
|
||||
m_StaticFriction: 1
|
||||
m_Bounciness: 0
|
||||
m_FrictionCombine: 3
|
||||
m_BounceCombine: 0
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86c56232f118b4c4caa7fc9d124fc344
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -8,8 +8,8 @@ Material:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: CheckpointMat
|
||||
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Parent: {fileID: 0}
|
||||
m_Shader: {fileID: -6465566751694194690, guid: c52a5eb90c085474582a223ce9475866, type: 3}
|
||||
m_Parent: {fileID: -876546973899608171, guid: c52a5eb90c085474582a223ce9475866, type: 3}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
@@ -22,63 +22,22 @@ Material:
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_TexEnvs: []
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _BumpScale: 1
|
||||
- _Cutoff: 0.5
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _GlossMapScale: 1
|
||||
- _Glossiness: 0.5
|
||||
- _GlossyReflections: 1
|
||||
- _Metallic: 0
|
||||
- _Mode: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.02
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _UVSec: 0
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _Color: {r: 0.1, g: 0.9, b: 0.3, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Floats: []
|
||||
m_Colors: []
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &8924139153543123182
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.ShaderGraph.Editor::UnityEditor.Rendering.BuiltIn.AssetVersion
|
||||
version: 0
|
||||
|
||||
Binary file not shown.
@@ -1,15 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!134 &13400000
|
||||
PhysicsMaterial:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Normal
|
||||
serializedVersion: 2
|
||||
m_DynamicFriction: 0.6
|
||||
m_StaticFriction: 0.6
|
||||
m_Bounciness: 0
|
||||
m_FrictionCombine: 0
|
||||
m_BounceCombine: 0
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36e82e5cf5450404999af634c1d3cbbd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 13400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -180,20 +180,18 @@ public class PlayerController : MonoBehaviour
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
// Toggle cursor lock/unlock avec clic droit (disabled when keybind menu is open)
|
||||
if (!KeyBindingUI.IsVisible && Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame)
|
||||
// Cursor lock: right-click unlocks, left-click re-locks (disabled when any UI panel is open)
|
||||
if (!ChatUI.IsVisible && !KeyBindingUI.IsVisible && Mouse.current != null)
|
||||
{
|
||||
if (Cursor.lockState == CursorLockMode.Locked)
|
||||
if (Cursor.lockState == CursorLockMode.Locked && Mouse.current.rightButton.wasPressedThisFrame)
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
Debug.Log("Cursor UNLOCKED");
|
||||
}
|
||||
else
|
||||
else if (Cursor.lockState != CursorLockMode.Locked && Mouse.current.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
Debug.Log("Cursor LOCKED");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,33 +372,19 @@ public class PlayerController : MonoBehaviour
|
||||
|
||||
public void OnJump(InputAction.CallbackContext context)
|
||||
{
|
||||
if (ChatUI.IsVisible) { isJumpPressed = false; jumpPressTime = 0f; return; }
|
||||
|
||||
if (context.started)
|
||||
{
|
||||
isJumpPressed = true;
|
||||
jumpPressTime = 0f;
|
||||
StatsTracker.Instance?.RegisterJump();
|
||||
Debug.Log("Jump Started");
|
||||
}
|
||||
else if (context.performed)
|
||||
{
|
||||
// Action validée (utile pour saut immédiat aussi)
|
||||
Debug.Log("Jump Performed");
|
||||
}
|
||||
else if (context.canceled)
|
||||
{
|
||||
// Touche relâchée
|
||||
float jumpForceFactor = Mathf.Clamp01(jumpPressTime / maxJumpHoldTime);
|
||||
if (IsGrounded())
|
||||
{
|
||||
PerformJump(jumpForceFactor * JumpForce);
|
||||
Debug.Log($"Jump Released after {jumpPressTime}s -> Force factor: {jumpForceFactor}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Jump Released but not grounded.");
|
||||
}
|
||||
|
||||
// Reset jump state so gauge goes back to 0
|
||||
isJumpPressed = false;
|
||||
jumpPressTime = 0f;
|
||||
}
|
||||
@@ -424,75 +408,30 @@ public class PlayerController : MonoBehaviour
|
||||
|
||||
public void OnForward(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.started)
|
||||
{
|
||||
isForwardHeld = true;
|
||||
Debug.Log("Forward Action Started");
|
||||
}
|
||||
else if (context.performed)
|
||||
{
|
||||
// Forward action performed
|
||||
Debug.Log("Forward Action Performed");
|
||||
}
|
||||
else if (context.canceled)
|
||||
{
|
||||
isForwardHeld = false;
|
||||
Debug.Log("Forward Action Canceled");
|
||||
}
|
||||
if (ChatUI.IsVisible) { isForwardHeld = false; return; }
|
||||
if (context.started) isForwardHeld = true;
|
||||
else if (context.canceled) isForwardHeld = false;
|
||||
}
|
||||
|
||||
public void OnBackwards(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.started)
|
||||
{
|
||||
isBackwardsHeld = true;
|
||||
Debug.Log("Backwards Action Started");
|
||||
}
|
||||
else if (context.performed)
|
||||
{
|
||||
Debug.Log("Backwards Action Performed");
|
||||
}
|
||||
else if (context.canceled)
|
||||
{
|
||||
isBackwardsHeld = false;
|
||||
Debug.Log("Backwards Action Canceled");
|
||||
}
|
||||
if (ChatUI.IsVisible) { isBackwardsHeld = false; return; }
|
||||
if (context.started) isBackwardsHeld = true;
|
||||
else if (context.canceled) isBackwardsHeld = false;
|
||||
}
|
||||
|
||||
public void OnLeft(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.started)
|
||||
{
|
||||
isLeftHeld = true;
|
||||
Debug.Log("Left Action Started");
|
||||
}
|
||||
else if (context.performed)
|
||||
{
|
||||
Debug.Log("Left Action Performed");
|
||||
}
|
||||
else if (context.canceled)
|
||||
{
|
||||
isLeftHeld = false;
|
||||
Debug.Log("Left Action Canceled");
|
||||
}
|
||||
if (ChatUI.IsVisible) { isLeftHeld = false; return; }
|
||||
if (context.started) isLeftHeld = true;
|
||||
else if (context.canceled) isLeftHeld = false;
|
||||
}
|
||||
|
||||
public void OnRight(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.started)
|
||||
{
|
||||
isRightHeld = true;
|
||||
Debug.Log("Right Action Started");
|
||||
}
|
||||
else if (context.performed)
|
||||
{
|
||||
Debug.Log("Right Action Performed");
|
||||
}
|
||||
else if (context.canceled)
|
||||
{
|
||||
isRightHeld = false;
|
||||
Debug.Log("Right Action Canceled");
|
||||
}
|
||||
if (ChatUI.IsVisible) { isRightHeld = false; return; }
|
||||
if (context.started) isRightHeld = true;
|
||||
else if (context.canceled) isRightHeld = false;
|
||||
}
|
||||
|
||||
// --- Bump collision with remote players ---
|
||||
@@ -510,6 +449,9 @@ public class PlayerController : MonoBehaviour
|
||||
{
|
||||
var remote = other.GetComponent<RemotePlayerController>();
|
||||
if (remote == null) return;
|
||||
// Ignore bumps during the remote's post-spawn grace window — otherwise
|
||||
// spawn-time overlap with the kinematic remote ball ejects us upward.
|
||||
if (!remote.BumpReady) return;
|
||||
|
||||
int id = other.gameObject.GetInstanceID();
|
||||
if (_lastBumpTime.TryGetValue(id, out float lastTime) && Time.time - lastTime < bumpCooldown)
|
||||
@@ -564,6 +506,16 @@ public class PlayerController : MonoBehaviour
|
||||
_isSquashing = false;
|
||||
}
|
||||
|
||||
public void ResetInputs()
|
||||
{
|
||||
isForwardHeld = false;
|
||||
isBackwardsHeld = false;
|
||||
isLeftHeld = false;
|
||||
isRightHeld = false;
|
||||
isJumpPressed = false;
|
||||
jumpPressTime = 0f;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// Clean up name label (it's not parented to the ball)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-670956545734759409
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 3
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b94fcd11afffcb142908bfcb1e261fba, type: 3}
|
||||
m_Name: MotionBlur
|
||||
m_EditorClassIdentifier: Unity.Postprocessing.Runtime::UnityEngine.Rendering.PostProcessing.MotionBlur
|
||||
active: 1
|
||||
enabled:
|
||||
overrideState: 1
|
||||
value: 1
|
||||
shutterAngle:
|
||||
overrideState: 0
|
||||
value: 270
|
||||
sampleCount:
|
||||
overrideState: 0
|
||||
value: 10
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8e6292b2c06870d4495f009f912b9600, type: 3}
|
||||
m_Name: PostProcessing Profile
|
||||
m_EditorClassIdentifier: Unity.Postprocessing.Runtime::UnityEngine.Rendering.PostProcessing.PostProcessProfile
|
||||
settings:
|
||||
- {fileID: -670956545734759409}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc446179a9ae97a4a8ad5c8aa1c2dd47
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -89,7 +89,7 @@ MeshRenderer:
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0}
|
||||
- {fileID: -876546973899608171, guid: 9535ecd79e34e1341bfe13a806935455, type: 3}
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fcf7219bab7fe46a1ad266029b2fee19, type: 3}
|
||||
m_Name: Readme
|
||||
m_EditorClassIdentifier:
|
||||
icon: {fileID: 2800000, guid: 727a75301c3d24613a3ebcec4a24c2c8, type: 3}
|
||||
title: URP Empty Template
|
||||
sections:
|
||||
- heading: Welcome to the Universal Render Pipeline
|
||||
text: This template includes the settings and assets you need to start creating with the Universal Render Pipeline.
|
||||
linkText:
|
||||
url:
|
||||
- heading: URP Documentation
|
||||
text:
|
||||
linkText: Read more about URP
|
||||
url: https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@latest
|
||||
- heading: Forums
|
||||
text:
|
||||
linkText: Get answers and support
|
||||
url: https://forum.unity.com/forums/universal-render-pipeline.383/
|
||||
- heading: Report bugs
|
||||
text:
|
||||
linkText: Submit a report
|
||||
url: https://unity3d.com/unity/qa/bug-reporting
|
||||
loadedLayout: 1
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8105016687592461f977c054a80ce2f2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -10,6 +10,6 @@ PhysicsMaterial:
|
||||
serializedVersion: 2
|
||||
m_DynamicFriction: 0.6
|
||||
m_StaticFriction: 0.6
|
||||
m_Bounciness: 0
|
||||
m_Bounciness: 0.2
|
||||
m_FrictionCombine: 0
|
||||
m_BounceCombine: 0
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -29,7 +29,9 @@ public class CameraOrbitKeyboard : MonoBehaviour
|
||||
{
|
||||
// On gère la souris nous-mêmes
|
||||
if (_axisController != null) _axisController.enabled = false;
|
||||
LockCursor();
|
||||
// Only lock cursor if no UI panel is open
|
||||
if (!ChatUI.IsVisible && !KeyBindingUI.IsVisible)
|
||||
LockCursor();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
@@ -55,16 +57,16 @@ public class CameraOrbitKeyboard : MonoBehaviour
|
||||
|
||||
var mouse = Mouse.current;
|
||||
|
||||
// Clic droit = toggle lock
|
||||
if (mouse != null && mouse.rightButton.wasPressedThisFrame)
|
||||
// Right-click unlocks, left-click re-locks (consistent with PlayerController)
|
||||
if (!ChatUI.IsVisible && !KeyBindingUI.IsVisible && mouse != null)
|
||||
{
|
||||
if (Cursor.lockState == CursorLockMode.Locked)
|
||||
if (Cursor.lockState == CursorLockMode.Locked && mouse.rightButton.wasPressedThisFrame)
|
||||
UnlockCursor();
|
||||
else
|
||||
else if (Cursor.lockState != CursorLockMode.Locked && mouse.leftButton.wasPressedThisFrame)
|
||||
LockCursor();
|
||||
}
|
||||
|
||||
if (KeyBindingUI.IsVisible) return;
|
||||
if (KeyBindingUI.IsVisible || ChatUI.IsVisible) return;
|
||||
|
||||
// Souris — seulement quand locked (delta infini, sans accrochage au bord)
|
||||
if (Cursor.lockState == CursorLockMode.Locked && mouse != null)
|
||||
|
||||
@@ -178,8 +178,8 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
if (playerRoot == null) return;
|
||||
playerRoot.SetActive(active);
|
||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||
if (pc != null) pc.enabled = active;
|
||||
var vehicle = playerRoot.GetComponentInChildren<NWH.VehiclePhysics2.VehicleController>(true);
|
||||
if (vehicle != null) vehicle.enabled = active;
|
||||
|
||||
if (active)
|
||||
{
|
||||
|
||||
@@ -116,12 +116,12 @@ public class DebugNetworkUI : MonoBehaviour
|
||||
if (state != null)
|
||||
ImGuiSkin.DrawField("Server Pos", $"({state.x:F1}, {state.y:F1}, {state.z:F1})");
|
||||
|
||||
var pc = FindFirstObjectByType<PlayerController>();
|
||||
if (pc != null && pc.isActiveAndEnabled)
|
||||
var vehicle = FindFirstObjectByType<NWH.VehiclePhysics2.VehicleController>();
|
||||
if (vehicle != null && vehicle.isActiveAndEnabled)
|
||||
{
|
||||
var pos = pc.transform.position;
|
||||
var pos = vehicle.transform.position;
|
||||
ImGuiSkin.DrawField("Live Pos", $"({pos.x:F1}, {pos.y:F1}, {pos.z:F1})");
|
||||
var rb = pc.GetComponent<Rigidbody>();
|
||||
var rb = vehicle.vehicleRigidbody;
|
||||
if (rb != null)
|
||||
{
|
||||
var v = rb.linearVelocity;
|
||||
|
||||
43
game/Assets/Scripts/Network/Generated/GameState.cs
Normal file
43
game/Assets/Scripts/Network/Generated/GameState.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// THIS FILE HAS BEEN GENERATED AUTOMATICALLY
|
||||
// DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING
|
||||
//
|
||||
// GENERATED USING @colyseus/schema 4.0.15
|
||||
//
|
||||
|
||||
using Colyseus.Schema;
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
using UnityEngine.Scripting;
|
||||
#endif
|
||||
|
||||
namespace RolldSchema {
|
||||
public partial class GameState : Schema {
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
[Preserve]
|
||||
#endif
|
||||
public GameState() { }
|
||||
[Type(0, "map", typeof(MapSchema<Player>))]
|
||||
public MapSchema<Player> players = null;
|
||||
|
||||
[Type(1, "string")]
|
||||
public string phase = default(string);
|
||||
|
||||
[Type(2, "float32")]
|
||||
public float countdown = default(float);
|
||||
|
||||
[Type(3, "int8")]
|
||||
public sbyte roundNumber = default(sbyte);
|
||||
|
||||
[Type(4, "int8")]
|
||||
public sbyte totalRounds = default(sbyte);
|
||||
|
||||
[Type(5, "int8")]
|
||||
public sbyte playersAlive = default(sbyte);
|
||||
|
||||
[Type(6, "string")]
|
||||
public string gameMode = default(string);
|
||||
|
||||
[Type(7, "string")]
|
||||
public string winnerName = default(string);
|
||||
}
|
||||
}
|
||||
2
game/Assets/Scripts/Network/Generated/GameState.cs.meta
Normal file
2
game/Assets/Scripts/Network/Generated/GameState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7c8bd319747bfa4a82569b7dc0458be
|
||||
85
game/Assets/Scripts/Network/Generated/Player.cs
Normal file
85
game/Assets/Scripts/Network/Generated/Player.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// THIS FILE HAS BEEN GENERATED AUTOMATICALLY
|
||||
// DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING
|
||||
//
|
||||
// GENERATED USING @colyseus/schema 4.0.15
|
||||
//
|
||||
|
||||
using Colyseus.Schema;
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
using UnityEngine.Scripting;
|
||||
#endif
|
||||
|
||||
namespace RolldSchema {
|
||||
public partial class Player : Schema {
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
[Preserve]
|
||||
#endif
|
||||
public Player() { }
|
||||
[Type(0, "float32")]
|
||||
public float x = default(float);
|
||||
|
||||
[Type(1, "float32")]
|
||||
public float y = default(float);
|
||||
|
||||
[Type(2, "float32")]
|
||||
public float z = default(float);
|
||||
|
||||
[Type(3, "float32")]
|
||||
public float vx = default(float);
|
||||
|
||||
[Type(4, "float32")]
|
||||
public float vy = default(float);
|
||||
|
||||
[Type(5, "float32")]
|
||||
public float vz = default(float);
|
||||
|
||||
[Type(6, "float32")]
|
||||
public float rx = default(float);
|
||||
|
||||
[Type(7, "float32")]
|
||||
public float ry = default(float);
|
||||
|
||||
[Type(8, "float32")]
|
||||
public float rz = default(float);
|
||||
|
||||
[Type(9, "float32")]
|
||||
public float rw = default(float);
|
||||
|
||||
[Type(10, "float64")]
|
||||
public double t = default(double);
|
||||
|
||||
[Type(11, "string")]
|
||||
public string name = default(string);
|
||||
|
||||
[Type(12, "float32")]
|
||||
public float colorR = default(float);
|
||||
|
||||
[Type(13, "float32")]
|
||||
public float colorG = default(float);
|
||||
|
||||
[Type(14, "float32")]
|
||||
public float colorB = default(float);
|
||||
|
||||
[Type(15, "float32")]
|
||||
public float avx = default(float);
|
||||
|
||||
[Type(16, "float32")]
|
||||
public float avy = default(float);
|
||||
|
||||
[Type(17, "float32")]
|
||||
public float avz = default(float);
|
||||
|
||||
[Type(18, "boolean")]
|
||||
public bool isEliminated = default(bool);
|
||||
|
||||
[Type(19, "boolean")]
|
||||
public bool isQualified = default(bool);
|
||||
|
||||
[Type(20, "boolean")]
|
||||
public bool isReady = default(bool);
|
||||
|
||||
[Type(21, "int8")]
|
||||
public sbyte checkpointIndex = default(sbyte);
|
||||
}
|
||||
}
|
||||
2
game/Assets/Scripts/Network/Generated/Player.cs.meta
Normal file
2
game/Assets/Scripts/Network/Generated/Player.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d8173f164ec47946a28c13d9638d8cf
|
||||
@@ -118,25 +118,22 @@ public class LobbyUI : MonoBehaviour
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null && playerRoot != null)
|
||||
{
|
||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||
if (pc != null)
|
||||
var setup = playerRoot.GetComponentInChildren<VehicleLocalSetup>(true);
|
||||
if (setup != null)
|
||||
{
|
||||
var vehicle = setup.GetComponent<NWH.VehiclePhysics2.VehicleController>();
|
||||
var rb = vehicle != null ? vehicle.vehicleRigidbody : setup.GetComponent<Rigidbody>();
|
||||
var localState = nm.GetLocalPlayerState();
|
||||
if (localState != null)
|
||||
if (localState != null && rb != null)
|
||||
{
|
||||
Vector3 spawnPos = new Vector3(localState.x, localState.y, localState.z);
|
||||
var rb = pc.GetComponent<Rigidbody>();
|
||||
if (rb != null)
|
||||
{
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
rb.position = spawnPos;
|
||||
}
|
||||
pc.transform.position = spawnPos;
|
||||
pc.SetSpawnPosition(spawnPos);
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
rb.position = spawnPos;
|
||||
setup.transform.position = spawnPos;
|
||||
}
|
||||
pc.enabled = true;
|
||||
pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor);
|
||||
if (vehicle != null) vehicle.enabled = true;
|
||||
setup.SetupLocal(nm.LocalPlayerName, nm.LocalPlayerColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,8 +157,8 @@ public class LobbyUI : MonoBehaviour
|
||||
|
||||
if (playerRoot != null)
|
||||
{
|
||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||
if (pc != null) pc.enabled = false;
|
||||
var vehicle = playerRoot.GetComponentInChildren<NWH.VehiclePhysics2.VehicleController>(true);
|
||||
if (vehicle != null) vehicle.enabled = false;
|
||||
playerRoot.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using Colyseus;
|
||||
using Colyseus.Schema;
|
||||
using RolldSchema;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton managing the Colyseus connection, room lifecycle, remote player spawning,
|
||||
@@ -29,7 +30,7 @@ public class NetworkManager : MonoBehaviour
|
||||
public string LastError { get; private set; } = "";
|
||||
|
||||
// Expose remote players for debug UI
|
||||
public Dictionary<string, RemotePlayerController> RemotePlayers => _remotePlayers;
|
||||
public Dictionary<string, RemoteVehicleSync> RemotePlayers => _remotePlayers;
|
||||
|
||||
// Local player info (set during join)
|
||||
public string LocalPlayerName { get; private set; } = "";
|
||||
@@ -70,9 +71,9 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
// --- Internals ---
|
||||
private Client _client;
|
||||
private Room<NetworkState> _room;
|
||||
private StateCallbackStrategy<NetworkState> _callbacks;
|
||||
private readonly Dictionary<string, RemotePlayerController> _remotePlayers = new();
|
||||
private Room<GameState> _room;
|
||||
private StateCallbackStrategy<GameState> _callbacks;
|
||||
private readonly Dictionary<string, RemoteVehicleSync> _remotePlayers = new();
|
||||
private float _broadcastTimer;
|
||||
private const float BROADCAST_INTERVAL = 0.01667f; // ~60/sec
|
||||
private bool _isJoining;
|
||||
@@ -111,7 +112,7 @@ public class NetworkManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkPlayer GetLocalPlayerState()
|
||||
public Player GetLocalPlayerState()
|
||||
{
|
||||
if (_room == null || _room.State.players == null || string.IsNullOrEmpty(LocalSessionId)) return null;
|
||||
_room.State.players.TryGetValue(LocalSessionId, out var player);
|
||||
@@ -154,6 +155,7 @@ public class NetworkManager : MonoBehaviour
|
||||
_callbacks.OnRemove(state => state.players, (key, player) => OnPlayerRemove(key, player));
|
||||
_callbacks.Listen(state => state.phase, (v, _) => _OnPhaseChanged(v));
|
||||
_callbacks.Listen(state => state.countdown, (v, _) => OnCountdownChanged?.Invoke(v));
|
||||
_callbacks.Listen(state => state.playersAlive, (newVal, oldVal) => { if (GameHUD.Instance != null) GameHUD.Instance.SetPlayersAlive((int)newVal); });
|
||||
|
||||
_room.OnMessage<EliminatedMsg>("eliminated", msg => { OnEliminated?.Invoke(msg.sessionId, msg.reason); });
|
||||
_room.OnMessage<QualifiedMsg> ("qualified", msg => { OnQualified?.Invoke(msg.sessionId); });
|
||||
@@ -163,6 +165,13 @@ public class NetworkManager : MonoBehaviour
|
||||
_room.OnMessage<ChatUI.ChatMessage>("chat", msg => { ChatUI.Instance?.ReceiveChatMessage(msg); });
|
||||
_room.OnLeave += OnRoomLeave;
|
||||
|
||||
// Seed players already present in the room (state decoded before callbacks were registered)
|
||||
if (_room.State.players != null)
|
||||
{
|
||||
foreach (var key in _room.State.players.Keys)
|
||||
OnPlayerAdd((string)key, (Player)_room.State.players[key]);
|
||||
}
|
||||
|
||||
OnConnected?.Invoke();
|
||||
}
|
||||
|
||||
@@ -182,7 +191,7 @@ public class NetworkManager : MonoBehaviour
|
||||
PrepareJoin(playerName, color);
|
||||
try
|
||||
{
|
||||
_room = await _client.JoinOrCreate<NetworkState>("arena", BuildJoinOptions(playerName, color));
|
||||
_room = await _client.JoinOrCreate<GameState>("arena", BuildJoinOptions(playerName, color));
|
||||
FinishJoin();
|
||||
}
|
||||
catch (Exception e) { HandleJoinError(e); }
|
||||
@@ -195,7 +204,7 @@ public class NetworkManager : MonoBehaviour
|
||||
PrepareJoin(playerName, color);
|
||||
try
|
||||
{
|
||||
_room = await _client.JoinById<NetworkState>(roomId, BuildJoinOptions(playerName, color));
|
||||
_room = await _client.JoinById<GameState>(roomId, BuildJoinOptions(playerName, color));
|
||||
FinishJoin();
|
||||
}
|
||||
catch (Exception e) { HandleJoinError(e); }
|
||||
@@ -210,7 +219,7 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
var opts = BuildJoinOptions(playerName, color);
|
||||
if (roomName != null) opts["roomName"] = roomName;
|
||||
_room = await _client.Create<NetworkState>("arena", opts);
|
||||
_room = await _client.Create<GameState>("arena", opts);
|
||||
FinishJoin();
|
||||
}
|
||||
catch (Exception e) { HandleJoinError(e); }
|
||||
@@ -251,21 +260,27 @@ public class NetworkManager : MonoBehaviour
|
||||
OnPhaseChanged?.Invoke(phase);
|
||||
}
|
||||
|
||||
private void OnPlayerAdd(string sessionId, NetworkPlayer player)
|
||||
private void OnPlayerAdd(string sessionId, Player player)
|
||||
{
|
||||
Debug.Log($"[Network] Player joined: {sessionId} ({player.name})");
|
||||
PlayerCount = _room.State.players?.Count ?? 0;
|
||||
|
||||
if (sessionId == LocalSessionId) return;
|
||||
if (_remotePlayers.ContainsKey(sessionId)) return; // prevent duplicate spawn
|
||||
|
||||
if (remotePlayerPrefab != null)
|
||||
{
|
||||
Vector3 spawnPos = new Vector3(player.x, player.y, player.z);
|
||||
GameObject remoteBall = Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity);
|
||||
remoteBall.name = $"RemotePlayer_{player.name}_{sessionId[..6]}";
|
||||
if (remotePlayerPrefab == null)
|
||||
{
|
||||
Debug.LogError("[Network] remotePlayerPrefab not assigned — cannot spawn remote vehicle.");
|
||||
return;
|
||||
}
|
||||
GameObject remote = Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity);
|
||||
remote.transform.position = spawnPos;
|
||||
remote.name = $"RemoteVehicle_{player.name}_{sessionId[..6]}";
|
||||
|
||||
var controller = remoteBall.GetComponent<RemotePlayerController>()
|
||||
?? remoteBall.AddComponent<RemotePlayerController>();
|
||||
var controller = remote.GetComponent<RemoteVehicleSync>()
|
||||
?? remote.AddComponent<RemoteVehicleSync>();
|
||||
|
||||
controller.Initialize(sessionId, player.name,
|
||||
new Color(player.colorR, player.colorG, player.colorB));
|
||||
@@ -277,7 +292,7 @@ public class NetworkManager : MonoBehaviour
|
||||
OnPlayerJoined?.Invoke(sessionId);
|
||||
}
|
||||
|
||||
private void OnPlayerRemove(string sessionId, NetworkPlayer player)
|
||||
private void OnPlayerRemove(string sessionId, Player player)
|
||||
{
|
||||
Debug.Log($"[Network] Player left: {sessionId}");
|
||||
PlayerCount = _room.State.players?.Count ?? 0;
|
||||
@@ -292,7 +307,7 @@ public class NetworkManager : MonoBehaviour
|
||||
OnPlayerLeft?.Invoke(sessionId);
|
||||
}
|
||||
|
||||
private void OnPlayerChange(string sessionId, NetworkPlayer player)
|
||||
private void OnPlayerChange(string sessionId, Player player)
|
||||
{
|
||||
if (sessionId == LocalSessionId) return;
|
||||
|
||||
@@ -312,20 +327,21 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
// ─── Position Broadcasting ────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Called by <see cref="VehicleLocalSetup"/> after the local vehicle is ready.
|
||||
/// Tells us which Rigidbody to broadcast each tick.
|
||||
/// </summary>
|
||||
public void RegisterLocalVehicle(Transform t, Rigidbody rb)
|
||||
{
|
||||
_localPlayer = t;
|
||||
_localPlayerRb = rb;
|
||||
Debug.Log($"[Network] Local vehicle registered: {t?.name}");
|
||||
}
|
||||
|
||||
private void BroadcastPosition()
|
||||
{
|
||||
if (_room == null || !IsConnected) return;
|
||||
|
||||
if (_localPlayer == null)
|
||||
{
|
||||
var pc = FindFirstObjectByType<PlayerController>();
|
||||
if (pc != null)
|
||||
{
|
||||
_localPlayer = pc.transform;
|
||||
_localPlayerRb = pc.GetComponent<Rigidbody>();
|
||||
}
|
||||
else return;
|
||||
}
|
||||
if (_localPlayer == null || _localPlayerRb == null) return;
|
||||
|
||||
Vector3 pos = _localPlayer.position;
|
||||
Vector3 vel = _localPlayerRb != null ? _localPlayerRb.linearVelocity : Vector3.zero;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using Colyseus.Schema;
|
||||
|
||||
// Must match server-side defineTypes field order exactly
|
||||
public partial class NetworkPlayer : Schema
|
||||
{
|
||||
[Type(0, "float32")] public float x = 0;
|
||||
[Type(1, "float32")] public float y = 5;
|
||||
[Type(2, "float32")] public float z = 0;
|
||||
[Type(3, "float32")] public float vx = 0;
|
||||
[Type(4, "float32")] public float vy = 0;
|
||||
[Type(5, "float32")] public float vz = 0;
|
||||
[Type(6, "float32")] public float rx = 0;
|
||||
[Type(7, "float32")] public float ry = 0;
|
||||
[Type(8, "float32")] public float rz = 0;
|
||||
[Type(9, "float32")] public float rw = 1;
|
||||
[Type(10, "float64")] public double t = 0;
|
||||
[Type(11, "string")] public string name = "";
|
||||
[Type(12, "float32")] public float colorR = 1;
|
||||
[Type(13, "float32")] public float colorG = 1;
|
||||
[Type(14, "float32")] public float colorB = 1;
|
||||
[Type(15, "float32")] public float avx = 0;
|
||||
[Type(16, "float32")] public float avy = 0;
|
||||
[Type(17, "float32")] public float avz = 0;
|
||||
// Game state — order must match server defineTypes exactly
|
||||
[Type(18, "boolean")] public bool isEliminated = false;
|
||||
[Type(19, "boolean")] public bool isQualified = false;
|
||||
[Type(20, "boolean")] public bool isReady = false;
|
||||
[Type(21, "int8")] public int checkpointIndex = 0;
|
||||
}
|
||||
|
||||
public partial class NetworkState : Schema
|
||||
{
|
||||
[Type(0, "map", typeof(MapSchema<NetworkPlayer>))]
|
||||
public MapSchema<NetworkPlayer> players;
|
||||
|
||||
[Type(1, "string")] public string phase = "lobby";
|
||||
[Type(2, "float32")] public float countdown = 0;
|
||||
[Type(3, "int8")] public int roundNumber = 1;
|
||||
[Type(4, "int8")] public int totalRounds = 4;
|
||||
[Type(5, "int8")] public int playersAlive = 0;
|
||||
[Type(6, "string")] public string gameMode = "race";
|
||||
[Type(7, "string")] public string winnerName = "";
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ce16348bc0580b49860d9bd80e7bec0
|
||||
@@ -24,10 +24,16 @@ public class RemotePlayerController : MonoBehaviour
|
||||
[Tooltip("Rotation slerp speed")]
|
||||
public float rotationSpeed = 24f;
|
||||
|
||||
[Header("Spawn")]
|
||||
[Tooltip("Seconds after spawn during which this remote ignores local bump interactions (avoids upward eject if balls overlap at spawn).")]
|
||||
public float spawnBumpGrace = 1.5f;
|
||||
|
||||
// Public info
|
||||
public string SessionId { get; private set; }
|
||||
public string PlayerName { get; private set; }
|
||||
public Color PlayerColor { get; private set; }
|
||||
public float SpawnTime { get; private set; }
|
||||
public bool BumpReady => Time.time - SpawnTime > spawnBumpGrace;
|
||||
|
||||
// --- Snapshot buffer ---
|
||||
private struct Snapshot
|
||||
@@ -61,6 +67,7 @@ public class RemotePlayerController : MonoBehaviour
|
||||
SessionId = sessionId;
|
||||
PlayerName = playerName;
|
||||
PlayerColor = color;
|
||||
SpawnTime = Time.time;
|
||||
_currentRotation = transform.rotation;
|
||||
_bufferCount = 0;
|
||||
_initialized = true;
|
||||
@@ -99,12 +106,17 @@ public class RemotePlayerController : MonoBehaviour
|
||||
|
||||
// Add a trigger collider slightly larger than the physics collider
|
||||
// so the local player can detect bumps
|
||||
var existingCollider = GetComponent<SphereCollider>();
|
||||
float baseRadius = existingCollider != null ? existingCollider.radius : 0.5f;
|
||||
_solidCollider = GetComponent<SphereCollider>();
|
||||
float baseRadius = _solidCollider != null ? _solidCollider.radius : 0.5f;
|
||||
var trigger = gameObject.AddComponent<SphereCollider>();
|
||||
trigger.isTrigger = true;
|
||||
trigger.radius = baseRadius * 1.15f; // 15% larger
|
||||
|
||||
// During the spawn grace window, disable the SOLID collider so the kinematic
|
||||
// remote can't physically eject an overlapping local player at spawn.
|
||||
// (The trigger stays — bump detection is gated by BumpReady in PlayerController.)
|
||||
if (_solidCollider != null) _solidCollider.enabled = false;
|
||||
|
||||
// Disable any player input on remote balls
|
||||
var playerInput = GetComponent<UnityEngine.InputSystem.PlayerInput>();
|
||||
if (playerInput != null)
|
||||
@@ -114,12 +126,16 @@ public class RemotePlayerController : MonoBehaviour
|
||||
if (playerController != null)
|
||||
playerController.enabled = false;
|
||||
|
||||
// Create floating name label
|
||||
// Create floating name label + speed trail
|
||||
CreateNameLabel();
|
||||
CreateTrail(color);
|
||||
|
||||
Debug.Log($"[RemotePlayer] Initialized: {playerName} ({sessionId[..6]}) color={color}");
|
||||
}
|
||||
|
||||
private SphereCollider _solidCollider;
|
||||
private bool _solidReenabled;
|
||||
|
||||
/// <summary>
|
||||
/// Called by NetworkManager when a state update arrives from the server.
|
||||
/// Pushes a new snapshot into the interpolation buffer.
|
||||
@@ -149,7 +165,16 @@ public class RemotePlayerController : MonoBehaviour
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!_initialized || _bufferCount == 0) return;
|
||||
if (!_initialized) return;
|
||||
|
||||
// Re-enable the solid collider once the grace window has elapsed.
|
||||
if (!_solidReenabled && BumpReady && _solidCollider != null)
|
||||
{
|
||||
_solidCollider.enabled = true;
|
||||
_solidReenabled = true;
|
||||
}
|
||||
|
||||
if (_bufferCount == 0) return;
|
||||
|
||||
// Render time = current time minus interpolation delay
|
||||
float renderTime = Time.time - interpolationDelay;
|
||||
@@ -248,15 +273,37 @@ public class RemotePlayerController : MonoBehaviour
|
||||
if (cam != null)
|
||||
{
|
||||
Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position;
|
||||
float camDist = lookDir.magnitude;
|
||||
lookDir.y = 0f;
|
||||
if (lookDir.sqrMagnitude > 0.001f)
|
||||
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
||||
|
||||
// Distance-based scale: keeps the pseudo readable when players are far apart.
|
||||
// Below 8 m → base size; above → grows linearly, capped at 8× to avoid screen takeover.
|
||||
float scaleFactor = Mathf.Clamp(camDist / 8f, 1f, 8f);
|
||||
_nameLabelObj.transform.localScale = Vector3.one * (0.1f * scaleFactor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject _nameLabelObj; // Keep reference for billboard update
|
||||
|
||||
private void CreateTrail(Color playerColor)
|
||||
{
|
||||
var trail = GetComponent<TrailRenderer>() ?? gameObject.AddComponent<TrailRenderer>();
|
||||
trail.time = 0.4f;
|
||||
trail.startWidth = 0.3f;
|
||||
trail.endWidth = 0.02f;
|
||||
trail.minVertexDistance = 0.1f;
|
||||
trail.autodestruct = false;
|
||||
trail.emitting = true;
|
||||
trail.material = new Material(Shader.Find("Sprites/Default"));
|
||||
// Use the player's chosen color so each remote has a visually distinct trail
|
||||
// (lobby presets avoid orange, so it never clashes with the local player's orange trail).
|
||||
trail.startColor = new Color(playerColor.r, playerColor.g, playerColor.b, 0.7f);
|
||||
trail.endColor = new Color(playerColor.r, playerColor.g, playerColor.b, 0f);
|
||||
}
|
||||
|
||||
private void CreateNameLabel()
|
||||
{
|
||||
GameObject labelObj = new GameObject("NameLabel");
|
||||
|
||||
283
game/Assets/Scripts/Network/RemoteVehicleSync.cs
Normal file
283
game/Assets/Scripts/Network/RemoteVehicleSync.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using UnityEngine;
|
||||
using NWH.VehiclePhysics2;
|
||||
|
||||
/// <summary>
|
||||
/// Remote vehicle controller. Attached to remote players spawned from the network.
|
||||
/// - Disables NWH local simulation (VehicleController + WheelControllers + AudioSources).
|
||||
/// - Sets the Rigidbody kinematic so we can drive it with snapshot interpolation
|
||||
/// via <see cref="Rigidbody.MovePosition"/> / <see cref="Rigidbody.MoveRotation"/>.
|
||||
/// - Local dynamic vehicles bounce naturally off the kinematic remote's colliders.
|
||||
/// - Adds a floating name label (distance-scaled) and a colored capsule marker.
|
||||
/// </summary>
|
||||
public class RemoteVehicleSync : MonoBehaviour
|
||||
{
|
||||
[Header("Interpolation")]
|
||||
public float interpolationDelay = 0.083f;
|
||||
public float maxExtrapolation = 0.08f;
|
||||
public float snapDistance = 12f;
|
||||
public float smoothingSpeed = 24f;
|
||||
public float rotationSpeed = 24f;
|
||||
|
||||
[Header("Spawn")]
|
||||
[Tooltip("Seconds after spawn during which colliders are disabled to avoid ejecting overlapping locals at connect.")]
|
||||
public float spawnGrace = 1.5f;
|
||||
|
||||
public string SessionId { get; private set; }
|
||||
public string PlayerName { get; private set; }
|
||||
public Color PlayerColor { get; private set; }
|
||||
public float SpawnTime { get; private set; }
|
||||
|
||||
private struct Snapshot
|
||||
{
|
||||
public double serverTime;
|
||||
public float localTime;
|
||||
public Vector3 position;
|
||||
public Vector3 velocity;
|
||||
public Quaternion rotation;
|
||||
public Vector3 angularVelocity;
|
||||
}
|
||||
|
||||
private const int BUFFER_SIZE = 16;
|
||||
private readonly Snapshot[] _buffer = new Snapshot[BUFFER_SIZE];
|
||||
private int _bufferCount;
|
||||
private int _newestIndex;
|
||||
private bool _initialized;
|
||||
|
||||
private Rigidbody _rb;
|
||||
private VehicleController _vehicle;
|
||||
private Collider[] _allColliders;
|
||||
private bool _collidersReenabled;
|
||||
private Quaternion _currentRotation = Quaternion.identity;
|
||||
|
||||
private GameObject _nameLabelObj;
|
||||
private TextMesh _nameLabel;
|
||||
private GameObject _markerObj;
|
||||
|
||||
public void Initialize(string sessionId, string playerName, Color color)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
PlayerName = playerName;
|
||||
PlayerColor = color;
|
||||
SpawnTime = Time.time;
|
||||
_bufferCount = 0;
|
||||
_currentRotation = transform.rotation;
|
||||
|
||||
// Disable NWH driving simulation: this remote is purely networked, no local AI/input.
|
||||
_vehicle = GetComponent<VehicleController>();
|
||||
if (_vehicle != null) _vehicle.enabled = false;
|
||||
|
||||
// Disable all wheel controllers so they don't apply suspension forces.
|
||||
foreach (var mb in GetComponentsInChildren<MonoBehaviour>(true))
|
||||
{
|
||||
var t = mb.GetType();
|
||||
if (t.FullName == "NWH.WheelController3D.WheelController" || t.FullName.Contains(".WheelController"))
|
||||
mb.enabled = false;
|
||||
}
|
||||
|
||||
// Mute audio sources (engine, skid, etc.) on remotes.
|
||||
foreach (var src in GetComponentsInChildren<AudioSource>(true))
|
||||
src.enabled = false;
|
||||
|
||||
// Make the rigidbody kinematic so we drive it from network snapshots.
|
||||
_rb = _vehicle != null ? _vehicle.vehicleRigidbody : GetComponent<Rigidbody>();
|
||||
if (_rb != null)
|
||||
{
|
||||
_rb.isKinematic = true;
|
||||
_rb.useGravity = false;
|
||||
_rb.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
_rb.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
|
||||
}
|
||||
|
||||
// Cache colliders and disable them during the grace window to avoid spawn-time ejection.
|
||||
_allColliders = GetComponentsInChildren<Collider>(true);
|
||||
foreach (var c in _allColliders)
|
||||
if (c != null && !c.isTrigger) c.enabled = false;
|
||||
|
||||
BuildNameLabel(playerName, color);
|
||||
BuildColorMarker(color);
|
||||
|
||||
_initialized = true;
|
||||
Debug.Log($"[RemoteVehicle] Initialized: {playerName} ({sessionId[..6]}) color={color}");
|
||||
}
|
||||
|
||||
public void SetTargetState(Vector3 position, Vector3 velocity, Quaternion rotation, double serverTime, Vector3 angularVelocity = default)
|
||||
{
|
||||
_newestIndex = (_newestIndex + 1) % BUFFER_SIZE;
|
||||
_buffer[_newestIndex] = new Snapshot
|
||||
{
|
||||
serverTime = serverTime,
|
||||
localTime = Time.time,
|
||||
position = position,
|
||||
velocity = velocity,
|
||||
rotation = rotation,
|
||||
angularVelocity = angularVelocity,
|
||||
};
|
||||
if (_bufferCount < BUFFER_SIZE) _bufferCount++;
|
||||
}
|
||||
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
foreach (var r in GetComponentsInChildren<Renderer>(true))
|
||||
if (r != null) r.enabled = visible;
|
||||
if (_nameLabelObj != null) _nameLabelObj.SetActive(visible);
|
||||
if (_markerObj != null) _markerObj.SetActive(visible);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!_initialized) return;
|
||||
|
||||
// Re-enable colliders once grace window has elapsed.
|
||||
if (!_collidersReenabled && Time.time - SpawnTime > spawnGrace)
|
||||
{
|
||||
if (_allColliders != null)
|
||||
foreach (var c in _allColliders)
|
||||
if (c != null && !c.isTrigger) c.enabled = true;
|
||||
_collidersReenabled = true;
|
||||
}
|
||||
|
||||
if (_bufferCount == 0) return;
|
||||
|
||||
float renderTime = Time.time - interpolationDelay;
|
||||
int oldestIdx = (_newestIndex - _bufferCount + 1 + BUFFER_SIZE) % BUFFER_SIZE;
|
||||
|
||||
Snapshot older = default, newer = default;
|
||||
bool found = false;
|
||||
for (int i = 0; i < _bufferCount - 1; i++)
|
||||
{
|
||||
int a = (oldestIdx + i) % BUFFER_SIZE;
|
||||
int b = (oldestIdx + i + 1) % BUFFER_SIZE;
|
||||
if (_buffer[a].localTime <= renderTime && _buffer[b].localTime >= renderTime)
|
||||
{
|
||||
older = _buffer[a];
|
||||
newer = _buffer[b];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 targetPos;
|
||||
Quaternion targetRot;
|
||||
if (found)
|
||||
{
|
||||
float span = newer.localTime - older.localTime;
|
||||
float t = span > 0.001f ? (renderTime - older.localTime) / span : 1f;
|
||||
t = Mathf.Clamp01(t);
|
||||
targetPos = Vector3.Lerp(older.position, newer.position, t);
|
||||
targetRot = Quaternion.Slerp(older.rotation, newer.rotation, t);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newest = _buffer[_newestIndex];
|
||||
float elapsed = renderTime - newest.localTime;
|
||||
if (elapsed < 0)
|
||||
{
|
||||
targetPos = _buffer[oldestIdx].position;
|
||||
targetRot = _buffer[oldestIdx].rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
float extTime = Mathf.Min(elapsed, maxExtrapolation);
|
||||
float damp = 1f - Mathf.Clamp01(elapsed / (maxExtrapolation * 2f));
|
||||
targetPos = newest.position + newest.velocity * extTime * damp;
|
||||
targetRot = newest.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
float dist = Vector3.Distance(transform.position, targetPos);
|
||||
Vector3 newPos;
|
||||
if (dist > snapDistance)
|
||||
{
|
||||
newPos = targetPos;
|
||||
_currentRotation = targetRot;
|
||||
}
|
||||
else
|
||||
{
|
||||
float lerpT = 1f - Mathf.Exp(-smoothingSpeed * Time.deltaTime);
|
||||
newPos = Vector3.Lerp(transform.position, targetPos, lerpT);
|
||||
}
|
||||
float rotLerpT = 1f - Mathf.Exp(-rotationSpeed * Time.deltaTime);
|
||||
_currentRotation = Quaternion.Slerp(_currentRotation, targetRot, rotLerpT);
|
||||
|
||||
if (_rb != null)
|
||||
{
|
||||
_rb.MovePosition(newPos);
|
||||
_rb.MoveRotation(_currentRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.position = newPos;
|
||||
transform.rotation = _currentRotation;
|
||||
}
|
||||
|
||||
if (_nameLabelObj != null)
|
||||
{
|
||||
_nameLabelObj.transform.position = transform.position + Vector3.up * 2.8f;
|
||||
var cam = Camera.main;
|
||||
if (cam != null)
|
||||
{
|
||||
Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position;
|
||||
float camDist = lookDir.magnitude;
|
||||
lookDir.y = 0f;
|
||||
if (lookDir.sqrMagnitude > 0.001f)
|
||||
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
||||
float scale = Mathf.Clamp(camDist / 8f, 1f, 8f);
|
||||
_nameLabelObj.transform.localScale = Vector3.one * (0.1f * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildNameLabel(string playerName, Color color)
|
||||
{
|
||||
_nameLabelObj = new GameObject("NameLabel");
|
||||
_nameLabelObj.transform.position = transform.position + Vector3.up * 2.8f;
|
||||
_nameLabelObj.transform.localScale = Vector3.one * 0.1f;
|
||||
|
||||
_nameLabel = _nameLabelObj.AddComponent<TextMesh>();
|
||||
_nameLabel.text = playerName;
|
||||
_nameLabel.fontSize = 144;
|
||||
_nameLabel.characterSize = 0.15f;
|
||||
_nameLabel.anchor = TextAnchor.MiddleCenter;
|
||||
_nameLabel.alignment = TextAlignment.Center;
|
||||
_nameLabel.color = color;
|
||||
if (PlayerController.LabelFont != null) _nameLabel.font = PlayerController.LabelFont;
|
||||
|
||||
var mr = _nameLabel.GetComponent<MeshRenderer>();
|
||||
if (PlayerController.LabelFont != null && PlayerController.LabelFont.material != null)
|
||||
mr.material = PlayerController.LabelFont.material;
|
||||
else
|
||||
{
|
||||
var s = Shader.Find("GUI/Text Shader") ?? Shader.Find("Unlit/Texture");
|
||||
if (s != null) mr.material = new Material(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildColorMarker(Color color)
|
||||
{
|
||||
_markerObj = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||
_markerObj.name = "RemoteMarker";
|
||||
DestroyImmediate(_markerObj.GetComponent<Collider>());
|
||||
_markerObj.transform.SetParent(transform, false);
|
||||
_markerObj.transform.localPosition = new Vector3(0f, 2.4f, 0f);
|
||||
_markerObj.transform.localScale = new Vector3(0.25f, 0.4f, 0.25f);
|
||||
|
||||
var r = _markerObj.GetComponent<Renderer>();
|
||||
if (r != null)
|
||||
{
|
||||
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
|
||||
var mat = new Material(shader);
|
||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
||||
else mat.color = color;
|
||||
if (mat.HasProperty("_EmissionColor")) mat.SetColor("_EmissionColor", color * 0.6f);
|
||||
mat.EnableKeyword("_EMISSION");
|
||||
r.material = mat;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_nameLabelObj != null) Destroy(_nameLabelObj);
|
||||
if (_markerObj != null) Destroy(_markerObj);
|
||||
Debug.Log($"[RemoteVehicle] Destroyed: {PlayerName}");
|
||||
}
|
||||
}
|
||||
109
game/Assets/Scripts/Network/VehicleLocalSetup.cs
Normal file
109
game/Assets/Scripts/Network/VehicleLocalSetup.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using UnityEngine;
|
||||
using NWH.VehiclePhysics2;
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent of <c>PlayerController.SetupLocalPlayer</c> for the NWH vehicle local player.
|
||||
/// Attaches a floating name label, a colored marker above the car so other players can
|
||||
/// spot us at distance, and registers the vehicle's Rigidbody with the NetworkManager
|
||||
/// so it is broadcast over the wire.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(VehicleController))]
|
||||
public class VehicleLocalSetup : MonoBehaviour
|
||||
{
|
||||
private GameObject _nameLabelObj;
|
||||
private TextMesh _nameLabel;
|
||||
private GameObject _markerObj;
|
||||
private VehicleController _vehicle;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_vehicle = GetComponent<VehicleController>();
|
||||
}
|
||||
|
||||
public void SetupLocal(string playerName, Color playerColor)
|
||||
{
|
||||
if (_vehicle == null) _vehicle = GetComponent<VehicleController>();
|
||||
var rb = _vehicle.vehicleRigidbody != null ? _vehicle.vehicleRigidbody : GetComponent<Rigidbody>();
|
||||
|
||||
// Register with NetworkManager so it broadcasts position from THIS Rigidbody.
|
||||
if (NetworkManager.Instance != null)
|
||||
NetworkManager.Instance.RegisterLocalVehicle(transform, rb);
|
||||
|
||||
BuildNameLabel(playerName, playerColor);
|
||||
BuildColorMarker(playerColor);
|
||||
|
||||
Debug.Log($"[VehicleLocal] Setup complete: {playerName} color={playerColor}");
|
||||
}
|
||||
|
||||
private void BuildNameLabel(string playerName, Color color)
|
||||
{
|
||||
if (_nameLabelObj != null) Destroy(_nameLabelObj);
|
||||
_nameLabelObj = new GameObject("LocalNameLabel");
|
||||
_nameLabelObj.transform.SetParent(transform.parent, false);
|
||||
_nameLabelObj.transform.localScale = Vector3.one * 0.1f;
|
||||
|
||||
_nameLabel = _nameLabelObj.AddComponent<TextMesh>();
|
||||
_nameLabel.text = playerName;
|
||||
_nameLabel.fontSize = 144;
|
||||
_nameLabel.characterSize = 0.15f;
|
||||
_nameLabel.anchor = TextAnchor.MiddleCenter;
|
||||
_nameLabel.alignment = TextAlignment.Center;
|
||||
_nameLabel.color = color;
|
||||
if (PlayerController.LabelFont != null) _nameLabel.font = PlayerController.LabelFont;
|
||||
|
||||
var renderer = _nameLabel.GetComponent<MeshRenderer>();
|
||||
if (PlayerController.LabelFont != null && PlayerController.LabelFont.material != null)
|
||||
renderer.material = PlayerController.LabelFont.material;
|
||||
else
|
||||
{
|
||||
var textShader = Shader.Find("GUI/Text Shader") ?? Shader.Find("Unlit/Texture");
|
||||
if (textShader != null) renderer.material = new Material(textShader);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildColorMarker(Color color)
|
||||
{
|
||||
// Cone-ish marker above the car so other players can identify us at distance.
|
||||
if (_markerObj != null) Destroy(_markerObj);
|
||||
_markerObj = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||
_markerObj.name = "LocalMarker";
|
||||
DestroyImmediate(_markerObj.GetComponent<Collider>());
|
||||
_markerObj.transform.SetParent(transform, false);
|
||||
_markerObj.transform.localPosition = new Vector3(0f, 2.4f, 0f);
|
||||
_markerObj.transform.localScale = new Vector3(0.25f, 0.4f, 0.25f);
|
||||
|
||||
var r = _markerObj.GetComponent<Renderer>();
|
||||
if (r != null)
|
||||
{
|
||||
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
|
||||
var mat = new Material(shader);
|
||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
||||
else mat.color = color;
|
||||
if (mat.HasProperty("_EmissionColor")) mat.SetColor("_EmissionColor", color * 0.6f);
|
||||
mat.EnableKeyword("_EMISSION");
|
||||
r.material = mat;
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (_nameLabelObj != null)
|
||||
{
|
||||
_nameLabelObj.transform.position = transform.position + Vector3.up * 2.8f;
|
||||
var cam = Camera.main;
|
||||
if (cam != null)
|
||||
{
|
||||
Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position;
|
||||
lookDir.y = 0f;
|
||||
if (lookDir.sqrMagnitude > 0.001f)
|
||||
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_nameLabelObj != null) Destroy(_nameLabelObj);
|
||||
if (_markerObj != null) Destroy(_markerObj);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5bf5e078a2ee9ed4fa95eacab5753f3a
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d1f3d6aaca8e97498f40d827f7c5216
|
||||
@@ -4,56 +4,49 @@ using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks per-session and per-round player statistics and uploads them to the game server.
|
||||
/// All HTTP calls use UnityWebRequest coroutines (WebGL-safe, no async/await).
|
||||
/// Tracks player statistics and uploads them to the game server every 30s + on disconnect.
|
||||
/// No dependency on round events — works even if Colyseus callbacks are broken.
|
||||
/// </summary>
|
||||
public class StatsTracker : MonoBehaviour
|
||||
{
|
||||
public static StatsTracker Instance { get; private set; }
|
||||
|
||||
private const string SERVER_URL = "https://game.rolld.kerboul.me";
|
||||
private const string SERVER_URL = "https://game.rolld.kerboul.me";
|
||||
private const float SEND_INTERVAL = 30f;
|
||||
private const float MIN_SEND_INTERVAL = 6f; // juste au-dessus du rate-limit serveur (5s)
|
||||
|
||||
// Cumulative session stats (accumulate across rounds)
|
||||
// Cumulative stats
|
||||
private float _totalDistance;
|
||||
private int _totalJumps;
|
||||
private float _maxSpeed;
|
||||
private int _racesPlayed;
|
||||
private int _qualifications;
|
||||
private int _eliminations;
|
||||
private int _bumpsGiven;
|
||||
private float _totalPlaytime;
|
||||
|
||||
// Per-round deltas (reset after each send)
|
||||
private float _roundDistance;
|
||||
private float _roundMaxSpeed;
|
||||
// Playtime
|
||||
private float _sessionStart;
|
||||
private float _playtimeSentSoFar; // how much playtime we already sent
|
||||
|
||||
// Tracking
|
||||
private Vector3 _lastPos;
|
||||
private bool _trackingActive;
|
||||
private bool _tracking;
|
||||
private string _cachedName = "";
|
||||
private PlayerController _pc;
|
||||
private float _lastSentTime = -999f;
|
||||
|
||||
private Rigidbody _rb;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
_sessionStart = Time.time;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
_pc = GetComponent<PlayerController>();
|
||||
_rb = GetComponent<Rigidbody>();
|
||||
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null)
|
||||
{
|
||||
nm.OnRoundStart += OnRoundStart;
|
||||
nm.OnRoundEnd += OnRoundEnd;
|
||||
nm.OnQualified += OnQualified;
|
||||
nm.OnEliminated += OnEliminated;
|
||||
nm.OnConnected += OnConnected;
|
||||
nm.OnConnected += OnConnected;
|
||||
nm.OnDisconnected += OnDisconnected;
|
||||
}
|
||||
}
|
||||
@@ -63,123 +56,90 @@ public class StatsTracker : MonoBehaviour
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null)
|
||||
{
|
||||
nm.OnRoundStart -= OnRoundStart;
|
||||
nm.OnRoundEnd -= OnRoundEnd;
|
||||
nm.OnQualified -= OnQualified;
|
||||
nm.OnEliminated -= OnEliminated;
|
||||
nm.OnConnected -= OnConnected;
|
||||
nm.OnConnected -= OnConnected;
|
||||
nm.OnDisconnected -= OnDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!_trackingActive || _rb == null || _pc == null || !_pc.enabled) return;
|
||||
if (!_tracking || _rb == null) return;
|
||||
|
||||
Vector3 pos = transform.position;
|
||||
float delta = Vector3.Distance(pos, _lastPos);
|
||||
if (delta < 20f) // sanity cap against teleports
|
||||
{
|
||||
_roundDistance += delta;
|
||||
_totalDistance += delta;
|
||||
}
|
||||
Vector3 pos = transform.position;
|
||||
float delta = Vector3.Distance(pos, _lastPos);
|
||||
if (delta < 20f) // filtre téléportations
|
||||
_totalDistance += delta;
|
||||
_lastPos = pos;
|
||||
|
||||
float speed = _rb.linearVelocity.magnitude;
|
||||
if (speed > _roundMaxSpeed) _roundMaxSpeed = speed;
|
||||
if (speed > _maxSpeed) _maxSpeed = speed;
|
||||
if (speed > _maxSpeed) _maxSpeed = speed;
|
||||
}
|
||||
|
||||
// ─── Public hooks ────────────────────────────────────────────────────
|
||||
// ─── Public hooks ─────────────────────────────────────────────────────
|
||||
|
||||
public void RegisterJump()
|
||||
{
|
||||
_totalJumps++;
|
||||
}
|
||||
public void RegisterJump() => _totalJumps++;
|
||||
public void RegisterBump() => _bumpsGiven++;
|
||||
|
||||
public void RegisterBump()
|
||||
{
|
||||
_bumpsGiven++;
|
||||
}
|
||||
|
||||
// ─── Event handlers ──────────────────────────────────────────────────
|
||||
// ─── Connection events ────────────────────────────────────────────────
|
||||
|
||||
private void OnConnected()
|
||||
{
|
||||
_cachedName = NetworkManager.Instance?.LocalPlayerName ?? "";
|
||||
_lastPos = transform.position;
|
||||
_trackingActive = true;
|
||||
_cachedName = NetworkManager.Instance?.LocalPlayerName ?? "";
|
||||
_lastPos = transform.position;
|
||||
_sessionStart = Time.time;
|
||||
_tracking = true;
|
||||
StartCoroutine(PeriodicSend());
|
||||
}
|
||||
|
||||
private void OnDisconnected()
|
||||
{
|
||||
_trackingActive = false;
|
||||
_totalPlaytime += Time.time - _sessionStart;
|
||||
SendStats(); // best-effort on disconnect
|
||||
_tracking = false;
|
||||
StopAllCoroutines();
|
||||
SendStats(); // envoi final best-effort
|
||||
}
|
||||
|
||||
private void OnRoundStart(int round, string mode, int totalRounds)
|
||||
// ─── Periodic send ────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator PeriodicSend()
|
||||
{
|
||||
_racesPlayed++;
|
||||
_roundDistance = 0f;
|
||||
_roundMaxSpeed = 0f;
|
||||
_lastPos = transform.position;
|
||||
_trackingActive = true;
|
||||
while (_tracking)
|
||||
{
|
||||
yield return new WaitForSeconds(SEND_INTERVAL);
|
||||
if (_tracking) SendStats();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEnd(int round)
|
||||
{
|
||||
_trackingActive = false;
|
||||
SendStats();
|
||||
_roundDistance = 0f;
|
||||
_roundMaxSpeed = 0f;
|
||||
}
|
||||
|
||||
private void OnQualified(string sessionId)
|
||||
{
|
||||
if (sessionId == NetworkManager.Instance?.LocalSessionId)
|
||||
_qualifications++;
|
||||
}
|
||||
|
||||
private void OnEliminated(string sessionId, string reason)
|
||||
{
|
||||
if (sessionId == NetworkManager.Instance?.LocalSessionId)
|
||||
_eliminations++;
|
||||
}
|
||||
|
||||
// ─── HTTP send ───────────────────────────────────────────────────────
|
||||
// ─── HTTP send ────────────────────────────────────────────────────────
|
||||
|
||||
private void SendStats()
|
||||
{
|
||||
// Prefer live name, fall back to cached (useful on disconnect where name is cleared)
|
||||
if (Time.time - _lastSentTime < MIN_SEND_INTERVAL) return;
|
||||
var nm = NetworkManager.Instance;
|
||||
string name = (nm != null && !string.IsNullOrEmpty(nm.LocalPlayerName))
|
||||
? nm.LocalPlayerName
|
||||
: _cachedName;
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
_lastSentTime = Time.time;
|
||||
StartCoroutine(DoSendStats(name));
|
||||
}
|
||||
|
||||
private IEnumerator DoSendStats(string playerName)
|
||||
{
|
||||
_totalPlaytime += Time.time - _sessionStart;
|
||||
_sessionStart = Time.time;
|
||||
float now = Time.time;
|
||||
float sessionSecs = now - _sessionStart;
|
||||
float playtimeToSend = sessionSecs - _playtimeSentSoFar;
|
||||
_playtimeSentSoFar = sessionSecs;
|
||||
|
||||
var payload = new StatsPayload
|
||||
{
|
||||
name = playerName,
|
||||
name = playerName,
|
||||
stats = new StatsData
|
||||
{
|
||||
totalDistance = _totalDistance,
|
||||
totalJumps = _totalJumps,
|
||||
maxSpeed = _maxSpeed,
|
||||
|
||||
racesPlayed = _racesPlayed,
|
||||
qualifications = _qualifications,
|
||||
eliminations = _eliminations,
|
||||
|
||||
bumpsGiven = _bumpsGiven,
|
||||
totalPlaytime = _totalPlaytime,
|
||||
totalDistance = _totalDistance,
|
||||
totalJumps = _totalJumps,
|
||||
maxSpeed = _maxSpeed,
|
||||
bumpsGiven = _bumpsGiven,
|
||||
totalPlaytime = playtimeToSend,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -196,7 +156,7 @@ public class StatsTracker : MonoBehaviour
|
||||
if (req.result != UnityWebRequest.Result.Success)
|
||||
Debug.LogWarning($"[Stats] Upload failed: {req.error}");
|
||||
else
|
||||
Debug.Log($"[Stats] Uploaded for {playerName}");
|
||||
Debug.Log($"[Stats] Sent for {playerName} — dist:{_totalDistance:F0}m spd:{_maxSpeed:F1}m/s jumps:{_totalJumps}");
|
||||
}
|
||||
|
||||
// ─── DTOs ─────────────────────────────────────────────────────────────
|
||||
@@ -210,9 +170,6 @@ public class StatsTracker : MonoBehaviour
|
||||
public float totalDistance;
|
||||
public int totalJumps;
|
||||
public float maxSpeed;
|
||||
public int racesPlayed;
|
||||
public int qualifications;
|
||||
public int eliminations;
|
||||
public int bumpsGiven;
|
||||
public float totalPlaytime;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ public class ChatUI : MonoBehaviour
|
||||
_pollTimer = POLL_INTERVAL; // poll immediately
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
// Movement keys are handled by NWH InputSystemVehicleInputProvider via Input System;
|
||||
// no manual reset needed when chat opens (NWH polls Input System each frame).
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51e21afb9dba1904bb425ac1fae825cb
|
||||
@@ -101,108 +101,7 @@ public class GameHUD : MonoBehaviour
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (_phase == "lobby" && !_localRaceActive) return;
|
||||
|
||||
ImGuiSkin.EnsureReady();
|
||||
var nm = NetworkManager.Instance;
|
||||
|
||||
// ── Countdown (center, large) ─────────────────────────────────────
|
||||
if (_phase == "countdown" && _countdown > 0f)
|
||||
{
|
||||
float scale = 1f + _countdownPulse * 0.4f;
|
||||
float fontSize = 96f * scale;
|
||||
var countStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = Mathf.RoundToInt(fontSize),
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
countStyle.normal.textColor = new Color(1f, 0.85f, 0.1f, 1f);
|
||||
GUI.Label(new Rect(0, Screen.height * 0.3f, Screen.width, 120f),
|
||||
Mathf.CeilToInt(_countdown).ToString(), countStyle);
|
||||
|
||||
// "Préparez-vous !" label below
|
||||
var subStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 22,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
subStyle.normal.textColor = new Color(1f, 1f, 1f, 0.8f);
|
||||
string modeLabel = _gameMode switch {
|
||||
"race" => "COURSE",
|
||||
"survival" => "SURVIVAL",
|
||||
"teams" => "ÉQUIPES",
|
||||
_ => _gameMode.ToUpper()
|
||||
};
|
||||
GUI.Label(new Rect(0, Screen.height * 0.3f + 110f, Screen.width, 36f),
|
||||
$"— {modeLabel} —", subStyle);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Top-left: Round & Mode ─────────────────────────────────────────
|
||||
float panelX = 12f;
|
||||
float panelY = 12f;
|
||||
float panelW = 220f;
|
||||
float panelH = 70f;
|
||||
|
||||
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
||||
GUI.DrawTexture(new Rect(panelX, panelY, panelW, panelH), _bgTex);
|
||||
GUI.color = Color.white;
|
||||
|
||||
var roundStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleLeft,
|
||||
fontSize = 14,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
roundStyle.normal.textColor = new Color(1f, 0.85f, 0.1f);
|
||||
GUI.Label(new Rect(panelX + 8f, panelY + 4f, panelW - 16f, 28f),
|
||||
$"ROUND {_roundNumber} / {_totalRounds}", roundStyle);
|
||||
|
||||
var modeStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleLeft, fontSize = 12 };
|
||||
modeStyle.normal.textColor = new Color(0.7f, 0.7f, 0.85f);
|
||||
string modeFull = _gameMode switch {
|
||||
"race" => "COURSE", "survival" => "SURVIVAL", "teams" => "ÉQUIPES", _ => _gameMode.ToUpper()
|
||||
};
|
||||
GUI.Label(new Rect(panelX + 8f, panelY + 32f, panelW - 16f, 24f), modeFull, modeStyle);
|
||||
|
||||
// ── Top-right: Players alive ──────────────────────────────────────
|
||||
float prX = Screen.width - 180f;
|
||||
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
||||
GUI.DrawTexture(new Rect(prX, panelY, 168f, panelH), _bgTex);
|
||||
GUI.color = Color.white;
|
||||
|
||||
var aliveStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 28,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
aliveStyle.normal.textColor = new Color(0.3f, 1f, 0.5f);
|
||||
GUI.Label(new Rect(prX, panelY + 2f, 168f, 40f), $"{_cachedPlayersAlive}", aliveStyle);
|
||||
|
||||
var aliveLabel = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
|
||||
aliveLabel.normal.textColor = new Color(0.6f, 0.6f, 0.7f);
|
||||
GUI.Label(new Rect(prX, panelY + 40f, 168f, 22f), "joueurs en jeu", aliveLabel);
|
||||
|
||||
// ── Round timer (top center) ──────────────────────────────────────
|
||||
float displayTimer = _timerRunning ? _roundTimer : (_localRaceActive ? _localRaceTimer : -1f);
|
||||
if (displayTimer >= 0f)
|
||||
{
|
||||
int mins = Mathf.FloorToInt(displayTimer / 60f);
|
||||
int secs = Mathf.FloorToInt(displayTimer % 60f);
|
||||
var timerStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 18,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
timerStyle.normal.textColor = new Color(0.85f, 0.85f, 0.9f, 0.9f);
|
||||
GUI.Label(new Rect(Screen.width * 0.5f - 60f, panelY, 120f, 40f),
|
||||
$"{mins:00}:{secs:00}", timerStyle);
|
||||
}
|
||||
|
||||
// Race UI disabled — free-roam mode has no rounds/countdown/timer.
|
||||
}
|
||||
|
||||
// Static accessors for cross-script use
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba062aa6c92b140379dbc06b43dd3b9b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a0c9218a650547d98138cd835033977
|
||||
folderAsset: yes
|
||||
timeCreated: 1484670163
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -1,134 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 727a75301c3d24613a3ebcec4a24c2c8
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 11
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMasterTextureLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,654 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &1
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12004, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_PixelRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 45
|
||||
width: 1666
|
||||
height: 958
|
||||
m_ShowMode: 4
|
||||
m_Title:
|
||||
m_RootView: {fileID: 6}
|
||||
m_MinSize: {x: 950, y: 542}
|
||||
m_MaxSize: {x: 10000, y: 10000}
|
||||
--- !u!114 &2
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children: []
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 466
|
||||
width: 290
|
||||
height: 442
|
||||
m_MinSize: {x: 234, y: 271}
|
||||
m_MaxSize: {x: 10004, y: 10021}
|
||||
m_ActualView: {fileID: 14}
|
||||
m_Panes:
|
||||
- {fileID: 14}
|
||||
m_Selected: 0
|
||||
m_LastSelected: 0
|
||||
--- !u!114 &3
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children:
|
||||
- {fileID: 4}
|
||||
- {fileID: 2}
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 973
|
||||
y: 0
|
||||
width: 290
|
||||
height: 908
|
||||
m_MinSize: {x: 234, y: 492}
|
||||
m_MaxSize: {x: 10004, y: 14042}
|
||||
vertical: 1
|
||||
controlID: 226
|
||||
--- !u!114 &4
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children: []
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 290
|
||||
height: 466
|
||||
m_MinSize: {x: 204, y: 221}
|
||||
m_MaxSize: {x: 4004, y: 4021}
|
||||
m_ActualView: {fileID: 17}
|
||||
m_Panes:
|
||||
- {fileID: 17}
|
||||
m_Selected: 0
|
||||
m_LastSelected: 0
|
||||
--- !u!114 &5
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children: []
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 466
|
||||
width: 973
|
||||
height: 442
|
||||
m_MinSize: {x: 202, y: 221}
|
||||
m_MaxSize: {x: 4002, y: 4021}
|
||||
m_ActualView: {fileID: 15}
|
||||
m_Panes:
|
||||
- {fileID: 15}
|
||||
m_Selected: 0
|
||||
m_LastSelected: 0
|
||||
--- !u!114 &6
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12008, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children:
|
||||
- {fileID: 7}
|
||||
- {fileID: 8}
|
||||
- {fileID: 9}
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1666
|
||||
height: 958
|
||||
m_MinSize: {x: 950, y: 542}
|
||||
m_MaxSize: {x: 10000, y: 10000}
|
||||
--- !u!114 &7
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12011, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children: []
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1666
|
||||
height: 30
|
||||
m_MinSize: {x: 0, y: 0}
|
||||
m_MaxSize: {x: 0, y: 0}
|
||||
m_LastLoadedLayoutName: Tutorial
|
||||
--- !u!114 &8
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children:
|
||||
- {fileID: 10}
|
||||
- {fileID: 3}
|
||||
- {fileID: 11}
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 30
|
||||
width: 1666
|
||||
height: 908
|
||||
m_MinSize: {x: 713, y: 492}
|
||||
m_MaxSize: {x: 18008, y: 14042}
|
||||
vertical: 0
|
||||
controlID: 74
|
||||
--- !u!114 &9
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12042, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children: []
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 938
|
||||
width: 1666
|
||||
height: 20
|
||||
m_MinSize: {x: 0, y: 0}
|
||||
m_MaxSize: {x: 0, y: 0}
|
||||
--- !u!114 &10
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children:
|
||||
- {fileID: 12}
|
||||
- {fileID: 5}
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 973
|
||||
height: 908
|
||||
m_MinSize: {x: 202, y: 442}
|
||||
m_MaxSize: {x: 4002, y: 8042}
|
||||
vertical: 1
|
||||
controlID: 75
|
||||
--- !u!114 &11
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children: []
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 1263
|
||||
y: 0
|
||||
width: 403
|
||||
height: 908
|
||||
m_MinSize: {x: 277, y: 71}
|
||||
m_MaxSize: {x: 4002, y: 4021}
|
||||
m_ActualView: {fileID: 13}
|
||||
m_Panes:
|
||||
- {fileID: 13}
|
||||
m_Selected: 0
|
||||
m_LastSelected: 0
|
||||
--- !u!114 &12
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Children: []
|
||||
m_Position:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 973
|
||||
height: 466
|
||||
m_MinSize: {x: 202, y: 221}
|
||||
m_MaxSize: {x: 4002, y: 4021}
|
||||
m_ActualView: {fileID: 16}
|
||||
m_Panes:
|
||||
- {fileID: 16}
|
||||
m_Selected: 0
|
||||
m_LastSelected: 0
|
||||
--- !u!114 &13
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12019, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_AutoRepaintOnSceneChange: 0
|
||||
m_MinSize: {x: 275, y: 50}
|
||||
m_MaxSize: {x: 4000, y: 4000}
|
||||
m_TitleContent:
|
||||
m_Text: Inspector
|
||||
m_Image: {fileID: -6905738622615590433, guid: 0000000000000000d000000000000000,
|
||||
type: 0}
|
||||
m_Tooltip:
|
||||
m_DepthBufferBits: 0
|
||||
m_Pos:
|
||||
serializedVersion: 2
|
||||
x: 2
|
||||
y: 19
|
||||
width: 401
|
||||
height: 887
|
||||
m_ScrollPosition: {x: 0, y: 0}
|
||||
m_InspectorMode: 0
|
||||
m_PreviewResizer:
|
||||
m_CachedPref: -160
|
||||
m_ControlHash: -371814159
|
||||
m_PrefName: Preview_InspectorPreview
|
||||
m_PreviewWindow: {fileID: 0}
|
||||
--- !u!114 &14
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12014, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_AutoRepaintOnSceneChange: 0
|
||||
m_MinSize: {x: 230, y: 250}
|
||||
m_MaxSize: {x: 10000, y: 10000}
|
||||
m_TitleContent:
|
||||
m_Text: Project
|
||||
m_Image: {fileID: -7501376956915960154, guid: 0000000000000000d000000000000000,
|
||||
type: 0}
|
||||
m_Tooltip:
|
||||
m_DepthBufferBits: 0
|
||||
m_Pos:
|
||||
serializedVersion: 2
|
||||
x: 2
|
||||
y: 19
|
||||
width: 286
|
||||
height: 421
|
||||
m_SearchFilter:
|
||||
m_NameFilter:
|
||||
m_ClassNames: []
|
||||
m_AssetLabels: []
|
||||
m_AssetBundleNames: []
|
||||
m_VersionControlStates: []
|
||||
m_ReferencingInstanceIDs:
|
||||
m_ScenePaths: []
|
||||
m_ShowAllHits: 0
|
||||
m_SearchArea: 0
|
||||
m_Folders:
|
||||
- Assets
|
||||
m_ViewMode: 0
|
||||
m_StartGridSize: 64
|
||||
m_LastFolders:
|
||||
- Assets
|
||||
m_LastFoldersGridSize: -1
|
||||
m_LastProjectPath: /Users/danielbrauer/Unity Projects/New Unity Project 47
|
||||
m_IsLocked: 0
|
||||
m_FolderTreeState:
|
||||
scrollPos: {x: 0, y: 0}
|
||||
m_SelectedIDs: ee240000
|
||||
m_LastClickedID: 9454
|
||||
m_ExpandedIDs: ee24000000ca9a3bffffff7f
|
||||
m_RenameOverlay:
|
||||
m_UserAcceptedRename: 0
|
||||
m_Name:
|
||||
m_OriginalName:
|
||||
m_EditFieldRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 0
|
||||
height: 0
|
||||
m_UserData: 0
|
||||
m_IsWaitingForDelay: 0
|
||||
m_IsRenaming: 0
|
||||
m_OriginalEventType: 11
|
||||
m_IsRenamingFilename: 1
|
||||
m_ClientGUIView: {fileID: 0}
|
||||
m_SearchString:
|
||||
m_CreateAssetUtility:
|
||||
m_EndAction: {fileID: 0}
|
||||
m_InstanceID: 0
|
||||
m_Path:
|
||||
m_Icon: {fileID: 0}
|
||||
m_ResourceFile:
|
||||
m_AssetTreeState:
|
||||
scrollPos: {x: 0, y: 0}
|
||||
m_SelectedIDs: 68fbffff
|
||||
m_LastClickedID: 0
|
||||
m_ExpandedIDs: ee240000
|
||||
m_RenameOverlay:
|
||||
m_UserAcceptedRename: 0
|
||||
m_Name:
|
||||
m_OriginalName:
|
||||
m_EditFieldRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 0
|
||||
height: 0
|
||||
m_UserData: 0
|
||||
m_IsWaitingForDelay: 0
|
||||
m_IsRenaming: 0
|
||||
m_OriginalEventType: 11
|
||||
m_IsRenamingFilename: 1
|
||||
m_ClientGUIView: {fileID: 0}
|
||||
m_SearchString:
|
||||
m_CreateAssetUtility:
|
||||
m_EndAction: {fileID: 0}
|
||||
m_InstanceID: 0
|
||||
m_Path:
|
||||
m_Icon: {fileID: 0}
|
||||
m_ResourceFile:
|
||||
m_ListAreaState:
|
||||
m_SelectedInstanceIDs: 68fbffff
|
||||
m_LastClickedInstanceID: -1176
|
||||
m_HadKeyboardFocusLastEvent: 0
|
||||
m_ExpandedInstanceIDs: c6230000
|
||||
m_RenameOverlay:
|
||||
m_UserAcceptedRename: 0
|
||||
m_Name:
|
||||
m_OriginalName:
|
||||
m_EditFieldRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 0
|
||||
height: 0
|
||||
m_UserData: 0
|
||||
m_IsWaitingForDelay: 0
|
||||
m_IsRenaming: 0
|
||||
m_OriginalEventType: 11
|
||||
m_IsRenamingFilename: 1
|
||||
m_ClientGUIView: {fileID: 0}
|
||||
m_CreateAssetUtility:
|
||||
m_EndAction: {fileID: 0}
|
||||
m_InstanceID: 0
|
||||
m_Path:
|
||||
m_Icon: {fileID: 0}
|
||||
m_ResourceFile:
|
||||
m_NewAssetIndexInList: -1
|
||||
m_ScrollPosition: {x: 0, y: 0}
|
||||
m_GridSize: 64
|
||||
m_DirectoriesAreaWidth: 110
|
||||
--- !u!114 &15
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12015, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_AutoRepaintOnSceneChange: 1
|
||||
m_MinSize: {x: 200, y: 200}
|
||||
m_MaxSize: {x: 4000, y: 4000}
|
||||
m_TitleContent:
|
||||
m_Text: Game
|
||||
m_Image: {fileID: -2087823869225018852, guid: 0000000000000000d000000000000000,
|
||||
type: 0}
|
||||
m_Tooltip:
|
||||
m_DepthBufferBits: 32
|
||||
m_Pos:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 19
|
||||
width: 971
|
||||
height: 421
|
||||
m_MaximizeOnPlay: 0
|
||||
m_Gizmos: 0
|
||||
m_Stats: 0
|
||||
m_SelectedSizes: 00000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
m_TargetDisplay: 0
|
||||
m_ZoomArea:
|
||||
m_HRangeLocked: 0
|
||||
m_VRangeLocked: 0
|
||||
m_HBaseRangeMin: -242.75
|
||||
m_HBaseRangeMax: 242.75
|
||||
m_VBaseRangeMin: -101
|
||||
m_VBaseRangeMax: 101
|
||||
m_HAllowExceedBaseRangeMin: 1
|
||||
m_HAllowExceedBaseRangeMax: 1
|
||||
m_VAllowExceedBaseRangeMin: 1
|
||||
m_VAllowExceedBaseRangeMax: 1
|
||||
m_ScaleWithWindow: 0
|
||||
m_HSlider: 0
|
||||
m_VSlider: 0
|
||||
m_IgnoreScrollWheelUntilClicked: 0
|
||||
m_EnableMouseInput: 1
|
||||
m_EnableSliderZoom: 0
|
||||
m_UniformScale: 1
|
||||
m_UpDirection: 1
|
||||
m_DrawArea:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 17
|
||||
width: 971
|
||||
height: 404
|
||||
m_Scale: {x: 2, y: 2}
|
||||
m_Translation: {x: 485.5, y: 202}
|
||||
m_MarginLeft: 0
|
||||
m_MarginRight: 0
|
||||
m_MarginTop: 0
|
||||
m_MarginBottom: 0
|
||||
m_LastShownAreaInsideMargins:
|
||||
serializedVersion: 2
|
||||
x: -242.75
|
||||
y: -101
|
||||
width: 485.5
|
||||
height: 202
|
||||
m_MinimalGUI: 1
|
||||
m_defaultScale: 2
|
||||
m_TargetTexture: {fileID: 0}
|
||||
m_CurrentColorSpace: 0
|
||||
m_LastWindowPixelSize: {x: 1942, y: 842}
|
||||
m_ClearInEditMode: 1
|
||||
m_NoCameraWarning: 1
|
||||
m_LowResolutionForAspectRatios: 01000000000100000100
|
||||
--- !u!114 &16
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12013, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_AutoRepaintOnSceneChange: 1
|
||||
m_MinSize: {x: 200, y: 200}
|
||||
m_MaxSize: {x: 4000, y: 4000}
|
||||
m_TitleContent:
|
||||
m_Text: Scene
|
||||
m_Image: {fileID: 2318424515335265636, guid: 0000000000000000d000000000000000,
|
||||
type: 0}
|
||||
m_Tooltip:
|
||||
m_DepthBufferBits: 32
|
||||
m_Pos:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 19
|
||||
width: 971
|
||||
height: 445
|
||||
m_SceneLighting: 1
|
||||
lastFramingTime: 0
|
||||
m_2DMode: 0
|
||||
m_isRotationLocked: 0
|
||||
m_AudioPlay: 0
|
||||
m_Position:
|
||||
m_Target: {x: 0, y: 0, z: 0}
|
||||
speed: 2
|
||||
m_Value: {x: 0, y: 0, z: 0}
|
||||
m_RenderMode: 0
|
||||
m_ValidateTrueMetals: 0
|
||||
m_SceneViewState:
|
||||
showFog: 1
|
||||
showMaterialUpdate: 0
|
||||
showSkybox: 1
|
||||
showFlares: 1
|
||||
showImageEffects: 1
|
||||
grid:
|
||||
xGrid:
|
||||
m_Target: 0
|
||||
speed: 2
|
||||
m_Value: 0
|
||||
yGrid:
|
||||
m_Target: 1
|
||||
speed: 2
|
||||
m_Value: 1
|
||||
zGrid:
|
||||
m_Target: 0
|
||||
speed: 2
|
||||
m_Value: 0
|
||||
m_Rotation:
|
||||
m_Target: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226}
|
||||
speed: 2
|
||||
m_Value: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226}
|
||||
m_Size:
|
||||
m_Target: 10
|
||||
speed: 2
|
||||
m_Value: 10
|
||||
m_Ortho:
|
||||
m_Target: 0
|
||||
speed: 2
|
||||
m_Value: 0
|
||||
m_LastSceneViewRotation: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_LastSceneViewOrtho: 0
|
||||
m_ReplacementShader: {fileID: 0}
|
||||
m_ReplacementString:
|
||||
m_LastLockedObject: {fileID: 0}
|
||||
m_ViewIsLockedToObject: 0
|
||||
--- !u!114 &17
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 52
|
||||
m_PrefabParentObject: {fileID: 0}
|
||||
m_PrefabInternal: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 1
|
||||
m_Script: {fileID: 12061, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_AutoRepaintOnSceneChange: 0
|
||||
m_MinSize: {x: 200, y: 200}
|
||||
m_MaxSize: {x: 4000, y: 4000}
|
||||
m_TitleContent:
|
||||
m_Text: Hierarchy
|
||||
m_Image: {fileID: -590624980919486359, guid: 0000000000000000d000000000000000,
|
||||
type: 0}
|
||||
m_Tooltip:
|
||||
m_DepthBufferBits: 0
|
||||
m_Pos:
|
||||
serializedVersion: 2
|
||||
x: 2
|
||||
y: 19
|
||||
width: 286
|
||||
height: 445
|
||||
m_TreeViewState:
|
||||
scrollPos: {x: 0, y: 0}
|
||||
m_SelectedIDs: 68fbffff
|
||||
m_LastClickedID: -1176
|
||||
m_ExpandedIDs: 7efbffff00000000
|
||||
m_RenameOverlay:
|
||||
m_UserAcceptedRename: 0
|
||||
m_Name:
|
||||
m_OriginalName:
|
||||
m_EditFieldRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 0
|
||||
height: 0
|
||||
m_UserData: 0
|
||||
m_IsWaitingForDelay: 0
|
||||
m_IsRenaming: 0
|
||||
m_OriginalEventType: 11
|
||||
m_IsRenamingFilename: 0
|
||||
m_ClientGUIView: {fileID: 0}
|
||||
m_SearchString:
|
||||
m_ExpandedScenes:
|
||||
-
|
||||
m_CurrenRootInstanceID: 0
|
||||
m_Locked: 0
|
||||
m_CurrentSortingName: TransformSorting
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eabc9546105bf4accac1fd62a63e88e6
|
||||
timeCreated: 1487337779
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a9bcd70e6a4b4b05badaa72e827d8e0
|
||||
folderAsset: yes
|
||||
timeCreated: 1475835190
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ad9b87dffba344c89909c6d1b1c17e1
|
||||
folderAsset: yes
|
||||
timeCreated: 1475593892
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,242 +0,0 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
[CustomEditor(typeof(Readme))]
|
||||
[InitializeOnLoad]
|
||||
public class ReadmeEditor : Editor
|
||||
{
|
||||
static string s_ShowedReadmeSessionStateName = "ReadmeEditor.showedReadme";
|
||||
|
||||
static string s_ReadmeSourceDirectory = "Assets/TutorialInfo";
|
||||
|
||||
const float k_Space = 16f;
|
||||
|
||||
static ReadmeEditor()
|
||||
{
|
||||
EditorApplication.delayCall += SelectReadmeAutomatically;
|
||||
}
|
||||
|
||||
static void RemoveTutorial()
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Remove Readme Assets",
|
||||
|
||||
$"All contents under {s_ReadmeSourceDirectory} will be removed, are you sure you want to proceed?",
|
||||
"Proceed",
|
||||
"Cancel"))
|
||||
{
|
||||
if (Directory.Exists(s_ReadmeSourceDirectory))
|
||||
{
|
||||
FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory);
|
||||
FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory + ".meta");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Could not find the Readme folder at {s_ReadmeSourceDirectory}");
|
||||
}
|
||||
|
||||
var readmeAsset = SelectReadme();
|
||||
if (readmeAsset != null)
|
||||
{
|
||||
var path = AssetDatabase.GetAssetPath(readmeAsset);
|
||||
FileUtil.DeleteFileOrDirectory(path + ".meta");
|
||||
FileUtil.DeleteFileOrDirectory(path);
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
static void SelectReadmeAutomatically()
|
||||
{
|
||||
if (!SessionState.GetBool(s_ShowedReadmeSessionStateName, false))
|
||||
{
|
||||
var readme = SelectReadme();
|
||||
SessionState.SetBool(s_ShowedReadmeSessionStateName, true);
|
||||
|
||||
if (readme && !readme.loadedLayout)
|
||||
{
|
||||
LoadLayout();
|
||||
readme.loadedLayout = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void LoadLayout()
|
||||
{
|
||||
var assembly = typeof(EditorApplication).Assembly;
|
||||
var windowLayoutType = assembly.GetType("UnityEditor.WindowLayout", true);
|
||||
var method = windowLayoutType.GetMethod("LoadWindowLayout", BindingFlags.Public | BindingFlags.Static);
|
||||
method.Invoke(null, new object[] { Path.Combine(Application.dataPath, "TutorialInfo/Layout.wlt"), false });
|
||||
}
|
||||
|
||||
static Readme SelectReadme()
|
||||
{
|
||||
var ids = AssetDatabase.FindAssets("Readme t:Readme");
|
||||
if (ids.Length == 1)
|
||||
{
|
||||
var readmeObject = AssetDatabase.LoadMainAssetAtPath(AssetDatabase.GUIDToAssetPath(ids[0]));
|
||||
|
||||
Selection.objects = new UnityEngine.Object[] { readmeObject };
|
||||
|
||||
return (Readme)readmeObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Couldn't find a readme");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnHeaderGUI()
|
||||
{
|
||||
var readme = (Readme)target;
|
||||
Init();
|
||||
|
||||
var iconWidth = Mathf.Min(EditorGUIUtility.currentViewWidth / 3f - 20f, 128f);
|
||||
|
||||
GUILayout.BeginHorizontal("In BigTitle");
|
||||
{
|
||||
if (readme.icon != null)
|
||||
{
|
||||
GUILayout.Space(k_Space);
|
||||
GUILayout.Label(readme.icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth));
|
||||
}
|
||||
GUILayout.Space(k_Space);
|
||||
GUILayout.BeginVertical();
|
||||
{
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.Label(readme.title, TitleStyle);
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
var readme = (Readme)target;
|
||||
Init();
|
||||
|
||||
foreach (var section in readme.sections)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(section.heading))
|
||||
{
|
||||
GUILayout.Label(section.heading, HeadingStyle);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(section.text))
|
||||
{
|
||||
GUILayout.Label(section.text, BodyStyle);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(section.linkText))
|
||||
{
|
||||
if (LinkLabel(new GUIContent(section.linkText)))
|
||||
{
|
||||
Application.OpenURL(section.url);
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(k_Space);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Remove Readme Assets", ButtonStyle))
|
||||
{
|
||||
RemoveTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
bool m_Initialized;
|
||||
|
||||
GUIStyle LinkStyle
|
||||
{
|
||||
get { return m_LinkStyle; }
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
GUIStyle m_LinkStyle;
|
||||
|
||||
GUIStyle TitleStyle
|
||||
{
|
||||
get { return m_TitleStyle; }
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
GUIStyle m_TitleStyle;
|
||||
|
||||
GUIStyle HeadingStyle
|
||||
{
|
||||
get { return m_HeadingStyle; }
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
GUIStyle m_HeadingStyle;
|
||||
|
||||
GUIStyle BodyStyle
|
||||
{
|
||||
get { return m_BodyStyle; }
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
GUIStyle m_BodyStyle;
|
||||
|
||||
GUIStyle ButtonStyle
|
||||
{
|
||||
get { return m_ButtonStyle; }
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
GUIStyle m_ButtonStyle;
|
||||
|
||||
void Init()
|
||||
{
|
||||
if (m_Initialized)
|
||||
return;
|
||||
m_BodyStyle = new GUIStyle(EditorStyles.label);
|
||||
m_BodyStyle.wordWrap = true;
|
||||
m_BodyStyle.fontSize = 14;
|
||||
m_BodyStyle.richText = true;
|
||||
|
||||
m_TitleStyle = new GUIStyle(m_BodyStyle);
|
||||
m_TitleStyle.fontSize = 26;
|
||||
|
||||
m_HeadingStyle = new GUIStyle(m_BodyStyle);
|
||||
m_HeadingStyle.fontStyle = FontStyle.Bold;
|
||||
m_HeadingStyle.fontSize = 18;
|
||||
|
||||
m_LinkStyle = new GUIStyle(m_BodyStyle);
|
||||
m_LinkStyle.wordWrap = false;
|
||||
|
||||
// Match selection color which works nicely for both light and dark skins
|
||||
m_LinkStyle.normal.textColor = new Color(0x00 / 255f, 0x78 / 255f, 0xDA / 255f, 1f);
|
||||
m_LinkStyle.stretchWidth = false;
|
||||
|
||||
m_ButtonStyle = new GUIStyle(EditorStyles.miniButton);
|
||||
m_ButtonStyle.fontStyle = FontStyle.Bold;
|
||||
|
||||
m_Initialized = true;
|
||||
}
|
||||
|
||||
bool LinkLabel(GUIContent label, params GUILayoutOption[] options)
|
||||
{
|
||||
var position = GUILayoutUtility.GetRect(label, LinkStyle, options);
|
||||
|
||||
Handles.BeginGUI();
|
||||
Handles.color = LinkStyle.normal.textColor;
|
||||
Handles.DrawLine(new Vector3(position.xMin, position.yMax), new Vector3(position.xMax, position.yMax));
|
||||
Handles.color = Color.white;
|
||||
Handles.EndGUI();
|
||||
|
||||
EditorGUIUtility.AddCursorRect(position, MouseCursor.Link);
|
||||
|
||||
return GUI.Button(position, label, LinkStyle);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 476cc7d7cd9874016adc216baab94a0a
|
||||
timeCreated: 1484146680
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class Readme : ScriptableObject
|
||||
{
|
||||
public Texture2D icon;
|
||||
public string title;
|
||||
public Section[] sections;
|
||||
public bool loadedLayout;
|
||||
|
||||
[Serializable]
|
||||
public class Section
|
||||
{
|
||||
public string heading, text, linkText, url;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fcf7219bab7fe46a1ad266029b2fee19
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- icon: {instanceID: 0}
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: a186f8a87ca4f4d3aa864638ad5dfb65, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -4,7 +4,8 @@
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "node --watch src/index.js"
|
||||
"dev": "node --watch src/index.js",
|
||||
"schema:gen": "schema-codegen src/schema/GameState.js --csharp --namespace RolldSchema --output ../../game/Assets/Scripts/Network/Generated/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@colyseus/core": "^0.17.39",
|
||||
|
||||
@@ -56,8 +56,12 @@ const gameServer = new Server({
|
||||
|
||||
app.post('/stats/update', (req, res) => {
|
||||
const parsed = statsUpdateSchema.safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.issues });
|
||||
if (!parsed.success) {
|
||||
console.warn('[Stats] Bad update request:', JSON.stringify(parsed.error.issues));
|
||||
return res.status(400).json({ error: parsed.error.issues });
|
||||
}
|
||||
const ok = Stats.update(parsed.data.name, parsed.data.stats);
|
||||
console.log(`[Stats] Update for "${parsed.data.name}": ok=${ok}`, JSON.stringify(parsed.data.stats));
|
||||
res.json({ ok });
|
||||
});
|
||||
|
||||
|
||||
@@ -2,28 +2,27 @@ const { Room } = require("@colyseus/core");
|
||||
const { GameState, Player } = require("../schema/GameState");
|
||||
const Chat = require("../chat/ChatManager");
|
||||
|
||||
const LOBBY_TIMEOUT = 30;
|
||||
const COUNTDOWN_DURATION = 3;
|
||||
const ROUND_END_DURATION = 5;
|
||||
|
||||
const QUALIFY_RATIO = 0.6;
|
||||
// Free-roam: no rounds, no phases, no checkpoints. Players connect, move around, leave.
|
||||
// Schema fields (phase, countdown, roundNumber, etc.) are kept to preserve the
|
||||
// handshake — but state.phase is pinned to "playing" forever and other fields are
|
||||
// left at their default values. To fully drop them, regenerate the C# schema and
|
||||
// rebuild WebGL.
|
||||
|
||||
class ArenaRoom extends Room {
|
||||
maxClients = 20;
|
||||
|
||||
onCreate(options) {
|
||||
this.setState(new GameState());
|
||||
this.state.phase = "playing"; // pinned: free-roam, no state machine
|
||||
this.state.gameMode = "free";
|
||||
this.setPatchRate(16); // ~62.5 Hz
|
||||
this.setMetadata({ name: options?.roomName || ('Salle #' + this.roomId.substring(0, 6)) });
|
||||
|
||||
this._phaseTimer = null;
|
||||
this._lobbyTimer = null;
|
||||
|
||||
console.log(`[ArenaRoom] Room ${this.roomId} created`);
|
||||
console.log(`[ArenaRoom] Room ${this.roomId} created (free-roam)`);
|
||||
|
||||
this.onMessage("position", (client, data) => {
|
||||
const player = this.state.players.get(client.sessionId);
|
||||
if (!player || player.isEliminated) return;
|
||||
if (!player) return;
|
||||
player.x = data.x ?? player.x;
|
||||
player.y = data.y ?? player.y;
|
||||
player.z = data.z ?? player.z;
|
||||
@@ -40,14 +39,6 @@ class ArenaRoom extends Room {
|
||||
player.t = Date.now();
|
||||
});
|
||||
|
||||
this.onMessage("ready", (client) => {
|
||||
const player = this.state.players.get(client.sessionId);
|
||||
if (!player || this.state.phase !== "lobby") return;
|
||||
player.isReady = true;
|
||||
console.log(`[ArenaRoom] ${client.sessionId} ready`);
|
||||
this._checkAllReady();
|
||||
});
|
||||
|
||||
this.onMessage("chat", (client, data) => {
|
||||
const player = this.state.players.get(client.sessionId);
|
||||
if (!player || !data.text) return;
|
||||
@@ -55,18 +46,9 @@ class ArenaRoom extends Room {
|
||||
if (msg) this.broadcast("chat", msg);
|
||||
});
|
||||
|
||||
this.onMessage("checkpointReached", (client, data) => {
|
||||
if (this.state.phase !== "playing") return;
|
||||
const player = this.state.players.get(client.sessionId);
|
||||
if (!player || player.isEliminated || player.isQualified) return;
|
||||
const expected = player.checkpointIndex;
|
||||
if (data.index !== expected) return;
|
||||
player.checkpointIndex = data.index + 1;
|
||||
const TOTAL_CHECKPOINTS = 5;
|
||||
if (player.checkpointIndex >= TOTAL_CHECKPOINTS) {
|
||||
this._qualifyPlayer(client.sessionId, "finish");
|
||||
}
|
||||
});
|
||||
// Accept legacy "ready" / "checkpointReached" silently so old clients don't error.
|
||||
this.onMessage("ready", () => {});
|
||||
this.onMessage("checkpointReached", () => {});
|
||||
}
|
||||
|
||||
onJoin(client, options) {
|
||||
@@ -82,199 +64,22 @@ class ArenaRoom extends Room {
|
||||
player.z = spawn.z;
|
||||
player.t = Date.now();
|
||||
this.state.players.set(client.sessionId, player);
|
||||
this._updatePlayersAlive();
|
||||
|
||||
if (this.state.players.size === 1 && this.state.phase === "lobby") {
|
||||
this._startLobbyTimer();
|
||||
}
|
||||
}
|
||||
|
||||
onLeave(client, consented) {
|
||||
console.log(`[ArenaRoom] ${client.sessionId} left`);
|
||||
this.state.players.delete(client.sessionId);
|
||||
this._updatePlayersAlive();
|
||||
if (this.state.phase === "playing") {
|
||||
this._checkRoundEndCondition();
|
||||
}
|
||||
}
|
||||
|
||||
onDispose() {
|
||||
this._clearAllTimers();
|
||||
console.log(`[ArenaRoom] Room ${this.roomId} disposed`);
|
||||
}
|
||||
|
||||
// ─── Phase transitions ──────────────────────────────────────────────
|
||||
|
||||
_startLobbyTimer() {
|
||||
if (this._lobbyTimer) return;
|
||||
this._lobbyTimer = setTimeout(() => this._startCountdown(), LOBBY_TIMEOUT * 1000);
|
||||
console.log(`[ArenaRoom] Lobby timer started (${LOBBY_TIMEOUT}s)`);
|
||||
}
|
||||
|
||||
_checkAllReady() {
|
||||
if (this.state.players.size < 2) return;
|
||||
let allReady = true;
|
||||
this.state.players.forEach((p) => { if (!p.isReady) allReady = false; });
|
||||
if (allReady) {
|
||||
clearTimeout(this._lobbyTimer);
|
||||
this._lobbyTimer = null;
|
||||
this._startCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
_startCountdown() {
|
||||
if (this.state.phase !== "lobby") return;
|
||||
this.state.phase = "countdown";
|
||||
this.state.countdown = COUNTDOWN_DURATION;
|
||||
console.log(`[ArenaRoom] Countdown started`);
|
||||
|
||||
const tick = () => {
|
||||
this.state.countdown -= 1;
|
||||
if (this.state.countdown <= 0) {
|
||||
this._startPlaying();
|
||||
} else {
|
||||
this._phaseTimer = setTimeout(tick, 1000);
|
||||
}
|
||||
};
|
||||
this._phaseTimer = setTimeout(tick, 1000);
|
||||
}
|
||||
|
||||
_startPlaying() {
|
||||
this.state.gameMode = "race";
|
||||
this.state.phase = "playing";
|
||||
this.state.countdown = 0;
|
||||
|
||||
this.state.players.forEach((p) => {
|
||||
p.isEliminated = false;
|
||||
p.isQualified = false;
|
||||
p.isReady = false;
|
||||
p.checkpointIndex = 0;
|
||||
});
|
||||
|
||||
this._updatePlayersAlive();
|
||||
|
||||
this.broadcast("roundStart", {
|
||||
round: this.state.roundNumber,
|
||||
mode: this.state.gameMode,
|
||||
totalRounds: this.state.totalRounds,
|
||||
});
|
||||
|
||||
console.log(`[ArenaRoom] Round ${this.state.roundNumber} started (race)`);
|
||||
|
||||
}
|
||||
|
||||
_endRound() {
|
||||
if (this.state.phase !== "playing") return;
|
||||
this._clearAllTimers();
|
||||
this.state.phase = "roundEnd";
|
||||
this.broadcast("roundEnd", { round: this.state.roundNumber });
|
||||
console.log(`[ArenaRoom] Round ${this.state.roundNumber} ended`);
|
||||
|
||||
if (this.state.roundNumber >= this.state.totalRounds) {
|
||||
this._phaseTimer = setTimeout(() => this._endGame(), ROUND_END_DURATION * 1000);
|
||||
} else {
|
||||
this._phaseTimer = setTimeout(() => this._nextRound(), ROUND_END_DURATION * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
_nextRound() {
|
||||
this.state.roundNumber += 1;
|
||||
this.state.phase = "lobby";
|
||||
this.state.players.forEach((p) => {
|
||||
p.isReady = false;
|
||||
const spawn = this._findSpawnPosition();
|
||||
p.x = spawn.x; p.y = spawn.y; p.z = spawn.z;
|
||||
});
|
||||
this._updatePlayersAlive();
|
||||
this._lobbyTimer = null;
|
||||
this._startLobbyTimer();
|
||||
console.log(`[ArenaRoom] Lobby for round ${this.state.roundNumber}`);
|
||||
}
|
||||
|
||||
_endGame() {
|
||||
this.state.phase = "gameEnd";
|
||||
let winner = "";
|
||||
let best = -1;
|
||||
this.state.players.forEach((p) => {
|
||||
const score = p.isQualified ? 1000 : p.checkpointIndex;
|
||||
if (score > best) { best = score; winner = p.name; }
|
||||
});
|
||||
this.state.winnerName = winner;
|
||||
this.broadcast("gameEnd", { winner });
|
||||
console.log(`[ArenaRoom] Game over — winner: ${winner}`);
|
||||
}
|
||||
|
||||
// ─── Elimination helpers ─────────────────────────────────────────────
|
||||
|
||||
_eliminatePlayer(sessionId, reason) {
|
||||
const player = this.state.players.get(sessionId);
|
||||
if (!player || player.isEliminated || player.isQualified) return;
|
||||
player.isEliminated = true;
|
||||
this._updatePlayersAlive();
|
||||
this.broadcast("eliminated", { sessionId, name: player.name, reason });
|
||||
console.log(`[ArenaRoom] ${player.name} (${sessionId}) eliminated: ${reason}`);
|
||||
this._checkRoundEndCondition();
|
||||
}
|
||||
|
||||
_qualifyPlayer(sessionId, reason) {
|
||||
const player = this.state.players.get(sessionId);
|
||||
if (!player || player.isQualified || player.isEliminated) return;
|
||||
player.isQualified = true;
|
||||
this._updatePlayersAlive();
|
||||
this.broadcast("qualified", { sessionId, name: player.name });
|
||||
console.log(`[ArenaRoom] ${player.name} (${sessionId}) qualified: ${reason}`);
|
||||
|
||||
const totalActive = this._getActiveCount();
|
||||
const qualifiedCount = this._getQualifiedCount();
|
||||
const toQualify = Math.ceil(totalActive * QUALIFY_RATIO);
|
||||
if (qualifiedCount >= toQualify) {
|
||||
this.state.players.forEach((p, id) => {
|
||||
if (!p.isQualified && !p.isEliminated) {
|
||||
this._eliminatePlayer(id, "too_slow");
|
||||
}
|
||||
});
|
||||
this._endRound();
|
||||
}
|
||||
}
|
||||
|
||||
_checkRoundEndCondition() {
|
||||
if (this.state.phase !== "playing") return;
|
||||
const alive = this._getAliveCount();
|
||||
if (alive === 0) this._endRound();
|
||||
}
|
||||
|
||||
_getAliveCount() {
|
||||
let n = 0;
|
||||
this.state.players.forEach((p) => { if (!p.isEliminated && !p.isQualified) n++; });
|
||||
return n;
|
||||
}
|
||||
|
||||
_getQualifiedCount() {
|
||||
let n = 0;
|
||||
this.state.players.forEach((p) => { if (p.isQualified) n++; });
|
||||
return n;
|
||||
}
|
||||
|
||||
_getActiveCount() {
|
||||
let n = 0;
|
||||
this.state.players.forEach((p) => { if (!p.isEliminated) n++; });
|
||||
return n;
|
||||
}
|
||||
|
||||
_updatePlayersAlive() {
|
||||
this.state.playersAlive = this._getAliveCount();
|
||||
}
|
||||
|
||||
_clearAllTimers() {
|
||||
if (this._phaseTimer) { clearTimeout(this._phaseTimer); this._phaseTimer = null; }
|
||||
if (this._lobbyTimer) { clearTimeout(this._lobbyTimer); this._lobbyTimer = null; }
|
||||
}
|
||||
|
||||
// ─── Spawn helper ────────────────────────────────────────────────────
|
||||
|
||||
_findSpawnPosition() {
|
||||
const MIN_DIST = 3.0;
|
||||
const SPAWN_Y = 5;
|
||||
const MIN_DIST = 5.0;
|
||||
const SPAWN_Y = 1.5;
|
||||
const RANGE = 20;
|
||||
const existing = [];
|
||||
this.state.players.forEach((p) => existing.push({ x: p.x, z: p.z }));
|
||||
@@ -285,7 +90,7 @@ class ArenaRoom extends Room {
|
||||
|
||||
let best = { x: 0, y: SPAWN_Y, z: 0 };
|
||||
let bestDist = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const cx = (Math.random() - 0.5) * RANGE;
|
||||
const cz = (Math.random() - 0.5) * RANGE;
|
||||
let minD = Infinity;
|
||||
|
||||
Reference in New Issue
Block a user