# Зміст

Вступ

У своїй курсовій роботі я планую розробити базу даних для гри “Змійка”. Відповідно буде розроблено саму гру (хоча основний акцент робитиму на базі даних).

Змійка на coffeescript

Актуальність проблеми. Розвиток технологій, а зокрема вебу, дозволив створювати повноцінні багатофункціональні додатки для веб-браузерів. Серед них особливе місце посідають ігри. Адже людям завжди цікаво грати у різноманітні ігри. І коли є можливість грати у вже звичному веб-переглядачі, звичайно, користувач віддасть перевагу саме такій грі (а не нативному аналогу).

Завдання розробника полягає у створенні функціонального та повноцінного продукту. А у випадку гри, це має бути не лише зручний інтерфейс, а й великий об’єм роботи з користувацькою інформацією на сервері (найпростішим прикладом є таблиця рекордів). І для створення таких ігор найкраще підходить веб. Значною перевагою браузерних ігор є можливість виконання на різних платформах. Таким чином гру можна запустити не лише на ПК, а й на планшеті чи смартфоні.

Мета. Отримати досвід розробки бази даних для онлайн-гри на прикладі гри “Змійка”. Розробити швидку та зручну у використанні базу даних. Ознайомитися з особливостями проектування API для взаємодії з базою даних.

Завдання

Основним завданням у цій курсовій роботі є розробка самої бази даних. У моєму випадку це база даних, яка потрібна для функціонування досить простої гри “Змійка”.

На перший погляд може здатися, що завдання є дуже простим, але насправді навіть для такої примітивної гри необхідна досить складна база даних. Список вимог до гри допоможе Вам у цьому переконатись:

  • Динамічний сервер гри (який доступний онлайн)
  • Широкий вибір рівнів для гри
  • Зручний інтерфейс для вибору рівня
  • Можливість швидко і легко створювати та налаштовувати нові рівні
  • Зручна авторизація
  • Збереження та обробка рекордів
  • Можливість ділитися власними рівнями
  • Можливість зіграти кілька рівнів навіть гостю (неавторизованому користувачу)

Так як весь цей функціонал передбачає активну роботу з даними користувача (сюди відносяться і всі налаштування рівнів), то віповідно і структура самої бази даних обіцяє бути досить складною.

Основні вимоги до бази даних:

  • Висока продуктивність
  • Зручність взаємодії
  • Оптимізоване збереження рекордів
  • Можливість швидко зробити вибірку необхідної інформації
  • Збереження списку усіх рівнів (як вбудованих так і користувацьких)
  • Збереження інформації про користувачів

Як помітно, на базу даних накладається досить велика відповідальність. І аби організувати швидку роботу доведеться добре помізкувати.

Стек технологій

В теперішніх умовах пріорітетною платформою для розробки є веб. Адже з розвитком телекомунікації та розповсюдженням інтернету, все більше і більше людей переходять до використання додатків у своєму веб-браузері. Відповідно і ринок потребує нових продуктів. Таким чином, зрозуміло, що розробка гри на платформі веб-браузера є дуже актуальною.

Як і більшість веб-проектів мій проект складається з двох частин:

  • Клієнтська частина
  • Серверна частина

Така організація проекту потребує детального розуміння усього стеку доступних у сучасному світі технологій. Сюди входять: технології доступні для розробки на клієнтській та серверній сторонах, а також протоколи та методи обміну даними між клієнтом та сервером.

Клієнтська частина

Сюди входить весь клієнтський код. Це той код який буде відображатись безпосередньо для клієнта. Під клієнтом розуміється браузер користувача. З цього випливає, що мені будуть доступно тільки такі технології як:

  • HTML - для верстки
  • CSS - для оформлення
  • JavaScript - для написання всієї логіки гри

Та з досвіду написання інших проектів я прийшов до висновку, що ці технології не володіють достатньою гнучкістю та можливостями.

Справа в тому, що кожна технологія має свої вади. Наприклад в HTML відсутня модульність і взагалі неможливо організувати шаблонування. Також суттєвою вадою цієї мови розмітки є синтаксис (це суб’єктивна позиція).

Якщо брати до уваги CSS, то тут ті ж проблема модульності і синтаксису. Але в додачу отримуємо проблеми з вкладеністю селекторів, дублюванням коду і тд.

