Стиль кода - TrueCat17/Ren-Engine GitHub Wiki

Вводная часть, объясняющая, почему эта статья очень важна.

Память человека далеко не так хороша, как иногда кажется. Если информация не используется некоторое время, она забывается. Нет, на это уходят не годы, и в большинстве случаев - даже не месяцы.

Стоит вам отвлечься от проекта на неделю, а то и вовсе на пару дней, как половина связанных с ним ваших знаний, примечаний и мысленных заметок будут вами забыты. Это не шутка и не преувеличение, всё действительно обстоит именно так.

Это напоминание нужно здесь для того, чтобы показать, что после написания части проекта вы перейдёте к другой части, а текущая вылетит у вас из головы, и когда вам придётся вернуться к ней, вы будете смотреть на неё так, будто вы - совсем другой человек.

Разумеется, вам хочется, чтобы в будущем вы без особых проблем поняли, что, зачем и почему именно так вы делаете сейчас. Отделять себя-настоящего (который имеет всю необходимую информацию или может её быстро найти) от себя-будущего (который либо всё забыл, либо помнит всё более чем смутно) - очень важно. По крайней мере, если вам не плевать на себя в будущем.

И поэтому вы должны писать код (впрочем, речь не только о нём) так, чтобы он был как можно более понятен человеку, который почти не знаком с вашим текущим проектом. Причём желательно, чтобы основная мысль была понятна с первого же взгляда. Делать это сложно, иногда и вовсе почти невозможно, но к этому всегда нужно стремиться, иначе в будущем всё будет очень, очень плохо.

Кроме того, не стоит забывать, что читается код гораздо чаще, чем пишется. Иногда - в несколько раз, почти всегда - в несколько десятков раз. Глупо экономить несколько секунд на "причёсывании" кода, если из-за этого в будущем в сумме вы потратите на несколько минут больше при его чтении.

Далее будут даны рекомендации, которые сэкономят вам в дальнейшем много времени, нервов и желания работать. Например, между программистами приняты некоторые соглашения для ускорения чтения и понимания кода.


snake_case

Например, что имена переменных и функций в питоне пишутся в стиле snake_case:

i = 0
street_id = 10

def get_number():
	return 4

long_long_name = 'abc'

CamelCase

Имена классов же пишутся в стиле CamelCase:

Car
MyClass
MySuperClass
HttpRequest # HTTP - аббревиатура

Константы

Констант в питоне нет, но если вы хотите указать, что переменная не будет меняться во время работы программы, то используйте такой стиль:

VERSION = "0.9.1"
COUNT_PLAYERS = 4
XML_PATH = "../file.xml" # XML - аббревиатура

Пробелы важны

Не забывайте ставить их возле арифметических операторов:

s = v * t + a * (t * t) / 2.0 # правильно
s=v*t+a*(t*t)/2.0 # неправильно

Также правильно ставьте их около запятых:

numbers = [4, 8, 15, 16, 23, 42] # правильно
numbers = [4 , 8 ,15,16, 23   , 42 ] # неправильно

Имена

Всё (переменные, функции, классы, модули, файлы и т. д.) должно иметь осмысленное имя.
Не x, a, b, qwe, zxc, igf132o или нечто подобное.

Хотя есть и исключения. Например, если это:

  • координаты (x, y, z),
  • каналы пикселя (r, g, b, a),
  • что-то из формул (s, t),
  • счётчики цикла (i, j, n),
  • некоторые другие случаи.

В любом случае, вы должны иметь причину поступать именно так.

Кроме того, не давайте имена транслитом. Если вы пятиклассник, которому месяц назад впервые дали компьютер, то это ещё можно понять, но вообще это выглядит невероятно глупо, убого и при этом ещё даже и не всегда понятно.

deva4ca = get_imya() # о мои глаза!
girl_name = get_name() # ok

shirina_okowka = get_stage_width() # это просто издевательство
width = get_stage_width() # ok

cena_bileta = polutit_tsenu('bilet') # никогда так не делайте
price = get_price('ticket') # так-то лучше

Лучше потратьте минуту на то, чтобы перевести слово и запомнить его, заодно подтяните английский.
Это касается так же и имён файлов.

Кстати, вдобавок, все эти ошибки, неоднозначности и замены сильно усложняют перевод. Представьте, что нашли мини-игру, которую хотели бы вставить в свою игру, но имена всех сущностей там - это какой-нибудь корейский или испанский транслит с кучей ошибок и неоднозначностей. Вот весело-то будет с этим разбираться.


