Hei! Nama saya Sergey, saya bertanggung jawab atas pengembangan front-end untuk proyek khusus di KTS . Proyek khusus adalah aplikasi periklanan kecil, seringkali dengan mekanisme yang agak rumit. Pada musim semi tahun 2020, bersama dengan tim proyek khusus VKontakte, kami membuat konsep dan mekanisme permainan untuk ulang tahun ke-5 Oreo di Rusia.
, , - Oreo, . , ββ - , . , , . , , . ββ, ββ , , .
2020 , , . . :
, , . , , . VK Mini App - -, .
:
( 200 ).
.
:
:
1.
, , .
2 MobX-: GameStore GameUI.
GameStore , , , (, ). GameStore . ( ) .
GameUI , .
, :
<Wrapper>
<Info />
<Background />
<Tower />
<MiniTower />
</Wrapper>
display: flex direction: column-reverse. css, . , - .
. z-indexβ . 100 -10 10px . 100 , .
export const TRANSLATIONS: number[] = Array.from(new Array(100)).map(
() => (Math.random() - 0.5) * 20
);
const translation = TRANSLATIONS[index % TRANSLATIONS.length];
. react-use-gesture. api , , , . useDrag, . .
const bind = useDrag(
({ last, direction: [, dirY], vxvy: [, vy] }) => {
if (dirY === 1 && vy > 0.2 && last) {
gameStore.click();
}
},
{
axis: 'y',
filterTaps: true,
}
);
click gameStore @observable swipes ( ), update gameUi. :
// GameStore
@action.bound
click(): void {
this.swipes += this.swipePower; //
this.rootStore.wsConnect
.sendMessage(WSSendEvent.swipe, { times: 1 })
this.gameUI.update({ count: this.swipePower });
}
// GameUI
@action
update({ count }: { count: number }): void {
this.uiInteraction = true;
this.newDiffCount = count;
setTimeout(() => {
this.uiInteraction = false;
//
}, NEW_OREO_ANIMATION_TIME + 100);
}
uiInteraction , , newDiffCount , ββ . , .
transition + transform. gameStore.swipes - gameUi.newDiffCount, ββ :
<Oreo
isNew={
(i + gameUi.newDiffCount >= gameStore.swipes) && gameUi.uiInteraction
}
/>
OreoWrapper = styled.div<{ isNew?: boolean }>`
transition: all ${NEW_OREO_ANIMATION_TIME}ms linear,
opacity ${NEW_OREO_ANIMATION_TIME / 3}ms linear;
opacity: 1;
${(props) =>
props.isNew &&
css`
opacity: 0;
transform: translate(-50%, -200px);
`}
`;
! . .
2.
. . -, . -, - , , . -, (, ββ) , .
:
, , . GameUi, observable uiPosition. , . . css - ( ) translate.
useDrag react-use-gesture, , - GameUi.
, , . min-max uiPosition.
@action
moveGame(deltaY: number): void {
if (Math.abs(deltaY) > 0) {
this.uiPosition = Math.min(
Math.max(0, this.uiPosition + deltaY),
Math.max(
0,
this.game.swipes * OREO_HEIGHT_PX -
windowHeight / 3
)
);
this.trackLastOreo =
this.towerHeight - this.uiPosition < windowHeight;
}
}
trackLastOreo , . , , - .
uiPosition @computed GameUi . . , - :
get miniTowerPosition(): number {
if (this.towerHeight === 0) {
return 0;
}
const uiPositionPercent = this.uiPosition / this.towerHeight;
return uiPositionPercent * this.miniTowerHeight;
}
, , - , .
:
const [{ y }, set] = useSpring(() => ({
y: gameUi.uiPosition,
}));
<Tower
style={{
transform: y.to(
(v: number) => `translate3d(0, ${v}px, 0)`
),
}}
>
react-spring , , , css . .
3.
, . , , . - , , .
- , , .
, React-: react-virtualized react-virtuoso.
, , , .
, N + , , .
, .
GameUI . - , . - + , , .
, . , . 100 80% :
// GameUi
export const OVERSCAN = 100;
export const OVERSCAN_THRESHOLD = OVERSCAN * 0.8;
@action.bound
_updateVisibleIndexesImmediately = (): void => {
const minVisibleIndex = Math.max(
0,
Math.floor(this.uiPosition / OREO_HEIGHT_PX)
);
const maxVisibleIndex = Math.min(
this.game.swipes,
Math.floor(
(this.uiPosition + this.rootStore.uiStore.windowHeight) / OREO_HEIGHT_PX
)
);
const [cachedMin, cachedMax] = this.cachedVisibleIndexes;
if (
(minVisibleIndex >= 0 &&
minVisibleIndex - OVERSCAN_THRESHOLD < cachedMin) ||
(maxVisibleIndex <= this.game.swipes &&
maxVisibleIndex + OVERSCAN_THRESHOLD > cachedMax)
) {
this.cachedVisibleIndexes = [
Math.max(0, minVisibleIndex - OVERSCAN),
Math.min(maxVisibleIndex + OVERSCAN, this.game.swipes),
];
}
};
(, ), .
:
const [minVisibleIndex, maxVisibleIndex] = gameUi.cachedVisibleIndexes;
const oreosBlock = useMemo(() => {
const oreos = [];
for (let i = minVisibleIndex; i < maxVisibleIndex; i += 1) {
oreos.push(
<Oreo
fillingId={gameStore.getCookieFiling(i)}
isNew={
(i + gameUi.newDiffCount >= gameStore.swipes) && gameUi.uiInteraction
}
index={i}
key={i}
/>
);
}
return oreos;
}, [minVisibleIndex, maxVisibleIndex]);
ββ . β β .
@computed
get invisibleHeight(): number {
const [minIndex] = this.cachedVisibleIndexes;
return minIndex * OREO_HEIGHT_PX;
}
<Tower
style={{
marginBottom: `${gameUi.invisibleHeight}px`,
transform: y.to(
(v: number) => `translate3d(0, ${v}px, 0)`
),
}}
>
, , , , , , DOM.
! , β¦ ?
4.
, β¦ . , :
. , margin-bottom translate :
margin-bottom: 3.99978e+07px;
transform: translate3d(0px, 3.99998e+07px, 0px);
( , ?), : css-, , . , .
, . , , , , . .
GameUI @computed , . ( - 1-2 ), :
@computed
get totalUiTowerBlocks(): number {
return Math.ceil(this.game.swipes / 1000000);
}
, , + 1. . baseHeight - ( ), basePosition - .
// TowerBlock
if (index === total) {
return oreos; //
}
return (
<Tower
style={{
marginBottom: `${baseHeight / total + (index === 0 ? 50 : 0)}px`,
transform: basePosition.to(
(v: number) => `translate3d(0, ${v / total}px, 0)`
),
}}
>
<TowerBlock
oreos={oreos}
total={total}
index={index + 1}
baseHeight={baseHeight}
basePosition={basePosition}
/>
</TowerWrapper>
);
marginBottom translate , , .
ββ .
5.
-, :
-
- ,
- , , , , .
, :
, , , , -. , .
, ββ . , 500 , 2000, , , . .
, 3 -, . , , , , -, - , . , ( translate ).
ββ , ( ). , . .
get currentScreenPx(): number {
const currentPointerSwipes = this.uiPosition / OREO_HEIGHT_PX;
const currentLevel = getCurrentLevelBySwipes(currentPointerSwipes);
const passedSwipes = currentPointerSwipes - currentLevel.swipes;
const startProgress = Math.min(
1,
passedSwipes / currentLevel.startSwipesRequired
);
const mainProgress = Math.max(
0,
(passedSwipes - currentLevel.startSwipesRequired) /
currentLevel.mainSwipesRequired
);
return (
startProgress * currentLevel.startScreenHeight +
mainProgress * currentLevel.mainScreenHeight +
currentLevel.startPositionPx
);
}
, .
, N + 1 N . .
6.
, β¦ . . , , - . , , , - .
, react-spring css . . . , css- :)
ββ gameUi -.
, .
, MobX @computed . , , . , . ββ x6 .
.
, , , , , . , , , DOM-, .
, :
, , !