Гуру слів, проблеми з Unity3d, і щасливий фінал у підсумку

Ідея гри та її особливості

Напевно, всі грали в якісь ігри, де потрібно складати слова. Хто не знає, що таке кросворд? Города.? Ще популярна гра (не пам'ятаю назви) — дається довге слово (мені чомусь запам'яталося "електрифікація"), і з нього складаються всілякі слова ("електрик", "фікція") і т. д. загалом, таких ігор є безліч — як і класичних (настільні ігри, листок і ручка), так і електронних.
Але нам завжди мало, ми хочемо більше і краще, чи не так?
На Заході є популярна настільна гра, де потрібно складати слова. Називається Scrabble, тут більше інформації. Правила прості — на квадратному ігровому полі спочатку є одне слово. Кожен гравець має певні фішки з літерами. У свій хід, він повинен викласти одну фішку так, щоб вийшло нове слово (чи кілька слів). Кожна буква має свою цінність (у балах), тому деякі слова цінніші, ніж інші. Рідкісні букви (наприклад, "Ф") дають більше балів.

Мій друг багато грався в Скреббл, і у нього виникла ідея — а чому б не змінити правила гри, щоб вона стала динамічніше? Коротко це звучить так — додай одну букву і збери нове слово.
Сказано — зроблено.

Приклад: є ігрове поле, де є слово "ЛАЗ". Додаємо букву "До" — отримуємо слово "ЛАК". Дуже просто, чи не так?

Деякі ігрові обмеження можна складати лише іменники, прибрані власні імена, географічні назви, імена, і т. д. Слова, які можна використовувати лише в множині, залишені (наприклад, "окуляри").
Я зробив прототип гри за пару днів, і вона мене захопила. З жахливим дизайном, з неповним словником, з тупим ІІ — але це було іграбельних і затягивающе.
І я серйозно взявся за розробку.
Залізний противник
З чого починається розробка гри? У нашому випадку — з іграбельного ядра, яке можна розширити. У нас — це гра з комп'ютером.
Взагалі ми планували кілька режимів гри. Але перший режим, який повинен бути у будь-якому випадку — це гра проти комп'ютера. Живих супротивників може не бути поруч, і тут комп'ютер наш виручить. Тому я почав написання гри з написання штучного інтелекту (ШІ).
Як взагалі працює ІЇ у нашій грі, коли йому переходить хід?
Перший етап — це пошук крайніх клітин. Крайньої кліткою я обізвав таку клітку, в якій є буква, і в якій по вертикалі або горизонталі є хоча б одна вільна клітинка. На картинці нижче такі клітини виділені червоними крапками.

Навіщо нам потрібні такі клітини? А тому що ми можемо доставити букву в порожню клітину (жовта точка), і отримати нове слово. Зверніть увагу, що слово зібрати можна двома способами. Можна поставити букву, почати з неї ж, і зібрати слово ГАЗ (Р перша). Або ж поставити букву останньої, і зібрати слово ЗАД (Д остання).

Другий етап — після того, як ми знайшли всі крайні клітини. Завдання наступна — скласти різноманітні ланцюжки, і перевірити, чи ми можемо скласти нове слово додавши попереду або позаду нову букву. Наприклад, для прикладу на картинці вище можливі наступні ланцюжка:
  • Л
  • ЛА
  • ЛАЗ
  • А
  • ЛА
  • АЗ
  • З
  • ЗА
  • ЗАЛ
