我正在尝试在React中重新创建一个旧的Flash游戏。游戏的目的是按下按钮一定时间。
这是旧游戏: http://www.zefrank.com/everysecond/index.html
这是我新的React实现: https://codesandbox.io/s/github/inspectordanno/every_second
我遇到了问题。释放鼠标时,我使用Moment.js时间库计算了从按下按钮到释放按钮之间的时间。如果timeDifference
和onMouseDown
事件之间的onMouseUp
在targetTime
之内,我希望游戏level
增加而targetTime
增加也是
我正在handleMouseUp
事件处理程序中实现此逻辑。我正在将预期时间打印到屏幕上,但是逻辑不起作用。另外,当我console.log()
次时,它们与正在打印到屏幕上的时间不同。我可以肯定timeHeld
和timeDifference
的更新不正确。
最初,我认为我处理事件处理程序的方式存在问题,我需要使用useRef()
或useCallback()
,但是在浏览了其他一些问题之后,我不太了解这些问题足以知道我是否必须在这种情况下使用它们。由于我不需要访问以前的状态,因此我认为不需要使用它们,对吗?
游戏逻辑位于此包装器组件中
import React, { useState } from 'react';
import moment from 'moment';
import Button from './Button';
import Level from './Level';
import TargetTime from './TargetTime';
import TimeIndicator from './TimeIndicator';
import Tries from './Tries';
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(.2);
const [isPressed, setIsPressed] = useState(false);
const [whenPressed, setPressed] = useState(moment());
const [whenReleased, setReleased] = useState(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = whenReleased.diff(whenPressed) / 1000;
let timeDifference = Math.abs(targetTime - timeHeld);
timeDifference = Math.round(1000 * timeDifference) / 1000; //rounded
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
setPressed(moment());
};
const handleMouseUp = () => {
setIsPressed(false);
setReleased(moment());
console.log(timeHeld);
console.log(timeDifference);
if (timeDifference <= .1) {
setLevel(level + 1);
setTargetTime(targetTime + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries(tries - 1);
}
if (tries === 1) {
setGameOver(true);
}
};
return (
<div>
<Level level={level}/>
<TargetTime targetTime={targetTime} />
<Button handleMouseDown={handleMouseDown} handleMouseUp={handleMouseUp} isGameOver={gameOver} />
<TimeIndicator timeHeld={timeHeld} timeDifference={timeDifference} isPressed={isPressed} gameStarted={gameStarted} />
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
)
}
export default TimerApp;
如果您要检查整个应用程序,请参阅沙箱。
答案 0 :(得分:1)
我认为这里发生了两件事:
当您调用setState
方法(例如setRelease(moment())
)时,关联变量(例如whenReleased
)的值不会立即更新。取而代之的是,它将重新渲染放入队列中,并且只有在渲染完成后才会更新该值。
事件处理程序(例如handleMouseUp
)是闭包。意味着它们从父作用域中捕获值。同样,仅在重新渲染时更新 。因此,当handleMouseUp
运行时,timeDifference
(和timeHeld
)将是上次渲染期间计算出的值。
因此,您需要进行的更改是:
timeDifference
事件处理程序中移动handleMouseUp
的计算。whenReleased
计算中使用timeDifference
,而需要使用设置为moment()
的局部变量(也可以也设置{{1} }通过whenReleased
,但在事件处理程序中将无法使用该值。setReleased
答案 1 :(得分:1)
如果您更新了函数内部的某些状态,然后尝试在同一函数中使用该状态,它将不会使用更新后的值。当调用函数时,函数会快照状态值,并在整个函数中使用状态值。在类组件的this.setState
中不是这种情况,但是在钩子中就是这种情况。 this.setState
也不会急切地更新值,但是它可以在同一函数中同时根据几件事进行更新(我不足以解释)。
要使用更新的值,您需要参考。因此,请使用useRef
钩子。 [docs]
我已修复您的代码,您可以在这里查看:https://codesandbox.io/s/everysecond-4uqvv?fontsize=14
可以用更好的方式编写它,但是您必须自己做。
在回答中也添加代码以完成操作(带有一些注释以说明内容,并提出改进建议):
import React, { useRef, useState } from "react";
import moment from "moment";
import Button from "./Button";
import Level from "./Level";
import TargetTime from "./TargetTime";
import TimeIndicator from "./TimeIndicator";
import Tries from "./Tries";
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(0.2);
const [isPressed, setIsPressed] = useState(false);
const whenPressed = useRef(moment());
const whenReleased = useRef(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = useRef(null); // make it a ref instead of just a variable
const timeDifference = useRef(null); // make it a ref instead of just a variable
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
whenPressed.current = moment();
};
const handleMouseUp = () => {
setIsPressed(false);
whenReleased.current = moment();
timeHeld.current = whenReleased.current.diff(whenPressed.current) / 1000;
timeDifference.current = Math.abs(targetTime - timeHeld.current);
timeDifference.current = Math.round(1000 * timeDifference.current) / 1000; //rounded
console.log(timeHeld.current);
console.log(timeDifference.current);
if (timeDifference.current <= 0.1) {
setLevel(level + 1);
setTargetTime(targetTime + 0.2);
} else if (timeDifference.current > 0.1 && tries >= 1) {
setTries(tries - 1);
// consider using ref for tries as well to get rid of this weird tries === 1 and use tries.current === 0
if (tries === 1) {
setGameOver(true);
}
}
};
return (
<div>
<Level level={level} />
<TargetTime targetTime={targetTime} />
<Button
handleMouseDown={handleMouseDown}
handleMouseUp={handleMouseUp}
isGameOver={gameOver}
/>
<TimeIndicator
timeHeld={timeHeld.current}
timeDifference={timeDifference.current}
isPressed={isPressed}
gameStarted={gameStarted}
/>
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
);
};
export default TimerApp;
PS:不要使用不必要的第三方库,尤其是像MomentJs这样的大型库。它们会大大增加您的套装尺寸。使用可以使用vanilla js轻松获得当前时间戳。 Date.now()
将为您提供当前的unix时间戳,您可以减去两个时间戳来获得以ms为单位的持续时间。
此外,您还有一些不必要的状态,例如gameOver
,只需检查if tries > 0
即可确定gameOver。
同样,您可以只使用targetTime
来代替level * .2
,而无需附加状态。
另外,whenReleased
不必是引用或状态,它可以只是mouseup处理程序中的局部变量。
答案 2 :(得分:1)
状态更新程序可以采用表示新状态的值,也可以采用将当前状态映射到新状态的函数。当您的状态取决于自身的变异时,后者是工作的正确工具。
如果您在使用模式的代码中更新位置,这应该可以工作
[value, setValue ] = useState(initial);
...
setValue(value + change);
到
[value, setValue ] = useState(initial);
...
setValue((curValue) => curValue + change);
例如,
if (timeDifference <= .1) {
setLevel((curLevel) => curLevel + 1);
setTargetTime((curTarget) => curTarget + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries((curTries) => {
const newTries = curTries - 1;
if (newTries === 1) {
setGameOver(true);
}
return newTries;
});
}
答案 3 :(得分:0)
释放鼠标时,我计算了从按下按钮到释放按钮之间的时间...
这不是真的 ...但是可以...只需将时差计算移至handleMouseUp()
...也-您不需要whenReleased