Это продолжение Части 1, которая раскрывает базовый функционал игры.
Чтобы быстро освежить в памяти, вот что мы построили в части 1:
Кажется довольно простым, не так ли? Есть некоторые улучшения, которые могут быть сделаны для улучшения нашего UX:
- Сделайте неправильное слово красным
- Сделайте правильное слово зеленым
- Сделать текущее слово подчеркиванием
- Сделать уже напечатанные слова зелеными
После этого мы можем отобразить статистику, которая показывает количество слов в минуту, и дать игроку отдохнуть после завершения раунда. А пока давайте сосредоточимся на раскрашивании нашего текста.
Добавление цветов и стилей к уже напечатанным словам
Чтобы добавить цвета, нам нужно иметь возможность отделять слова, которые были напечатаны, текущее слово и слова, которые нужно ввести. Мы уже знаем текущее слово, поэтому нам нужно построить два других. В index.tsx добавьте два новых состояния с именами alreadyTypedWords
и wordsToBeTyped
под состоянием wordIdx с помощью useMemo.
// ... const [wordIdx, setWordIdx] = useState<number>(0); const alreadyTypedWords = useMemo( () => quotesSplit.slice(0, wordIdx).join(" "), [quotesSplit, wordIdx] ); const wordsToBeTyped = useMemo( () => quotesSplit.slice(wordIdx + 1, quotesSplit.length).join(" "), [quotesSplit, wordIdx] ); // ...
Затем давайте поместим его в абзац font-mono
соответственно.
// ... <h1 className="mb-4">Typeracer</h1> <p className="font-mono"> <span className="text-green-600">{alreadyTypedWords} </span> <span className="text-black underline">{currentWord}</span> <span className="text-black"> {wordsToBeTyped}</span> </p> // ...
Не забудьте поставить пробел после уже набранных слов и перед wordsToBeTyped
для разделения слов. Вот как выглядит наша игра после этих улучшений:
После этого улучшения давайте заставим наше приложение определять цвет текущего слова, определяемого при вводе игрока. Зеленый цвет означает правильный, красный – неправильный.
Раскрашивание текущего пользовательского ввода
В этой части давайте посмотрим, как с этим справляется оригинальная игра typeracer:
Мы можем вычесть, что:
- Зеленый текст идет перед красным текстом
- Красный текст стоит перед текущим набранным словом (подчеркнуто)
Давайте соответствующим образом обновим пользовательский интерфейс, добавив два интервала для каждого цвета:
// ... <span className="text-green-600">{alreadyTypedWords} </span> <span className="text-green-600"></span> // correct word <span className="text-red-700 bg-red-200"></span> // wrong word <span className="text-black underline">{currentWord}</span> <span className="text-black"> {wordsToBeTyped}</span> // ...
Вы могли заметить, что есть два text-green-600
, которые можно объединить, но мы сделаем это позже.
После обновления пользовательского интерфейса нам нужно создать два состояния, одно из которых содержит правильную часть слова, а другое — неправильную часть слова. Вы можете попытаться понять это самостоятельно, а затем перейти к решению ниже, если вы застряли.
Добавьте этот код ниже состояния alreadyTypedWords
:
// ... const correctGreenWord = useMemo(() => { if (currentWord) { let i = 0; while (i < text.length) { if (text[i] != currentWord[i]) { break; } i++; } return text.slice(0, i); // return only the correct part } return ""; }, [currentWord, text]); const wrongRedWord = useMemo( () => currentWord?.slice( correctGreenWord.length, text.length ), [correctGreenWord, currentWord, text] ); // ...
Чтобы получить правильное зеленое слово, мы должны перебрать каждый символ и выяснить, когда текущий символ не соответствует слову. Если не совпадает, то начинается неправильное красное слово, которое начинается с последней правильной части до ввода пользователем.
Последним состоянием будет currentWordTail
, которое отрезает currentWord
от text.length
до currentWord.length
, но поскольку это простая операция, я просто закодирую ее без состояния. Также не забудьте сгруппировать alreadyTypedWords
и correctGreenWord
для максимальной эффективности. Вот последнее обновление пользовательского интерфейса:
// ... <span className="text-green-600">{alreadyTypedWords} {correctGreenWord}</span> <span className="text-red-700 bg-red-200">{wrongRedWord}</span> <span className="underline">{currentWord?.slice(text.length)}</span> <span className="text-black"> {wordsToBeTyped}</span> // ...
Окончательно! Вот как должно выглядеть наше приложение:
Это хорошо. Но можно сделать небольшое улучшение, которое заключается в том, чтобы сфокусировать панель ввода, когда игрок открывает или обновляет приложение. Есть два способа сделать это: создать ссылку на ввод с помощью useRef
или использовать собственный document.getElementById
, а затем добавить наш ввод в качестве идентификатора. Поскольку это простая функциональность, я буду использовать подход id. Давайте закодируем это:
// ...imports const inputId = "typeracer-input"; export default function TyperacerPage() { // ... <input className="w-full border-black border px-4 py-2" onChange={(text) => setText(text.target.value)} value={text} id={inputId} /> // ... }
Затем, ниже состояния неправильного RedWord, давайте создадим useEffect, который фокусирует наш ввод:
useEffect(() => { document.getElementById(inputId)?.focus(); }, []);
С этим изменением каждый раз, когда мы обновляем или открываем страницу, она будет фокусироваться на панели ввода. Аккуратно, верно?
Это весь код для index.tsx
для части 2:
import { useEffect, useMemo, useState } from "react"; import quotes from "./quotes.json"; type Quote = { quote: string; movieName: string; }; const randomQuote = (): Quote => quotes[Math.floor(quotes.length * Math.random())]; const inputId = "typeracer-input"; export default function TyperacerPage() { const [quote, setQuote] = useState<Quote>(); const [text, setText] = useState<string>(""); const [currentWord, setCurrentWord] = useState<string>(); const quotesSplit = useMemo(() => quote?.quote.split(" ") ?? [], [quote]); const [wordIdx, setWordIdx] = useState<number>(0); const alreadyTypedWords = useMemo( () => quotesSplit.slice(0, wordIdx).join(" "), [quotesSplit, wordIdx] ); const wordsToBeTyped = useMemo( () => quotesSplit.slice(wordIdx + 1, quotesSplit.length).join(" "), [quotesSplit, wordIdx] ); const correctGreenWord = useMemo(() => { if (currentWord) { let i = 0; console.log(text); while (i < text.length) { console.log(text[i]); console.log(currentWord[i]); if (text[i] != currentWord[i]) { break; } i++; } return text.slice(0, i); } return ""; }, [currentWord, text]); const wrongRedWord = useMemo( () => currentWord?.slice(correctGreenWord.length, text.length), [correctGreenWord, currentWord, text] ); useEffect(() => { document.getElementById(inputId)?.focus(); }, []); useEffect(() => { setWordIdx(0); setText(""); }, [quotesSplit]); useEffect(() => { setCurrentWord(quotesSplit[wordIdx]); }, [wordIdx, quotesSplit]); useEffect(() => { const latestLetter = text?.charAt(text.length - 1); if (latestLetter != " " && wordIdx != quotesSplit.length - 1) return; const textWithoutTrailingSpace = text?.replace(/\s*$/, ""); if (textWithoutTrailingSpace == currentWord) { console.log(text); setText(""); setWordIdx(() => wordIdx + 1); } }, [text, currentWord, wordIdx, quotesSplit]); useEffect(() => { if (wordIdx == quotesSplit.length) { setQuote(randomQuote()); } }, [wordIdx, quotesSplit]); return ( <div className="px-20"> <h1 className="mb-4">Typeracer</h1> <p className="font-mono"> <span className="text-green-600">{alreadyTypedWords} {correctGreenWord}</span> <span className="text-red-700 bg-red-200">{wrongRedWord}</span> <span className="underline">{currentWord?.slice(text.length)}</span> <span className="text-black"> {wordsToBeTyped}</span> </p> <input className="w-full border-black border px-4 py-2" onChange={(text) => setText(text.target.value)} value={text} id={inputId} /> </div> ); }
Запоздалые мысли
Изначально я планировал включить статистику, такую как скорость набора текста, название цитаты и т. д., во вторую часть, но, поскольку эта часть будет слишком длинной, я обновлю статистику и, возможно, некоторые незначительные улучшения в части 3. Следите за обновлениями, чтобы узнать больше. обновления. До встречи!