А от у JavaScript отримуємо С-подібний синтаксис та відсутність підтримки звичних класів. І змиритись з цим я теж не зміг. Також основною з проблем є такзваний callback-hell.

Насправні вище вказано лише деякі проблеми цих технологій. Але вже цей набір змушує задуматись над альтернативами. І звичайно таких альтернатив є надзвичайно багато.

Так як основний шматок коду відведеться для опису логіки, тому спершу я вирішив обрати заміну для JavaScript. Тут мій вибір одразу впав на CoffeeScript. Це невелика мова програмування, яка компілюється в JavaScript. Основними її достойностями є:

  • Чистий синтаксис (навіяний мовою Ruby)
  • Надзвичайно велика кількість “синтаксичного цукру”
  • Реалізація класичного ООП (класи, наслідування і тд)
  • Компілювання в JS без втрати читаності коду

Наступним претендентом на виліт став HTML. На його місце прийшов Jade. Його переваги:

  • Чистий синтаксис (подібний до CoffeeScript)
  • Доступні модульність, змінні, цикли, наслідування, умовні оператори і тд.

Коли ж черга дійшла до CSS, я вирішив замінити його препроцесором Stylus. Це дало змогу:

  • Використовувати чистий та зручний синтаксис
  • Розбити стилі на модулі
  • Використовувати змінні, функції та інше
  • Зручно описувати вкладеність

Що стосується сторонніх бібліотек, то я прийняв рішення не використовувати їх. Причини є досить простими:

  • Висока швидкість завантаження сторінки
  • Швидке виконання коду
  • Мала кількість місць можливого застосування

На клієнтській стороні також необхідно організувати взаємодію з сервером. І тут для розробників доступний зручний спосіб взаємодії - AJAX. У браузері цей метод взаємодії можна реалізувати з допомогою доступного інтерфейсу XMLHttpRequest. Особливість цього методу полягає у можливості взаємодіяти з сервером асинхронно та без перезавантаження сторінки.

Серверна частина

В області технологій, які доступні на сервері взагалі можна заблукати. Але тут на допомогу мені прийшли тренди, які актуальні серед веб-розробників. Останнім часом високої популярності набула платформа Node.js. Ця платформа дозволяє писати серверний код на JavaScript (що само по собі є досить привабливо). Та у моєму випадку на заміну JS приходить CoffeeScript.

Варто зазначити, що Node.js є досить дружелюбною та зручною платформою. Адже тут (на відміну від браузера) з’явився дуже зручний менеджер пакетів, вирішено проблему модульності, додано можливість повноцінної роботи з файловою системою та інші системні функції притаманні для мови програмування загального призначення.

Для написання самого сервера за основу було взято міні-фреймворк Express.js. Він надає чудові можливості для написання API. Також присутні зручні інтеграції з популярними шаблонізаторами та бібліотеками.

Системою керування базами даних було обрано MongoDB. Причин обрати саме її є дуже багато. Вирішаючими стали:

  • Пряма і повна підтримка нативних JS-об’єктів (у вигляді JSON)
  • Висока продуктивність та легка розширюваність
  • Надзвичайно зручний принцип взаємодії з базою даних

Використання цієї СУБД дозволило позбутись зайвої абстракції у вигляді SQL. Відпала необхідність описувати схеми таблиць і тд. В умовах платформи Node.js використання MongoDB є найлогічнішим. Адже можна просто зберігати об’єкти у БД так як вони є у програмі (JSON - об’єкти).

Отож завдання сервера:

  • Робота з БД
  • Компілювання та віддача клієнтського коду
    • Обробка шаблонів
    • API (прикладний програмний інтерфейс)

Для того аби автоматизувати процес збірки проекту я обрав Gulp.js. Він займатиметься:

  • Динамічною компіляцією JS-коду гри
  • Слідкування за змінами у файлах проекту
  • Перезавантаження клієнта або сервера при відповідних змінах
  • Оптимізацією та мініфікацією клієнтського коду

Проектування структури бази даних

Для оптимального збереження інформації у базі даних необхідно провести розподіл сутностей за значенням. Це дозволить утворити прозору структуру збереження інформації та виділити зв’язки між даними.