Очевидно, що зі збільшенням поля і заповнення поля кількість і довжина ланцюжків буде зростати. Тому було прийнято вольове рішення навіть на максимальному рівні складності обмежити довжину ланцюжків 10 символів. Як показала практика, навіть при такому обмеженні комп'ютер без проблем розправляється з людиною (як мінімум зі мною і моїм другом).
Третій етап — перевірити, які ланцюжка є валідними (тобто, можна скласти нове слово). Я думаю, ви здогадалися, яка проблема здається очевидною — ми не можемо використовувати тупий пошук, не вистачить ніяких ресурсів порівнювати мільйони разів рядка (якщо шукати, просто перебираючи послідовно елементи). На розум відразу приходить використовувати бінарний пошук — благо це словник, і ми можемо порівнювати слова.
загалом, так я і зробив. Правда, я це все ще оптимізував. В чому суть — я хитро відсортував словник. Наприклад, у нас є ланцюжок довжиною в 5 символів. Очевидно, що нове слово буде довжиною рівно в 6 символів. Тобто, по довжині ланцюжка ми точно знаємо довжину слова. Тому словник відсортований спочатку по довжині слова, а всередині — ще за алфавітом.
Ось приклад. У нас є ланцюжок СА. Якщо ми допишемо в кінець букву Д — вийде слово САД, і це дуже добре. Тому що ми робимо:
  • шукаємо в словнику всі слова, довжиною в 3 букви (довжина ланцюжка + 1)
  • серед цих слів шукаємо все, що починаються на СА
Уважні помітили, що так ми можемо знайти лише ті слова, які можна скласти методом дописування літери до ланцюжку в кінці. А як же ті слова, які можна скласти, дописавши букву на початку ланцюжка? Адже ми можемо скласти слово, і додавши спереду букву — наприклад, ОСА. Виходить, нам потрібно шукати трилітерні слова, які закінчуються на СА -а словник у нас на таке не заточений.
Виходом став ще один словник, який відсортований також спочатку по довжині слів, а далі — по ДРУГІЙ букві слова. Ось витяг з нашого словника:

ярд
орк
йорж
оса
позов
вісь
дуб
зуб
куб
...
Виходить, в обох випадках ми шукаємо не повне слово, а підрядок (ланцюжок).
І, нарешті, фінальний етап — це сортування отриманих слів за їх ціну (вартість вважається як сума балів, помножена на довжину слова), і видача результату в залежності від складності.
Про складність. Є гарна фраза — завдання ІЇ не виграти, а красиво здатися. В даному випадку ця фраза себе повністю виправдовує. Дати ІІ грати на повну — означає крах людини, бінарний пошук і сучасні смартфони роблять перебір варіантів швидко. Тому у комп'ютера є певні рамки — він повинен триматися від людини +- опреленное кількість балів. Там є ще додаткові моменти — наприклад, він не може збирати слова, які менше визначеної вартості.
до Речі, в режимі складності "Ультра" всі обмеження з комп'ютера зняті. Якщо хтось знає багато довгих слів з буквами Ь, Ф, Ъ — зіграйте, це буде цікавий поєдинок.
Ще один лайфхак, доступний людині, але недоступний машині. Можна скласти слово, і додавши букву в середину цього слова. Наприклад, у нас є ланцюжок М*МА (зірочка — це пуста клітка). Якщо ми додамо замість зірочки А — отримаємо слово МАМА. Так от, комп'ютер так не шукає — користуйтеся цим, обманюйте залізяку (я так роблю постійно).
Як завершення, хочу сказати наступне. Я боявся, що ШІ буде гальмувати — і спочатку взявся оптимізувати його по максимуму — ніяких рядків, масиви char-ів, бінарний пошук з оптимизациями під конкретну задачу. Але нічого не гальмувало — взагалі. Тобто, навіть мій давній android-смартфон без затримок відпрацьовує завдання перебору навіть на полі великого розміру. Висновок — не таке вже й слабке залізо на смартфонах.
Ігрова модель. Робота зі словниками
Перше, що потрібно в іграх такого роду — це словник. Чим більш повний він буде, тим менше проблем буде від гнівних користувачів. Причому якщо не буде якогось рідкісного слова, це не велика проблема. А якщо не буде слова "кот" — це дуже погано. Тому перший етап — складання словника. Ах так, два словника — гра-то і російською, і англійською є.
Спочатку я прочитав знайшов базу на 37 тисяч слів. Я зрадів. Але рано. Ця база був результат парсера, написаного автором давним-давно. Як результат, багатьох слів не було, а багато були невідповідні (були власні імена, і — о жах — прикметники). Це було погано. Мій друг скинув мені ще пару файликів зі словами — там діло було ще гірше — траплялися цифри, і все в такому роді.
Я написав утилітку на java, якій на вхід я даю "сміттєвий" файл з усіма словами, і даю список допустимих символів. А вона викидає зі словника повтори, прибирає слова, в яких є неправильні літери, відрізає занадто довгі слова (навряд чи хтось складе в такій грі слово більше 15 літер), і сортує їх у алфавітному порядку (не зовсім алфавітному, але про це пізніше). Після цього життя стало легше. З'явилася нова порція слів — додав їх в кінець "сміттєвого" файлу, запустив утилітка — на виході готові до вживання слова.
Хочу сказати, що скільки ми не шукали словників, всіх слів ми не знайшли (та й напевно не знайдемо ніколи). Ми багато грали в гру самі, і постійно виписували на листочок слова, які потім я вручну додавав в гру. Цей процес продовжується і зараз — хоч нові слова ми знаходимо вже рідше і рідше. Я підозрюю, що просто у нас обмежений словниковий запас :)
Наступний етап — у нас був словник, де було багато-багато слів, і були такі слова, про які ми нічого не знали (ви знаєте, що таке ТРОТ? А це такий варіант алюру, кінь так скаче. Я не знав особисто, на жаль). Що робити з такими словами? Просто викинути не варіант, тому що є люди, які розумніші за мене і мого друга, і знають, що таке цей звір ТРОТ. Тому я накидав за пару годин утилітка, яка зліва список слів, а праворуч браузер для виділеного слова:

