Как вам варик задеанонить эту е*тую Оксану, подломить ей инсту, вытащить оттуда что-нибудь пикантное и шантажировать ее, что бы отдала нам ноут?
на форумах заказываем пробив сотового номера Оксаны, а к нему пробив координат ближайшей соты, приезжаем на адрес завтра до 12:00, ждём появления звуков болгарки для детекта помещения, устраиваем аварию на электросети и режем оптику, в суматохе проникаем в помещение бригадой в форменной одежде и втыкаем в ноут USB-троян (злоумышленную клавиатуру), но чот приз маловат, жаль. не разгуляешься на килобакс.
Есть вариант лучше! Берём в заложников детей сотрудников ruvds и требуем от них ноутбукВот так и выглядит отчаяние: интеллектуалы-айтишники уже готовы опуститься до популярнейших статей УК. \_()_/
Мы пока не поняли как эту подсказку использовать. Мы в принципе сам смысл поняли, а к чему именно нужно применить подсказку, не поняли.К вечеру уже обвиняли друг друга:
Классический крыса-кун. У него PuTTy и ssss уже готовы к операции
да он по любому организатор под прикрытием, потом бабки залутает
Есть какой-то рассказ Neuromancer, фразы из него выдернуты в разном порядке и из них собрали эту новеллу
а ещё клавиши обозначены как do re mi
Похоже на французский
я кажется понял мысль, только поймать не могу
В данной статье рассмотрим все этапы разработки: от зарождения идеи до имплементации отдельных частей приложения, в том числе выборочно будут предоставлены некоторые кастомные куски кода.
Данная статья может быть полезна тем, кто только задумывается или начинает разрабатывать игры или мобильные приложения.
Скриншот готовой игрыПорядок заголовков не является структурированным по каким-либо параметрам, кроме хронологической последовательности.
Добрый день, уважаемые читатели. Конечно же мы с Вами не знакомы, однако если бы были, то Вы бы знали, что ещё с раннего детства после приобретения отцом персонального компьютера у меня была мечта стать разработчиком игр, что в дошкольном возрасте выражалось в "создании" платформеров мелом на асфальте для ребят во дворе, а в школьном - отдельными играми в тетрадках, в том числе и во время уроков.
Разработчиком игр (пока что) так я и не стал, но стал веб-разработчиком, имея на данный момент 8+ лет коммерческого опыта. Однажды на глаза попалась весьма интересная библиотека-фреймворк для создания пошаговых игр, документация которой была незамедлительно изучена. Предлагалось создавать такие игры, как крестики-нолики или шахматы, однако это показалось скучным и мне захотелось пойти дальше - сделать нечто посложнее и похожее на то, во что я играл ещё в детстве. Вероятно, старички заметят некоторое сходство финального результата с той игрой, на которую делался акцент.
Сперва было принято решение создать изометрический мир, который мог бы поддерживать бесконечную прокрутку по горизонтали. После тщательного изучения статей на тему "Как создавать изометрические миры" и им подобных, а также просмотра библиотек для JavaScript и React, которые якобы должны помогать выполнять данную задачу, дело перешло к практике.
Времени на это было потрачено немало и готовых подходящих решений так и не было найдено. Ну что ж, напишем своё. По сути весь наш мир это набор квадратиков-тайлов, которые находятся рядом друг с другом и на соседний квадратик можно перейти в восьми направлениях.
Стартовая позиция игрока, где можно наблюдать изометрический мир, а также возможные направления движенияСперва отрисуем ячейки мира построчно. Пускай они будут размером 64x64 пикселя. Далее развернём наш контейнер таким образом, чтобы он выглядел изометрично:
.rotate { transform: rotateX(60deg) rotateZ(45deg); transform-origin: left top;}
При имплементации данного подхода к отрисовке можно наблюдать, что необходимые нам "строки" мира на самом деле идут не прямо, а зигзагом, так как мы развернули нашу карту. Таким образом, каждую из ячеек необходимо позиционировать абсолютно с учетом текущего индекса строки, а также индекса колонки:
const cellOffsets = {};export function getCellOffset(n) { if (n === 0) { return 0; } if (cellOffsets[n]) { return cellOffsets[n]; } const result = 64 * (Math.floor(n / 2)); cellOffsets[n] = result; return result;}
Использование:
import { getCellOffset } from 'libs/civilizations/helpers';// ...const offset = getCellOffset(columnIndex);// ...style={{ transform: `translateX(${(64 * rowIndex) + (64 * columnIndex) - offset}px) translateY(${(64 * rowIndex) - offset}px)`,}}
Для увеличения производительности нам необходимо перерисовывать
только те ячейки карты, которые сейчас являются видимыми на экране.
Для этого был использован компонент FixedSizeGrid
из
модифицированной версии библиотеки react-window
с
учетом нашего поворота и расположений ячеек, код которого здесь
приводить не буду. Из того, что не получилось - это сделать
бесконечную прокрутку мира. После изучений исходного различных
библиотек для бесконечного скролла / слайдеров и тп. подходящего
решения найдено не было. Что ж, значит наш мир будет с границами по
всем бокам.
Для отображения поворотов наших юнитов, а также анимации атаки, используются png-спрайты. Поиск графических элементов занял очень большой промежуток времени, было очень сложно найти хотя бы какие-то картинки возможных юнитов для нашей игры. Изначально ещё до поиска игра выглядела вот так:
Игра до поиска графических элементовВообще сам я не дизайнер и попросить кого-либо нарисовать тоже возможности нет, потому каждый кадр спрайта пришлось ручками размещать в необходимую позицию. Для примера, финальный спрайт вертолёта выглядит вот так:
Спрайт вертолётаИгра поддерживает 4 языка и, если честно, мне непонятно, зачем в
несложных приложениях разработчики подключают массивные библиотеки
типа react-i18next
. Давайте напишем похожее кастомное
решение, которое уместится в чуть более чем 100 строк с учетом
красивой разметки кода, а также будет поддерживать определение
языка девайса пользователя, переключение языков в реальном времени
и сохранение последнего выбора пользователя. Здесь используется
redux
, однако данный код можно адаптировать и под
другие реактивные хранилища. Да, здесь нет некоторых фишек больших
библиотек типа поддержки переменных в строках, однако в таком
проекте нам это и не нужно. И да, эту библиотеку можно использовать
как легковесную замену react-i18next
(или подобным) в
уже существующем проекте.
import React, { Component } from 'react';import PropTypes from 'prop-types';import { connect } from 'react-redux';import get from 'lodash/get';import set from 'lodash/set';import size from 'lodash/size';import { emptyObj, EN, LANG, PROPS, langs } from 'defaults';import { getLang } from 'reducers/global/selectors';import en from './en';export function getDetectedLang() { if (!global.navigator) { return EN; } let detected; if (size(navigator.languages)) { detected = navigator.languages[0]; } else { detected = navigator.language; } if (detected) { detected = detected.substring(0, 2); if (langs.indexOf(detected) !== -1) { return detected; } } return EN;}const options = { lang: global.localStorage ? (localStorage.getItem(LANG) || getDetectedLang()) : getDetectedLang(),};const { lang: currentLang } = options;const translations = { en,};if (!translations[currentLang]) { try { translations[currentLang] = require(`./${currentLang}`).default; } catch (err) {} // eslint-disable-line}export function setLang(lang = EN) { if (langs.indexOf(lang) === -1) { return; } if (global.localStorage) { localStorage.setItem(LANG, lang); } set(options, [LANG], lang); if (!translations[lang]) { try { translations[lang] = require(`./${lang}`).default; } catch (err) {} // eslint-disable-line }}const mapStateToProps = (state) => { return { lang: getLang(state), };};export function t(path) { const { lang = get(options, [LANG], EN) } = get(this, [PROPS], emptyObj); if (!translations[lang]) { try { translations[lang] = require(`./${lang}`).default; } catch (err) {} // eslint-disable-line } return get(translations[lang], path) || get(translations[EN], path, path);}function i18n(Comp) { class I18N extends Component { static propTypes = { lang: PropTypes.string, } static defaultProps = { lang: EN, } constructor(props) { super(props); this.t = t.bind(this); } componentWillUnmount() { this.unmounted = true; } render() { return ( <Comp {...this.props} t={this.t} /> ); } } return connect(mapStateToProps)(I18N);}export default i18n;
Использование:
import i18n from 'libs/i18n';// ...static propTypes = { t: PropTypes.func,}// ...const { t } = this.props;// ...{t(['path', 'to', 'key'])}// ...или тоже самое, но слегка медленнее{t('path.to.key')}// ...export default i18n(Comp);
Игра поддерживает мультиплеер в реальном времени для устройств с Android 9 или выше (возможно, будет работать и на 8-м, однако данное предположение не проверялось) с рейтингом и таблицей лидеров.
Сам движок не поддерживает ходы в реальном времени, потому
многопользовательский режим построен таким образом, что все события
происходят в один и тот же ход, в это же время существуют кулдауны
на определенные действия, которые реализованы через
requestAnimationFrame
. На Android 7 и ранее такой
подход почему-то просто-напросто не работает.
Кастомный код библиотеки, которая регистрирует
requestAnimationFrame
, для интересующихся (не
забывайте также, что после регистрации колбека его нужно и
отрегистрировать, что обычно происходит по завершении колбека или
на анмаут компонента):
import isFunction from 'lodash/isFunction';let lastTime = 0;const vendors = ['ms', 'moz', 'webkit', 'o'];for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`]; window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`] || window[`${vendors[x]}CancelRequestAnimationFrame`];}if (!window.requestAnimationFrame) { window.requestAnimationFrame = (callback) => { const currTime = new Date().getTime(); const timeToCall = Math.max(0, 16 - (currTime - lastTime)); const id = window.setTimeout(() => { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; };}if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = (id) => { clearTimeout(id); };}let lastFrame = null;let raf = null;const callbacks = [];const loop = (now) => { raf = requestAnimationFrame(loop); const deltaT = now - lastFrame; // do not render frame when deltaT is too high if (deltaT < 160) { let callbacksLength = callbacks.length; while (callbacksLength-- > 0) { callbacks[callbacksLength](now); } } lastFrame = now;};export function registerRafCallback(callback) { if (!isFunction(callback)) { return; } const index = callbacks.indexOf(callback); // remove already existing the same callback if (index !== -1) { callbacks.splice(index, 1); } callbacks.push(callback); if (!raf) { raf = requestAnimationFrame(loop); }}export function unregisterRafCallback(callback) { const index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } if (callbacks.length === 0 && raf) { cancelAnimationFrame(raf); raf = null; }}
Использование:
import { registerRafCallback, unregisterRafCallback } from 'client/libs/raf';// ...registerRafCallback(this.cooldown);// ...componentWillUnmount() { unregisterRafCallback(this.cooldown);}
Стандартная имплементация Lobby
из библиотеки
движка мне не подходила, так как она открывала ещё одно новое
websocket-подключение на каждый инстанс игры, но
мне также нужно было передавать данные пользователя и таблицу
лидеров по своему уже существующему
websocket-подключению, потому, чтобы не плодить
подключения, здесь снова было использовано собственное решение на
основе библиотеки primus
. На стороне клиента
подключение хендлится сбилдженной библиотекой от примуса, которое
также выложил на npm с именем primus-client
. Вы можете
сами сбилдить себе подобную клиентскую библиотеку для определенной
версии примуса через функцию save
на стороне
сервера.
Видео геймплея многопользовательского режима можно наблюдать ниже:
В игре присутствует несколько звуков - старта игры, атаки и победы. Для проигрывания звука - также кастомная библиотека (для музыки - схожий подход):
import { SOUND_VOLUME } from 'defaults';const Sound = { audio: null, volume: localStorage.getItem(SOUND_VOLUME) || 0.8, play(path) { const audio = new Audio(path); audio.volume = Sound.volume; if (Sound.audio) { Sound.audio.pause(); } audio.play(); Sound.audio = audio; },};export function getVolume() { return Sound.volume;}export function setVolume(volume) { Sound.volume = volume; localStorage.setItem(SOUND_VOLUME, volume);}export default Sound;
Использование:
import Sound from 'client/libs/sound';// ...Sound.play('/mp3/win.mp3');
Окно настроек игры
Сборка web-части осуществляется вебпаком. Однако тут нужно
учитывать особенности путей к файлам, ведь в процессе разработке на
локалхосте или на сервере в продакшене они являются относительными
корня домена, а для приложения в Cordova наши
файлы будут размещены по протоколу file://
и потому
после сборки нам необходимо провести некоторые преобразования, а
именно:
const replace = require('replace-in-file');const path = require('path');const options = { files: [ path.resolve(__dirname, './app/*.css'), path.resolve(__dirname, './app/*.js'), path.resolve(__dirname, './app/index.html'), ], from: [/url\(\/img/g, /href="\//g, /src="\//g, /"\/mp3/g], to: ['url(./img', 'href="./', 'src="./', '"./mp3'],};replace(options) .then((results) => { console.log('Replacement results:', results); }) .catch((error) => { console.error('Error occurred:', error); });
Приложение разрабатывалось в течении года, находится в Google Play Store с середины сентября, а значит уже прошло три месяца. Общее количество установок - 46, из которых ещё непонятно, сколько там на самом деле настоящих людей. Если коротко, то это провал. Однако был приобретен первичный опыт как разработки игр, так и мобильных приложений.
Из того, что было задумано, но не получилось:
Более сложный геймплей
Бесконечная прокрутка карты по горизонтали
Продвинутый ИИ компьютера
Поддержка мультиплеера на всех устройствах
Сейчас понятно, что подобные игры мало кому интересны, так что в прогрессе изучение Unity, и возможно через некоторое время появится ещё одна игра в жанре tactical rts.
Можно. Для интересующихся - ссылка на приложение на Google Play Store.
P.S. Отдельное спасибо музыканту Anton Zvarych за предоставленную фоновую музыку.
Указывающая вниз полосатая красно-белая стрелка даёт понять, что вы находитесь в Японии, с большой вероятностью, на острове Хоккайдо или, возможно, на острове Хонсю рядом с горами.
base(M, f*)
с внутренней
архитектурой ResNet50.NUMBER_OF_SCREENSHOTS
в показанном ниже коде.
'''Given a GeoGuessr map URL (e.g. https://www.geoguessr.com/game/5sXkq4e32OvHU4rf)take a number of screenshots each one step further down the road and rotated ~90 degrees.Usage: "python file_name.py https://www.geoguessr.com/game/5sXkq4e32OvHU4rf"'''from selenium import webdriverimport timeimport sysNUMBER_OF_SCREENSHOTS = 4geo_guessr_map = sys.argv[1]driver = webdriver.Chrome()driver.get(geo_guessr_map)# let JS etc. loadtime.sleep(2)def screenshot_canvas(): ''' Take a screenshot of the streetview canvas. ''' with open(f'canvas_{int(time.time())}.png', 'xb') as f: canvas = driver.find_element_by_tag_name('canvas') f.write(canvas.screenshot_as_png)def rotate_canvas(): ''' Drag and click the <main> elem a few times to rotate us ~90 degrees. ''' main = driver.find_element_by_tag_name('main') for _ in range(0, 5): action = webdriver.common.action_chains.ActionChains(driver) action.move_to_element(main) \ .click_and_hold(main) \ .move_by_offset(118, 0) \ .release(main) \ .perform()def move_to_next_point(): ''' Click one of the next point arrows, doesn't matter which one as long as it's the same one for a session of Selenium. ''' next_point = driver.find_element_by_css_selector('[fill="black"]') action = webdriver.common.action_chains.ActionChains(driver) action.click(next_point).perform()for _ in range(0, NUMBER_OF_SCREENSHOTS): screenshot_canvas() move_to_next_point() rotate_canvas()driver.close()
conda
. Мне понравился README репозитория.
Раздел
requirements был достаточно понятным и на новом Ubuntu 20.04 у
меня не возникло никаких проблем.
python -m classification.inference --image_dir ../images/ lat lngcanvas_1616446493 hierarchy 44.002556 -72.988518canvas_1616446507 hierarchy 46.259434 -119.307884canvas_1616446485 hierarchy 40.592514 -111.940224canvas_1616446500 hierarchy 40.981506 -72.332581
Чтобы выяснить, насколько PlaNet сравнима с интуицией человека, мы позволили ей соревноваться с десятью много путешествовавшими людьми в игре Geoguessr (www.geoguessr.com).
В сумме люди и PlaNet сыграли в 50 раундов. PlaNet выиграла 28 из 50 раундов с медианной погрешностью локализации в 1131,7 км, в то время как медианная погрешность людей составляла 2320,75 км.
Графическая демонстрация, в которой вы сможете посоревноваться против описанной в статье системы с глубоким обучением находится здесь: https://tibhannover.github.io/GeoEstimation/. Также мы создали многофункциональный веб-инструмент, поддерживающий загрузку и анализ пользовательских изображений: https://labs.tib.eu/geoestimation
for
.
for line_words in OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="")
import osOUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]OUTPUT_IMAGE[4][6] = "O"os.system("cls||clear")for line_words in OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="")
os.system("cls||clear")
OUTPUT_IMAGE
, для того чтобы очищать все ранее
нарисованные в игровом поле объекты.while
True
.while True
функцию
time.sleep(1)
, для того чтобы ограничить FPS.
from time import sleepfrom os import systemOUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]x = 0y = 0while True: sleep(1) system("cls||clear") OUTPUT_IMAGE[y][x] = "O" for line_words in OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="") y += 1 x += 1 OUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]
def SetImage(image: str, x: int, y: int): pass
image = " O\n'|'\n |"#игрок
def SetImage(x: int, y: int, image: str): x_start = x x = x y = y for word in image: if word == "\n": x = x_start y += 1 else: x += 1 try: OUTPUT_IMAGE[y][x] = word except IndexError: break
try: except()
для того чтобы небыло ошибок
если объект имеет X и Y слишком мальенькие или слишком большие.x_start
Это X, с которого нужно начинать рисовать при
увеличении Y (при символе "\n")
from time import sleepfrom os import systemOUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]def SetImage(x: int, y: int, image: str): x_start = x x = x y = y for word in image: if word == "\n": x = x_start y += 1 else: x += 1 try: OUTPUT_IMAGE[y][x] = word except IndexError: breakwhile True: sleep(1) system("cls||clear") SetImage(x=3,y=4,image=" O\n'|'\n |") for line_words in OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="") OUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]
from time import sleepfrom os import systemOUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]px = 0py = 0def SetImage(x: int, y: int, image: str): x_start = x x = x y = y for word in image: if word == "\n": x = x_start y += 1 else: x += 1 try: OUTPUT_IMAGE[y][x] = word except IndexError: breakwhile True: sleep(1) system("cls||clear") SetImage(x=px,y=py,image=" O\n'|'\n |") for line_words in OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="") px += 1 py += 1 OUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]
def GetSizeObject(img: str): w = 0 weights = [] h = [word for word in img if word == "\n"] for word in img: if word == "\n": weights.append(w) w = 0 else: w += 1 try: return {"w": max(weights), "h":len(h)} except ValueError: return {"w": 0, "h":0}
from time import sleepfrom os import systemOUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]px = 3py = 3def SetImage(x: int, y: int, image: str): global OUTPUT_IMAGE x_start = x x = x y = y for word in image: if word == "\n": x = x_start y += 1 else: x += 1 try: OUTPUT_IMAGE[y][x] = word except IndexError: breakdef GetSizeObject(img: str): w = 0 weights = [] h = [word for word in img if word == "\n"] h.append(1) for word in img: if word == "\n": weights.append(w) w = 0 else: w += 1 try: return {"w": max(weights), "h":len(h)} except ValueError: return {"w": 0, "h":0}player_image = " O\n'|'\n |"def draw(): global OUTPUT_IMAGE sleep(1) system("cls||clear") for line_words in OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="") OUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]while True: SetImage(x=px,y=py,image=player_image) print(GetSizeObject(img=player_image)) draw()
y
больше y2 - h2 + h
и y - h
меньше чем y2 + h2 - h
y2
больше y - h + h2
и y2 -
h2
меньше чем y + h - h2
y
x
, а вместо h
w
.x
больше x2 - w2 + w
и x - w
меньше чем x2 + w2 - w
x2
больше x - w + w2
и x2 -
w2
меньше чем x + w - w2
def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int): if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2): if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2): return True return False
True
если объекты соприкасаются, и
False
если нет.
from time import sleepfrom os import systemOUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]def SetImage(x: int, y: int, image: str): global OUTPUT_IMAGE x_start = x x = x y = y for word in image: if word == "\n": x = x_start y += 1 else: x += 1 try: OUTPUT_IMAGE[y][x] = word except IndexError: breakdef GetSizeObject(img: str): w = 0 weights = [] h = [word for word in img if word == "\n"] h.append(1) for word in img: if word == "\n": weights.append(w) w = 0 else: w += 1 try: return {"w": max(weights), "h":len(h)} except ValueError: return {"w": 0, "h":0}def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int): if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2): if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2): return True return Falseplayer_image = " O\n'|'\n |"cube_image = "____\n| |\n----"cx = 5#cy = 4 #Меняйте эти координаты для того чтобы менять позиции игрока и кубаpx = 10 #py = 3#def draw(): global OUTPUT_IMAGE sleep(1) system("cls||clear") for line_words in OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="") OUTPUT_IMAGE = [ [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",], ]while True: SetImage(x=px,y=py,image=player_image) SetImage(x=cx,y=cy,image=cube_image) print("is clash: ",IsClash( x=px, x2=cx, y=py, y2=cy, h=GetSizeObject(img=player_image)["h"], h2=GetSizeObject(img=cube_image)["h"], w=GetSizeObject(img=player_image)["w"], w2=GetSizeObject(img=cube_image)["w"], )) draw()
While True
:
from time import sleepwhile True: sleep(0.1)
+----game| + -- | | -- main.py| \ --lib| +--lib.py -> class Game()| \|+---
from time import sleepclass Game(): def __init__(self): self.OUTPUT_IMAGE = [] # здесь игровое поле def draw_all(self): for line_words in self.OUTPUT_IMAGE: for word in line_words: print(word, end="") print("\n", end="") def run(self): while True: self.draw_all() sleep(0.1)
set_image()
,
size_object()
, is_clash()
, и все те
которые являются игровым движком, и которые я описал выше.create_object()
и переменную
self.OBJECTS
, функцию create_object()
я
использую для создания объектов, она принимает параметры
img
, name
, x
,
y
, up
, rigid
,
data
.img
картинка объектаname
имя объекта (дом, трава, житель, еда и.т.п.)x
X объектаy
Y объектаup
если этот параметр True, то объект рисуется над
игроком, иначе игрок его перекрывает собойrigid
твердость, игрок не может пройти через этот
объект (еще не реализовано)data
личные данные объекта, его личные
характеристики
def CreateObject(self,x: int, y: int, img: str, name: str = None, up: bool = False, rigid: bool = False, data: dict = {}): size_object = self.GetSizeObject(img=img) self.OBJECTS.append( {"name": name, "x": x, "y": y, "up": up, "rigid": rigid, "h":size_object["h"], "w":size_object["w"], "id":uuid4().hex, "data":data, "img": img} )
up
,
использовать его в объекте Home
, Т.Е. чтоб дом
закрывал собой игрока. Для этого я сделал функцию CheckAll(),
циклом for проходился по всем объектам, и рисовал их на исходящей
картинке, Т.Е. использовать функцию SetImage(x: int, y: int,
img:str), подавая в нее X и Y объекта, и картинку.up_of_payer_objects
, и если у
объекта стоял up=True, то я добавлял его в список, не рисуя его на
поле. После рисовал самого игрока, и только этого я проходил циклом
for по объектам в up_of_payer_objects, рисуя их, тем самым они были
над игроком.
def CheckAll(self): up_of_payer_objects = [] for object_now in range(len(self.OBJECTS)): if object_now["up"]: up_of_payer_objects.append(object_now) continue self.SetImage(x=object_now["x"],y=object_now["y"],image=object_now["img"])
self.OBJECTS
, но который хранится в переменной
self.PLAYER
.X
, Y
,
img
, и.т.п. получить можно с помощью ключей, проще
говоря это словарь (dict). С таким игроком и объектами уже можно
было работать, двигать, вычислить столкновения. Я начал с
движения.
def CheckAll(self): self.CheckKeysObjects() ....
self.WALK_LEFT_PLAYER
self.WALK_RIGHT_PLAYER
self.WALK_UP_PLAYER
self.WALK_DOWN_PLAYER
d
, то мы переменную
self.WALK_RIGHT_PLAYER
делаем True
.False
, для того чтобы сбросить все прошлые результаты,
а-то игрок не остановится.
def CheckKeysObjects(self): #делаю все переменные в False, чтоб сбросить прошлые результаты self.WALK_LEFT_PLAYER = False self.WALK_RIGHT_PLAYER = False self.WALK_UP_PLAYER = False self.WALK_DOWN_PLAYER = False #а тут уже проверяю нажатия if keyboard.is_pressed("a"): self.WALK_LEFT_PLAYER = True elif keyboard.is_pressed("d"): self.WALK_RIGHT_PLAYER = True if keyboard.is_pressed("w"): self.WALK_UP_PLAYER = True elif keyboard.is_pressed("s"): self.WALK_DOWN_PLAYER = True
CheckAll()
проверяю все
перменные отвечающие за движение, узнаю, куда двигается игрок.True
, узнаем какая, и двигаем предмет
в противоположную сторону.
def CheckAll(self): self.CheckKeysObjects() # check moves up_of_payer_objects = [] for object_now in range(len(self.OBJECTS)): self.PLAYER["img"] = self.PLAYER["image_normal"] if self.WALK_LEFT_PLAYER: self.OBJECTS[object_now]["x"] += 1 elif self.WALK_RIGHT_PLAYER: self.OBJECTS[object_now]["x"] -= 1 if self.WALK_UP_PLAYER: self.OBJECTS[object_now]["y"] += 1 elif self.WALK_DOWN_PLAYER: self.OBJECTS[object_now]["y"] -= 1
time.sleep()
, и библиотеку threading
для
того чтобы запустить 2 функции одновременно, спавн еды и основной
игровой цикл. Функция спавна еды SpawnEat()
это просто
функция которая при запуске генерирует на случайных местах еду,
вызывая для каждой единицы еды функцию
CreateObject()
.self.PLAYER["hungry"]
, это его голод, в самом
начале он равен 100 ед., его я буду уменьшать если игрок ходит и
тратит энегрию (типа энергию, ее в игре нет) или увеличивать если
игрок что-то съел.MinimizeHungry()
, она
вызывается каждые 5 секунд, и просто отнимает у игрока 2 единицы
голода. Это я сделал для того чтобы игроку пришлось двигаться, а не
стоять на месте.Eat()
, эта функция которая
вызывается в отдельном потоке от игрового цикла. Она проверяет не
слишком ли много еды на карте, если еды больше 10 ед. то НЕ
вызывает функцию SpawnEat()
, если меньше 10 ед. то
вызывает SpawnEat()
.
def Eat(self): while True: sleep(4) if len([i for i in self.OBJECTS if i["name"] == "meat"]) < 10: self.SpawnEat() sleep(1) self.MinimizeHungry()
Start()
, для запуска основного цикла:
def Start(self): while True: self.CheckAll() self.DrawAll() sleep(0.01)
run()
, которая запускает всю игру.
def run(self): proc1 = threading.Thread(target=self.Start) proc1.start() proc2 = threading.Thread(target=self.Eat) proc2.start()
CheckAll()
и CheckKeysObjects()
. В
CheckKeysObjects()
я проверял не нажал ли игрок на
кнопку E
. Если нажал, то ставил переменную
self.PRESS_E
в True
.CheckAll()
, проверял, не еда ли нынешний
объект в цикле for
, если еда то проверял не
сталкивается ли с ним игрок, если сталкивается то проверял
переменную self.PRESS_E
, и если она в
True
то тогда просто удалял объект, и увеличивал
голод, Т.Е. переменную self.PLAYER["hungry"]
.
for object_now in range(len(self.OBJECTS)): .... if self.OBJECTS[object_now]["name"] == "meat": items_objects.append(object_now) is_clash = self.IsClash( x=self.OBJECTS[object_now]["x"], y=self.OBJECTS[object_now]["y"], h=self.OBJECTS[object_now]["h"], w=self.OBJECTS[object_now]["w"], x2=self.PLAYER["x"], y2=self.PLAYER["y"], h2=self.PLAYER["h"], w2=self.PLAYER["w"], ) if is_clash: if self.PRESS_E: try: self.PLAYER["hungry"] += self.HUNGRUY_ADD del self.OBJECTS[object_now] break except IndexError: pass
Скажу наперед, это все мне нужно будет переписовать, когда я буду делать инветнарь
self.PLAYER["inventory"]
, там хранятся 4 яцчейки, вот
в таком виде:
"inventory":{ "0":{"status":"space","name":"#0", "minimize_image":"#0"}, "1":{"status":"space","name":"#1", "minimize_image":"#1"}, "2":{"status":"space","name":"#2", "minimize_image":"#2"}, "3":{"status":"space","name":"#3", "minimize_image":"#3"},}
цифры
просто номера ячеек.status
этот ключ хранит в себе значение, пуста яйчейка
или нет. Если пуста то space, если же там есть предмет, то там
хранится имя предмета.name
хранит в себе имя предмета, оно будет
использовано когда игрок будет класть предмет.minimize_image
эта уменьшенная картинка предмета
которая изображается в инвентаре игрока.CheckKeysObjects()
, при нажатии на X
предмет будет бросаться на землю, и также при нажатии на кнопку
E
будет вызываться функция self.UseEat()
,
которую мы сейчас будем разбирать.self.UseEat()
представляет из себя
проход по всем ячейкам инвентаря, в поисках еды, и если еда
найдена, то она удаляется из инвентаря, и к голоду добавляется 10
единиц. Для удаление предмета из инвентаря я сделал функцию
self.DestroyItem()
, в которую подается индекс ячейки,
и вся ячейкой просто становится по дефолту пустой и без ничего.
def DestroyItem(self,index_item: str): item = self.PLAYER["inventory"][index_item] self.PLAYER["inventory"][index_item] = self.PLAYER["default_inventory_item"](index_item) self.PLAYER["inventory_must_update"] = True return item
def CheckKeysObjects(self): self.WALK_LEFT_PLAYER = False self.WALK_RIGHT_PLAYER = False self.WALK_UP_PLAYER = False self.WALK_DOWN_PLAYER = False if key("a"): self.WALK_LEFT_PLAYER = True elif key("d"): self.WALK_RIGHT_PLAYER = True if key("w"): self.WALK_UP_PLAYER = True elif key("s"): self.WALK_DOWN_PLAYER = True if key("f"): self.KEY_F = True else: self.KEY_F= False if key("e"): self.UseEat()
def UseEat(self): for inventory_item in range(len(self.PLAYER["inventory"])): if self.PLAYER["inventory"][str(inventory_item)]["name"] == "meat": if self.PLAYER["hungry"] + self.ADD_HUNGRY_COUNT < 100.0: self.PLAYER["hungry"] += self.ADD_HUNGRY_COUNT self.DestroyItem(index_item=str(inventory_item))
X
вызывается функция self.QuitItem()
, в ней проходит
цикл for по всем ячейкам инвентаря, и если ключ
["status"]
не ровняется "space"
, то эту
ячейку удаляем с помощью ранее рассмотренной функции
self.DestroyItem()
, и создаем объект на основе того
что был в ячейке, X и Y ставит игрока, как бы он бросил его возле
себя.
def QuitItem(self): for inventory_item in range(len(self.PLAYER["inventory"])): if self.PLAYER["inventory"][str(inventory_item)]["status"] != "space": self.CreateObject( img=self.PLAYER["inventory"][str(inventory_item)]["img"], x=self.PLAYER["x"], y=self.PLAYER["y"], name=self.PLAYER["inventory"][str(inventory_item)]["name"], data=self.PLAYER["inventory"][str(inventory_item)]["data"], ) self.DestroyItem(index_item=str(inventory_item)) break
main.py
.Дело было в начале 90-х, компьютера не было, но было желание поиграть в гонки ) Показал мне друг как можно на тетрадном листе бумаги в клеточку играть в гонки. А еще говорят, что есть настольная игра с такими правилами. И что чуть ли не все играли в эту игру в университете за парами.
Можно прочитать правила в Википедии, прочитав это и ничего не зная, я бы не понял ). Главное правило игры - правила перемещения машинки. Правило 2 - не можешь сделать ход попав в дорогу - ты проиграл.
Ход 1: машинка стоит на месте и может ехать в точки, которые расположены вокруг той точки в которую направлен вектор движения машинки. В начале игры вектор нулевой, никуда не направлен, значит мы можем пойти в точки вокруг машинки. Делаем ход в точку над машинкой.
Ход 2: Машинка передвигается на одну клетку вертикально и ноль клеток горизонтально. Это и есть текущая скорость машинки (1 по вертикали и 0 по горизонтали). Вектор скорости направлен на клетку сверху от машинки. Вокруг этой точки машинка может выбрать точки для выполнения хода. Сделаем ход в левую-верхнюю точку.
Ход 3: Машинка передвигается на две клетки вертикально и одну горизонтально. Вектор движения машинки соответственно изменяется.
Ход 4: Машинка передвигается на 3 клетки вертикально.
Теперь машинка движется со скоростью 3 клетки вверх и 0 клеток влево/вправо. Машинка имеет скорость и не может за один ход полностью остановиться или сделать резкий поворот в сторону. В данном случае машинка может:
Ускориться если выбрать верхние (дальние от машинки) узлы. Если ход в верхнюю-левую точку, то скорость будет 4 вверх и 1 влево. Если ход в верхнюю-правую точку, то скорость будет 4 вверх и 1 вправо.
Не изменять скорость если выбрать средний ряд. В этом случае появляется возможность поворота в сторону под более большим углом.
Понизить скорость выбрав нижний (ближайший к машинке) ряд. Стоит выбирать при приближению к резкому повороту.
Генератор случайной трассы. Трасса должна уметь поворачивать в любом направлении на любое число градусов. Элементы трассы должны быть произвольной длины и ширины.
Положение пользователя на игровой карте должно быть показано машинкой. Машинка должна уметь поворачиваться в сторону движения. Игровая карта может быть любого размера.
На игровой карте нужно различать следующие типы полей:
3.1 поле, куда машинка может сделать безопасный ход
3.2 поле, куда машинка может сделать ход, но это будет выход за
трассу
3.3 поле, принадлежащее дороге
3.4 поле, не принадлежащее дороге
3.5 текущее поле с машинкой
Показывать отрезки пройденного пути на игровой карте
Возможность начать новую игру сначала
Отображение текущей скорости
Возможность изменения настроек программы через файл
конфигурации. Список необходимых значений в файле конфигурации:
7.1 Ширина фрагмента дороги
7.2 Количество фрагментов дороги для генерации новой трассы
7.3 Максимальный угол поворота дороги влево
7.4 Максимальный угол поворота дороги направо
7.5 Минимальная длина участка дороги
7.6 Максимальная длина участка дороги
7.7 Положение машинки на игровой карте
Сделать генерацию пейзажа вокруг дороги для приятного юзабилити
Показывать дорожные знаки предупреждающие об опасном повороте
Для разработки программы выбран язык программирования C#. Целевая рабочая среда .NET 5.0. Графическая среда WPF. Среда разработки Visual Studio 2019.
Самы простой способ нарисовать трассу - воспользоваться фигурой Polyline. Но, встал вопрос, как рассчитывать нахождение принадлежности точки этой фигуре и от этого варианта пришлось отказаться. Самый надежный вариант - это представить трассу как массив прямоугольников. Для скругления поворотов трассы нужно дополнительно строить окружности в местах пересечения элементов трассы. Диаметр окружности - это ширина элемента дороги.
Для каждого сгенерированного элемента трассы нужно хранить:
Точку начала (X и Y)
Точку конца (X и Y)
Ширину элемента трассы (в данной реализации ширина трассы одинаковая для всех элементов, задел на будущее)
Длину элемента трассы
Угол наклона элемента трассы к оси oX
Положение пользователя на игровой карте отображается в виде машинки. Чтобы игровая карта могла быть любого размера, нужно чтобы при выполнении хода пользователя двигалась карта относительно машинки, а не машинка относительно карты. Это значит, что машинка будет всегда в одной точке на игровой карте, а при выполнении хода нужно запоминать отклонение машинки от ее начальной точки и перерисовывать элементы трассы учитывая данное отклонение.
/// <summary>/// Текущий сдвиг карты по осям/// </summary>int _deltaX, _deltaY = 0;
Изображение машинки сделано в виде машины с четко выделенным задом и передом. Чтобы было понятно пользователю текущее направление движения машинки нужно поворачивать машинку в сторону направления движения.
Каждый выполненный ход сохраняем в списке _pathList - список отрезков пройденного пути.
/// <summary>/// Список отрезков пройденного пути/// </summary>List<PathElement> _pathList = new List<PathElement>();
Отрезок пройденного пути должен хранить следующие данные:
Точка начала отрезка (X и Y)
Точка конца отрезка (X и Y)
Смещение машинки относительно карты в момент выполнения хода
Зная точку начала и конца отрезка можно вычислить угол наклона
отрезка к оси oX.
AC - изменение координаты X от начала до конца отрезка
BC - изменение координаты Y от начала до конца отрезка
/// <summary>/// Текущий угол наклона пути/// </summary>public double Angle{ get { if (ToY == FromY && ToX > FromX) return 90; if (ToY == FromY && ToX < FromX) return -90; if (ToX == FromX && ToY > FromY) return 180; if (ToX == FromX && ToY < FromY) return 0; if (ToY <= FromY && ToX >= FromX) return -Math.Atan((ToX - FromX) / (ToY - FromY)) * 180 / Math.PI; if (ToY <= FromY && ToX <= FromX) return - Math.Atan((ToX - FromX) / (ToY - FromY)) * 180 / Math.PI; if (ToY >= FromY && ToX <= FromX) return Math.Atan((ToY - FromY) / (ToX - FromX)) * 180 / Math.PI - 90; if (ToY >= FromY && ToX >= FromX) return Math.Atan((ToY - FromY) / (ToX - FromX)) * 180 / Math.PI + 90; return 0; }}
Для выполнения хода на игровой карте расположены узлы - кнопки (Button) на расстоянии 20 пикселей друг от друга, 39 рядов по 39 кнопок. Таким образом достигается функционал выполнения нового хода.
Было сказано, что при выполнении хода машинка остается на месте, а элементы карты меняют свое положение. Это значит, что при каждом выполнении хода необходимо проверять все узлы карты чтобы понять куда попадает в данный момент данный узел:
поле, куда машинка может сделать безопасный ход
поле, куда машинка может сделать ход, но это будет выход за трассу
поле, принадлежащее дороге
поле, не принадлежащее дороге
текущее поле с машинкой
При проверке нужно найти поля в которые машинка может сделать ход.
Для этого учитывается текущая скорость по оси X и по оси Y.
Проверка нахождения точки дороге:
Дорога представляет собой массив прямоугольников (элементы дороги)
и кругов (стыки элементов дорог).
Проверка принадлежности точки кругу рассчитывается по формуле:
Проверка принадлежности точки прямоугольнику немного
сложнее:
Из точки C (это точка, которую проверяем на принадлежность
прямоугольнику) опустим перпендикуляр на отрезок AB (точки A и B -
точки начала и конца элемента дороги соответственно). Данный
перпендикуляр - это высота треугольника. Поиск факта принадлежности
точки прямоугольнику сводится к проверке:
Существует 3 различные ситуации где может находиться точка по
отношению к прямоугольнику:
h > (ширина элемента дороги / 2) - точка не принадлежит прямоугольнику
h <= (ширина элемента дороги / 2) - точка принадлежит прямоугольнику если ABC < 90 и CAB < 90
высота h опускается не на отрезок AB, а на его продолжение. Хотя и длина высоты удовлетворяет нашей формуле, но если ABC > 90 или CAB > 90, то точка не принадлежит прямоугольнику.
Формула нахождения площади треугольника зная координаты его вершин:
С другой стороны, формула нахождения площади треугольника зная его высоту:
Данных двух формул достаточно для расчета высоты треугольника (а еще Вы могли не заморачиваться с площадью и рассчитать проще используя:
но я это понял на момент написания статьи)
Формула нахождения длины отрезка по его координатам:
Формула нахождения углов треугольника зная длины сторон треугольника:
При выполнении каждого хода записываем новый отрезок пройденного
пути.
Повторюсь, что отрезок пройденного пути должен хранить следующие
данные:
Точка начала отрезка (X и Y)
Точка конца отрезка (X и Y)
Смещение машинки относительно карты в момент выполнения хода
Угол наклона к оси oX рассчитывается автоматически
Для перерисовки пройденного пути нужно удалить нарисованные элементы пути ранее, перебрать сохраненные элементы пути и нарисовать их с помощью фигуры "Line" каждый не забывая использовать смещение данного элемента.
Для реализации данной фичи сделаем в главном меню программы элемент типа MenuItem, реализуем событие мыши Click, пропишем очиску всех элементов игровой карты, очистку сохраненных элементов списков пройденного пути, элементов дороги, обнуление переменных сдвига карты и текущей скорости машинки.
Физика прототипа игры такова, что мы имеем две скорости: скорость по оси oX, скорость по оси oY.
Под текущей скоростью машинки подразумеваем большее значение между двух скоростей по осям.
Чтобы изменять переменные в игре имеет смысл вынести значения данных переменных в конфигурационный файл:
Ширина фрагмента дороги
Количество фрагментов дороги для генерации новой трассы
Максимальный угол поворота дороги влево
Максимальный угол поворота дороги направо
Минимальная длина участка дороги
Максимальная длина участка дороги
Положение машинки на игровой карте
{ "RoadWidth": "100", "RoadElementsCount": "20", "MinAngle": "-60", "MaxAngle": "60", "MinRoadLength": "100", "MaxRoadLength": "200", "UserPosition": { "X": "400", "Y": "400" }}
В зависимости от данных настроек игровое поле выглядит по-разному:
Для более приятного впечатления к игре нужно добавить как можно большее количество различных элементов (в рамках разумности конечно):
Заполнить игровое поле "Землей" - изображение, заполняющее все поле
В момент выполнения хода мы каждый раз проверяем тип каждого узла игровой карты. В данный метод кода можно добавить генерацию "Елок" в случае если поле данного узла не принадлежит дороге. Чтобы елок не было слишком много, ограничим шанс появления елки в 20%.
var random = new Random();var elkaChance = random.Next(1, 101);if (elkaChance < 20){// рисуем елку}
Чтобы дорожное полотно было похоже на дорожное полотно можно нанести прерывистую разметку проходящую посередине элементов дорог, как раз соединяя точку начала и конца каждого элемента дороги. Чтобы данная разметка была более естесственной, воспользуемся фигурой Polyline передав фигуре коллекцию точек элементов дороги.
var polyLinePointCollection = new PointCollection();foreach (var roadElement in _roadElements){ if (polyLinePointCollection.Count == 0) polyLinePointCollection.Add(new Point(roadElement.StartPoint.X + 5 + _deltaX, roadElement.StartPoint.Y + _deltaY)); polyLinePointCollection.Add(new Point(roadElement.EndPoint.X + 5 + _deltaX, roadElement.EndPoint.Y + _deltaY));}var polyLine = new Polyline(){ Points = polyLinePointCollection, Stroke = Brushes.White, StrokeDashArray = new DoubleCollection() { 6, 4 }, StrokeThickness = 2};
Зная всю информацию об сгенерированных элементах дороги можно сравнивать каждый элемент дороги с предыдущим элементом и высчитать разницу углов поворота фигур. Если полученное значение > 70, то покажем знак опасности в точке начала элемента дороги.
Бывают случаи когда один угол +178, а второй -178. Реальная разница между данными углами всего 4. Для решения данной задачи нужно добавить условие, что угол будет опасным при разнице углов меньше 290. Формула изменится к данному виду: