[Godot, Разработка игр] Механики для реализации платформера на Godot engine. 2 часть
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Здравствуйте, это продолжение предыдущей статьи о создании игрового персонажа в GodotEngine. Я наконец понял, как реализовать некоторые механики, такие как второй прыжок в воздухе, карабканье по, и прыжок от стены. Первая часть была более простой по насыщенности, так как с чего-то же нужно было начинать, чтобы потом доработать или переделать.
Для начала я решил собрать весь предыдущий код, чтобы те, кто использовали информацию из предыдущей статьи поняли, как я представлял себе программу полностью
extends KinematicBody2D
# Константы
const GRAVITY: int = 40
const MOVE_SPEED: int = 120 # Скорость перемещения персонажа в пикселях
const JUMP_POWER: int = 80 # Скорость прыжка
# Переменные
var velocity: Vector2 = Vector2.ZERO
func _physics_process(_delta: float) -> void:
# Ниже вставлять вызовы функций перемещения
move_character() # Перемещение персонажа
jump()
# Ниже можно ничего не трогать
self.velocity.y += GRAVITY
self.velocity = self.move_and_slide(self.velocity, Vector2(0, -1))
func move_character() -> void:
var direction: float = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
self.velocity.x = direction * MOVE_SPEED
func jump() -> void:
if self.is_on_floor():
if Input.is_action_pressed("ui_accept"): # Я вспомнил про событие ui_accept
# Оно вмещает в себя нажатие прыжка
self.velocity.y -= JUMP_POWER
Надеюсь тем, кто читал предыдущую статью стало примерно понятно, как всё работает. Теперь вернёмся к разработке.
Машина состояний
Машина состояний(в моём понимании) — часть программы, что определяет состояние чего либо: в воздухе, на полу, на потолке, или на стене, а также определяет что должно происходить с персонажем в том или ином месте. В GodotEngine есть такая вещь как enum, что создаёт перечисление, где каждый элемент является, заданной в коде, константой. Думаю лучше покажу это на примере:
enum States { # Создаётся перечисление States, к константам которого можно обращаться через States.IN_AIR, States.ON_FLOOR...
IN_AIR, # В воздухе
ON_FLOOR, # На полу
ON_WALL # На стене
}
Данный код можно смело положить в самое начало скрипта игрового персонажа и держать в голове, что он существует. Следом инициализируем переменную в нужном месте var current_state: int = States.IN_AIR, которая равна нулю, если использовать print. Далее нужно как-то определять что игрок в текущем состоянии будет делать. Думаю многим опытным разработчикам пришедшим из C++ знакома конструкция switch () {case:}. В GDScript есть похожая адаптированная конструкция, хотя и switch также есть в планах у разработчиков. Конструкция называется match. Думаю будет правильнее показать данную конструкцию в деле, так как рассказывать будет сложнее, чем показывать:
func _physics_process(_delta: float) -> void:
# Ниже функции перемещения
match (self.current_state):
States.IN_AIR:
# Вызов методов что доступны в воздухе.
self.move_character()
States.ON_FLOOR:
# Вызов методов, что доступны на земле.
self.move_character()
self.jump()
States.ON_WALL:
# вызов методов, что доступны, если мы упремся лицом в стену. Пока кроме перемещения ничего нет.
self.move_character()
# Ниже будет остальной код
Но мы до сих пор не меняем состояния. Нужно создать отдельную функцию, которую будем вызывать перед match-ем, чтобы изменять переменную current_state которую стоит добавить в код к остальным переменным. А функцию назовём update_state().
func update_state() -> void:
# Тут всё зависит от запланированных разработчиком возможностей персонажа.
if self.is_on_floor():
self.current_state = self.States.ON_FLOOR
elif self.is_on_wall() and !self.is_on_floor():
# Когда персонаж только на стене.
self.current_state = self.States.ON_WALL
elif self.is_on_wall() and self.is_on_floor():
# Ситуация угла. Будем в данном случае на стене.
self.current_state = self.States.ON_WALL
else: # Во всех других случаях будем в воздухе
self.current_state = self.states.IN_AIR
Теперь, когда машина состояний готова, мы можем добавлять уйму функций. В том числе и добавить анимации к персонажу… Даже не так… Мы можем добавить тонну анимаций персонажу. Система стала модульной. Но на этом мы не закончили с кодом. Я сказал в начале, что покажу, как делать дополнительный прыжок в воздухе, карабканье по, и прыжок от стены. Начнём по порядку.
Дополнительный прыжок в воздухе
Во-первых, добавьте вызов прыжка в состоянии States.IN_AIR в наш match, который мы чуток доработаем.
Вот код нашего прыжка, который я исправил:
func jump() -> void:
# Старую проверку в мусор. Мы сделаем её позже.
if Input.is_action_pressed("ui_accept"): # Назначаем в настройках событие
if self.current_state == self.States.ON_FLOOR:
# Как раньше, но добавляем проверку через текущее_состояние
self.velocity.y -= JUMP_POWER
elif (self.current_state == self.States.IN_AIR or self.current_state == self.States.ON_WALL)
and self.second_jump == true:
# Тут проверяем на другие состояния и можем ли мы вообще прыгнуть второй раз
self.velocity.y = -JUMP_POWER
# Сбрасываем накопленное ускорение падения и совершаем прыжок
self.second_jump = false
# Не забудьте добавить var second_jump: bool = true в самый верх. и в update_state()
# Добавьте после if self.is_on_floor(): self.second_jump = true # чтобы сбрасывать состояние прыжка после приземления на пол.
В комментариях к коду в принципе сказано, как я переделал программу, надеюсь вы понимаете мои слова там. Но фактически этих исправлений хватит чтобы изменить механику прыжка и улучшить до двойного. На то чтобы изобрести следующие методы мне потребовалась пара месяцев. Я их написал фактически позавчера, 1 октября 2020 года.
Карабканье по стенам
К нашему сожалению, Нормаль стены GodotEngine не позволяет узнать, из чего следует, что нам придётся создать небольшой костыль. Для начала я сделаю сноску имеющихся на данный момент переменных, чтобы можно было проще сказать что изменилось.
extends KinematicBody2D
# Сигналы
signal timer_ended # нужно чтобы заставить работать yield в wall_jump, что основан на обмане управления.
# Константы
const GRAVITY: int = 40
const MOVE_SPEED: int = 120 # Скорость перемещения персонажа в пикселях
const JUMP_POWER: int = 80 # Скорость прыжка
const WALL_JUMP_POWER: int = 60 # Сила прыжка от стены. Нужно для соответственной функции
const CLIMB_SPEED: int = 30 # Скорость вскарабкивания
# Переменные
var velocity: Vector2 = Vector2.ZERO
var second_jump: bool = true
var climbing: bool = false # Нужно чтобы определять, карабкается ли игрок по стене, или нет.
var timer_working: bool = false
var is_wall_jump: bool = false # Нужно, чтобы определить, а от стены ли мы прыгаем
var left_pressed: bool = false # Для искусственного зажатия кнопки влево
var right_pressed: bool = false # Для искусственного зажатия кнопки вправо
var current_state: int = States.IN_AIR
var timer: float = 0 # счётчик таймера, что будет встроен в _process(delta: float)
var walls = [false, false, false] # определения стен и потолка. Нулевой и второй - стены. Первый - потолок.
# Пока нужны только нулевой и второй
# Перечисления
enum States {
IN_AIR, # В воздухе
ON_FLOOR, # На полу
ON_WALL # На стене
}
# И я сделаю чуть больше чем сказал, добавив метод _process() с самодельным таймером
func _process(delta: float):
if timer_working:
timer -= delta
if timer <= 0:
emit_signal("timer_ended")
timer = 0
Теперь нужно определять по какой стене игрок карабкается.
Вот дерево сцены, что вам стоит подготовить для реализации определителя стороны стены
Разместите 2 Area2D по бокам персонажа и CollisionShape2D обоих не должны пересекаться с персонажем. Подпишите соответственно объекты WallLeft/WallRight и присоедините сигналы _on_body_endered и _on_body_exited к единственному скрипту персонажа. Вот код который нужен чтобы определять стены(Добавить в самый конец скрипта):
# Надеюсь тут всё интуитивно понятно
# Если нет, то комментарии вам в помощь
func _on_WallRight_body_entered(_body):
if (_body.name != self.name):
self.walls[0] = true # Если засечённый объект не мы, объект слева - стена
func _on_WallRight_body_exited(_body):
self.walls[0] = false # Когда тело вышло из коллизии другого объекта - стены слева нет
func _on_WallLeft_body_entered(_body):
if (_body.name != self.name):
self.walls[2] = true # Если засечённый объект не мы, объект справа - стена
func _on_WallLeft_body_exited(_body):
self.walls[2] = false # Когда тело вышло из коллизии другого объекта - стены справа нет
Приступим к методу карабканья. В коде всё будет сказано за меня
func climbing() -> void:
if (self.walls[0] or self.walls[2]): # Если стена слева или стена справа есть
# Создайте новый action в настройках и назовите ui_climb. Об этом я уже говорил в первой части.
self.climbing = Input.is_action_pressed("ui_climb")
else:
self.climbing = false
И нужно переписать управление move_character() для того, чтобы можно было не просто держаться, а карабкаться вверх вниз, благо у нас есть direction
func move_character() -> void:
var direction: float = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
if !self.climbing:
self.velocity.x = direction * MOVE_SPEED
else:
self.velocity.y = direction * CLIMB_SPEED
И исправляем наш _physics_process()
func _physics_process(_delta: float) -> void:
# Ниже функции перемещения
match (self.current_state):
States.IN_AIR:
self.move_character()
States.ON_FLOOR:
self.move_character()
self.jump()
States.ON_WALL:
self.move_character()
# Ниже можно ничего не трогать
if !self.climbing:
self.velocity.y += GRAVITY
self.velocity = self.move_and_slide(self.velocity, Vector2(0, -1))
Теперь персонаж должен уметь карабкаться по стенам.
Прыжок от стены
Теперь реализуем прыжок от стены.
func wall_jump() -> void:
if Input.is_action_just_pressed("ui_accept") and Input.is_action_pressed("ui_climb"):
# Если нажата 1 раз кнопка прыжка и зажата кнопка карабканья
self.is_wall_jump = true # Мы прыгаем от стены = да
self.velocity.y = -JUMP_POWER # Изменяем ускорение до -JUMP_POWER
if walls[0]: # Если стена слева
self.timer = 0.5 # Установить self.timer на 0.5 секунды
self.timer_enabled = true # Включаем таймер
self.left_pressed = true # ставим переменную left_pressed на да
yield(self, "timer_ended") # Дожидаемся срабатывания сигнала timer_ended
self.left_pressed = false # отпускаем left_pressed
if walls[2]: # Если стена справа
self.timer = 0.5 # Установить self.timer на 0.5 секунды
self.timer_enabled = true # Включаем таймер
self.right_pressed = true # ставим переменную right_pressed на да
yield(self, "timer_ended") # Дожидаемся срабатывания сигнала timer_ended
self.right_pressed = false # отпускаем right_pressed
self.is_wall_jump = false # Прыгнули. Больше не на стене
Добавляем вызов этого метода в наш match -> States.ON_WALL и мы присоединили наш метод к остальной части _physics_process().
Заключение
В данной статье я показал реализацию относительно сложных механик(для начинающих) в GodotEngine. Но это ещё не последняя часть серии статей, поэтому попрошу тех, кто знает как реализовать мною показанные методы в этой статье лучше, писать о них в комментарии. Я, да и многие читающие, будем благодарны за качественные и быстрые решения.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка игр, Godot] Механики для реализации платформера на игровом движке GodotEngine
- [C++, Разработка мобильных приложений, Разработка игр] Qt? ImGUI? wxWidgets? Пишем свое
- [Дизайн игр, Игры и игровые приставки, Монетизация игр, Развитие стартапа, Разработка игр] Разбор игры RAID: Shadow Legends (монетизация через поведенческую психологию)
- [Работа с 3D-графикой, Разработка игр, WebGL] Рендеринг каустики воды в реальном времени (перевод)
- [Разработка игр, Карьера в IT-индустрии, Продвижение игр, Биографии гиков, Игры и игровые приставки] Plague M.D. Я остался в России и меня поглотила Чума. Сопли и нытье прогера
- [Разработка игр, Unity, Физика] Симуляция волос и тканей с Unity Cloth на мобильных устройствах
- [Работа с 3D-графикой, Разработка игр, Игры и игровые приставки] Портирование Detroit: Become Human с Playstation 4 на PC (перевод)
- [Open source, Разработка игр] Разработчики Amnesia опубликовали исходный код игры
- [Игры и игровые приставки, Карьера в IT-индустрии, Логические игры, Разработка игр] Работа и карьера геймдизайнера. Часть 2. Основы игровой механики
- [Разработка под iOS, Разработка игр, Разработка под Android, Unity, Дизайн игр] Alt: City Online. Как я в одиночку создавал «Gta Online» для мобильных устройств. Часть 1
Теги для поиска: #_godot, #_razrabotka_igr (Разработка игр), #_chisto_programmirovanie (чисто программирование), #_gdscript, #_godot, #_razrabotka_igr (
Разработка игр
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:21
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Здравствуйте, это продолжение предыдущей статьи о создании игрового персонажа в GodotEngine. Я наконец понял, как реализовать некоторые механики, такие как второй прыжок в воздухе, карабканье по, и прыжок от стены. Первая часть была более простой по насыщенности, так как с чего-то же нужно было начинать, чтобы потом доработать или переделать. Для начала я решил собрать весь предыдущий код, чтобы те, кто использовали информацию из предыдущей статьи поняли, как я представлял себе программу полностью extends KinematicBody2D
# Константы const GRAVITY: int = 40 const MOVE_SPEED: int = 120 # Скорость перемещения персонажа в пикселях const JUMP_POWER: int = 80 # Скорость прыжка # Переменные var velocity: Vector2 = Vector2.ZERO func _physics_process(_delta: float) -> void: # Ниже вставлять вызовы функций перемещения move_character() # Перемещение персонажа jump() # Ниже можно ничего не трогать self.velocity.y += GRAVITY self.velocity = self.move_and_slide(self.velocity, Vector2(0, -1)) func move_character() -> void: var direction: float = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") self.velocity.x = direction * MOVE_SPEED func jump() -> void: if self.is_on_floor(): if Input.is_action_pressed("ui_accept"): # Я вспомнил про событие ui_accept # Оно вмещает в себя нажатие прыжка self.velocity.y -= JUMP_POWER Надеюсь тем, кто читал предыдущую статью стало примерно понятно, как всё работает. Теперь вернёмся к разработке. Машина состояний Машина состояний(в моём понимании) — часть программы, что определяет состояние чего либо: в воздухе, на полу, на потолке, или на стене, а также определяет что должно происходить с персонажем в том или ином месте. В GodotEngine есть такая вещь как enum, что создаёт перечисление, где каждый элемент является, заданной в коде, константой. Думаю лучше покажу это на примере: enum States { # Создаётся перечисление States, к константам которого можно обращаться через States.IN_AIR, States.ON_FLOOR...
IN_AIR, # В воздухе ON_FLOOR, # На полу ON_WALL # На стене } Данный код можно смело положить в самое начало скрипта игрового персонажа и держать в голове, что он существует. Следом инициализируем переменную в нужном месте var current_state: int = States.IN_AIR, которая равна нулю, если использовать print. Далее нужно как-то определять что игрок в текущем состоянии будет делать. Думаю многим опытным разработчикам пришедшим из C++ знакома конструкция switch () {case:}. В GDScript есть похожая адаптированная конструкция, хотя и switch также есть в планах у разработчиков. Конструкция называется match. Думаю будет правильнее показать данную конструкцию в деле, так как рассказывать будет сложнее, чем показывать: func _physics_process(_delta: float) -> void:
# Ниже функции перемещения match (self.current_state): States.IN_AIR: # Вызов методов что доступны в воздухе. self.move_character() States.ON_FLOOR: # Вызов методов, что доступны на земле. self.move_character() self.jump() States.ON_WALL: # вызов методов, что доступны, если мы упремся лицом в стену. Пока кроме перемещения ничего нет. self.move_character() # Ниже будет остальной код Но мы до сих пор не меняем состояния. Нужно создать отдельную функцию, которую будем вызывать перед match-ем, чтобы изменять переменную current_state которую стоит добавить в код к остальным переменным. А функцию назовём update_state(). func update_state() -> void:
# Тут всё зависит от запланированных разработчиком возможностей персонажа. if self.is_on_floor(): self.current_state = self.States.ON_FLOOR elif self.is_on_wall() and !self.is_on_floor(): # Когда персонаж только на стене. self.current_state = self.States.ON_WALL elif self.is_on_wall() and self.is_on_floor(): # Ситуация угла. Будем в данном случае на стене. self.current_state = self.States.ON_WALL else: # Во всех других случаях будем в воздухе self.current_state = self.states.IN_AIR Теперь, когда машина состояний готова, мы можем добавлять уйму функций. В том числе и добавить анимации к персонажу… Даже не так… Мы можем добавить тонну анимаций персонажу. Система стала модульной. Но на этом мы не закончили с кодом. Я сказал в начале, что покажу, как делать дополнительный прыжок в воздухе, карабканье по, и прыжок от стены. Начнём по порядку. Дополнительный прыжок в воздухе Во-первых, добавьте вызов прыжка в состоянии States.IN_AIR в наш match, который мы чуток доработаем. Вот код нашего прыжка, который я исправил: func jump() -> void:
# Старую проверку в мусор. Мы сделаем её позже. if Input.is_action_pressed("ui_accept"): # Назначаем в настройках событие if self.current_state == self.States.ON_FLOOR: # Как раньше, но добавляем проверку через текущее_состояние self.velocity.y -= JUMP_POWER elif (self.current_state == self.States.IN_AIR or self.current_state == self.States.ON_WALL) and self.second_jump == true: # Тут проверяем на другие состояния и можем ли мы вообще прыгнуть второй раз self.velocity.y = -JUMP_POWER # Сбрасываем накопленное ускорение падения и совершаем прыжок self.second_jump = false # Не забудьте добавить var second_jump: bool = true в самый верх. и в update_state() # Добавьте после if self.is_on_floor(): self.second_jump = true # чтобы сбрасывать состояние прыжка после приземления на пол. В комментариях к коду в принципе сказано, как я переделал программу, надеюсь вы понимаете мои слова там. Но фактически этих исправлений хватит чтобы изменить механику прыжка и улучшить до двойного. На то чтобы изобрести следующие методы мне потребовалась пара месяцев. Я их написал фактически позавчера, 1 октября 2020 года. Карабканье по стенам К нашему сожалению, Нормаль стены GodotEngine не позволяет узнать, из чего следует, что нам придётся создать небольшой костыль. Для начала я сделаю сноску имеющихся на данный момент переменных, чтобы можно было проще сказать что изменилось. extends KinematicBody2D
# Сигналы signal timer_ended # нужно чтобы заставить работать yield в wall_jump, что основан на обмане управления. # Константы const GRAVITY: int = 40 const MOVE_SPEED: int = 120 # Скорость перемещения персонажа в пикселях const JUMP_POWER: int = 80 # Скорость прыжка const WALL_JUMP_POWER: int = 60 # Сила прыжка от стены. Нужно для соответственной функции const CLIMB_SPEED: int = 30 # Скорость вскарабкивания # Переменные var velocity: Vector2 = Vector2.ZERO var second_jump: bool = true var climbing: bool = false # Нужно чтобы определять, карабкается ли игрок по стене, или нет. var timer_working: bool = false var is_wall_jump: bool = false # Нужно, чтобы определить, а от стены ли мы прыгаем var left_pressed: bool = false # Для искусственного зажатия кнопки влево var right_pressed: bool = false # Для искусственного зажатия кнопки вправо var current_state: int = States.IN_AIR var timer: float = 0 # счётчик таймера, что будет встроен в _process(delta: float) var walls = [false, false, false] # определения стен и потолка. Нулевой и второй - стены. Первый - потолок. # Пока нужны только нулевой и второй # Перечисления enum States { IN_AIR, # В воздухе ON_FLOOR, # На полу ON_WALL # На стене } # И я сделаю чуть больше чем сказал, добавив метод _process() с самодельным таймером func _process(delta: float): if timer_working: timer -= delta if timer <= 0: emit_signal("timer_ended") timer = 0 Теперь нужно определять по какой стене игрок карабкается. Вот дерево сцены, что вам стоит подготовить для реализации определителя стороны стены Разместите 2 Area2D по бокам персонажа и CollisionShape2D обоих не должны пересекаться с персонажем. Подпишите соответственно объекты WallLeft/WallRight и присоедините сигналы _on_body_endered и _on_body_exited к единственному скрипту персонажа. Вот код который нужен чтобы определять стены(Добавить в самый конец скрипта): # Надеюсь тут всё интуитивно понятно
# Если нет, то комментарии вам в помощь func _on_WallRight_body_entered(_body): if (_body.name != self.name): self.walls[0] = true # Если засечённый объект не мы, объект слева - стена func _on_WallRight_body_exited(_body): self.walls[0] = false # Когда тело вышло из коллизии другого объекта - стены слева нет func _on_WallLeft_body_entered(_body): if (_body.name != self.name): self.walls[2] = true # Если засечённый объект не мы, объект справа - стена func _on_WallLeft_body_exited(_body): self.walls[2] = false # Когда тело вышло из коллизии другого объекта - стены справа нет Приступим к методу карабканья. В коде всё будет сказано за меня func climbing() -> void:
if (self.walls[0] or self.walls[2]): # Если стена слева или стена справа есть # Создайте новый action в настройках и назовите ui_climb. Об этом я уже говорил в первой части. self.climbing = Input.is_action_pressed("ui_climb") else: self.climbing = false И нужно переписать управление move_character() для того, чтобы можно было не просто держаться, а карабкаться вверх вниз, благо у нас есть direction func move_character() -> void:
var direction: float = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") if !self.climbing: self.velocity.x = direction * MOVE_SPEED else: self.velocity.y = direction * CLIMB_SPEED И исправляем наш _physics_process() func _physics_process(_delta: float) -> void:
# Ниже функции перемещения match (self.current_state): States.IN_AIR: self.move_character() States.ON_FLOOR: self.move_character() self.jump() States.ON_WALL: self.move_character() # Ниже можно ничего не трогать if !self.climbing: self.velocity.y += GRAVITY self.velocity = self.move_and_slide(self.velocity, Vector2(0, -1)) Теперь персонаж должен уметь карабкаться по стенам. Прыжок от стены Теперь реализуем прыжок от стены. func wall_jump() -> void:
if Input.is_action_just_pressed("ui_accept") and Input.is_action_pressed("ui_climb"): # Если нажата 1 раз кнопка прыжка и зажата кнопка карабканья self.is_wall_jump = true # Мы прыгаем от стены = да self.velocity.y = -JUMP_POWER # Изменяем ускорение до -JUMP_POWER if walls[0]: # Если стена слева self.timer = 0.5 # Установить self.timer на 0.5 секунды self.timer_enabled = true # Включаем таймер self.left_pressed = true # ставим переменную left_pressed на да yield(self, "timer_ended") # Дожидаемся срабатывания сигнала timer_ended self.left_pressed = false # отпускаем left_pressed if walls[2]: # Если стена справа self.timer = 0.5 # Установить self.timer на 0.5 секунды self.timer_enabled = true # Включаем таймер self.right_pressed = true # ставим переменную right_pressed на да yield(self, "timer_ended") # Дожидаемся срабатывания сигнала timer_ended self.right_pressed = false # отпускаем right_pressed self.is_wall_jump = false # Прыгнули. Больше не на стене Добавляем вызов этого метода в наш match -> States.ON_WALL и мы присоединили наш метод к остальной части _physics_process(). Заключение В данной статье я показал реализацию относительно сложных механик(для начинающих) в GodotEngine. Но это ещё не последняя часть серии статей, поэтому попрошу тех, кто знает как реализовать мною показанные методы в этой статье лучше, писать о них в комментарии. Я, да и многие читающие, будем благодарны за качественные и быстрые решения. =========== Источник: habr.com =========== Похожие новости:
Разработка игр ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:21
Часовой пояс: UTC + 5