Jump/JumpGame.cs
using System;
/// <summary>
/// 跳一跳主逻辑:平台生成、蓄力跳跃、落点判定与计分。
/// GameObjectSystem 会在场景加载时自动实例化(无需挂载到场景),
/// 但编辑器的编辑模式下也会 tick,所以所有逻辑都用 Game.IsPlaying 拦截。
/// 整个世界在运行时生成,不依赖场景里已有的内容(模板对象会被屏蔽)。
/// </summary>
public sealed class JumpGame : GameObjectSystem<JumpGame>
{
// ---- 手感参数(可在 项目设置 → Systems 里覆盖)----
/// <summary> 蓄力从 0 到 1 需要的秒数 </summary>
[Property] public float ChargeTime { get; set; } = 2.0f;
/// <summary> 轻点时的跳跃距离 </summary>
[Property] public float MinJumpDistance { get; set; } = 130f;
/// <summary> 蓄满时的跳跃距离 </summary>
[Property] public float MaxJumpDistance { get; set; } = 780f;
/// <summary> 落点距平台中心小于该值算"完美",触发连击加分 </summary>
[Property] public float PerfectRadius { get; set; } = 20f;
/// <summary> 平台顶面半宽的随机范围 </summary>
[Property] public Vector2 PlatformHalfSize { get; set; } = new Vector2( 48, 90 );
/// <summary> 相邻平台中心距离的随机范围 </summary>
[Property] public Vector2 CenterDistance { get; set; } = new Vector2( 250, 470 );
/// <summary> 每一步方向的随机偏转角(度,左右交替施加) </summary>
[Property] public Vector2 TurnAngle { get; set; } = new Vector2( 14, 34 );
// ---- 常量 ----
const float KillDepth = 420f; // 低于附近平台顶面多少算坠落出局
const float Gravity = 2400f; // 自由落体阶段的重力(仅用于坠落表现)
static readonly Color[] PlatformColors =
{
new Color( 0.98f, 0.74f, 0.62f ),
new Color( 0.64f, 0.86f, 0.74f ),
new Color( 0.99f, 0.88f, 0.58f ),
new Color( 0.66f, 0.80f, 0.95f ),
new Color( 0.93f, 0.77f, 0.90f ),
new Color( 0.96f, 0.96f, 0.94f ),
};
// ---- 对外状态(HUD / 相机读取)----
public int Score { get; private set; }
public int Streak { get; private set; }
public bool HasJumped { get; private set; }
public bool IsGameOver { get; private set; }
public bool IsFalling => _falling;
public bool IsTutorial => _tutorialActive;
/// <summary> 当前难度(开始菜单选择) </summary>
public JumpDifficulty Difficulty { get; private set; } = JumpDifficulty.Normal;
/// <summary> 是否在开始菜单(背景由 AI 自动演示跳跃) </summary>
public bool InMenu => _menuMode;
/// <summary> 新手演示字幕(非空时 HUD 显示) </summary>
public string TutorialText { get; private set; }
public Vector3 PiecePosition { get; private set; }
// ---- 内部状态 ----
GameObject _root;
JumpPiece _piece;
JumpCamera _camera;
readonly List<JumpPlatform> _platforms = new();
readonly List<(GameObject Go, TimeSince Born, Vector3 Scale)> _dots = new();
static GameObject _sharedRoot; // 热重载/重建时复用同一个世界根,避免堆叠多套场景
JumpPlatform _current;
JumpPlatform _target;
bool _worldReady;
bool _wasDown;
bool _charging;
float _power;
float _dirAngle;
float _turnSide = 1f;
bool _menuMode = true; // 开局进入开始菜单,背景 AI 自动跳
// 简单模式辅助线(蓄力时显示预测弹道与落点环)
const int AssistDotCount = 14;
static readonly Color AssistHitColor = new Color( 0.30f, 0.78f, 0.42f );
static readonly Color AssistMissColor = new Color( 0.92f, 0.32f, 0.28f );
readonly List<GameObject> _assistDots = new();
GameObject _assistRing;
ModelRenderer _assistRingRenderer;
bool _assistVisible;
static readonly Model BoxModel = Model.Load( "models/dev/box.vmdl" );
// 新手演示
bool _tutorialActive;
bool _tutorialDone; // 每个 play 会话只演示一次,重开不再播
bool _waitingForInput; // 开局等待期:玩家 3 秒内按下则跳过演示,闲置则播放
TimeSince _sinceSpawn;
float _demoPower;
TimeSince _tutorialT;
TimeSince _sinceTutorialEnd;
TimeSince _dotTimer;
static readonly Model SphereModel = Model.Load( "models/dev/sphere.vmdl" );
// 飞行状态
bool _flying;
bool _falling;
Vector3 _jumpFrom;
Vector3 _jumpTo;
float _jumpTime;
float _jumpDuration;
float _jumpHeight;
float _prevBaseZ;
float _killZ;
Vector3 _fallVelocity;
public JumpGame( Scene scene ) : base( scene )
{
Listen( Stage.UpdateBones, 0, Tick, "JumpGame.Tick" );
}
void Tick()
{
if ( !Game.IsPlaying ) return;
EnsureWorld();
var down = Input.Down( "Attack1" ) || Input.Down( "Jump" );
// Q:从游戏 / 结算画面返回开始菜单
if ( !InMenu && Input.Pressed( "Menu" ) )
{
EnterMenu();
return;
}
if ( IsGameOver )
{
if ( down && !_wasDown ) ResetGame();
_wasDown = down;
return;
}
if ( _piece.IsValid() ) PiecePosition = _piece.BasePosition;
TickTrailDots();
UpdateAssistLine();
if ( _flying )
{
TickFlight( Time.Delta );
_wasDown = down;
return;
}
// 开始菜单背景 / 新手演示:AI 自动表演"蓄力→起跳→落点"
if ( _menuMode || _tutorialActive )
{
// 菜单里支持键盘选难度(数字键 1/2/3)
if ( _menuMode )
{
if ( Input.Pressed( "Slot1" ) ) { StartGame( JumpDifficulty.Easy ); return; }
if ( Input.Pressed( "Slot2" ) ) { StartGame( JumpDifficulty.Normal ); return; }
if ( Input.Pressed( "Slot3" ) ) { StartGame( JumpDifficulty.Hard ); return; }
}
TickAutoJump( Time.Delta );
_wasDown = false;
return;
}
if ( TutorialText != null && _sinceTutorialEnd > 2.4f ) TutorialText = null;
// 开局 3 秒观察窗:玩家自己按下 → 跳过演示;一直闲置 → 播放演示
if ( _waitingForInput )
{
if ( down && !_wasDown )
{
_waitingForInput = false;
_tutorialDone = true;
}
else if ( _sinceSpawn > 3f )
{
_waitingForInput = false;
StartTutorial();
return;
}
}
// 地面状态:按下蓄力,松开起跳
if ( down && !_wasDown )
{
_charging = true;
_power = 0f;
}
if ( _charging )
{
if ( down )
{
_power = Math.Min( _power + Time.Delta / ChargeTime, 1f );
_piece?.SetCharge( _power );
}
else
{
_charging = false;
Launch();
}
}
_wasDown = down;
}
// ---- 世界搭建 ----
void EnsureWorld()
{
if ( _worldReady ) return;
_worldReady = true;
// 复用旧的世界根(热重载等场景下避免堆叠多套平台/相机),并清掉其动态子物体。
// 注意:Play 停止后 static 引用可能指向已卸载的旧场景对象(IsValid 仍为真),
// 必须校验场景归属,否则向旧场景根挂子物体直接断言失败
if ( _sharedRoot.IsValid() && _sharedRoot.Scene == Scene )
{
_root = _sharedRoot;
foreach ( var child in _root.Children.ToArray() )
{
if ( child.IsValid() ) child.Destroy();
}
}
else
{
_sharedRoot = null;
_root = new GameObject( true, "JumpGame" );
_sharedRoot = _root;
// 屏蔽场景里模板留下的所有对象(示例相机、方块等),只跑我们生成的世界。
// Scene 本身就是根 GameObject,顶层对象都在它的 Children 里
foreach ( var go in Scene.Children )
{
if ( go.IsValid() && go != _root ) go.Enabled = false;
}
}
CreateLight();
CreateCamera();
CreateHud();
BuildInitialWorld();
}
void CreateLight()
{
var sun = new GameObject( true, "Sun" );
sun.Parent = _root;
sun.WorldRotation = Rotation.LookAt( new Vector3( -0.5f, -0.6f, -1.1f ).Normal );
var light = sun.AddComponent<DirectionalLight>();
light.LightColor = new Color( 1f, 0.96f, 0.90f );
light.Shadows = true;
light.ShadowCascadeCount = 2;
var ambientGo = new GameObject( true, "Ambient" );
ambientGo.Parent = _root;
ambientGo.AddComponent<AmbientLight>().Color = new Color( 0.48f, 0.52f, 0.60f );
}
void CreateCamera()
{
var go = new GameObject( true, "JumpCamera" );
go.Parent = _root;
var cam = go.AddComponent<CameraComponent>();
cam.IsMainCamera = true;
cam.Priority = -100;
cam.FieldOfView = 55f;
cam.BackgroundColor = new Color( 0.86f, 0.89f, 0.93f );
_camera = go.AddComponent<JumpCamera>();
}
void CreateHud()
{
var go = new GameObject( true, "Hud" );
go.Parent = _root;
go.AddComponent<ScreenPanel>();
go.AddComponent<JumpHud>();
}
void BuildInitialWorld()
{
Score = 0;
Streak = 0;
HasJumped = false;
IsGameOver = false;
_charging = false;
_power = 0f;
_flying = false;
_falling = false;
_dirAngle = 0f;
foreach ( var p in _platforms ) p.DestroyAll();
_platforms.Clear();
_target = null;
if ( _piece.IsValid() ) _piece.GameObject.Destroy();
var first = CreatePlatform( Vector2.Zero, 80f, 80f, 100f, PlatformColors[0] );
_current = first;
var pieceGo = new GameObject( true, "Piece" );
pieceGo.Parent = _root;
_piece = pieceGo.AddComponent<JumpPiece>();
_piece.Init( first );
SpawnNextPlatform();
_camera?.SnapTo( PiecePosition = _piece.BasePosition );
// 菜单背景演示 / 开局引导(本会话第一次进入时先等 3 秒:
// 玩家 3 秒内自己按下 → 跳过演示直接玩;一直闲置 → 自动播放演示)
if ( _menuMode )
{
_waitingForInput = false;
TutorialText = null;
_tutorialT = 0;
_demoPower = ComputeDemoPower();
}
else if ( _tutorialDone )
{
TutorialText = null;
_waitingForInput = false;
}
else
{
_waitingForInput = true;
_sinceSpawn = 0;
TutorialText = null;
}
}
/// <summary> 计算让棋子正好落在目标平台中心的蓄力值 </summary>
float ComputeDemoPower()
{
if ( !_target.IsValid() || !_piece.IsValid() ) return 0.35f;
var dx = _target.CenterX - _piece.BasePosition.x;
var dy = _target.CenterY - _piece.BasePosition.y;
var dist = MathF.Sqrt( dx * dx + dy * dy );
return Math.Clamp( ( dist - MinJumpDistance ) / ( MaxJumpDistance - MinJumpDistance ), 0.05f, 1f );
}
/// <summary> 进入开始菜单(背景 AI 演示) </summary>
public void EnterMenu()
{
_menuMode = true;
_tutorialActive = false;
ResetGame();
}
/// <summary> 从开始菜单选择难度进入正式游戏 </summary>
public void StartGame( JumpDifficulty difficulty )
{
Difficulty = difficulty;
_menuMode = false;
_tutorialActive = false;
ResetGame();
Sound.Play( "sounds/kenney/ui/ui.navigate.forward.sound", PiecePosition, 0f );
}
/// <summary> 开局演示:AI 自动表演一次蓄力跳跃,落点对准目标中心 </summary>
void StartTutorial()
{
_tutorialDone = true;
_tutorialActive = true;
_tutorialT = 0;
TutorialText = "Watch! Hold to charge, release to jump";
_demoPower = ComputeDemoPower();
}
void TickAutoJump( float dt )
{
var t = (float)_tutorialT;
if ( t < 0.9f )
{
// 先停顿一拍,让玩家看清开局
}
else if ( t < 1.7f )
{
// 模拟蓄力:力度爬升到"刚好能落在中心"的值
_power = Math.Min( ( t - 0.9f ) / 0.8f, 1f ) * _demoPower;
_piece?.SetCharge( _power );
}
else
{
Launch();
}
}
/// <summary> 演示跳跃的轨迹点:棋子飞过的地方留下小圆点并渐渐消散 </summary>
void SpawnTrailDot( Vector3 basePos )
{
var go = new GameObject( true, "TrailDot" );
go.Parent = _root;
var r = go.AddComponent<ModelRenderer>();
r.Model = SphereModel;
r.Tint = new Color( 0.25f, 0.28f, 0.34f );
var size = SphereModel.Bounds.Size.x;
var scale = 14f / size;
var dotScale = new Vector3( scale, scale, scale );
go.LocalScale = dotScale;
go.WorldPosition = basePos + Vector3.Up * ( SphereModel.Bounds.Size.z * scale * 0.5f + 2f );
_dots.Add( ( go, 0, dotScale ) );
}
void TickTrailDots()
{
for ( int i = _dots.Count - 1; i >= 0; i-- )
{
var dot = _dots[i];
var age = (float)dot.Born;
if ( age >= 0.9f || !dot.Go.IsValid() )
{
if ( dot.Go.IsValid() ) dot.Go.Destroy();
_dots.RemoveAt( i );
continue;
}
dot.Go.LocalScale = dot.Scale * ( 1f - age / 0.9f );
}
}
void ResetGame()
{
BuildInitialWorld();
}
JumpPlatform CreatePlatform( Vector2 center, float halfX, float halfY, float topZ, Color color )
{
var go = new GameObject( true, "Platform" );
go.Parent = _root;
var platform = go.AddComponent<JumpPlatform>();
platform.Init( center, halfX, halfY, topZ, color );
_platforms.Add( platform );
return platform;
}
/// <summary>
/// 在当前平台前方生成下一块。左右严格交替、相对主轴(+X)定角(不累加),
/// 保证路线像原版一样蛇形前进、下一块平台始终留在画面内。
/// </summary>
void SpawnNextPlatform()
{
var prev = _current;
_turnSide = -_turnSide;
_dirAngle = _turnSide * Game.Random.Float( TurnAngle.x, TurnAngle.y ) * MathF.PI / 180f;
var dist = Game.Random.Float( CenterDistance.x, CenterDistance.y );
var px = prev.CenterX + MathF.Cos( _dirAngle ) * dist;
var py = prev.CenterY + MathF.Sin( _dirAngle ) * dist;
var halfX = Game.Random.Float( PlatformHalfSize.x, PlatformHalfSize.y );
var halfY = Game.Random.Float( PlatformHalfSize.x, PlatformHalfSize.y );
var topZ = Math.Clamp( prev.TopZ + Game.Random.Float( -50f, 60f ), 40f, 210f );
if ( _target.IsValid() ) _target.IsTarget = false;
_target = CreatePlatform( new Vector2( px, py ), halfX, halfY, topZ, Game.Random.FromArray( PlatformColors ) );
_target.IsTarget = true;
}
void PruneOldPlatforms()
{
while ( _platforms.Count > 3 )
{
var old = _platforms[0];
_platforms.RemoveAt( 0 );
old.DestroyAll();
}
}
// ---- 跳跃与判定 ----
/// <summary> 依据蓄力值计算一次跳跃的落点/时长/高度(起跳与辅助线共用) </summary>
(Vector3 to, float duration, float height) PlanJump( float power )
{
var from = _piece.BasePosition;
var toTarget = new Vector2( _target.CenterX - from.x, _target.CenterY - from.y );
var dir = toTarget.Normal;
var dist = Math.Min( MinJumpDistance + power * ( MaxJumpDistance - MinJumpDistance ), MaxJumpDistance );
return ( new Vector3( from.x + dir.x * dist, from.y + dir.y * dist, from.z ),
0.42f + 0.22f * power,
150f + 60f * power );
}
void Launch()
{
if ( _target is null || !_piece.IsValid() ) return;
HasJumped = true;
var (to, duration, height) = PlanJump( _power );
_jumpFrom = _piece.BasePosition;
_jumpTo = to;
_jumpDuration = duration;
_jumpHeight = height;
_jumpTime = 0f;
_prevBaseZ = _jumpFrom.z;
_killZ = Math.Min( _current.TopZ, _target.TopZ ) - KillDepth;
_flying = true;
_falling = false;
_piece.SetCharge( 0f );
_piece.Impulse( 3.4f );
Sound.Play( "sounds/footsteps/footstep-concrete-jump.sound", _jumpFrom, 0f );
}
void TickFlight( float dt )
{
if ( !_piece.IsValid() ) return;
if ( !_falling )
{
_jumpTime += dt;
var u = Math.Min( _jumpTime / _jumpDuration, 1f );
var x = MathX.Lerp( _jumpFrom.x, _jumpTo.x, u );
var y = MathX.Lerp( _jumpFrom.y, _jumpTo.y, u );
// 抛物线:起点 0、终点 0、顶点 jumpHeight
var z = _jumpFrom.z + 4f * _jumpHeight * u * ( 1f - u );
_piece.SetBasePosition( new Vector3( x, y, z ) );
// 演示跳跃时留下轨迹点,标出抛物线
if ( _tutorialActive && _dotTimer > 0.055f )
{
_dotTimer = 0;
SpawnTrailDot( new Vector3( x, y, z ) );
}
// 下降阶段做落点判定(帧间跨过平台顶面才算落上,任何帧率下都准确)
if ( z <= _prevBaseZ )
{
ResolveLanding( x, y, z, _prevBaseZ );
}
_prevBaseZ = z;
// 抛物线走完仍没落到任何平台 → 转自由落体
if ( u >= 1f && !_falling && _flying )
{
_falling = true;
_fallVelocity = new Vector3(
( _jumpTo.x - _jumpFrom.x ) / _jumpDuration,
( _jumpTo.y - _jumpFrom.y ) / _jumpDuration,
-4f * _jumpHeight / _jumpDuration );
}
}
if ( _falling )
{
_fallVelocity -= Vector3.Up * Gravity * dt;
var pos = _piece.BasePosition + _fallVelocity * dt;
_piece.SetBasePosition( pos );
ResolveLanding( pos.x, pos.y, pos.z, pos.z - _fallVelocity.z * dt );
if ( pos.z < _killZ )
{
if ( _menuMode )
{
ResetGame();
return;
}
OnGameOver();
}
}
}
void ResolveLanding( float x, float y, float baseZ, float prevBaseZ )
{
// 先判"正好跨过某个平台顶面"→ 成功落上
foreach ( var p in LandablePlatforms() )
{
if ( !InsidePlatform( p, x, y ) ) continue;
if ( prevBaseZ >= p.TopZ && baseZ <= p.TopZ )
{
LandOn( p, x, y );
return;
}
}
// 已经低于平台顶面还处在平台投影内 → 撞侧面,转坠落(游戏失败)
if ( _flying && !_falling )
{
foreach ( var p in LandablePlatforms() )
{
if ( !InsidePlatform( p, x, y ) ) continue;
if ( baseZ < p.TopZ )
{
_falling = true;
if ( !_menuMode )
Sound.Play( "sounds/player_use_fail.sound", new Vector3( x, y, baseZ ), 0f );
var horizontal = _jumpDuration > 0f
? new Vector3( ( _jumpTo.x - _jumpFrom.x ) / _jumpDuration, ( _jumpTo.y - _jumpFrom.y ) / _jumpDuration, 0 )
: Vector3.Zero;
_fallVelocity = horizontal + Vector3.Up * ( -4f * _jumpHeight * ( 2f * Math.Min( _jumpTime / _jumpDuration, 1f ) - 1f ) / _jumpDuration );
return;
}
}
}
}
IEnumerable<JumpPlatform> LandablePlatforms()
{
if ( _target.IsValid() ) yield return _target;
if ( _current.IsValid() && _current != _target ) yield return _current;
}
bool InsidePlatform( JumpPlatform p, float x, float y )
{
return MathF.Abs( x - p.CenterX ) <= p.HalfX + LandingMargin()
&& MathF.Abs( y - p.CenterY ) <= p.HalfY + LandingMargin();
}
float LandingMargin() => _piece.IsValid() ? _piece.Radius * 0.55f : 14f;
void LandOn( JumpPlatform p, float x, float y )
{
_flying = false;
_falling = false;
// 困难模式:落点不吸附平台中心,棋子停在真实落点
if ( Difficulty == JumpDifficulty.Hard )
_piece.SetBasePosition( new Vector3( x, y, p.TopZ ) );
else
_piece.PlaceOn( p );
_piece.Impulse( -3.4f );
if ( p == _target )
{
if ( _menuMode )
{
// 背景演示不计分;力度在新目标生成后重算(见下方 SpawnNextPlatform 之后)
_tutorialT = 0;
}
else if ( _tutorialActive )
{
// 演示落点不计分,把控制权交还玩家
_tutorialActive = false;
_sinceTutorialEnd = 0;
TutorialText = "Your turn! Hold & release";
}
else
{
var centerDist = MathF.Sqrt( ( x - p.CenterX ) * ( x - p.CenterX ) + ( y - p.CenterY ) * ( y - p.CenterY ) );
if ( centerDist <= PerfectRadius )
{
Streak++;
Score += Math.Min( Streak * 2, 16 );
var handle = Sound.Play( "sounds/kenney/ui/ui.upvote.sound", p.Center, 0f );
if ( handle.IsValid() ) handle.Pitch = 1f + 0.1f * Math.Min( Streak - 1, 8 );
}
else
{
Streak = 0;
Score += 1;
Sound.Play( "sounds/kenney/ui/ui.downvote.sound", p.Center, 0f );
}
}
_current = p;
p.IsTarget = false;
_target = null;
SpawnNextPlatform();
PruneOldPlatforms();
// 新目标生成后再计算演示力度,保证背景 AI 每一跳都成功落在中心
if ( _menuMode )
{
_tutorialT = 0;
_demoPower = ComputeDemoPower();
}
}
else
{
// 落回原平台(力度不足):用回退音效区分
Sound.Play( "sounds/kenney/ui/ui.navigate.back.sound", p.Center, 0f );
}
}
// ---- 简单模式辅助线 ----
void UpdateAssistLine()
{
// 玩家蓄力(简单模式)或新手教学演示蓄力时显示;开始菜单背景不显示
var autoCharging = _tutorialActive && _power > 0.01f;
var want = ( _charging || autoCharging ) && !IsGameOver
&& ( Difficulty == JumpDifficulty.Easy || _tutorialActive )
&& _target.IsValid() && _piece.IsValid();
if ( !want )
{
if ( _assistVisible ) HideAssist();
return;
}
EnsureAssistObjects();
var (to, _, height) = PlanJump( _power );
var from = _piece.BasePosition;
for ( int i = 0; i < AssistDotCount; i++ )
{
var u = ( i + 1 ) / (float)AssistDotCount;
var x = MathX.Lerp( from.x, to.x, u );
var y = MathX.Lerp( from.y, to.y, u );
var z = from.z + 4f * height * u * ( 1f - u );
_assistDots[i].WorldPosition = new Vector3( x, y, z + SphereModel.Bounds.Size.z * 0.06f );
_assistDots[i].Enabled = true;
}
// 落点环:会落在平台上就贴其顶面并变绿,否则悬空变红
var surface = from.z;
var hit = false;
foreach ( var p in LandablePlatforms() )
{
if ( InsidePlatform( p, to.x, to.y ) )
{
surface = p.TopZ;
hit = true;
break;
}
}
_assistRing.WorldPosition = new Vector3( to.x, to.y, surface + 2f );
_assistRingRenderer.Tint = hit ? AssistHitColor : AssistMissColor;
_assistRing.Enabled = true;
_assistVisible = true;
}
void EnsureAssistObjects()
{
if ( _assistDots.Count == AssistDotCount && _assistDots.All( d => d.IsValid() ) && _assistRing.IsValid() ) return;
HideAssist();
_assistDots.Clear();
var dotScale = 9f / SphereModel.Bounds.Size.x;
for ( int i = 0; i < AssistDotCount; i++ )
{
var go = new GameObject( false, "AssistDot" );
go.Parent = _root;
go.LocalScale = new Vector3( dotScale, dotScale, dotScale );
var r = go.AddComponent<ModelRenderer>();
r.Model = SphereModel;
r.Tint = new Color( 0.30f, 0.34f, 0.42f );
_assistDots.Add( go );
}
_assistRing = new GameObject( false, "AssistRing" );
_assistRing.Parent = _root;
var bs = BoxModel.Bounds.Size;
_assistRing.LocalScale = new Vector3( 30f / bs.x, 30f / bs.y, 2.2f / bs.z );
_assistRingRenderer = _assistRing.AddComponent<ModelRenderer>();
_assistRingRenderer.Model = BoxModel;
}
void HideAssist()
{
foreach ( var dot in _assistDots )
{
if ( dot.IsValid() ) dot.Enabled = false;
}
if ( _assistRing.IsValid() ) _assistRing.Enabled = false;
_assistVisible = false;
}
void OnGameOver()
{
if ( IsGameOver ) return;
IsGameOver = true;
if ( !_menuMode )
{
Sound.Play( "sounds/kenney/ui/ui.button.deny.sound", PiecePosition, 0f );
JumpScores.Record( Score, Difficulty );
}
}
}