Аби забезпечити швидку та зручну взаємодію з БД потрібно детально продумати її структуру. Це дозволить уникнути різноманітних проблем у подальшому.

На початковому етапі потрібно визначити та виділити основні сутності з якими необхідно взаємодіяти у базі даних.

У зв’язку з використанням такої СКБД як MongoDB усі сутності називаються колекціями (аналог таблиць), а записи у цих колекціях називаються об’єктами. Простими словами колекція - це набір об’єктів, які можна об’єднати за певною суттю. Важливо зазначити, що дотримуватись жорсткої типізації у MongoDB не потрібно, тому і сказати, що кожен об’єкт обов’язково матиме певний набір полів теж не можна.

Виділення сутностей

Так як у моїй грі передбачається активна робота з даними користувача, то доречним буде виділити окрему колекцію, яка буде займатись обробкою та збереженням цих даних. Зберігатиметься ця інформація у колекції “Користувачі”.

Наступним видом даних є таблиця рекордів. Вона буде зберігатись у колекції “Рекорди”. У цій колекції зберігається список рекордів усіх користувачів.

Ще одним видом даних є рівні, які зберігатимуться у колекціх “Рівні”. Тут зберігатимуться як рівні якористувачів (створені самими користувачами) так і базові рівні (які доступні всім користувачам). Рівнем являється набір налаштувань для гри (таких як висота та ширина карти і тд).

Об’єднання сутностей

Тепер можна приступити до утворення зв’язків між сутностями. Одразу можна виділити такі зв’язки “Користувач <– Рекорд –> Рівень”, “Користувач <– Рівень”.

Інформація у колекції “Рекорди” пов’язана з інформацією у колекції “Користувачі” (це зв’язок багато до одного) та з таблицею “Рівні” (однин до одного). Це означає, що кожен рекорд належить певному користувачу (користувач може мати список з кількох рекордів, які встановлені на різних рівнях). Кожен запис міститиме посилання на користувача (який встановив рекорд), посилання на рівень (на якому було встановлено), саме значення рекорду та час його встановлення.

Як і у випадку з рекордами у колекції “Рівні” є зв’язок з даними користувача, адже кожен користувач може створювати свої рівні. Але додатковим ускладненням є те, що користувачі можуть ділитись своїми рівнями.

Загальний опис архітректури БД

На основі попередніх кроків можна описати набір усіх сутностей, полів та зв’язків між ними. Отриману базу даних можна зобразити так:

Користувачі

  • ID
  • Ім’я
  • Пароль
  • Час реєстрації

Рівні

  • ID
  • Назва
  • ID власника
  • {налаштування}

Рекорди

  • ID
  • Значення
  • ID власника
  • ID рівня
  • Час встановлення

Реалізація

Маючи повністю спроектовану базу даних, можна приступити до реалізації серверної логіки.

Потрібно зауважити, що саму гру уже написано. У ній вже реалізовано всі необхідні функції за виключенням усієї логіки що стосується взаємодії з сервером.

За цих умов можна приступити до створення сервера. До завершення роботи над клієнтом можна повернутись тільки після готової реалізації API.

Проектування API

Першим кроком буде проектування API, для взаємодії з клієнтом. API (Application Programming Interface) є інтерфейсом, за допомогою якого, у випадку клієнт-серверних застосунків, можна реалізувати методи взаємодії між клієнтом та сервером. Це дозволяє логічно розділити та упорядковати передавання та отримання даних.

Виходячи з поставленої задачі, API, який надається сервером клієнту, повинен включати методи:

  • Для авторизації користувача (реєстрація, вхід та вихід з системи)
  • Створення, видалення та збереження рівнів
  • Виведення переліку доступних рівнів
  • Збереження та вивід рекордів

Реалізація серверної логіки

Розпочнемо з авторизації. Спершу потрібно написати функцію, яка зберігатиме користувача в базі даних:

save_user = (name, pass) ->
  time = Date.now()
  db.users.save
    name: name
    pass: pass_hash(pass, time)
    time: time

Ця функція приймає ім’я та пароль нового користувача. Виконує збереження у колекції users об’єкту, яки містить ім’я користувача, пароль у захищеному вигляді та час реєстрації.

У випадку використання SQL бази даних, розмір коду був би набагато більший. Додатково потрібно створювати нову таблицю:

CREATE TABLE IF NOT EXISTS users (
  id int NOT NULL AUTO_INCREMENT,
  name varchar(530) NOT NULL UNIQUE,
  password varchar(42) NOT NULL,
  time int NOT NULL,
  PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

А сам запит до БД виглядав би приблизно так:

INSERT INTO users VALUES (
  NULL,
  "name",
  "password",
  "time"
)

Тут і проявляється вся зручність MongoDB.

Тепер можна створити функцію, яка займається обробкою переданих через API параметрів:

app.post '/register', (req, res) ->
  regex = /^[a-z0-9_]{3,42}$/
  name = req.body.name.trim()
  pass = req.body.pass.trim()

  if name.match(regex) and pass.match(regex)
    db.users.findOne {name: name}, (user) ->
      if not user?
        save_user name, pass
        res.cookie 'snake_user_name', name
        res.redirect '/'
      else res.redirect '/register'
  else res.redirect '/register'

Тут отримані параметри було перевірено на валідність, далі йде перевірка чи ім’я цього користувача ще не зайняте (існує у базі даних) і тільки після цього користувач зберігається у базі даних.

Коли користувач зареєстрований необхідно передбачити вхід.

app.post '/login', (req, res) ->
  name = req.body.name.trim()
  pass = req.body.pass.trim()

  db.users.findOne {name: name}, (user) ->
    if user.pass is pass_hash(pass, user.time)
      res.cookie 'snake_user_name', name
      res.redirect '/'
    else res.redirect '/login'

Де перевіряємо на наявність у базі даних пари імені та пароля. І тільки тоді надаємо доступ.

Функція виходу дуже проста:

app.get '/logout', (req, res) ->
  res.clearCookie 'snake_user_name'
  res.redirect '/'

З автентифікацією завершено, можна перейти до додавання рівнів.

save_level = (level, user_name, lvl_name, cur_user = user_name) ->
  return if user_name isnt cur_user
  db.users.findOne {name: user_name}, (user) ->
    db.levels.findOne {name: lvl_name, uid: user._id}, (lvl) ->
      level._id = lvl._id if lvl
      level.name = lvl_name
      level.uid = user._id
      db.levels.save level

Функція збереже переданий рівень в БД. Особливим є те, що можна як додавати нові рівні, так і зберігати наявні.

На прикладі цієї функції можна обгрунтувати недоліки JavaScript, адже на цій мові аналог виглядатиме так:

var save_level = function(level, user_name, lvl_name, cur_user) {
  if (cur_user == null) {
    cur_user = user_name;
  }
  if (user_name !== cur_user) {
    return;
  }
  db.users.findOne({ name: user_name }, function(user) {
    db.levels.findOne({
      name: lvl_name,
      uid: user._id
    }, function(lvl) {
      if (lvl) {
        level._id = lvl._id;
      }
      level.name = lvl_name;
      level.uid = user._id;
      db.levels.save(level);
    });
  });
};

Виглядає досить монструозно і читається на порядок важче. А вкінці і проявився callback-hell.

Тепер потрібно передбачити обробники обох ситуацій. Для додавання рівня є обробник:

app.post '/new_level', (req, res) ->
  lvl_name = req.body.name.trim()
  if lvl_name.match(/^[a-z0-9_]{1,23}$/)
    save_level get_lvl_from_file(1), user, lvl_name, user

Для збереження існуючого рівня:

app.post '/save_level/:user/:level', (req, res) ->
  user = req.cookies.snake_user_name or 'default'
  level = req.body
  if user isnt 'default'
    save_level level, req.params.user, req.params.level, user

Коли є рівні, а їх може бути багато, потрібно подумати як виводити їх список. Список усіх рівнів буде виводитись на початковому екрані. І відповідно обробник виглядатиме так:

app.get '/', (req, res) ->
  name = req.cookies.snake_user_name or 'default'
  db.users.findOne {name: name}, (user) ->
    db.levels.find(uid: user._id).sort {name: 1}, (levels) ->
      names = for lvl in levels then lvl.name
      res.render 'levels', levels: names, user: name

По цьому списку можна перейти на якийсь окремий рівень. Для відображення рівня є обробник:

app.get '/:user/:level', (req, res) ->
  user_name = req.cookies.snake_user_name or 'default'
  level = req.params.level
  db.users.findOne {name: req.params.user}, (user) ->
    db.levels.findOne {name: level, uid: user._id}, (lvl) ->
      res.render 'game', lvl if lvl?

Рівень є, користувач є, залишилась робота з рекордами. Так зберігаємо рекорд (для заданого користувача на заданому рівні):

save_score = (score, user_name, lvl_name) ->
  return if user_name is 'default'
  db.users.findOne {name: user_name}, (user) ->
    db.levels.findOne {name: lvl_name, uid: user._id}, (lvl) ->
      db.scores.findOne {uid: user._id, lid: lvl._id}, (last_score) ->
        if last_score?
          return if last_score.value > score.value
          score.uid = user._id
          score.lid = lvl._id
          score._id = last_score._id
        score.time = Date.now()
        db.scores.save score

І невеличкий обробник:

app.post '/save_score/:user/:level', (req, res) ->
  score = req.body
  save_score score, req.params.user, req.params.level

Ну і останнім є видалення рівня. Так його можна видалити:

app.get '/delete_level/:user/:name', (req, res) ->
  return if req.params.user is 'default'
  db.users.findOne {name: req.params.user}, (user) ->
    db.levels.findOne {uid: user._id, name: req.params.name}, (lvl) ->
      db.scores.remove {lid: lvl._id}
      db.levels.remove {_id: lvl._id}

Крім рівня потрібно видалити і всі рекорди, які до нього прив’язані.

На цьому розробка серверної частини і завершилась. Звичайно, на прикладах наведено лише схематичний код. Насправді, аби забезпечити стабільну роботу програми, необхідно передбачати багато помилок та некоректностей.

Також опущено велику частину, яка займається будуванням самої сторінки. Можу лише сказати, що мені вдалось з допомогою шаблонізатора Jade і препроцесора Stylus відділити клієнтський код від серверного, що дуже спростило читання та розуміння серверної логіки.

Повний початковий код програми доступний публічно і розповсюджується як OpenSource (ліцензія MIT). Тому завжди можна ознайомитись з всією структурою проекту. А проект є досить об’ємним. І навіть з використанням мого стеку технологій, його розмір сягає приблизно півтори тисячі рядків. Тому зрозуміло, що весь код програми приводити в курсовій роботі просто недоцільно.

Демонстрація роботи програми

Звичайно, будь-яка програма спершу має якось інсталюватись на робочій машині. У випадку мого додатку, його інсталяція необхідна лише на одному сервері, а всі інші (клієнти) можуть повноцінно нею користуватись з допомогою веб-переглядача. Не є обов’язковим наявність інтернету, адже сервер можна запустити локально, а клієнти локальної мережі спокійно можуть до нього під’єднатись.

У цьому розділі я постараюсь вичерпно описати весь процес використання програми, яки складається з встановлення, запустку та самого використання.

Процес інсталяції

Для серверів та й взагалі у більшості випадків, для встановлення подібного програмного забезпечення потрібно активно користуватись командним рядком (далі термінал). Як вже було зазначено, вихідний код моєї програми вільно розповсюджуєтья. Репозиторій програми знаходиться на сайті GitHub. В репозиторії також скорочено описано процес встановлення.

Оскільки мій додаток працює на платформі Node.js, то відповідно на сервері повинно бути встановлене це програмне забезпечення. Завантажити інсталятор і ознайомитись з процесом встановлення можна на офіційному сайті Node.js.

Далі необхідно встановити програмне забезпечення для СУБД MongoDB. Аналогічно, вся інформація та інсталятор знаходяться на офіційному сайті MongoDB.

Також на комп’ютері має бути встановлена система контролю версій GIT, адже саме її я використовую у своєму проекті. Знову ж таки знайти інформацію, як її встановити не складає ніяких проблем, для цього достатньо скористатись пошуком в інтернеті.

Після успішного встановлення цих продуктів перейдемо до встановлення мого додатку. Спершу потрібно відкрити вікно термінала і перейти в папку де можна створити проект. Далі виконаємо команду, яка завантажить з репозиторію на GitHub весь вихідний код:

$ git clone https://github.com/rapkin/snake

В поточній папці має з’явитись нова папка під назвою snake, потрібно перейти в неї. Далі потрібно завантажити залежності до проекту, зробити це надзвичайно просто , потрібно лише виконати дві команди:

$ npm install
$ npm install -g gulp coffee-script

На цьому весь процес встановлення завершено. Як виявилось, це не так вже й складно.

Запуск програми

Аби запустити програму, сервер баз даних MongoDB вже має бути запущено.

У моєму проекті доступно два види середовищ запуску, які призначені для:

  • Процесу розробки (на локальній машині розробника)
  • Запуску на сервері

Для зручності та автоматизації розробки я використовую Gulp.js. І на машині розробника запуск відбувається з допомогою однієї команди:

$ gulp

Це розпочне процес збирання проекту і автоматично запустить локальний сервер програми.

Після запуску можна спокійно відкривати проект у редакторі та вносити зміни в код. В процесі написання та збереження коду, зміни будуть автоматично відслідковуватись і відбуватиметься перезавантаження сторінки або цілого сервера (якщо у його коді відбудуться зміни).

У випадку запуску на сервері потрібно виконати команду

$ coffee server

Це просто запустить сервер гри (ніякого процесу збирання не відбуватиметься).

Використання програми

Коли сервер запущено можна перейти на сторінку з грою (по замовчуванню за адресою localhost:4242). І першою буде сторінка з переліком рівнів, які доступні гостю.

Перелік рівнів гостя

Можна спробувати пограти у гру на базових рівнях, але при перезавантаженні сторінки рекорди буде втрачено, а якщо було відредаговано рівень, зберегти його у базі даних не вдасться. Так виглядає сторінка з грою:

Сторінка з грою

Аби отримати доступ до вказаних функцій необхідно пройти авторизацію (реєстрацію у випадку відсутності аккаунту, або вхід). Для входу потрібно натиснути кнопку LOGIN, після чого користувач потрапить на сторінку з формою входу.

Сторінка входу

Після введення та відправки логіна та пароля, при успішному вході, користувач потрапить на сторінку зі списком його рівнів

Перелік рівнів користувача

Тут появилась можливість видалити будь-який рівень, або додати новий. Аби додати новий, потрібно натиснути велику зелену кнопку з плюсом. Після цього користувач портапляє на сторінку з формою для вводу назви рівня.

Створення нового рівня

Після відправки форми в списку рівні появиться новий рівень (з вказаною назвою). Користувач може почати гру на цьому рівні, відредагувати його, або видалити. Також його рекорди на кожному з рівнів зберігаються у базі даних.

Думаю на цьому огляд роботи програми можна завершити.

Висновок

Розробка гри та й бази даних для неї виявилась дуже захоплючим процесом. Під час роботи над своєю курсовою роботою мені вдалось ознайомитись та активно попрацювати з набором чудових технологій. А написання деяких частин програми (особливо самої гри) власноруч виявилось непоганою розминкою для мозку.

У своїй курсовій роботі я намагався зробити акцент на сучасних тенденція та трендах у світі розробки. В процесі розробки мені довелось активно ознайомлюватись та обирати підходящий інструмент з великого переліку доступних (як виявилось це теж досить складне завдання).

Не менш важливим є досвід проектування архітектури повноцінного додатку. Адже мало вміти писати код. Як показує досвід інших розробників значну частину процесу розробки відводиться на проектування архітектури. І це дуже важливо, адже погане проектування приводить до нестерпних наслідків та подальшого переписування та перепроектування (що звичайно ж краде набагато більше часу та зусиль).

Список використаної літератури

  1. Офіційна документація до СУБД MongoDB - http://docs.mongodb.org/manual/
  2. Офіційна документація по Node.js - https://nodejs.org/api/
  3. Офіційна документація мови CoffeeScript - http://coffeescript.org/
  4. Документація до неофіційного драйверу MongoDB для Node.js (Mongojs) - https://github.com/mafintosh/mongojs
  5. Офіційна документація до бібліотеки Express.js - http://expressjs.com/4x/api.html
  6. Офіційна документація по шаблонізатору Jade - http://jade-lang.com/reference/
  7. Офіційна документація до препроцесора Stylus - http://learnboost.github.io/stylus/