@@ -2,6 +2,7 @@ import styles from './styles.module.css'
22import { Head } from "#components/Head"
33import type { RouteMeta } from "#router"
44import { useEffect , useRef , useState } from "react"
5+ import { makeFrameCounter } from "#components/makeFrameCounter"
56
67export const meta : RouteMeta = {
78 title : 'Suika Game' ,
@@ -11,6 +12,8 @@ export const meta: RouteMeta = {
1112export default function SuikaGamePage ( ) {
1213 const canvasRef = useRef < HTMLCanvasElement > ( null )
1314 const [ score , setScore ] = useState ( 0 )
15+ const [ ups , setUps ] = useState ( '' )
16+ const [ fps , setFps ] = useState ( '' )
1417
1518 useEffect ( ( ) => {
1619 const canvas = canvasRef . current !
@@ -24,7 +27,21 @@ export default function SuikaGamePage() {
2427 ctx . scale ( devicePixelRatio , devicePixelRatio )
2528
2629 const controller = new AbortController ( )
27- start ( controller . signal , ctx , setScore )
30+ const updateCounter = makeFrameCounter ( 200 )
31+ const frameCounter = makeFrameCounter ( 60 )
32+
33+ const formatter = new Intl . NumberFormat ( 'en-US' , {
34+ maximumFractionDigits : 1 ,
35+ minimumFractionDigits : 1 ,
36+ } )
37+
38+ start ( {
39+ signal : controller . signal ,
40+ ctx,
41+ setScore,
42+ onUpdate : ( dt ) => setUps ( formatter . format ( updateCounter ( dt ) ) ) ,
43+ onFrame : ( dt ) => setFps ( formatter . format ( frameCounter ( dt ) ) )
44+ } )
2845
2946 return ( ) => {
3047 controller . abort ( )
@@ -35,6 +52,8 @@ export default function SuikaGamePage() {
3552 < div className = { styles . main } >
3653 < div className = { styles . head } >
3754 < Head />
55+ < output > UPS: { ups } </ output >
56+ < output > FPS: { fps } </ output >
3857 < output > Score: { score } </ output >
3958 </ div >
4059 < canvas ref = { canvasRef } className = { styles . canvas } />
@@ -44,7 +63,7 @@ export default function SuikaGamePage() {
4463
4564/**
4665 * When 2 entities of the same level (index) touch,
47- * they fuse in 1 single entity of the next level.
66+ * they fuse into 1 single entity of the next level.
4867 *
4968 * The new entity is created at the position of the touch,
5069 * with velocity being the average of the 2 original entities.
@@ -60,7 +79,7 @@ const CHAIN = [
6079 { r : 60 , color : '#118ab2' , score : 11 } ,
6180 { r : 70 , color : '#7209b7' , score : 20 } ,
6281 { r : 80 , color : '#d90429' , score : 50 } ,
63- { r : 90 , color : '#ef23efff ' , score : 100 } ,
82+ { r : 90 , color : '#ef23ef ' , score : 100 } ,
6483 { r : 100 , color : '#ffd60a' , score : 200 } ,
6584 { r : 110 , color : '#003566' , score : 500 } ,
6685 { r : 120 , color : '#3f0139' , score : 1000 } ,
@@ -75,18 +94,31 @@ type Entity = {
7594 y : number
7695 vx : number
7796 vy : number
97+ /** r**3 */
98+ mass : number
7899}
79100
80- function start ( signal : AbortSignal , ctx : CanvasRenderingContext2D , setScore : ( update : ( prev : number ) => number ) => void ) {
101+ function start ( {
102+ signal,
103+ ctx,
104+ setScore,
105+ onUpdate,
106+ onFrame,
107+ } : {
108+ signal : AbortSignal ,
109+ ctx : CanvasRenderingContext2D ,
110+ setScore : ( update : ( prev : number ) => number ) => void
111+ onUpdate : ( dt : number ) => void
112+ onFrame : ( dt : number ) => void
113+ } ) {
81114 /** All entities currently in the game */
82115 const entities : Entity [ ] = [ ]
83116 /** the maximum level (index) of entity present in the game */
84117 let max = 0
85- let nextId = 0
86118 /** what will be dropped when the user clicks (index in CHAIN) */
87119 let handId = 0
88120 /** preview of the next handId (index in CHAIN), will become handId when the user clicks */
89-
121+ let nextId = 0
90122 /** position at which to drop the new entity (handId) on click */
91123 let mouseX = 0
92124
@@ -102,9 +134,10 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
102134 r : base . r ,
103135 color : base . color ,
104136 x,
105- y : base . r ,
137+ y : DROP_Y ,
106138 vx : 0 ,
107139 vy : 0 ,
140+ mass : base . r ** 3 ,
108141 } )
109142 handId = nextId
110143 nextId = Math . floor ( Math . random ( ) * max )
@@ -115,18 +148,23 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
115148 const CONTAINER_HEIGHT = 1000
116149 const containerX = ctx . canvas . width / devicePixelRatio / 2 - CONTAINER_WIDTH / 2
117150 const containerY = ctx . canvas . height / devicePixelRatio - CONTAINER_HEIGHT
151+ const DROP_Y = 50
118152
119- let lastTime = performance . now ( )
153+ let lastTime = 0
120154 let rafId = requestAnimationFrame ( function loop ( time ) {
121155 rafId = requestAnimationFrame ( loop )
122- const dt = ( time - lastTime ) / 16.6667
156+ const diff = time - lastTime
123157 lastTime = time
124- if ( dt > 1 ) return // skip frame if too much time has passed
158+ if ( diff === time || diff > 1000 ) return
159+ onFrame ( diff / 1000 )
160+ const dt = diff / 16.6667
125161
126- const steps = 20
162+ const steps = Math . ceil ( dt / 0.1 ) * 3
163+ const timeStep = diff / 1000 / steps
164+ const dti = dt / steps
127165
128166 for ( let step = 0 ; step < steps ; step ++ ) {
129- const dti = dt / steps
167+ onUpdate ( timeStep )
130168
131169 // Update entities
132170 for ( const entity of entities ) {
@@ -183,6 +221,7 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
183221 y : ( a . y + b . y ) / 2 ,
184222 vx : ( a . vx + b . vx ) / 2 ,
185223 vy : ( a . vy + b . vy ) / 2 - 7 , // slight upward boost on merge
224+ mass : base . r ** 3 ,
186225 } )
187226 if ( newId < CHAIN . length - 2 )
188227 max = Math . max ( max , newId )
@@ -231,13 +270,11 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
231270
232271 if ( speed > 0 ) continue // Objects separating
233272
234- const massA = a . r ** 3
235- const massB = b . r ** 3
236- const impulse = 2 * speed / ( massA + massB ) * 0.8 // restitution coefficient
237- a . vx -= impulse * massB * normalX
238- a . vy -= impulse * massB * normalY
239- b . vx += impulse * massA * normalX
240- b . vy += impulse * massA * normalY
273+ const impulse = 2 * speed / ( a . mass + b . mass ) * 0.8 // restitution coefficient
274+ a . vx -= impulse * b . mass * normalX
275+ a . vy -= impulse * b . mass * normalY
276+ b . vx += impulse * a . mass * normalX
277+ b . vy += impulse * a . mass * normalY
241278 }
242279 }
243280 }
@@ -257,7 +294,7 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
257294 ctx . fillStyle = base . color
258295 ctx . beginPath ( )
259296 const x = Math . max ( containerX + base . r , Math . min ( containerX + CONTAINER_WIDTH - base . r , mouseX ) )
260- ctx . arc ( x , 50 , base . r , 0 , Math . PI * 2 )
297+ ctx . arc ( x , DROP_Y , base . r , 0 , Math . PI * 2 )
261298 ctx . fill ( )
262299 }
263300
0 commit comments