Правила и рекомендации для разработчиков движка

Введение

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

Язык

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

Многим может показаться полезным распространить правила изложенные здесь на разработку ПО в общем, но наша цель - покрыть только те ситуации, которые наиболее распространены в нашем проекте.

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

Правила работы

#1: Сначала проблема

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

../../_images/best_practices1.png

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

#2: Чтобы решить проблему, сначала убедитесь в её существовании

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

../../_images/best_practices2.png

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

Подход к разработке основанный на вере в то, что проблемы могут возникнуть в любой момент и программа должна быть готова их решить к тому моменту когда они появятся - называется "Разработкой на перспективу" и характеризуется такими идеями как:

  • Я думаю, в будущем, пользователям может быть полезно...

  • Думаю, пользователям в конечном итоге понадобится...

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

#3: Проблема должна быть сложной или частой

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

../../_images/best_practices3.png

Ответ на вопрос: "Cтоит ли решать проблему?" будет зависеть от того, насколько сложно пользователю её обойти. Эта сложность может быть выражена как:

  • Сложность проблемы

  • Частота возникновения проблемы

Если проблема слишком сложна для большинства пользователей, софт должен предложить готовое решение для неё. Аналогично, если проблема достаточно проста и пользователь может её обойти - предлагать решение необязательно.

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

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

#4: Решение должно быть обсуждено с другими

It is often the case that, when users stumble upon problems, they are only immersed in their own project, so they will naturally try to solve the problem from their own perspective, thinking only about their use case.

Because of this, user proposed solutions don't always contemplate other use cases that developers are often aware of, so they are often biased towards their own requirements.

../../_images/best_practices4.png

For developers, the perspective is different. They may find the user's problem too unique to justify a solution (instead of a user workaround), or maybe they will suggest a partial (usually simpler or lower level) solution that applies to a wider range of known problems, and leave the rest of the solution up to the user.

In any case, before attempting a contribution, it is important to discuss the actual problems with the other developers or contributors, so a better agreement on implementation can be reached.

The only exception, in this case, is when an area of code has a clear owner (agreed by the other contributors), who talks to users directly and has the most knowledge to implement a solution directly.

Also, Godot's philosophy is to favor ease of use and maintenance over absolute performance. Performance optimizations will be considered, but they may not be accepted if they make something too difficult to use or if they add too much complexity to the codebase.

#5: To each problem, its own solution

For programmers, it is always a most enjoyable challenge to find the most optimal solutions to problems. Things, however, may go overboard sometimes and programmers will try to come up with solutions that solve as many problems as possible.

The situation will often take a turn for the worse when, in order to make this solution appear even more fantastic and flexible, the pure speculation-based problems (as described in #2) also make their appearance on stage.

../../_images/best_practices5.png

The main problem is that, in reality, it rarely works this way. Most of the time, just writing an individual solution to each problem results in code that is simpler and more maintainable.

Additionally, solutions that target individual problems are better for the users, as they find something that does exactly what they need, without having to learn and remember a more complex system they will only need for simple tasks.

Big and flexible solutions also have an additional drawback which is that, over time, they are rarely flexible enough for all users, which keep requesting more functions added (and making the API and codebase more and more complex).

#6: Cater to common use cases, leave the door open for the rare ones

This is a continuation of the previous point, which further explains why this way of thinking and designing software is preferred.

As mentioned before (in point #2), it is very difficult for us (as human beings who design software) to actually understand all future user needs. Trying to write very flexible structures that cater to many use cases at once is often a mistake.

We may come up with something we believe is brilliant, but when it's actually used, we will find that users will never even use half of it, or that they will require features that don't quite accommodate our original design, forcing us to either throw it away or make it even more complex.

The question is then, how to design software that gives users what we know they need, but that is flexible enough to allow them to do what we don't know they might need in the future?

../../_images/best_practices6.png

The answer to this question is that, to ensure users still can do what they want to do, we need to give them access to a low level API that they can use to achieve what they want, even if it's more work for them because it means reimplementing some logic that already exists.

In real-life scenarios, these use cases will be at most rare and uncommon anyway, so it makes sense a custom solution needs to be written. This is why it's important to still provide users the basic building blocks to do it.

#7: Solutions must be local

When looking for a solution to a problem, be it implementing a new feature or fixing a bug, sometimes the easiest path is to add data or a new function in the core layers of code.

The main problem here is, adding something to the core layers that will only be used from a single location far away will not only make the code more difficult to follow (split in two), but also make the core API larger, more complex, more difficult to understand in general.

This is bad, because readability and cleanness of core APIs is always of extreme importance given how much code relies on it, and because it's key for new contributors as a starting point to learning the codebase.

../../_images/best_practices7.png

The common reasoning for wanting to do this is that it's usually less code to simply add a hack in the core layers.

Despite this, this practice is not advised. Generally, the code for a solution should be closer to where the problem originates, even if it involves more code, duplicated, more complex or is less efficient. More creativity might be needed, but this path is always the advised one.

#8: Don't use complex canned solutions for simple problems

Not every problem has a simple solution and, many times, the right choice is to use a third party library to solve the problem.

As Godot requires to be shipped in a large amount of platforms, we just can't link libraries dynamically. Instead, we bundle them in our source tree.

../../_images/best_practices8.png

As a result, we are very picky with what goes in, and we tend to prefer smaller libraries (in fact, single header ones are our favorite). Only in cases where there is no other choice we end up bundling something larger.

Also, libraries must use a permissive enough license to be included into Godot. Some examples of acceptable licenses are Apache 2.0, BSD, MIT, ISC, and MPL 2.0. In particular, we cannot accept libraries licensed under the GPL or LGPL since these licenses effectively disallow static linking in proprietary software (which Godot is distributed as in most exported projects). This requirement also applies to the editor, since we may want to run it on iOS in the long term. Since iOS doesn't support dynamic linking, static linking the only option on that platform.