Именование файлов

Есть различные кодировки, их форматы, файловые системы и их настраиваемые свойства, поэтому если не обращать на все эти различия внимания, то раз за разом будут возникать ситуации, когда у вас всё работает, а у некоторых друзей - нет.

Во-первых, нужно значительно сузить набор допустимых символов в именах файлов.
Разрешены только латинские буквы (a-z), цифры (0-9), подчёркивание (_) и точка (.). Они поддерживаются во всех файловых и операционных системах.

Кириллица, иероглифы, буквы вроде ö или а́, всевозможные тире и дефисы, спец. символы (,+!@#$%^&*()"'~№;:?[]<>/| и прочие) - под запретом. Пробел, кстати, тоже. Не все программы корректно его обрабатывают. Будет обидно натравить какой-нибудь оптимизатор на графику или музыку, а после его работы вдруг обнаружить, что теперь нужно переименовывать пару сотен файлов.

Во-вторых, важно обращать внимание на расширение файла и регистр букв. Если файл называется some_image.png, то к нему нельзя обращаться как к some_image (скорее всего не заработает), и уж тем более как к Some_Image.png (будет работать только на системах с регистронезависимостью).

Поэтому обычно рекомендуется не использовать заглавные буквы, чтобы не было путаницы. Исключения составляют имена файлов шрифтов - они всегда начинаются с заглавной буквы (так принято, не знаю, почему). Из-за этого и имена шрифтов тоже следует указывать с большой буквы.


Комментарии

Это специальные части в коде, игнорируемые компьютером и нужные именно человеку.
В Ren-Engine'е и в питоне начинаются со знака # и продолжаются до конца строки.

Не забывайте писать их, если они дадут необходимые объяснения.

Возможно, вы сейчас сильно торопитесь, и потому код не сильно хорошего качества? Запишите, чтобы потом не забыть это исправить!

Или, быть может, вам нужна максимальная производительность, и более простые и понятные идеи слишком медленны? Тогда не забудьте расписать, как это всё работает, и почему нельзя сделать проще.
Кстати, довольно часто во время написания "почему нельзя" в итоге можно придти к выводу, что всё-таки можно.
См. метод утёнка.

Или код-то, в принципе, понятен. Но далеко не сразу понятно, зачем он вообще нужен, какую задачу он решает, и почему бы его вообще не удалить? Такие моменты тоже встречаются, и они должны быть объяснены.


Без комментариев

Обратная сторона комментариев: не пишите их в коде, если он очевиден. При этом это где-то около 99% кода, в некоторых редких случаях эта цифра опускается до 90%. Если она у вас меньше - ваш код, скорее всего, нужно переписывать.

Как делать не надо:

my_var = 0 # приравниваем my_var к нулю

if some_var < other_var: # если some_var < other_var
	func(some_var) # вызываем функцию func, передавая some_var в качестве параметра

Как можно заметить, комментарии в этом примере не только описывают очевидное и просто отвлекают, но ещё и занимают места в 2 раза больше, чем сам код.

Наблюдение: обычно в подобном коде комментарии не говорят ничего важного, если вдруг встретилась действительно сложная для понимания часть кода.
Вероятно, это происходит из-за того, что человек просто хочет увеличить покрытие кода комментариями, при этом не затруднив себя придумыванием сложных объяснений.

Что происходит в голове таких людей - загадка.


Логические переменные

Переменные, которые имеют 2 значения, должны быть логического типа (bool), а не числами (int).

Неправильно:

init:
	$ a = 0
# ...
label some:
	$ a = 1
# ...
label start:
	# ...
	if a == 1:
		"Text"

Правильно:

init:
	$ a = False
# ...
label some:
	$ a = True
# ...
label start:
	# ...
	if a: # о сравнении с True в if см. далее
		"Text"

Условия

Многие новички (а таких довольно много среди тех, кто пытается создавать свои игры) плохо понимают, как работают условия.

if var == True: # в результате часто можно видеть такой код
if var:         # вместо такого

if var == False: # или такой
if not var:      # вместо такого

Условия принимают выражения, которые после выполнения приводятся к логическому типу bool. В конечном итоге условие выполняется, если после приведения получилось True, поэтому делать некоторые сравнения внутри if нет никакого смысла.

Ещё несколько примеров:

if n != 0: # числа приводятся к True, если не равны 0
if n:      # тоже самое, но проще

if s == '': # строки (+ массивы и словари) приводятся к True, если не пусты
if not s:   # так лучше

Else

Тоже позволяет упростить условия. Никогда не следует забывать об этой удобной штуке (как и о elif, впрочем).

Неправильно:

if var:
	# ...

if not var:
	# ...

Правильно:

if var:
	# ...
else:
	# ...

Безусловно

Иногда условия можно вообще выбросить:

$ r = random.randint(1, 3) # случайное число от 1 до 3 включительно

if r == 1:
	rn "Привет!"

elif r == 2:
	rn "Ты тоже тут?"

elif r == 3:
	rn "Давно не виделись. Здравствуй."

Так гораздо лучше:

python:
	# выбор случайного элемента из массива с фразами
	text = random.choice([
		"Привет!",
		"Ты тоже тут?",
		"Давно не виделись. Здравствуй.",
	])
rn text

Опять же, в таком виде гораздо проще и безопаснее добавлять новые или исправлять старые фразы.


Массивы и циклы

Сильно повышают удобство. Это не какие-то страшные штуки, которые непонятно зачем нужны.

Например, представим себе игру, в которой студент учится и готовится к научной выставке. От начала до конца в игре проходит 4 месяца. Если у игрока раз в месяц оценки или подготовка проекта ниже необходимого, то игра заканчивается досрочно.

Вот как НЕ надо это реализовывать (примерно такой код был в 1 крупной игре):

init:
	# необходимые уровни оценок для каждого месяца
	$ rating1 = 60
	$ rating2 = 70
	$ rating3 = 80
	$ rating4 = 80
	
	# уровни завершённости проекта
	$ project1 = 10
	$ project2 = 30
	$ project3 = 60
	$ project4 = 95
	
	# текущие месяц, оценки и завершённость проекта
	$ month = 0
	$ rating = 0
	$ project = 0

# переключение на следующий месяц и проверка
label next_month:
	$ month += 1
	
	if month == 1 and (rating < rating1 or project < project1):
		jump bad_end
	if month == 2 and (rating < rating2 or project < project2):
		jump bad_end
	if month == 3 and (rating < rating3 or project < project3):
		jump bad_end
	if month == 4 and (rating < rating4 or project < project4):
		jump bad_end

Только там было 10 месяцев.

Итак, что же здесь не так.

Во-первых, если в блоке инициализации есть только код питона, то вместо постоянных значков $ лучше сразу использовать блок init python.

Во-вторых, вместо кучи однотипных переменных лучше просто завести массивы. Вдобавок они позволят заменить кучу условий одним, но с использованием индекса.

К тому же, всё это сильно упростило бы нам жизнь, если бы мы решили увеличить кол-во месяцев с 4 до, например, тех же 10. Слишком уж легко допустить и не заметить опечатку среди такого кол-ва однотипного кода.

Правильный вариант кода:

init python:
	ratings = [60, 70, 80, 80] # необходимые уровни оценок для каждого месяца
	projects = [10, 30, 60, 95] # уровни завершённости проекта
	
	# текущие месяц, оценки и завершённость проекта
	month = 0
	rating = 0
	project = 0

# переключение на следующий месяц и проверка
label next_month:
	$ month += 1
	
	if rating < ratings[month - 1] or project < projects[month - 1]:
		jump bad_end

15 строк вместо 30, и это всего на 4-х месяцах! Очевидно, что этот код гораздо короче, понятнее и меньше подвержен ошибкам и багам.

Ещё стоит отметить, что индексы в массивах начинаются с 0, а не с 1, поэтому в предпоследней строке и нужно вычитание.


Общая часть

Тоже самое относится и к функциям и label-секциям. Если видите почти полное дублирование, то, возможно, стоит вынести его в отдельный блок, после чего вызывать его там, где нужно.

Вот это вот:

label my_event:
	if some == 0:
		"Начало-1"
		# Общая середина на 30 строк
		"Конец-1"
	elif some == 1:
		"Начало-2"
		# Общая середина на 30 строк
		"Конец-2"
	else:
		"Начало-3"
		# Общая середина на 30 строк
		"Конец-3"

Следует заменить на это:

label my_event_common:
	# Общая середина на 30 строк

label my_event:
	if some == 0:
		"Начало-1"
		call my_event_common
		"Конец-1"
	elif some == 1:
		"Начало-2"
		call my_event_common
		"Конец-2"
	else:
		"Начало-3"
		call my_event_common
		"Конец-3"

Конечно, может показаться, что это лишь всё усложняет, но только до тех пор, пока вы и вправду не представите там 30 одних и тех же строк. К тому же, во время постоянных исправлений, переделываний и улучшений рано или поздно какая-то часть обязательно будет пропущена, и заметить это будет довольно сложно.


Метки и функции

Иногда label-секции используются просто как функции в питоне, с использованием лишь этого самого питона:

label my_init:
	$ value = random.randint(10, 100)
	$ need_value = 500
	
	if random.random() < 0.15:
		$ need_value = 400

label some:
	# ...
	call my_init
	# ...

В таком случае, вполне возможно, что стоит заменить my_init на обычную функцию питона:

init python:
	def my_init():
		global value, need_value
		
		value = random.randint(10, 100)
		need_value = 500
		
		if random.random() < 0.15:
			need_value = 400

label some:
	# ...
	$ my_init()
	# ...

К тому же, это позволит сделать у функции аргументы, если вдруг из другой части сценария понадобится её вызов с другими параметрами.


Кэширование

В некоторых случаях функции постоянно вызываются с одинаковыми параметрами, а время их выполнения может длиться сильно дольше желаемого.
В таких случаях применяют кэширование: вычислили значение 1 раз, запомнили его, а потом просто возвращаем его без повторных тяжёлых вычислений.

Важно: не следует применять это на функциях, которые и так работают достаточно быстро.

Реализовать это можно примерно следующим образом:

def my_hard_function(image, r, g, b, a):
	cache = my_hard_function.__dict__
	key = (image, r, g, b, a)
	if key in cache:
		return cache[key]
	
	# стандартное вычисление результата
	res = im.recolor(image, r, g, b, a)
	
	cache[key] = res # сохранение в кэше
	return res

Конечно, im.recolor нельзя назвать тяжёлой операцией, но это просто пример.

В качестве кэша здесь используется __dict__-объект самой функции, что удобно для незамусоривания глобального пространства имён.
В качестве ключа же используются аргументы функции (в случае необходимости там должны быть и "внешние" данные, от которых зависит результат).

Внимание, среди аргументов могут быть только хэшируемые объекты (если упростить, то это простые или неизменяемые объекты: числа, строки и кортежи, но не списки, словари и множества). Обычно этого более чем достаточно, но, если что, иногда и "конвертирование" списков (и т. п.) в кортежи может быть оправданным с точки зрения производительности.

Опытные питонисты тут сразу вспомнили о декораторе lru_cache, который будет удобнее и функциональнее вышеуказанного "ручного" варианта, однако декораторы имеют огромные и нерешаемые проблемы при сохранении, поэтому использовать их невозможно.


Форматы изображений, музыки и шрифтов

Правильный выбор форматов может сократить "вес" вашей игры в несколько раз. При прочих равных игра в 400 МБ будет скачана и сыграна большее кол-во раз, чем аналогичная, но весом в 1500 МБ.

  • Не стоит пытаться хранить фоны в форматах со сжатием без потерь (png). jpg с качеством 95% на глаз не отличается от оригинала, зато весит гораздо меньше. Больше снизит качество сжатие изображения при выводе на экран, ведь далеко не у всех размер монитора будет соответствовать размеру ваших фонов, и далеко не все играют в полноэкранном режиме.

  • Вообще, форматы изображений png и jpg довольно старые, и, по возможности, лучше использовать webp. По сравнению с предыдущими форматами он сжимает на 30-40% лучше. В нём имеется как режим без потерь (как png), так и с потерями (jpg).

  • Музыку и звуки лучше хранить в формате opus (его контейнер - ogg, как и у vorbis). При сравнении opus-64kb/s, vorbis-192kb/s и mp3-320kb/s вероятность услышать разницу стремится к нулю. Это означает, что opus в 3 раза эффективнее vorbis и в 5 раз - mp3. Более того - он весьма хорош даже на 32kb/s, что просто немыслимо для почти всех прочих форматов. Используйте для аудио-файлов именно его.

  • Шрифты обычно в сумме весят мало, но т. к. они используются сразу же при запуске, сокращение их размера хорошо скажется на скорости этого запуска, поэтому лучше использовать woff2 (занимает в 3 раза меньше места, чем ttf).

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

  • Разумеется, обработка многих сотен файлов должна производиться самописными скриптами на том же питоне, либо специальными программами, предназначенными для массовых операций, но ни в коем случае не в ручную по 1 файлу за раз.