Особлива фішка — це англійська мова. Половина слів там одночасно і іменники, і ще половина частин мови. А оскільки ні я, ні мій друг не є носіями мови, нам складно було працювати з таким словником. Ми обмежилися тим, що короткі слова (до 5 букв включно) перевірили на en.wiktionary, щоб там було noun. Загалом, підхід не дуже, але самі погані слова (типо uivag) ми викинули. Одразу на запитання, де взялося таке слово — це не я, це парсер так напарсил, а ми потім расхлебывали :).
Ми прекрасно усвідомлюємо, що в словниках є неправильні слова, і ми потихеньку чистимо словники. Але як бути з тими словами, які не потрапили до словника, а вводять їх? Рішення було на поверхні — якщо людина збирає слово, якого немає у словнику, я відправляю його на Google Analytics. План простий — експортуємо дані з csv, я пишу софт, який вычленит унікальні слова з цього потоку. Цей же софт веде базу, де ми будемо позначати слова як переглянуті (якщо нам відправлять 768 разів слово ПОЛКУ, я задолбусь 768 разів його додавати). І ми потихеньку будемо удосконалювати свій словник.
Також в найближчому оновленні ми додамо можливість вести пользовательський словник. Якщо користувач точно впевнений, що таке слово є в словнику, а ось у нас його немає — він зможе додати це слово.
Я ще хотів взяти готові словники (у форматі dict), і якісь бібліотеки для роботи з ними. Можливо, для інших мов ми так і зробимо. Але, боюся, навіть у цьому випадку ми не покриємо на 100% всі потрібні нам слова мови.
загалом, словники — це дуже непросто. Точніше, поганий словник — це взагалі нескладно, а ось хороший словник — це взагалі складно. Зробити з першого разу його не вийде, і ми зараз на стадії постійного їх поліпшення.
Дизайн
Дизайн — це складно. Дизайн — це не моє.
У друга є художник, який намалював нам те, що ви бачите на скріншотах. Це не той глянець, які ви бачите в Candy Crush. З іншого боку, поточний стиль приємний оку, і підходить грі такого типу. Дизайном я задоволений.
Як і належить, між дизайном і його розміром є деякі проблеми. Великі бэкграунды я максимально перетиснув, зберіг в jpeg — я зберіг розмір apk, але нічого не поліпшив відеокарті. А ось елементи інтерфейсу я максимально робив 9patch. Так я домігся приблизно однакового відображення на різних екранах.
Якщо ви відкриєте гру, і відкриєте будь діалогове вікно — це 9patch. І синій бекграунд, і кнопки — це 9patch. Для себе я зрозумів, що я буду максимально наполягати на 9patch там, де це можливо.
А ось картинка для тих, хто не може відкрити гру:

Судячи з усього, ми виграли.
Перша версія — Unity3d
я Почав писати на Unity3d. У мене і маленький досвід був (перед цим ми випустили іншу іграшку на Unity3d, але це вже інша історія), і 5-я версія безкоштовна, і класна система UI з коробки… загалом, спокусився я.
Що сказати. Гра гальмувала. На комп'ютері все ок, але на телефоні — постійно були дрібні лаги, хоч я і нічого не робив. Спочатку я грішив на неоптімізірованность, але ближче до релізу я захвилювався — начебто і іграбельних все, але дрібні такі лаги псували всю малину. І я почав копати матеріали з оптимізації.
Спочатку я заміряв FPS. Він був поганий — він стрибав. Від 10 до 60. Коли він був 10 — грати було погано, скролінг панелі з буквами був нечіткий і ривками. Я засмутився, і почав застосовувати всі поради з інтернету. Я погіршив якість текстур, повырубал V-sync, встановив нижче налаштування якості. Я примусово виставив сумісність лише з OpenGL 2 (писали, що це допомагає). Загалом, я перепробував усе, що міг.
Потім я зробив наступне — я створив нову порожню сцену. Туди помістив лічильник FPS. І запустив гру. FPS був близько 50 — я трохи засмутився, тому що порожня сцена чомусь отьедает вже 10 кадрів — але ще не дуже, тому що він (FPS) майже не стрибав. Після цього я додав на сцену Canvas, додав Image (повноекранний бекграунд 480*800, з самим примітивним шейдером — здається, Vertex Unlit) — і запустив знову. FPS впав до 40. Я взагалі засмутився.
У мене не було проблем з архітектурою ігри, C# схожий на java, подекуди навіть покомфортніше (привіт, властивості), а подекуди- гірше немає такої свободи у роботі з enums). Я написав без проблем весь код, зробив весь UI. Але я нічого не міг зробити з гальмами. Я порився в інтернеті, і знайшов купу схожих проблем в інших людей. Вони писали, що після появи 5-й версії Unity3d їх проекти почали жахливо гальмувати. Судячи з усього, я ще одна жертва. Я пробував ставити Unity 5.2 (спочатку в мене була 5.3), пробував і бету 5.4. Безрезультатно.
Я розчарувався в Unity3d, надія зарелизить гру через день-два звалилася. Я не міг випустити гальмуючу пазл-гру, внутрішній хтось у мені не давав це зробити. Я вирішив повернутися до витоків.
Перехід на libgdx
Я створив новий libgdx-проект, і почав портування гри. libgdx — це те, що мене не підводило. Деякі речі там робити довше (тому що все ручками), але от до продуктивності питань у мене не виникало ніколи.
Архітектура вийшла, звичайно, зовсім інша, ніж на Unity3d. Єдина частина, яку я переніс без змін — це робота зі словниками і ІІ. Я тупо скопіював файли з Unity3d, змінив розширення .cs .java, і поправив код. І це нормально працювало. Це доводить, що мова не має значення — важливий алгоритм і ідея.
Головний клас я зробив сінглтоном, і обізвав його Core:
Core.javapackage ua.com.umachka.word.guru;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import ua.com.umachka.word.guru.localize.Localize;
import ua.com.umachka.word.guru.screen.game.logic.WordDict;
import ua.com.umachka.word.guru.screen.game.logic.ai.SolutionSearcher;
import ua.com.umachka.word.guru.screen.splash.SplashScreen;
import ua.com.umachka.word.guru.settings.Settings;
public class Core extends Game {
private static Core instance = new Core();
private SpriteBatch batch;
private Assets assets;
private Localize localize;

private WordDict dict;
private PlatformInteraction platformInteraction;

private SolutionSearcher searcher;

private float appTimeInSeconds = 0f;

private Core() {}

public static Core getInstance() {
return instance;
}

public void setPlatformInteraction(PlatformInteraction platformInteraction) {
this.platformInteraction = platformInteraction;
}

public PlatformInteraction getPlatformInteraction() {
return platformInteraction;
}

@Override
public void create () {
batch = new SpriteBatch();

assets = new Assets();
assets.loadAll();

localize = new Localize();
localize.loadLanguage(Settings.getInstance().getLanguage());

dict = new WordDict();
dict.load("en");

searcher = new SolutionSearcher();
searcher.setDict(dict);

Gdx.input.setCatchBackKey(true);

setScreen(new SplashScreen(batch));
}

public SpriteBatch getSpriteBatch() {
return batch;
}

public TextureRegion getRegion(String regionName) {
return assets.getRegion(regionName);
}

public BitmapFont getFont(int size) {
return assets.getFont(size);
}

public String text(String tag) {
return localize.text(tag);
}

public Localize localize() {
return localize;
}

public WordDict getDict() {
return dict;
}

public Assets assets() {
return assets;
}

public SolutionSearcher getSearcher() {
return searcher;
}

@Override
public void render() {
appTimeInSeconds += Gdx.graphics.getDeltaTime();
super.render();
}

@Override
public void dispose() {
platformInteraction.reportGameEvent("app-session-length: " + (int) appTimeInSeconds + " sec");
super.dispose();
}

}
Написав базовий клас для всіх екранів:
BaseScreeen.javapackage ua.com.umachka.word.guru.screen;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
public class BaseScreen extends ScreenAdapter {
private Stage stage;
private SpriteBatch batch;
class BackPressListener extends InputListener {
@Override
public boolean keyDown(InputEvent event, int keycode) {
if (keycode == Keys.ESCAPE || keycode == Keys.BACK) {
onBackOrEscapePressed();
return true;
}
return false;
}
}

public BaseScreen(SpriteBatch batch) {
if (batch == null) {
batch = new SpriteBatch();
}
stage = new Stage(new ScreenViewport(), batch);
stage.addListener(new BackPressListener());
}

@Override
public void render(float delta) {
Gdx.gl.glClear( GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT );

stage.act(delta);
stage.draw();
}

@Override
public void show() {
Gdx.input.setInputProcessor(stage);
}

public void addActor(Actor actor) {
stage.addActor(actor);
}

public void onBackOrEscapePressed() {
}

public Stage getStage() {
return stage;
}

public SpriteBatch getBatch() {
return batch;
}

public void postTask(float delay, Runnable task) {
stage.addAction(Actions.sequence(Actions.delay(delay), Actions.run(task)));
}

public void repeatTask(float interval, Runnable task) {
stage.addAction(Actions.forever(Actions.sequence(Actions.run(task), Actions.delay(interval))));
}

public float getHeightPixels(float percent) {
return stage.getHeight() * percent;
}

public float getWidthPixels(float percent) {
return stage.getWidth() * percent;
}

}
Ще зробив кілька допоміжних класів — типо AssetManager, LocalizeManager. Це тривіальні речі, їх немає сенсу описувати навіть. Локалізацію я беру з json файлів. Хотілося б відзначити, що останні libgdx версії дозволяють робити текст кольоровим, размечая його тегами [COLOR]. Нам якраз це потрібно було (в одному Label зробити кілька кольорів), і ця можливість припала дуже до речі.
Гра логічно поділена на 4 екрану (сплеш, початковий вибір мови, меню, ігровий екран). Кожен екран — це окремий package, по можливості елементи екран — окремі класи (наприклад, є екран меню, і там є верхня панель — я цю панель роблю окремим класом). Потім простіше правити іграшку.
По можливості логіка винесена в клас Model. Вона зберігає стан поля, список слів, які вже були використані. Модель ж відсилає повідомлення, якщо в ній відбуваються якісь зміни. Вийшов приблизно патерн Model-Presenter. Presenter — це GameScreen, який і показує картинку, і обробляє введення користувача.
Для підтримки мультиэкранности я використовував Table. Хто не знає — це менеджер розкладки для UI-елементів. Розкладка називається TableLayout. Можна розставити елементи в комірках віртуальної таблиці, і налаштувати кожну клітинку. Загалом, досить зручно, і іноді виходить зробити те, що ручками в unity3d зробити важко.
Всі картинки в грі я запакував в один текстурний атлас — 1024*1024. Частина місця в атласі ще залишилася вільною — це приємно. Це, до речі, один з бонусів libgdx (та й взагалі низькорівневих движків) — можна точно контролювати, які ресурси скільки місця вони займають. Unity3d сама пакує картинки в атласи, але я так до кінця і не зрозумів, як вона це робить, і головне — як подивитися фінальний результат, скільки атласів і якого розміру вийшло.
Звуки і музика. У нас їх немає. Баба з возу — кобилі легше :)
Текстові ресурси — словники, локалізація. Локалізація — json. Json взагалі відмінний формат, якщо розмір даних не занадто великий.
Словники — plain text. Як може хтось зауважив вище, словників у нас по два однакових на мову, але по різному відсортованих. Можна сортувати, звичайно, і при запуску програми — але краще пожертвувати кількома сотнями кілобайт розміру програми, ніж гальмами під час запуску.
Шрифти я використовував TrueType, ttf. У libgdx є бібліотека для роботи з ними, і вона добре працює. Пам'яті такі шрифти їдять більше, звичайно, ніж просто текстура, але і результат краще візуально набагато.
Що хочу сказати. Версію на Unity3d я писав близько місяця. Переписав ж за три дні на libgdx. Порівняння нечесне, тому що вже були підготовлені ассеты, і основна частина логіки була написана. Але за моїми відчуттями, мені на libgdx потрібно було б все одно менше часу, ніж на Unity3d.
Свою роль зіграли кілька факторів. Перший — я добре знаю libgdx, і не дуже — Unity3d. По друге — ідея гри і все що потрібно, у мене було. Головне результат. А він такий — замість 16 мегабайт гальмівного АПК (це я в Unity3d вирізав підтримку Android x86, щоб досягти такого розміру, а інакше — 20 +) — я отримав APK у 8 мегабайт, і швидкий при цьому (стабільних 60 FPS в с полноэкранными напівпрозорими бэкграундами). Плюс можливість легко взаємодіяти з android-кодом. Мінус — портування на ios буде складніше, особливо у світлі останніх новин про libgdx. Але портування на ios незрозуміло коли буде і чи буде взагалі, а стабільна android версія потрібна вже зараз.
Для себе я зробив висновок — для своїх проектів я буду використовувати libgdx, і розвивати свій движок, побудований поверх нього. Це дозволить мені не здригатися, коли Unity3d оновиться до чергової версії, і не думати, як тепер пройде збірка на Android, не буде гальмувати додаток. З іншого боку, я буду періодично мацати чергові версії Unity3d — може, ситуація покращиться, хто знає. Мені взагалі здається, що Unity3d йдуть у бік десктопної розробки, і тому не особливо піклуються про продуктивності на мобільних пристроях.
Монетизація
Перша версія повнофункціональна і безкоштовна, немає ніяких додаткових можливостей, які можна купити за гроші. Але є реклама — банер в ігровому полі внизу, і повноекранна при завершенні гри. В майбутньому ми плануємо ввести деякі бонуси, які можна придбати через in-app — додати букву, змінити букву і т. д. Більше поки не замислювалися про це. Спочатку треба довести його до розуму (словник та інші режими гри), а далі можна думати про монетизацію.
Плани по апдейта
Планів маса. Хочемо додати пользовательський словник. Хочеться додати гру по мережі (я планую накидати сервер на Netty, є певний досвід). Додамо ачівкі. Додамо нові мови. Ідей дуже багато, а часу мало :) Але найближчим оновлення точно увійде пользовательський словник — це потрібна фішка.
Підсумки
Ця гра стала другою, яку я написав, і в яку я сам із задоволенням граю. Якщо запитаєте, яка перша — вона не моя, фріланс, і вона ще не вийшла. І мені здається, це вірний шлях — писати ігри, в які сам із задоволенням бавишся. А писати їх, такі ігри, потрібно з задоволенням :)
P. S. Якщо є якісь нюанси з питань ігрової логіки або libgdx — пишіть у коментарях. Я розумію, що я не можу охопити всю тему, і що міг пропустити, тому з задоволенням відповім на будь-які питання

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.