The make the explanation easier to visualize, I’ll first explain it using only two colors channels (red and cyan), so that the color space is 2D and we can easily make images of everything.

This is the two-channel color space we will use for the explanation. In this space, colors have two coordinates, which represents the red and cyan amounts respectively. (0, 0) correspond to black, (1, 1) to white, (1, 0) to red and (0, 1) to cyan.
The principle that we will exploit is that there is a straightforward way to map one triangle to another, and that this mapping preserves lines. This second part simply means that if P maps to P’ and Q maps to Q’, and R is a point between P and Q, then the map of R is a point R’ between P’ and Q’.
The mapping is called an affine map, and it works as follows:
Below are the barycentric coordinates of important points, and what they map to:
| Point | Coordinate | Mapped point |
|---|---|---|
| A | (1, 0, 0) | A’ |
| B | (0, 1, 0) | B’ |
| C | (0, 0, 1) | C’ |
| (A + B) / 2 | (0.5, 0.5, 0) | (A’ + B’) / 2 |
| (A + B + C) / 3 | (0.333, 0.333, 0.333) | (A’ + B’ + C’) / 3 |
Here are a few things about these coordinates:
So how do we use this transformation to map colors of an image?
Let’s start with one color:
Let’s start with a simple example where we map a greyish red to a brighter red.


Here is how it transforms the image:


The original image and the mapped image. The yellow and blue markers correspond to colors marked in the color space above.
Here is the same idea, but with two input colors. The main difference is that we now have more triangles; everything else stays the same.

And here is the result.


The original and mapped image. The yellow and blue markers correspond to colors marked in the color space above as before, but now the color below the blue marker is mapped to a target color we specified.
The same idea applies to 3 channel color spaces, but here we rely on the affine mapping between tetrahedra as the basis: Given two tetrahedra ABCD and A’B’C’D’, calculate the barycentric coordinates of a point p (a, b, c, d) such that p = aA + bB + cC + dD, then use these to calculate a new point p’ = aA’ + bB’ + cC’ + dD’.
The processing algorithm then becomes the following:
Most of the processing can be done in advance, so that each pixel requires two computations:
The matrices are all computed in advance, so the main bottleneck is determining which tetrahedron the color falls into.
Since the algorithm is a pixel-wise operation, the algorithm is easy to parallelize.
There are two things to watch out for:
As a starting point to help him, I prepared this list of questions to assess how much he already knew. I don’t think being able to answer any of these questions necessarily makes you a better programmer (you may well have used and implemented pools without knowing what they are, for example), however, having some terminology and theory under the belt can be very helpful to structure your thoughts, find books or other resources, or finding ways to hone your craft.
Maybe this list can help other programmers looking for filling in gaps in their basic knowledge to discover where those gaps may be, so here it is.
I do not use ChatGPT for work. For good reasons, it is prohibited where I work (porting PC games to consoles):
At least for the time being, a human is still the best way to get a game made for one platform onto another.
That said, I have been revisiting implementing classical data structures for a personal project, and I find the bot a compelling coding assistant. In this post, I describe my experience.
Since ChatGPT 4 is rate-limited, this post is mostly about ChatGPT 3.5. Although ChatGPT 4 is indeed noticeably better, I think the same points still apply, just to a different degree.
It has a vast knowledge of algorithms and data structures. You can ask it questions like:
It can give you good references on specific data structures or algorithms. It can suggest books (often even where inside the book), papers, and websites. What I find extremely useful is a structured reading list of a topic.
It knows a lot about C#, the standard library, and even the IDE I use. You can ask it, for example, if C# provides a library implementation of a binary search tree. (It does, in the form of SortedSet).
It can translate from one language to another. Translating code between languages is not too hard for programmers, even if they don’t fully know one of the languages. But this task can be annoying and might require a lot of research, especially when dealing with code that use many library functions. While there are automated tools out there, their results often need plenty of manual tweaking.
It can interpret code. In many cases, it can recognize the algorithm that a piece of code is trying to implement. It is therefore able to add useful comments to a piece of code, choose (moderately) better names, and write useful XML documentation. The latter is especially wonderful for the boilerplate comments you need to write for container classes.
It can generate small sets of useful unit tests. As explained below, there are limitations to this.
It can do useful transformations that go beyond code transformations available in typical IDEs, quite successfully in many cases. Here are some examples:
Because it can transform code so quickly, it can be very useful to evaluate alternative styles or implementation details. Even when not correct, this can inspire alternative implementations that would not occur to me naturally. I would not have coded four different iteration methods just to see how they look, for example.
You can ask it to spot bugs. It does not always work, but sometimes it does. It is usually a good idea to ask it to write a unit test that will show the existence of a bug that it found, as it can be easier to check the test (and then run it) than to check the veracity of the bug report itself. It is especially good for detecting bugs in tediously expressed code (such as very long methods or a problem in a long XML document). I have used it to spot errors in my Rider XML layout file.
You can ask it for domain-specific applications of algorithms. For example, I learned that a stack can be used in the algorithm to calculate the convex hull of a 2D point set. As a game developer, I like this because too many examples in computer science are taken from the world of business. (If you don’t specify it will mimic the rest of computer science except where the algorithms tend to be used in a certain domain, or if you already made it focus on another domain during the conversation.)
You cannot trust it. You have to carefully vet any code you use, or any answer it gives you about anything really . In some cases, it will change working code into a non-working version to do what you ask. The request “Can you shorten this” can sometimes lead to more elegant code, but it can also simply remove some code that is needed.
The best way to vet its code is of course by using a set of unit tests (and I think as a result I have leveled up my own test writing skills).
Although it knows a lot about Git too, as a user that knows Git only moderately well, I am too scared to type any of its suggestions in the command line.
It sometimes makes up stuff that does not exist. How many times it excited me by suggesting a very compelling paper, C# feature, or data structure that does not exist!
It has a very good feel for things that would be nice if it was possible. It often suggests C# code that could work, but does not. For example, it has suggested internal static classes for private extension methods, or extending from classes that turn out to be sealed, or overloads of generic methods where only the type constraints differ.
It sometimes suggests how to configure Rider to do something that is in fact impossible. This can be quite frustrating because you never know if instructions could be off because it applies to a different version or not.
It sometimes makes assumptions that are not true. For example, it is reasonable to come up with a list of things to test, but it may assume you implement an algorithm for objects that implement an interface. Of course, this can usually be fixed, but if you cannot do it within an iteration or two drift kicks in (see below) and you cannot get a preferred solution.
It is not good at giving links to high-quality reference implementations. Requests for these usually don’t work, or the answer points to low-quality code, or code that is very obscure. Sometimes I would prefer a reference made by a human, especially when published in a journal, for example, rather than have a questionable version created by the bot.
When you push it, it can become silly. If you ask it for 50 new methods for a class, some of them will be duplicates, or not useful.
It drifts in style and focus. It tends to use new variable names, or a new brace style, or new idioms in successive answers. This makes it frustrating trying to tweak code. It will often switch the programming language too (almost always to Python, in my case).
This, combined with the quantity of text it can generate, makes it less useful to write cohesive code that it can in principle write. For example, it generally cannot produce a full set of unit tests for a class. Up to a point (using a list as a base), this can be broken up into successive requests, but it tends to forget aspects of the conversation and may neglect to produce all the tests.
The drift means sometimes tweaking a method too much will make it break — it forgets what the method is supposed to do or try too hard to do what you ask.
Here are how these exchanges typically look:
Me: Can you please give me a C# method for quicksorting an array?
ChatGPT: Sure! *Gives code*
Me: Can you please use clearer names?, and call the method simply “Sort”.
ChatGPT: Certainly! *Gives code with better names but now the local variables start with underscores*
Me: Please do not use underscores in variable names.
ChatGPT: *Fixes the variable names but now it is not quicksort anymore*.
Me: Please change it back to quicksort.
ChatGPT: I apologize for misunderstanding your previous message. *Gives new quicksort code. It still has goodish variable names but annoyingly they are different.*
Me: Can you use the names you had before.
ChatGPT: *Gives the code with the right variable names but now it is Python.*
Me: …
So instead of moving forward with the tasks at hand, it makes another sidewise change so you move diagonally. In my experience, it is almost impossible to get it back on track after two or three of these diagonal movements, so when this happens I give up or start over.
It is not good at more advanced algorithms. For example, it was not able to produce a correct version of 4-way merge sort. (This is also the type of algorithm that seems a little daunting to me as a human programmer.) When failing in this way, it usually gives an algorithm that looks structurally correct. However, when testing it does not give the desired result. Occasionally after a bit of prompting you can get it to fix small errors, but after about three tries it drifts and the further iterations take you farther from a solution. I usually do not try to debug these wrong solutions: to me, it feels more likely to be fantasy code than code that can work but has an error in it.
It sometimes comes up with ideas that are not quite right but also not quite wrong. For example, in Algorithms (2011, Sedgewick and Wayne), one of the exercises (2.1.14) asks you to implement what they call “dequeue sort”, a terribly bad name since the previous exercise involves “deck sort”, and both algorithms are explained with a deck of cards. I wondered if the algorithm perhaps had a better name out in the wild so I asked ChatGPT if it recognized my implementation, and it responded that it is a version of gnome sort. At first, I thought it was completely wrong, but then it occurred to me that it kind of works like gnome sort if the gnome stays stationary and instead you rotated the list. This is the type of connection that would be impossible for me to discover (other than accidentally).
So far, this happened very rarely; I cannot think of another example.
Despite its current limitations, it is very impressive technology, and I do wonder about the future of it and us. Here are some things I worry about.
In the future, who will make the content? Much of the chatbot’s programming knowledge must come from sources like Stack Exchange and GeeksForGeeks. However, if AI bots increasingly handle user questions, these sites might struggle to keep the audience necessary to live on. While there will likely always be questions beyond the scope of a chatbot trained on a fixed dataset, will it be enough for sites like Stack Exchange to thrive? The same goes for other knowledge sources, such as books.
(Of course, once a chatbot can interact with tools like compilers and sandboxes to perform tests, it could generate knowledge similar to humans, which would dramatically alter the situation.)
Who will control the AI? Current limitations on AI input and output size prevent the creation of entire books or code libraries. Even if these limitations are overcome, coherence may still be an issue. Assuming this problem is solved, will average people be able to harness AI on this scale?
For example, the idea of an AI generating a book on an obscure topic tailored to your existing knowledge is alluring and would be an incredible boon for education, but if only a few companies can do this, their monopolies could become the gateway to knowledge and other great things. AI has the potential to be incredibly beneficial for humanity, but its positive impact may not be realized if controlled solely by companies that don’t prioritize the common good.
What will happen to knowledge workers? It’s easy to imagine AI replacing humans in various fields. For now, the secrecy of console manufacturers and the AI’s current limitations prevent it from taking over my specific job of porting games. But this may change in the future. And even without that, an AI, guided by an experienced developer, could already handle tasks typically assigned to junior programmers, reducing the demand for their roles.
What dangerous code can be produced that slips through the cracks? Errors in AI-generated code can be subtle and might still go unnoticed even with careful testing. Although this also occurs with human-generated code, the rapid pace of AI code creation could make it impossible to examine all the code at the right level and address all the issues that may arise.
Will we survive? It is maybe a bit dramatic to ask this, especially since the AI is still so silly at times. But even now, what will ChatGPT do if it had the following:
Maybe providing it with a profit-making loop will be difficult. But it seems within reach, and once you have that, you have an AI that has money and people that listen to it. It is certainly something to think about.
After using ChatGPT a lot, you certainly run into the limitations of its intelligence; you can clearly see it is not thinking about stuff; you get a feel for how the probability works that causes it to make the sideways changes as it is trying to comply with requests.
Even where it is good it may still not be quite good enough. For example, as a result of my investigation into algorithms, I have a big code base that is not documented. I could have ChatGPT do it, and it will probably do a decent job. But I am reluctant to do it simply because I will have to do it in batches and I know it will generate the XML docs with minor inconsistencies that will bother me too much and I will feel compelled to change them. And I am not sure I want to spend hours editing a computer’s dry XML comments…
That said, even with the drawbacks, I have found ChatGPT to be a valuable resource. The ability to quickly ask basic questions and receive helpful answers beats any other methods available to us.
Interestingly, using ChatGPT stimulates me to think differently about the code I’m working with, making me more aware of the subtleties I need to consider. This reminds me of the way search engines have changed our thought processes. When formulating a question, we often anticipate the engine’s biases and consider how we need to flavor our search query to obtain what we need. We don’t look for “brick” we look for “brick movie”.
While ChatGPT is particularly suitable to explore a topic such as data structures and algorithms, which is already abstract and has abundant resources available, I wonder how useful it would be in more hands-on, gooey domains like game development. And I’m not sure how helpful ChatGPT would be for beginners who don’t have the experience and thinking tools needed to analyze and interpret its responses.
I will continue to use ChatGPT where possible. If you haven’t already, I would recommend checking it out, but: buyer beware!
]]>A bug is when a program behaves in an unexpected way.
Bugs ruin games for players, and our processes are designed to keep bugs out of the final product. Removing bugs — debugging — is one part of this and an important programming skill.
Here, I share my thoughts on how to think about bugs, how to get rid of them, and how to get better at it.
The main points I want to get across are:
Consider this bug:
When I hit play, I expect to see my avatar, but I don’t.
We would be surprised if a game with this bug had code like the following:
// Hide the player when the level loads OnLevelLoad += HidePlayer;
More likely, a less direct problem in the code leads to the behavior we see. We call the observed behavior a symptom. When somebody asks how a bug manifests, they are asking what the symptoms of the bug are.
To find the cause of a symptom, we must find out why it happens. And then why that happens, and so on. This list is the causal chain that leads to the bug. The causal chain for the bug above may look like this:
(Symptom) When the level loads, I expect to see my avatar, but instead I don’t. Why?
Here is code that demonstrates this bug:
class Player
{
private Invetory invetory;
private void Update()
{
if(invetory.Full()) { ... }
MovePlayer();
}
private void MovePlayer() { ... }
}
The first cause outside the software that we can change, is the direct external cause, and is where we can stop our chain. The direct external cause of any bug is always one of the following:
Just above the external cause in the chain is the root cause. As I will explain later, the root cause is the best place to implement an intervention.
All the causes above the root cause in the chain are surface causes, and the first surface cause in the chain is the direct cause of the bug. When we cannot fix a bug at the root cause, we can implement a workaround or partial fix at one of the surface causes.
When we are confronted with a bug, we do not know the causal chain of the bug. Instead, we have a tree of possibilities. A partial tree for the bug above, may look like this:

To find the cause of a bug, we must move down this tree to the root cause. At each level, we do experiments to rule out all but one of the possible causes. We then drill down this cause by listing all its possible causes and repeat the process. Eventually, we arrive at a cause that is external. The cause just above this is the root cause.
Computers don’t use magic.
They follow deterministic rules, and when computers fail, there is a reason, and by systematically working our way through the possibilities you can always find the reason.
Some steps need a dash of creativity, but effective debugging is a systematic process. You need to understand cause and effect, make analytical inferences from the knowledge at your disposal, organize tasks in an efficient order, track your progress, and record findings for future usage.
Removing bugs revolves around two principal processes:
There are important reasons to keep these processes separate in your mind.
Bugs are often reported by the QA team (if your project is big enough to have one), or by players. But even so, you will often have to write up bug descriptions.
If you have a QA team on your project, you probably already have guidelines on how to effectively report bugs. If you don’t, you can follow these.
Use a standard bug report format. Vague reports of bugs introduce an unnecessary extra step into the debugging process: figuring out what the bug actually is. You and your team can avoid this by using precise language when talking about bugs.
A bug report requires the following:
A well-crafted bug description has three components:
In a sentence:
When I did X, I expected Y, but got Z.
For example:
When I collected a new sword, I expected to find it in my inventory, but instead my inventory was unchanged.
Use this format even when informally reporting bugs, or when formulating the bug to yourself. This format
Some types of behavior are harder to frame in this form, for example bugs that are sporadic, are seen at unpredictable times, or involve frequencies. Here are examples of dealing with this:
When jumping, I expect to come down again, but sometimes (3/5) I keep rising and I must exit the level to continue.
When I start the second level, I expect to continue playing, but instead the game exits at a random point (seen between first and third waves of enemies).
When I run the game 10 times, I expect each of the two monsters to appear about 5 times, but instead the green monster appears 8 times.
Use standard tests. You can also gain from using formal, named test cases, which allows efficient communication of what breaks where. For example, if you have a flow chart of the player login cases, with labeled nodes, you can say:
When going through the login process through node A, I expected to reach node B but instead an error message displayed with the message “Already logged in”.
Don’t use ambiguous expressions. By themselves, simplistic descriptors, like these below, confuse and waste time. Provide enough detail to know exactly what happened.
Add support material. Debugging aids like the following can help clarify a bug report and provide useful information to the programmer who has to fix it. Add it to the report if it is available.
To fix a bug we can follow this process:
At any point in this process, you or your team may decide that continuing would be too expensive, in which case you “give up” on trying to fix the bug, and leave it as a “known issue”.
Review the bug description, and make sure you have enough information to understand what the bug is and when it occurs. If there is missing information, it will save time to try to get it before you start debugging.
If you cannot get the missing information (maybe an anonymous player reported it), and are still expected to solve the bug, use experiments to fill in the gaps. For example, leaderboard-bugs are usually platform specific. If a bug report omits the platform, you will have to experiment on different platforms to see where the bug occurs.
During the debugging process you may need to reproduce the bug multiple times, so it pays to make it as easy as possible.
Start clean. Make sure the code is in a good state before you begin. This depends on your workflow, but typically means you need to create a new branch from the branch where the bug was reported from, with no local changes. This makes it easy to undo any code changes you make while debugging, or recover to a specific state if you need to check your work.
Make it easy to reach the bug. A bug in level 10 for example may require you to play through the entire game. If you make it possible to start at level 10 or to skip levels, then it is much easier to reproduce the bug.
Make it easy for the bug to occur. For example, a certain bug may only occur when fighting a certain enemy type. A useful step would be to change the game to always spawn this enemy type.
You may even fake the conditions of a bug. For example, if the player is supposed to get an achievement after killing 100 enemies, but does not, set the counter to 99 so you can trigger the conditions after one kill.
Make it easy for you to see diagnostic information related to the bug. For example, if the user signs out of the game, the input controller may stop working. Whether to process input is usually controlled by a bunch of conditions — seeing these on screen would make it easy to see which one is incorrect.
The first step towards removing a bug is to follow the reproduction steps and verify that you see the bug. If you reproduce:
If you cannot reproduce, you need solve this mystery, using this tree, and perform experiments to determine why you cannot reproduce the bug. Start by speaking to the original reporter if possible, and try to see if the steps are correct or any detail of the environment was omitted.

If the bug does not exist, you may decide to not delve deeper down the tree. However, it may still be beneficial to do so, and then move on to step 10 to implement defenses if the bug turns out to be fixed by another change.
Reproduce the bug more than once. If you are dealing with a sporadic bug, you may be misled when you don’t see the bug, thinking it is fixed when it is in fact just not manifesting this round. Replicating multiple times may reveal that the bug is sporadic. You need to use judgement when deciding whether (and how many times) to reproduce. Keep in mind that if you are dealing with a sporadic bug and you are misled, you will need backtrack once you discover your mistake, since you cannot trust the outcome of your experiments.
List multiple possibilities. You may form a suspicion of what causes a bug and may be tempted to try to fix it at once.
Don’t give in to this urge. Instead come up with at least a few possible causes first:
When you cannot list causes. At times, you may not be able to think of any possible causes, perhaps after you ruled out all causes on the original list.
This happens when you lack information, or knowledge, or creativity.
Some of these strategies take time, and occasionally you may consider the bug “too expensive to diagnose”. In this case, you may implement a workaround to address a surface cause or even the symptom directly, and proceed to Step 9.
Direct versus deeper causes. When listing possible causes for a bug, you need to list direct causes. Although we want to expose the root cause, if you start guessing at root causes you will have too many possibilities to test.
For example, not initializing a variable is a common root cause, but it could be the cause of any bug.
Start with direct causes — in Step 8 you go to the next level in the tree of possibilities.
(Shortcuts are possible, but are risky and unnecessary.)
An experiment is some action that gives you information about the source of the bug. Examples of typical experiments are:
Good experiments are tests that confirm or rule possible causes quickly.
Experiments should be:
If you misinterpret the results of an experiment, you will go down the wrong branch of possible causes and waste a lot of time.
public void Move(Vector3 deltaPosition)
{
player.transform.position += deltaPosition;
Debug.Log("Move reached");
}
In this example, we will not know if an exception is thrown.
public void Move(Vector3 deltaPosition)
{
Debug.Log("Move reached");
player.transform.position += deltaPosition;
}
In this example, we will know if the method is reached, and also finishes.
public void Move(Vector3 deltaPosition)
{
Debug.Log("Move reached");
player.transform.position += deltaPosition;
Debug.Log("Move end reached");
}
Debug.Log("The log is working");
Debug.Assert(false, "Asserts are working");
Don’t do experiments in the order that they occur to you. Instead, assign rough time estimates to experiments, and likelihoods to the causes they rule out. Then put experiments first that are quicker to do and rule out likelier causes.
Don’t include the time of interventions in your estimations for ordering experiments. In the end, you will implement only one intervention, and no matter what order you follow, the time it will take will be the same. Therefore, it should also not affect the order.
Run tests in parallel when it makes sense to do so. For example, a group of your experiments may involve seeing if certain break points are hit when following the reproduction steps. It is efficient to add all the break points and perform the reproduction steps once and see which are being hit.
This step is straightforward: do the experiments in the order you decided in the previous step, and stop once you have the culprit cause.
If you make changes to the source code, use source control to help keep track of your experiments. Make it possible to recover experiments made in code so that you can check your work later if required. Document what you are doing, and note the results of your experiments.
While doing experiments, reproduce the bug exactly in the same way each time. This ensures that you are not mislead when there are multiple causes for a bug. (If there are multiple causes for the bug, that will be uncovered during more testing, perhaps as you verify at the end of Steps 9–12.)
If we are at the root cause, we can continue to the next step.
Otherwise, the cause we uncovered in the previous step is treated as the new bug. We reformulate the bug in the When X I expect Y but Z-form, and continue to Step 1. Reproducing the new bug is a sanity check to make sure you are on track.
How do we know we are at the root cause? If we ask “why”, and the answer is an external cause — that is a cause outside the code:
Once you are sure what causes the bug, you can fix it.
Always verify that your intervention fixed the bug.
When verifying, also test other cases, not just the reproduction case.
Make sure you understand why the intervention works and that it makes sense, otherwise you may implement a fake fix that works only partially or introduce more bugs.
Usually, you should implement an intervention at the root cause.
Consider the example bug we examined before, with this causal chain:
When the level loads, I expect to see my avatar, but instead I don’t.
This bug can be “fixed” in any position in the chain (except the last, of course).
The fixes will all “work”, but they are not equal.
At times, fixing the root cause may be too expensive (for example, if you have to redesign the architecture of your game to ensure a predictable initialization order). When this is the case, you can implement a workaround at a surface cause.
Commit any changes you made during this step.
Once a bug is fixed, see if you can make the code more robust and have it reveal problems related to the bug you solved. For example, if a problem was caused by a null reference of a field, see if you can put in a check to warn if the field is null.
Always verify that the defenses you add work as expected.
As an example, suppose you had this scenario:
public void InitGame()
{
//InitPlayer();
InitInventory();
}
public void InitPlayer() => player = new Player();
public void InitInventory()
{
if (player != null)
{
inventory = new Inventory(player);
}
}
public void ShowInventory() => inventory.Show();
When calling ShowInventory, a null pointer exception will be thrown. Uncommenting InitPlayer fixes it (and perhaps somebody did it to fix another bug). Although that works, it would be good to put in a check in the InitInventory method to flag a null player instead of simply ignoring it.
public void InitGame()
{
InitPlayer();
InitInventory();
}
public void InitPlayer() => player = new Player();
public void InitInventory()
{
if (player == null)
{
Error.Throw("Player null when creating the inventory.”);
}
inventory = new Inventory(player);
}
public void ShowInventory() => inventory.Show();
If you implement a surface intervention:
For example, suppose that you want the player to only jump when it is in the OnFloor state, but because of a bug that you cannot fix the state is always BelowGround. You decide to implement a surface intervention as shown below:
public void Jump()
{
/* Logically, this should be PlayerState, but because of a
bug in the animation logic the state is always
PlayerState.BelowGround at this point.
*/
if(state == PlayerState.BelowGround)
{
PlayJumpAnimation();
}
else if(state == PlayerState.BelowGround)
{
Error.Warn("This state is logically correct but never
occurred because of a bug. Perhaps the bug has been
fixed. Check the code.");
}
}
Sometimes a fix involves a change that is not obviously correct. This may tempt a programmer to change (or remove) it in the future. If the code is not obvious, add comments to clarify, especially if the symptom is far removed from the bug.
public void Die()
{
//May happen in rare cases when fighting multiple enemies
if(dead) return; //Otherwise the death animation plays again
...
}
Commit any changes you made during this step.
In complex projects, it is not uncommon for a fix to break something in an unrelated system. Unit tests and regression tests will help, but even so you should consider how your changes could affect the rest of the game.
The following thought experiment will help:
Assume there is a bug. Now where is it? How can you find it?
You can start with inspecting references to variables or methods affected by your change. In particular, look out for code that executed that did not before or vice versa .
If you changed how a variable or property is calculated, find all references to it and see whether the changes you made would affect any of the code.
It is very common to introduce a bug when changing the nullness of a variable, so be especially wary in this case.
If you changed what a method does, find all references to it and see if the change could affect the code.
It is very common to introduce a bug when changing the nullness of a method’s return value, so be especially wary of this case.
Example. The player was not moving, and it turns out it was because the player variable was never initialized, so the player’s update was always skipped. Here is the code:
void Start() {}
void Update()
{
if(player == null) return;
player.Update();
boss.Update();
}
The fix is to assign the player variable (and remove the null check that is not necessary anymore).
void Start()
{
player = new Player();
}
void Update()
{
player.Update();
boss.Update();
}
But that is not a complete fix, as the boss variable is also not initialized, and our fix will make the game crash when we run it.
void Start()
{
player = new Player();
boss = new Boss();
}
void Update()
{
player.Update();
boss.Update();
}
Commit any changes you made during this step.
The last step is to undo any changes you made during the diagnosis proses, and to verify that the bug stays fixed afterward.
Some of your diagnostics may be useful to keep for the future:
Commit any changes you made during this step.
Building zero-defect software is very expensive, and a counter-productive strategy for games.
But bugs are not only bad for the players if they land in the end-product; bugs — even small bugs — that live in the code-base during development are problematic.
Keep the following in mind in deciding where to strike the balance between “too expensive to fix” and “too expensive to keep around”.
Bugs annoy and slow down developers and testers. A system that does not work as intended (up to that point) prevents everyone on the team to do their work effectively.
Bug diagnosis cannot reliably be scheduled. The process of finding a bug is a search through possibilities that stops once the cause is found; the time this takes is unknown. The more bugs in your game, the less sure you can be of the shipment date.
Bugs can hide the existence of other bugs. For example, if a bug prevents a block of code from executing, you don’t know if another bug hides in that block.
Trivial bugs that spam the error console or log hide more serious bugs. Often serious error conditions are missed because there are too many trivial errors or warnings that are ignored.
A bug signals a problem that can cause other bugs in the future, or that causes other bugs not yet seen. A problem that appears small now can grow as the root cause manifests itself in other ways. It can also manifest itself through code that other developers write, and will waste their time as they try to track down the problem.
The older a bug is, the harder it is to diagnose and remove. A bug introduced today is easy to fix — you still remember how the code works, the amount of code where it can hide is small, and fresher bugs don’t obscure the results of experiments.
Bugs have a negative psychological impact on the team. Programmers will be less careful to introduce new bugs in an already buggy system, or will dread having to make changes in problematic areas. A buggy system is not good for morale.
A few types of bugs crop up regularly, and these bug patterns have similar debugging strategies. Here are examples. For example,
When X, I expect Y but instead nothing happens.
Here is an instance of this pattern:
When I press the “Play” button, I expect the game level to load but instead nothing happens.
We can use generic strategies to diagnose these bugs. Here are a few patterns and their strategies.
When X, I expect Y but instead nothing happens.
Find the place in the code where Y is implemented. Between X and Y, you will find code that is (abstractly) of the form if (X and C1 and C2 and…) Y. Do experiments to find out which of the conditions (X, C1, C2) are failing.
When X, I expect Y, but instead Z happens that prevents Y.
Between X and Y, you will have a sequence of statements (abstractly) S1, S2, … The strategy is to find the first S where Z happens.
When X, I expect an object to still be in state A, but instead it changes to B.
Find instances where the state is changed. If there are only a few instances of this, do experiments to find out which one is the culprit, and drill down from there. If there are many instances, wrap the state in a variable that reports when it changes to the unwanted state. Use this to find the faulty caller, and drill down from there.
When X, I expect an object to change from state A to B, but instead it changes to state C instead.
There are two possibilities: the state is miscalculated, or it is calculated correctly but the change is applied the wrong number of times. Do an experiment to distinguish between these cases.
When X, I expect to see A, but I don’t.
When you cannot see something, there are only a few possible causes:
Run the game and look for the object using your editor tools. If you don’t find it, and it was there from the beginning, you have a case 3 pattern; if it was not there from the beginning, you have a case 1 pattern. If you do find it, a few quick experiments through inspection will tell you which of the possibilities apply. Again, if the state was like that from the beginning you have a case 1 pattern, otherwise a case 3 pattern.
I listed common patterns above, but you will spot other patterns in your code base. When you do, develop and document a strategy to debug these very quickly.
When tracking a pattern bug, and your strategy fails to diagnose the bug, use the 12-step process the find the bug, and update the strategy to account for the new possible cause. Patterns with strategies that become too complicated as they are updated after several failures seize to be useful and need to be pruned from your collection.
It is important to keep track of where you are in the debugging process. Use a checklist to keep track of the steps (remembering that Steps 4–8 get repeated).
Also keep track of the tree of causes, and especially note the experiments and outcomes.
Your progress documentation:
I designed the following checklist that works with the process described here. You can print out copies and use them to make systematic notes about your debugging progress.
Download Debug Checklist (PDF)

Sometimes you will be able to make a guess about the cause of a bug, and you may want to investigate that instead of following the systematic approach. This often happens as you work down the tree.
There are good reasons to be wary of guessing like this:
You may be wrong. If you are wrong, you are wasting time pursuing them.
You may mistake a deep cause for a root cause.For example, if you suspect a certain crash happens after dismissing a dialog because the game does not get unpaused, you may “fix” it by Unpausing the game when the dialog button is being pressed. However, it may turn out that additional cleanup is necessary, and the method that does that in addition to the unpausing the game is not subscribed to the dialog’s dismissal. So, the fix is only partial.
You may implement a fake fix. It can happen that the bug looks like it is fixed, but instead there are now two bugs — the original, and a new one that obscures the old one. This can happen because you don’t understand the chain of cause and effect. In the previous example, the real fix may be to simply call unpause on the player.
Calling unpause on the entire game also does that, but it also calls unpause on other objects — left active for ambient animation in the background — that (wrongly) resets certain values and lead to other bugs. Later, when discovering this, the fix may be to not call unpause on the game, leading the original bug to re-occur.
As you get more experienced, these guesses will tend to become more accurate, and you may decide to ignore this advice. If your guess looks correct:
Working closely with the QA team is crucial for delivering a high-quality game. Here are some guidelines to save time and ultimately deliver a better product.
Use shared standards for bug reporting and tests. This makes it easier to ensure the bug has the right information no matter where it comes from, and to follow the same processes as you start the debugging process. Using the same standard tests helps to communicate bugs and reproduction steps precisely.
Use common vocabulary when talking about the game and bug conditions. To prevent confusion, it is important to use the same terms when describing elements of the game, such as enemies, items, places, gameplay units, game modes, player actions, etc., especially since terms may not be spelled out in the game. For example, does “the second level” mean the second mission or the second campaign (a collection of missions)?
Although it is not possible to establish the entire game’s terminology upfront, you can make a start by exporting lists of gameplay elements from the game, and then add any missing terms to this terminology sheet as team members discover them. Make sure everyone is aware of this sheet and uses the vocabulary in it.
Use common vocabulary for error conditions. For game error conditions, use standard terms when they exist, such as “z-fighting”, “ghosting”, and “foot sliding”. If your game has common but non-standard problems, give those error conditions names, and use them consistently.
Do your own verification in steps 9 – 12. In projects with a QA team, programmers may sometimes feel tempted to skip some of the verification steps, assuming that their fixes are likely correct, and that any mistakes will be caught by the QA team. Too often, fixes end up being incomplete or wrong, or causes even more problems.
When this happens:
Provide tools to make testing more efficient. The tools mentioned in Step 2 to speed up debugging can speed up testing. These include modifications to the game for easier reproduction of bugs (such as level skip, invulnerability, or deterministic enemy generation) and access to in-game diagnostic tools like loggers, performance monitors, and visual aids. Specialized tools for taking screenshots that allow you to match them with the log (in time) and your game world (in space) can be an incredible time saver.
Use best practices to ensure that these tools or modifications do not end up in production builds.
Talk to testers about their needs, which may be different from those of developers.
Help testers understand the potential scope of your fix. In a complex project, the root cause may be distant from the symptom, so that the area where the fix is made is not immediately obvious for somebody that does not know the code. In one project I worked on, if you unplugged the network cable, the game would crash because of a null pointer exception in a dialog box. It would be impossible for a tester to know the UI was changed unless I told them. It is useful to add information about your fix to the ticket, and explain what parts of the game could vulnerable because of it so they can test it thoroughly.
Sometimes expected behavior is reported as bugs. When this happens, it is worth speaking to the reporter, if possible, about why this happened. It is also worth alerting the rest of the team of a possible issue that can be addressed with better communication or game design.
Many bugs recur. When you see a bug for the second or third time, investigate why it recurs. Possible causes include:
The fix was undone. This often happens when the fix causes another bug. If you don’t notice this, and reapply the fix, the programmer who undid your fix will have their bug recur, and undo your fix again! When two (or more) bugs live together in this unhealthy relationship, they need to be solved together.
It is another occurrence of the same cause. For example, the original bug was caused by a missing reference on an enemy prefab, and the new bug is caused by the same missing reference on a different enemy. In this case, the defense you implemented the first time was not robust, so make it more robust. Then go through the enemies systematically (or use a tool to do so) and look for other missing references of the same field.
There was a missing case. Certain behaviors have complex logic. For example, if you fade to black and back between scene changes, when dialogs are displayed, or a user walks into solid objects in a VR game, there are lots of cases to handle (consider, for example, what should happen when the user is already walking into an object when the scene transitions).
These types of bugs often occur when not working to a detailed specification. Logic is added bit-by-bit, and because the problem is not evaluated systematically, the logic becomes more complex than it needs to be. It would pay to write the detailed specification, and re-implement the code to follow it.
Changes made to duplicated code was not made everywhere. For example, if logic for checking that a text field only allows from a specific set of characters is replicated for each UI element, problems solved on one place will not solve problems in another. In this case, a refactoring is usually required to address the bug properly.
There is no shortage of bugs to fix, and over the span of a career you will get plenty of opportunities to practice.
If you are a new programmer with some time to spend on technical self-improvement, it may be worth doing a few weeks of intense debugging to experiment with processes and techniques. Delve into a code base, or answer questions on Game Development StackExchange.
Another way to practice is through an exercise we used at Gamelogic at the time:
The purpose of a bug-finding post-mortem is to find ways to improve bug diagnosis, and it is done for bugs that took an unexpectedly long time to diagnose. The purpose is to uncover issues that you can address to prevent these problems from tripping you up in the future.
During a debugging post-mortem, ask questions like these:
Keep track of the bugs you encounter, how they were diagnosed, how long it took, and what intervention was implemented.
This is occasionally useful as a reference when dealing with a bug you have dealt with before, making it much faster to diagnose and fix.
You can use bug tracking software to keep your history, but you need to add how you diagnosed the bug and what intervention you implemented to the ticket after you solved it.
A bug history allows you to spot common mistakes. Mistakes like these can always be prevented in principle (through coding practices, processes, tools, and libraries). For example, if you tend to swop geocoordinates by accident, you can use one of these ways to prevent it:
Over time, your bug history will allow you to improve estimates for the probabilities of certain bug causes, and the times to perform more complex experiments.
Finally, your bug history can also help you spot deeper weaknesses. Looking at the root causes and human causes of bugs points to where we can improve architecture, our programming practices, and our understanding of the system we are trying to model.
For example, in one team we had a swarm of bugs caused by programmers not understanding how other programmers hooked up and implemented their complex GUI components. Eventually, we adopted a canonical design that all GUI components must follow. Since then, bugs caused by misunderstanding the setup or implementation of a GUI component has been rare.
Misconceptions are one of the three external causes of bugs, so knowledge that removes misconceptions reduces the number of bugs you will create in the future.
Learning the math of physics, or the details of the Facebook API, or the darker corners of C#, will not only decrease the number of bugs you introduce in the future, it will also lead to more innovative games, faster development, better testing procedures, and better debugging experiments.
Use your bug diary to find the weak spots in your knowledge.
Debugging tools are used to run experiments effectively. The most important tool to master is your IDE’s debugger. How to use debuggers is a rich topic, and falls outside the scope of this article. Here are resources to get you started for two common IDEs:
Find out what specialized tools and techniques programmers use to debug in the areas of game development you work in, and learn to use them. This will save you a ton of time.
Here are some examples of what I mean:
Bugs disrupt the development process, and therefore dealing with bugs effectively is important to streamline development. And because computers are deterministic devices, we can use a scientific process to diagnose bugs in the most efficient way possible. By looking at our process over time, we can improve our process to create fewer bugs and find them faster. We do not have to be slaves to our bugs.
This article was originally inspired in 2017 by a bug that plagued my team at the time for a month, and the original ideas were sharpened by the many discussions I had with programmers Esteban Gaetes and Omar Rojo (the phrase “Computers don’t use magic” is his).
The 2023 update came about after a deep dive I undertook with Ross Adams into the causes of bugs and the processes the programmers use at 24-Bit Games.
We asked programmers about their debugging processes (among other things), and from these discussions, I added additional refinements from veterans Matt Benic (on setting up your debug environment), Diorgo Jonkers (on replicating more than once), and Johan van Staden (about anticipating how changes can break code elsewhere). Johan’s enthusiastic advocacy for checklists also inspired the checklist I made for this process.
We asked everyone (including testers and producers) about bugs and how it affects development; this shaped the emphasis a lot on the ideas discussed here.
After talking to the head of QA, Brandon Wolff, I made some tweaks to the bug reporting section.
Side-by-side debugging sessions with Peter Msimanga were especially helpful to discover some of the dangers one can encounter and to test the checklist in physical form.
Omar Rojo gave me additional ideas after reading the new draft that I incorporated. For example, he suggested to include debugging aids in reports and to ensure experiments don’t rely on slow processes.
The bug report format is from Joel Spolsky.
Some of the ideas in this article is my take on those in Effective Debugging: 66 Specific Ways to Debug Software and Systems by Diomidis Spinellis and Chapter 23 Debugging in Code Complete by Steve McConnell.
]]>A buffer is a simple data structure that allows you to add any number of elements, but it only retains the last n, where n is the capacity of the buffer.`
Here are some examples of where buffers are used:
There are only a handful of actions we need to support, shown below.
public interface IBuffer<T> : IEnumerable<T>
{
public int Count { get; }
public int Capacity { get; }
public void Insert(T item);
public void Clear();
}
A practical implementation will support more methods and properties, as we will see below.
Buffers are commonly implemented as ring buffers, where an array is used with front and back pointers that updated as items are added. When reading the contents, we start at the front pointer, increasing until we reached the back pointer (wrapping around if necessary).
public class RingBuffer<T> : IBuffer<T>
{
private const string ArgumentMustBePositive = "Argument must be positive.";
private int front;
private int back;
private readonly T[] items;
public int Count { get; private set; }
public int Capacity { get; }
public RingBuffer(int capacity)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity), ArgumentMustBePositive);
Capacity = capacity;
items = new T[capacity];
ResetPointers();
}
public void Insert(T item)
{
items[back] = item;
back++;
if (back == Capacity)
{
back = 0;
}
if (Count < Capacity)
{
Count++;
}
else
{
front++;
if (front == Capacity)
{
front = 0;
}
}
AssertCountInvariants();
}
public void Clear()
{
ReInitialize();
ResetPointers();
}
public IEnumerator<T> GetEnumerator()
{
if (front < back)
{
for (int i = front; i < back; i++)
{
yield return items[i];
}
}
else
{
for (int i = front; i < Capacity; i++)
{
yield return items[i];
}
for (int i = 0; i < back; i++)
{
yield return items[i];
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/*
Not strictly necessary for value types. For reference types,
this prevents having ghost references to objects and
so leak memory.
*/
private void ReInitialize()
{
if (front < back)
{
for (int i = front; i < back; i++)
{
items[i] = default;
}
}
else
{
for (int i = front; i < Capacity; i++)
{
items[i] = default;
}
for (int i = 0; i < back; i++)
{
items[i] = default;
}
}
}
public void ResetPointers()
{
front = 0;
back = 0;
Count = 0;
AssertCountInvariants();
}
[AssertionMethod, Conditional(GLDebug.Debug)]
private void AssertCountInvariants()
{
GLDebug.Assert(Count <= Capacity);
if (front < back)
{
Debug.Assert(Count == back - front);
}
else
{
Debug.Assert(Count == Capacity - front + back);
}
}
private static Exception EmptyBufferInvalid()
=> new InvalidOperationException(BufferCantBeEmpty);
}
The AssertCountInvariants method is to assert invariants after we modified the buffer. This helps flush out implementation bugs. When copying an algorithm where the implementation is presumably correct, scaffolding such as this may look pointless. However, it is important to learn how to use this type of scaffolding when designing or implementing an algorithm, so I keep them in. Besides, implementations that appear in books and papers have more bugs than one would hope, and therefore you should add checks like this even in code you find elsewhere.
When clearing out the container, we set the array values back to their defaults, to prevent leaks when the items are reference types.
In the implementation above, we always expect the capacity to be positive. This prevents us from having to check whether we can write the array element when we insert elements. If zero-capacity buffers are required, a separate class can be used instead.
public class ZeroCapacityBuffer<T> : IBuffer<T>
{
public int Count => 0;
public int Capacity => 0;
public T First => throw Buffer.EmptyBufferInvalid();
public T Last => throw Buffer.EmptyBufferInvalid();
/// <summary>
/// This method has no effect, since the capacity is 0.
/// </summary>
/*
Just like other buffers, it is not illegal to insert items
when we are at capacity. So we don't throw an exception; we
simply do nothing.
*/
public void Insert(T item) { }
//Nothing to do since there are no elements.
public void Clear() { }
public IEnumerator<T> GetEnumerator()
=> throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Using buffers with a capacity of two is a common case, so we may choose a (very slightly) more efficient implementation that does not use an array.
Keeping track of how many elements there are makes the code slightly complicated and annoying. We could avoid it with one or two tricks, for example, implementing the buffer so that it is always full. Often, this just moves the trickiness outside the class so the class user has to deal with it.
public sealed class FullCapacity2Buffer<T> : IBuffer<T>
{
private T item1;
private T item2;
private bool firstIsItem1;
public int Count { get; private set; }
public int Capacity => 2;
public T First
=> (Count == 0) ? throw Buffer.EmptyBufferInvalid() : FirstUnsafe;
public T Last
=> (Count == 0) ? throw Buffer.EmptyBufferInvalid() : LastUnsafe;
private T FirstUnsafe => firstIsItem1 ? item1 : item2;
private T LastUnsafe => firstIsItem1 ? item2 : item1;
public FullCapacity2Buffer() => Clear();
public IEnumerator<T> GetEnumerator()
{
if(Count > 0) yield return FirstUnsafe;
if(Count > 1) yield return LastUnsafe;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Insert(T item)
{
if (firstIsItem1)
{
item1 = item;
}
else
{
item2 = item;
}
if (Count < 2)
{
Count++;
}
firstIsItem1 = !firstIsItem1;
}
public void Clear()
{
Count = 0;
item1 = default;
item2 = default;
firstIsItem1 = false; //true after first insertion
}
}
The ring buffer implementation above allows us to add two very useful features with little effort:
Here is how that looks:
public class RingBuffer<T> : IBuffer<T>
{
// ...
public T First
=> Count > 0
? this[0]
: throw Buffer.EmptyBufferInvalid();
public T Last
=> Count > 0
? this[Count - 1]
: throw Buffer.EmptyBufferInvalid();
public T this[int index]
{
get
{
ValidateIndex(index);
return items[GetRealIndex(index)];
}
set
{
ValidateIndex(index);
items[GetRealIndex(index)] = value;
}
}
private bool IndexInRange(int index) => 0 <= index && index < Count;
private int GetRealIndex(int index)
{
Debug.Assert(IndexInRange(index));
(index + front) % Capacity;
}
[AssertionMethod]
private void ValidateIndex(int index)
{
if (!IndexInRange(index))
throw new ArgumentOutOfRangeException(nameof(index));
}
Public methods check the index, and throw an exception when it is invalid. Thereafter, we don’t validate it again — instead, we assert it to be valid in case we made a mistake.
If you don’t need random access, you can implement a buffer with a queue, which is much simpler. The queue has some additional overhead and may be less efficient, although there is no reason to worry about this unless it shows up as a problem when profiling.
public class BufferWithQueue<T> : IRingBuffer<T>
{
private readonly System.Collections.Generic.Queue<T> queue;
public int Count => queue.Count;
public int Capacity { get; }
public T First => queue.First();
public T Last => queue.Peek();
public BufferWithQueue(int capacity)
{
Capacity = capacity;
queue = new System.Collections.Generic.Queue<T>(capacity);
}
public void Insert(T item)
{
if (queue.Count == Capacity)
{
queue.Dequeue();
}
queue.Enqueue(item);
}
public void Clear() => queue.Clear();
public IEnumerator<T> GetEnumerator() => queue.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
To make a buffer that can be resized, you can use the appropriate fixed-sized buffers as shown below.
public class ResizeableBuffer<T> : IBuffer<T>
{
private IBuffer<T> buffer;
public int Count => buffer.Count;
public int Capacity => buffer.Capacity;
public T First => buffer.First;
public T Last => buffer.Last;
public ResizeableBuffer(int capacity)
=> buffer = new RingBuffer<T>(capacity);
public void Insert(T item) => buffer.Insert(item);
public void Clear() => buffer.Clear();
public IEnumerator<T> GetEnumerator() => buffer.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Resize(int newCapacity)
{
if (newCapacity < 0)
throw new ArgumentOutOfRangeException(nameof(newCapacity), "Argument must be positive");
if (newCapacity == 0)
{
buffer = new ZeroCapacityBuffer<T>();
return;
}
var newBuffer = new RingBuffer<T>(newCapacity);
foreach (var item in buffer.Take(newCapacity))
{
newBuffer.Insert(item);
}
buffer = newBuffer;
}
}
Sequences where each element is defined as a function of a few previous terms, can be implemented iteratively with a buffer. Here is how the Fibonacci sequence can be calculated:
public static int GetFibonacci(int n)
{
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), "Can't be negative.");
if (n == 0 || n == 1) return 1;
var buffer = new RingBuffer<int>(2);
buffer.Insert(1);
buffer.Insert(1);
for (int i = 0; i < n - 2; i++)
{
buffer.Insert(buffer[0] + buffer[1]);
}
return buffer.Last;
}
The example below shows how to use a buffer to apply a median filter to a list.
public class StatsExample
{
public static IEnumerable<float> MedianFilter(IEnumerable<float> list)
{
float Median(float a, float b, float c) =>
(a > b) ^ (a > c)
? a
: (b < a) ^ (b < c)
? b
: c;
int windowSize = 3;
var buffer = new RingBuffer<float>(windowSize);
foreach (float item in list)
{
buffer.Insert(item);
if (buffer.Count >= windowSize)
{
yield return Median(buffer[0], buffer[1], buffer[2]);
}
}
}
}
It is common to execute code only when the value of variable changes. This is typically implemented by storing the previous value too. Using a change detector shown below can make the code cleaner and safer.
public class ChangeDetector<T>
{
private readonly IBuffer<T> buffer;
private readonly IEqualityComparer<T> comparer;
public event Action<T, T> OnValueChanged;
public T Value
{
get => buffer.Last;
set
{
buffer.Insert(value);
if (buffer.Count == 2 && !comparer.Equals(Value, PreviousValue))
{
OnValueChanged?.Invoke(Value, PreviousValue);
}
}
}
public T PreviousValue => buffer.First;
public ChangeDetector(IEqualityComparer<T> comparer = null)
{
this.comparer = comparer ?? EqualityComparer<T>.Default;
buffer = new RingBuffer<T>(2);
}
public void Clear() => buffer.Clear();
}
Here is an example of using this class:
public class ChangeDetectExample
{
private readonly ChangeDetector<int> guess = new();
public ChangeDetectExample()
=> guess.OnValueChanged += (value, previousValue) => Console.WriteLine($"You changed your guess from {previousValue} to {value}");
public void Run() => guess.Value = AskGuess();
public int AskGuess() => 0; /* Real implementation omitted */
}
The code below shows how to use buffers to implement a differentiator and integrator, and then to use those classes to implement a PID controller.
public sealed class Differentiator
{
private readonly IBuffer<float> buffer;
public float Value
{
get => buffer.Last;
set => buffer.Insert(value);
}
public float PreviousValue => buffer.First;
/*
Technically to be a derivative we need to divide by the time.
If we assume a constant sample rate, this is a constant, that
can be absorbed by the PID filter.
*/
public float Difference =>
buffer.Count == 2
? Value - PreviousValue
: throw new InvalidOperationException("Not enough values set to calculate a derivative");
public Differentiator() => buffer = new RingBuffer<float>(2);
}
public sealed class Integrator
{
private readonly IBuffer<float> buffer;
public float Value
{
get => buffer.Last;
set => buffer.Insert(value);
}
public float PreviousValue => buffer.First;
/*
Technically, we need to scale each interval by the time between
samples. We assume the sample rate is constant, and that it can
be absorbed by the factor in the PID controller.
*/
public float Sum => buffer.Sum();
public Integrator(int sumWindow) =>
buffer = new RingBuffer<float>(sumWindow);
}
public sealed class PidController
{
private readonly Differentiator differentiator;
private readonly Integrator integrator;
private readonly float differentiatorFactor;
private readonly float integrationFactor;
private readonly float proportionalFactor;
public float Value
{
get => differentiator.Value; //Could also use integrator
set
{
differentiator.Value = value;
integrator.Value = value;
}
}
public float FilteredValue =>
proportionalFactor * Value
+ differentiatorFactor * differentiator.Difference
+ integrationFactor * integrator.Sum;
public PidController(int integrationWindow, float proportionalFactor, float differentiatorFactor, float integrationFactor)
{
differentiator = new Differentiator();
integrator = new Integrator(integrationWindow);
this.proportionalFactor = proportionalFactor;
this.differentiatorFactor = differentiatorFactor;
this.integrationFactor = integrationFactor;
}
}
]]>Normals are vectors perpendicular to the mesh. Mathematically there are two such normals (in opposite directions), however, in mesh rendering only one side of a triangle is visible, and by convention, only one of the two normals is used — the one pointing away from the visible face.

Normals are used in lighting calculations, and for a mesh we can specify a normal for each vertex.
Roughly, the orientation of a triangle determines how much light it will reflect (it also depends on the angle of the light and camera).
We can quite easily determine the normals for a triangle by taking the cross product of the vectors made from any two sides.
And indeed, Unity provides a convenience method that does this, and we have been using it for the normal calculations on our 2D meshes. In this case, the three normals will be the same.
But they need not be the same, and when they are different, behind the scene the system will interpolate them so that for each position on the triangle, there will be a normal calculated based on how far the point is to the vertices of the triangle. If we represent the normals as colors, we can get a picture of how this will look:

As you can see, the colors are smooth, and indeed so are the normals. The effect is that the triangle is rendered as if its normals are changing smoothly across the triangle, making it look curved! This is the reason you cannot see where the triangles are in a sphere or cylinder.
When two triangles have a vertex in the same position, we have a choice whether to share it (so there is just one instance in our vertex list), or not (so there are two copies of the vertex in our vertex list). Which one you choose is determined by whether you need one of the other lists (UVs, normals, or tangents) to have separate data for vertices of different triangles.
In the case of normals, if the normals are the same for all vertices at a point, the surface will look smooth. If we want flat surfaces instead, we need to have vertices separate. (Of course, we may need different things at different places, so our decision need not be made uniform over the entire mesh). We already looked at how to calculate correct normals for flat surfaces. How do we calculate normals for smooth objects?
There are two ways we can go about it:
1. Since the shape is generated procedurally, we (probably) have a way to calculate the correct normal directly. This is often the case with mathematical objects (spheres, cylinders, etc.). In fact, if we have a parameterization of the surface, we there is a general purpose formula for the normal.
2. If it is not possible to calculate a normal directly, we can compute flat normals for each triangle a vertex position lies in, and average them out. Of course, averaging directions need some care.
If the surface is “fairly smooth”, you can probably get away with a straight linear average and normalize:
normal = ((normal_1 + normal_2 + ... + normal_i) / i).normalize;
This solution has some problems:
For many every-day purposes you should not have to worry about these issues.
Tangents are usually used by bump shaders, and like normal maps convey information about the surface’s orientation at a vertex. Tangents are parallel to the surface at the vertex (that is, perpendicular to the normals), and lie in the U direction of the texture at that vertex. A tangents is represented as a Vector4; the first three components is the direction of the tangent, and the last component is 1 or -1, used to flip the tangents.



Normally, you would use your knowledge of the shape directly to calculate the tangent, although technically you could use calculations from UVs and the vertices to construct a tangent.
It also seems that Unity calculates decent tangents if you don’t provide any, although I have no idea whether this is robust when the meshes are more complicated.
In this example, we will be generating a mesh for a open-ended double sided cylinder with smooth normals. Even though the mesh is double sided, we will only do calculations for the outside. The inside will be automatically be generated from a special method (given at the end).
Here is a simplified schematic of the vertex setup we will use. Notice that two vertices are duplicated; this is so we can have separate UVs so that triangles 7,8,1 and 7, 6, 1 will be textured correctly. Other vertexes between neighboring triangles are shared, since we are making a smooth mesh.

We will have three parameters:
Vertices. We already saw how to calculate vertices for a circle (but this time we duplicate the first and last vertex). We can use this for the green vertices; for the red vertices we simply replicate the green ones and adjust their y coordinate to the height of the cylinder.
There are 2n + 2 vertices in total.
Triangles.
UVs. For the UVs, we simply divide the rectangle into n segments. The bottom UVs are given by (i / n, 0), and the top UVs by (i / n, 1), where i ranges from 0 to n.
Normals. Normals are parallel to the XZ plane, and point away from the central axis of the cylinder. Below the direction of the normals and tangents are shown from the top.

The normals in this case is easily calculated from the vertices:
normal[i] = vertex[i]; normal[i].y = 0; //ignore height normal[i].Normalize();
Tangents. The tangents are also easily calculated from the vertices, or even easier from the normals.
tangent[i] = normal[i].PerpXZ();
Remember:
Test all debugging techniques with a simple quad first. If you use any debugging technique for the first time, make sure to test it on a quad first to see if you get the results you expect. This will eliminate a common source of confusion: incorrect debug code or assets.
Use a generic way to build double sided meshes. Remember triangles are visible from only one side. If you want to see the other side too, you need to replicate all lists, and flip normals, and flip triangles. The MeshData class class below can be used to combine meshes, as well as make them double sided.
Render arrows in normal and tangent directions. For the latter, you may also want to use a special texture that clearly marks the positive U direction with arrows; you should then compare this with the tangents and make sure they match. Also, render a plus or minus depending on whether the w component of the tangent is +1 or -1. Notice that the gradient texture below also allows us to see UV seems.

Use a special shader and material to visualize normals as colors. In this case, put a sphere with the same debug material in your scene and compare the colors of your mesh with those on the sphere. It is important whether your debug shader uses world space or local space. If the latter, the sphere needs to be in the same orientation as the mesh; otherwise it does not matter. The code for such as shader is given below. The vector components range between -1 and 1, but colors go from 0 to one, so we need to map vectors to colors. There are two basic ways:
0.5*x + 0.5. This method allows you to distinguish opposite vectors, but can be hard to read.abs(x). This is easier to read (the redder, the more in the +/- x direction something lies). However, it cannot distinguish between opposite vectors, and having normals in the opposite direction is a common bug. 

Build a special debug light rig. This can be used instead of the shader described above. Either use three or six lights. In both setups, we have red, green, and blue lights along the directions of the axes, but in the second we one in each of the two directions. This setup can be more useful if you also need to see the #D shape of the object (and is quicker to implement if you don’t have a shader at hand. If you use this method, remember to remove other light sources, including ambient light, to avoid confusion.


Show the bounding box info. This will help you spot when points are too far apart (and far from the camera) or too close together to make the mesh visible. Note It is not enough to render the bounding box, as the bounding box too will be invisible if it is too big or small.
Shader "Debug/Normals"
{
Properties
{
[Toggle(USE_WORLD_SPACE)]
_UseWorldSpace("Use World Space", Float) = 0
[Toggle(USE_STANDARD_COLOR_MAP)]
_UseStandardColorMap("Use Standard Color Map", Float) = 0
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// include file that contains UnityObjectToWorldNormal helper function
#pragma shader_feature USE_WORLD_SPACE
#pragma shader_feature USE_STANDARD_COLOR_MAP
#include "UnityCG.cginc"
struct v2f
{
// we'll output world space normal as one of regular ("texcoord") interpolators
half3 normal : TEXCOORD0;
float4 pos : SV_POSITION;
};
// vertex shader: takes object space normal as input too
v2f vert(float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
#ifdef USE_WORLD_SPACE
// UnityCG.cginc file contains function to transform
// normal from object to world space, use that
o.normal = UnityObjectToWorldNormal(normal);
#else
o.normal = normal;
#endif
//o.normal = normal;// UnityObjectToWorldNormal(normal);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 c = 0;
#ifdef USE_STANDARD_COLOR_MAP
c.rgb = i.normal * 0.5 + 0.5;
#else
c.rgb = abs(i.normal);// *0.5 + 0.5;
#endif
return c;
}
ENDCG
}
}
}
public class MeshData
{
public List<Vector3> vertices;
public List<int> triangles;
public List<Vector2> uvs;
public List<Vector3> normals;
public List<Vector4> tangents;
public static MeshData Combine(List<MeshData> meshes)
{
MeshData combinedMesh = new MeshData
{
vertices = new List<Vector3>(),
triangles = new List<int>(),
uvs = new List<Vector2>(),
normals = new List<Vector3>(),
tangents = new List<Vector4>()
};
int vertexIndexOffset = 0;
foreach(var mesh in meshes)
{
combinedMesh.vertices.AddRange(mesh.vertices);
combinedMesh.triangles.AddRange(mesh.triangles.Select(index => index + vertexIndexOffset));
if(mesh.uvs != null)
{
combinedMesh.uvs.AddRange(mesh.uvs);
}
if(mesh.normals != null)
{
combinedMesh.normals.AddRange(mesh.normals);
}
if(mesh.tangents != null)
{
combinedMesh.tangents.AddRange(mesh.tangents);
}
vertexIndexOffset += mesh.vertices.Count;
}
return combinedMesh;
}
public MeshData Flip()
{
return new MeshData
{
vertices = vertices,
uvs = uvs,
normals = MeshBuilderUtils.FlipNormals(normals),
triangles = MeshBuilderUtils.FlipTriangles(triangles),
tangents = tangents
};
}
//Does not check if already double sided
public MeshData GetDoubleSided()
{
var meshes = new List<MeshData>
{
this,
this.Flip()
};
return Combine(meshes);
}
}
using Gamelogic.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
public enum MeshType{
XYZ,
XY,
XZ
}
[Serializable]
public class DebugInfo
{
[ReadOnly]
public int vertexCount = 0;
[ReadOnly]
public int triangleIndexCount = 0;
[ReadOnly]
public int normalCount = 0;
[ReadOnly]
public int uvCount = 0;
[ReadOnly]
public Bounds bounds;
}
[RequireComponent(typeof(MeshFilter))]
public class MeshBuilder: MonoBehaviour
{
private readonly static Color DebugSphereColor = new Color(1, 0.25f, 0);
private readonly static Color DebugNormalColor = new Color(1, 1, 0);
private readonly static Color DebugTangentColor = new Color(1, 0, .5f);
private const float SmallestValidTriangleArea = 0.01f;
[Header("Debug Options")]
[SerializeField]
private DebugInfo debugInfo = new DebugInfo();
[SerializeField]
private bool drawDebugVertices = false;
[SerializeField]
private float debugSphereRadius = 0.1f;
[SerializeField]
private bool drawDebugLabels = false;
[SerializeField]
private bool printDebugInfo = false;
[SerializeField]
private bool drawDebugNormals = false;
[SerializeField]
private bool drawDebugTangents = false;
[SerializeField]
private MeshType meshType = MeshType.XYZ;
//Use property instead to ensure initialization
private MeshFilter meshFilter;
//We keep this as a variable so we can draw debug Info
private List<Vector3> vertices;
//Same
private List<Vector3> normals;
//Same
private List<Vector4> tangents;
private GUIStyle vertexLabelStyle;
[HideInInspector]
[SerializeField]
protected GeometryDebug meshDebug;
protected MeshFilter MeshFilter
{
get
{
if (meshFilter == null)
{
meshFilter = GetComponent<MeshFilter>();
//cannot be null since MeshFilter is required.
}
return meshFilter;
}
}
public void UpdateMesh()
{
if (meshDebug == null)
{
meshDebug = new GeometryDebug();
}
meshDebug.Clear();
DestroyOldMesh();
Preprocess();
var mesh = new Mesh();
vertices = CalculateVertices();
debugInfo.vertexCount = vertices.Count;
DebugLog("Vertices", debugInfo.vertexCount);
mesh.SetVertices(vertices);
var triangles = CalculateTriangles();
debugInfo.triangleIndexCount = triangles.Count;
DebugLog("Triangles", debugInfo.triangleIndexCount);
mesh.SetTriangles(triangles, 0);
//We set triangles before doing this test so that the number of
//vertex indices and their range can be checked before we execute
//the code below, that will
ValidateTriangleAreas(triangles);
var uvs = CalculateUvs(vertices);
if (uvs == null)
{
//can be null if subclass
debugInfo.uvCount = -1;
DebugLog("Uvs", null);
}
else
{
debugInfo.uvCount = uvs.Count;
DebugLog("Uvs", debugInfo.uvCount);
mesh.SetUVs(0, uvs);
}
normals = CalculateNormals();
if (normals == null)
{
//can be null if subclass does override default normals
DebugLog("Normals", null);
mesh.RecalculateNormals();
normals = new List<Vector3>();
mesh.GetNormals(normals);
debugInfo.normalCount = normals.Count;
}
else
{
debugInfo.normalCount = normals.Count;
DebugLog("Normals", debugInfo.normalCount);
mesh.SetNormals(normals);
}
tangents = CalculateTangents();
if (tangents == null)
{
tangents = new List<Vector4>();
mesh.GetTangents(tangents);
}
else
{
mesh.SetTangents(tangents);
}
mesh.RecalculateBounds();
debugInfo.bounds = mesh.bounds;
meshFilter.sharedMesh = mesh;
}
virtual protected List<Vector4> CalculateTangents()
{
var tangent = new Vector4(1, 0, 0, 1);
return new List<Vector4>
{
tangent,
tangent,
tangent,
tangent
};
}
private void ValidateTriangleAreas(List<int> triangles)
{
for (int i = 0; i < triangles.Count / 3; i++)
{
int vertexIndexA = triangles[3 * i];
int vertexIndexB = triangles[3 * i + 1];
int vertexIndexC = triangles[3 * i + 2];
var vertexA = vertices[vertexIndexA];
var vertexB = vertices[vertexIndexB];
var vertexC = vertices[vertexIndexC];
float area = MeshBuilderUtils.AreaOfTriangle(vertexB - vertexA, vertexC - vertexA);
if (area < SmallestValidTriangleArea)
{
Debug.LogWarning(
string.Format(
"Triangle is too small. <{0}, {1}, {2}>, <{3}, {4}, {5}>",
vertexIndexA, vertexIndexB, vertexIndexC, vertexA, vertexB, vertexC
));
}
}
}
public void OnDrawGizmos()
{
if (vertices == null) return; //can happen if no mesh was ever created.
//if(vertexLabelStyle == null)
{
vertexLabelStyle = new GUIStyle();
vertexLabelStyle.normal.textColor = Color.white;
vertexLabelStyle.alignment = TextAnchor.MiddleCenter;
}
if(drawDebugVertices)
{
meshDebug.Radius = debugSphereRadius;
for (int i = 0; i < vertices.Count; i++)
{
var vertex = vertices[i];
var spherePosition = transform.TransformPoint(vertex);
float radius = transform.lossyScale.magnitude * debugSphereRadius;
GeometryDebug.DrawDot(spherePosition, radius, DebugSphereColor, meshType);
}
if (meshDebug != null)
{
meshDebug.Draw(transform, meshType);
}
}
if (drawDebugNormals)
{
for (int i = 0; i < normals.Count; i++)
{
var vertex = vertices[i];
var transformedPosition = transform.TransformPoint(vertex);
var normal = normals[i];
var transformedNormal = transform.TransformDirection(normal);
float length = 2 * transform.lossyScale.magnitude * debugSphereRadius;
GeometryDebug.DrawArrow(transformedPosition, transformedNormal, length, DebugNormalColor);
}
}
if (drawDebugTangents)
{
for (int i = 0; i < tangents.Count; i++)
{
var vertex = vertices[i];
var transformedPosition = transform.TransformPoint(vertex);
var normal = tangents[i];
var transformedNormal = transform.TransformDirection(normal);
float length = 2 * transform.lossyScale.magnitude * debugSphereRadius;
var label = (normal.w > 0) ? "+" : ((normal.w < 0) ? "-" : null);
GeometryDebug.DrawArrow(transformedPosition, transformedNormal, length, DebugTangentColor, label);
}
}
if (drawDebugLabels)
{
for (int i = 0; i < vertices.Count; i++)
{
var vertex = vertices[i];
var spherePosition = transform.TransformPoint(vertex);
float radius = transform.lossyScale.magnitude * debugSphereRadius;
Handles.Label(spherePosition + Vector3.up * radius / 2, i.ToString(), vertexLabelStyle);
}
}
}
virtual protected List<Vector3> CalculateVertices()
{
return MeshBuilderUtils.QuadVertices();
}
virtual protected List<Vector2> CalculateUvs(List<Vector3> vertices)
{
return MeshBuilderUtils.GetStandardUvsXY(vertices, true, true);
}
virtual protected void Preprocess() { }
virtual protected List<int> CalculateTriangles()
{
return new List<int>
{
0, 3, 1,
1, 3, 2
};
}
virtual protected List<Vector3> CalculateNormals()
{
return null;
}
[ContextMenu("Update Mesh")]
protected void UpdateMeshTest()
{
UpdateMesh();
}
private void DestroyOldMesh()
{
if (MeshFilter.sharedMesh != null)
{
if (Application.isPlaying)
{
Destroy(MeshFilter.sharedMesh); //prevents memory leak
}
else
{
DestroyImmediate(MeshFilter.sharedMesh);
}
}
}
protected void DebugLog(string label, object message)
{
if (!printDebugInfo) return;
if (message == null)
{
DebugLog(label, "null");
}
else
{
Debug.Log(label + ": " + message.ToString(), this);
}
}
protected void DebugAddDotXY(Vector3 position, GLColor color)
{
meshDebug.AddDotXY(position, color);
}
public void DebugAddArrow(Vector3 position, Vector3 direction, GLColor color)
{
meshDebug.AddArrow(position, direction, color);
}
private void OnValidate()
{
if (MeshFilter.sharedMesh == null) return;
if (vertices == null)
{
vertices = MeshFilter.sharedMesh.vertices.ToList();
DebugLog("Vertices", "Refreshed from mesh.");
}
if(normals == null)
{
normals = MeshFilter.sharedMesh.normals.ToList();
DebugLog("Normals", "Refreshed from mesh.");
}
if(tangents == null)
{
tangents = MeshFilter.sharedMesh.tangents.ToList();
DebugLog("Tangents", "Refreshed from mesh.");
}
}
}
public class GeometryDebug
{
public List<Vector3> dotPositions;
public List<GLColor> dotColors;
public List<Vector3> arrowPositions;
public List<Vector3> arrowDirections;
public List<GLColor> arrowColors;
public List<Color> colorMap = new List<Color>
{
Color255(255, 70, 70),
Color255(255, 150, 0),
Color255(255, 255, 60),
Color255(150, 220, 0),
Color255(25, 174, 255),
Color255(186, 0, 255),
Color255(255, 0, 193),
Color255(0, 0, 0),
Color255(255, 255, 255),
};
private static GUIStyle labelStyle;
public static GUIStyle LabelStyle {
get
{
if(labelStyle == null)
{
labelStyle = new GUIStyle
{
normal = new GUIStyleState { textColor = Color.white }
};
}
return labelStyle;
}
}
private static Color Color255(int r, int g, int b, int a = 255)
{
return new Color(r / 255f, g / 255f, b / 255f, a / 255f);
}
public float Radius
{
get; set;
} = 0.1f;
public GeometryDebug()
{
dotPositions = new List<Vector3>();
dotColors = new List<GLColor>();
arrowPositions = new List<Vector3>();
arrowDirections = new List<Vector3>();
arrowColors = new List<GLColor>();
}
public void Clear()
{
dotPositions.Clear();
dotColors.Clear();
arrowPositions.Clear();
arrowDirections.Clear();
arrowColors.Clear();
}
public void AddDotXY(Vector3 position, GLColor color)
{
dotPositions.Add(position);
dotColors.Add(color);
}
public void AddArrow(Vector3 position, Vector3 direction, GLColor color)
{
arrowPositions.Add(position);
arrowDirections.Add(direction);
arrowColors.Add(color);
}
public void Draw(Transform transform, MeshType meshType = MeshType.XYZ)
{
for(int i = 0; i < dotPositions.Count; i++)
{
var position = transform.TransformPoint(dotPositions[i]);
float radius = transform.lossyScale.magnitude * Radius;
Color color = colorMap[(int)dotColors[i]];
DrawDot(position, radius, color, meshType);
}
for(int i = 0; i < arrowPositions.Count; i++)
{
var position = transform.TransformPoint(arrowPositions[i]);
var direction = transform.TransformDirection(arrowDirections[i]);
float size = transform.lossyScale.magnitude * 0.2f;
Color color = colorMap[(int)arrowColors[i]];
DrawArrow(position, direction, size, color);
}
}
public static void DrawArrow(Vector3 position, Vector3 direction, float size, Color color, string label = null)
{
Handles.color = color;
Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(direction), size, EventType.Repaint);
if (label != null)
{
Handles.Label(position + size * direction, label, LabelStyle);
}
}
public static void DrawDot(Vector3 position, float radius, Color color, MeshType meshType)
{
switch (meshType)
{
case MeshType.XYZ:
Gizmos.color = color;
Gizmos.DrawWireSphere(position, radius);
break;
case MeshType.XY:
Handles.color = color;
Handles.DrawSolidDisc(position, Vector3.forward, radius);
break;
case MeshType.XZ:
Handles.color = color;
Handles.DrawSolidDisc(position, Vector3.forward, radius);
break;
}
}
}
]]>But if you can do some math, it can be very useful, especially for procedural techniques and special effects. Below are what I believe the best books covering topics relevant to games.
I left out two broad topics: calculus and statistics. Both those are also useful in games, but less often and in more specific contexts.
— Eric Lengyel
This concise book has four chapters, arranged in order of importance. The first three chapters are essential reading. They deal with vectors, matrices, transformations, and geometry. The material on vectors, matrices, and transformations is solid, but the geometry chapter is a bit light. Even so, its section on normals transformation would have saved me a lot of time the first time I had to do one.
The final chapter is a treat; it deals with projective geometry, and other types of algebra that makes geometry more elegant, and illuminates curiosities such as why the cross product only exists in certain dimensions. Although it is interesting, this is unlikely to be useful to the typical game developer.

— Christer Ericson
Although this is not strictly a mathematics book, the bulk of the content is about geometry, and this is a great book to learn about geometric programming. Despite the title, many of the algorithms are useful in other contexts (such as navigation, for example).
What I really like about this book is that it is very practical — it does not gloss over the issues you will face when you write an actual program. There are chapters on geometrical robustness, and optimization, which are very useful if you write real code.

— Philip J. Schneider and David H. Eberly
This tome is a very useful reference that contains hundreds of algorithms. It discusses many recurring geometric problems: finding distances, intersections, projections, angles, normals between or of various primitives. It also discusses some other topics such as space partitioning, point in polygon or polyhedra tests, and Boolean operations on polygons and polyhedra (a topic not covered in many books).

— Max K. Agoston
This book covers a wide variety of topics related to rendering, including projection, lighting and shaders, and rasterization.
There are also some chapters on physics, random numbers, and interpolation. There is a good chapter discussing the intricacies of floating-point numbers.

— Gerald Farin and Dianne Hansford
Most linear algebra books (and I guess university courses) focus on the algebraic aspect of linear algebra. This viewpoint is of limited use to game developers. But this book is centered around geometry. It covers a large number of topics, including 2D and 3D triangulations, barycentric coordinates, lightning and shading, homogeneous coordinates, polygons, Bézier curves, and many more.
It also covers the “usual” linear algebra topics such as eigenvectors and determinants. But the book shows you their geometric interpretation.

— Satyan L. Devadoss and Joseph O’Rourke
This book beautiful — very visual. It covers more advanced geometric algorithms dealing with triangulations, Voronoi diagrams, curves, and polyhedra. It is a bit more theoretical than some of the other algorithm books listed here, but it has compact algorithm descriptions (with time complexity shown) that is useful to reference. This is the right book to get a solid base if you do a lot of geometric coding.

— Andrew J. Hanson
The sections on quaternions in Foundations of Game Engine Development, Volume 1 are sufficient for practical purposes. Unfortunately, most books that delve deeper into quaternions are either very mathematical (using concepts from abstract algebra), or very confusing. This book falls into the latter category, and sometimes it is difficult to understand what the author means.
However, if you can fill in the blanks, this book covers many interesting topics. It gives you four different viewpoints, that are helpful for understanding quaternions and rotations more intuitively. It also considers some applications (such as orientation along 3D curves and surfaces) in a lot of depth.

Project structure is not rocket science. You need to:
Each team and each project is different, and it is impossible to anticipate all the consequences of decisions at the beginning. It is better to have simple rules that people can follow easily, and adjust over time as certain pain points emerge.
It may not be immediately obvious why naming is part of the structure. The reasons are:
What do we want the project structure to do? Ideally, the project structure should:
As I said before, your team may land on a different set of principles. The ones you choose reflect your processes (and culture). It is not so important what your principles are, but that you think about how your project structure can support them.
Coming up with rules in order to uphold the principles you came up with is more difficult, and even in a team there may be sharp differences. These differences usually reflect differences in workflow and pain-points, so they are expected and healthy. But it means you need involvement from different members of the team (if not everyone, at least someone from each functional unit) not only to get their buy-in, but also to avoid decisions that affect the workflow of some members negatively.
As an example of how to make (and think) about these rules, I will describe what we do in our studio.
Name assets and folders well. The most basic technique of naming things well is to name them what they are. It sounds obvious but people don’t do it. Maybe its because they are concentrating on the task rather than the thing they are naming at that moment.
I find that if I ask someone what something is, they give me a response that will make a good name. “What is this?” It is the bump map for the octopus. OctopusBumpmap. “What is this?” “It’s the button that takes you back to the main menu” BackToMainMenuButton or perhaps MainMenuButton or even BackButton. (The trick can work on yourself too, but it is harder to answer concretely when you already know what it is. Telling someone else forces you to be precise.)
Some bad names arise because something changes over time. This used to be the MainMenuButton, but now it brings up a popup instead. When something changes (what it is or does) the name should change too. It is for this reason that I strongly advise against using names to refer to things in code, because this increases the cost of name changes and therefore bad names are more common.
For programmers, I highly recommend the sections in Code Complete about naming (Section 7.3, Chapter 11, and Section 12.7).
Follow conventions across studio and projects. Conventions do two things:
Consider what is used to find assets. There are two basic ways: through search, and through navigating a tree structure. For the former, you would prefer to use more unique names, with the latter you will prefer a well-balanced tree. These two things together usually mean names tend to be a bit longer; not only because fewer duplicates are possible, but they are also less is desirable so that you can identify the asset you are looking for in search results.
Do what a reasonable person may expect. Where would an outsider go to look for things? What would they think they should be called? Over time a group of people will drift away from expected defaults as their process becomes more specific, but when making decisions about something for the first time, try to make decisions that will not surprise other developers.
Keep scenes, code, and assets used for testing separate. This makes it easier to strip them out when you deploy, and in addition, this helps people work together. (This is discussed further later).
Using something like Unity cloud to make builds. This has several advantages, but the main one is as long as the code is committed, anyone can make a build; the settings are consistent, and it’s straightforward to get it to the next step (usually onto various test devices). And besides, you then don’t have to worry about where builds should be.
Keep third-party code and tools separate. This makes it easier to update (and also prevents you from accidentally changing them).
Prevent messing with the structure of third-party code and tools. Some developers like to remold third-party code into their own form. The downside is that this makes updating these assets very expensive. I would definitely not recommend doing this with for example the Facebook SDK that is bound to change a few times during the development of the project. One exception is that you may wish to remove examples and documentation out of the project.
The last two points do not apply to other assets such as models and textures. It makes sense to bring that into the structure of the projects, and those are unlikely to need updates.
Split files so that as far as is possible, only one person will need to work on it. This may be difficult with certain files, such as scene files, but even there you can often design things to avoid people having to work on the same scene at the same time. (In Unity, you could use additive scene loading, prefabs, etc.)
Use test scenes to develop new features or assets. Once you have the code or asset in a stable form, move it into the main scene. When you follow this strategy, use a folder called FeaturesInDevelopment and call the scene by the feature or asset: VampireAnimations, HUD, CollisionAvoidanceAI. For this to work, you may need to mock certain aspects of the game. Some developers will make a copy of the main scene and use that; but the additional clutter may make it difficult to develop the feature.
This is simply common sense: it may be putting similar things together (such as textures or bird models), or it may be putting the components of a thing together, such as all the script files of the UI.
This is a category of smaller rules that makes things prettier and (sometimes) more usable.
InputButton, OKButton, CancelButton (and not ButtonInput, ButtonOK or ButtonCancel). This makes it easier to find particular buttons in lists. If you want to group them, put them in a folder. (Although you would of course not do BumpmapBat, DiffuseBat, DetailBat.)In this post I show some examples of rolling out your line meshes. To develop a full-fledged multi-purpose line solution is a big project, in practice you will usually build a solution closely tailored for your application. Here we will look at a few specific-purpose solutions to go over some of the main concepts. You should be able to put these together into something that is suitable for your own use-cases.
This post builds on Generating procedural meshes in Unity. Since writing that post, I realized I missed some steps in the process I proposed.
Here are the steps used here:
However, when we break down our calculations into primitives, we follow a different path from step 4 onwards. Here are the steps I used to design the mesh builders for this post:
I also realized the “trianglesPerRad” is a silly metric to use since it is so hard to visualize radians. Instead, use trianglesPerRevolution. I always start with raw triangleCount for sectors, and convert that to a calculated amount once the whole thing is working.
Geometric code always have lots of annoying cases where the naive solutions breaks down. It is not uncommon to have more “special case” code than normal case code. It is especially tricky because it is hard to exhaust all the special possibilities.
When it comes to line code, here are some situations that can cause problems.
Too few points. For example, the broken line mesh builder below checks for intersections of two lines parallel to consecutive segments, which means at least three points is required. Special code is required if there are fewer than three points. You can usually see this situation easily in code
for(i = 0 ... count – k) func(points[i], points[i + 1], ..., points[i + k]) //requires at least k + 1 points. </p>
Points that are too close. This is a problem if we need the direction of the segment between the two points, for example, to calculate a perpendicular.
Intersections of lines that are too parallel. Parallel lines either don’t intersect, or they coincide. In either case, the intersection is not useful, and we need special code to handle parallel lines. Even when lines are merely almost parallel can cause problems – the intersection may be a point very far from the area we are interested in (for example, too far from the camera). In broken line mesh builders, there are two cases of almost parallel: a vertex whose adjacent segments makes an angle of almost 180 degrees, and a vertex whose adjacent segments makes an angle of almost 360 degrees. The first case is much easier handled (we can make a stand-in intersection perpendicular to the segments). The second case needs special code (and in fact, that code is fairly complicated and not given here).
Lines are very thick. Thismay cause lines to overlap themselves in unexpected ways, messing up the triangulation.
Points very far apart. I thought that very long lines might also cause problems, but it seems Unity handles lines 10000 units long and 0.01 units thick fine.
We will be using some generic methods to generate shapes we use across multiple builders: quads and sectors. We looked at both of these in the previous post.
In the previous post, I used a base class that was suitable for building simple meshes. However, it is not great for meshes made from different parts. Instead, these are the main classes to support the builders for this post. Sourcecode is given at the end.
MeshData is a simple class with all the data we need: vertices, triangles, UVs and normals. It also has a Compose method that allows you to compose a list of MeshData instances into a single one.MeshDataMeshBuilder is a new base class suitable for combined meshes. Instead of requiring you to override different methods for vertices, triangles, and so on, you only have to implement a single method that returns everything in a MeshData object. (This class extends the original base class MeshBuilder.)MeshBuilderUtils contains code to generate sectors and quads, as well as other helpers, mostly to do geometry.GeometryDebug is a new class that handles the display of debugging information (points and arrows). You normally don’t work with this class directly, but instead with methods provided in MeshBuilder.(This is still not the setup I will use for a library. I would prefer to be able to have MeshData objects for primitive shapes, and compose those using a generic builder. That is for a future post.)
Here we will look at three types of line builder:

A line segment with round corners is simply a long thin rectangle with two half-circles stuck onto two opposite sides, which means we could use the square and circular sector meshes of the previous post if we wanted to. Here we will do it from scratch, although the thinking is the same as for the previous meshes.
Decide on the parameters: Two endpoints (point0 and poin1), width, triangleCount.
Potential Special Cases: Two end points are too close; thickness is 0.
Decide on primitives: Two sectors and a quad.
Calculate parameters of primitives:


Form a series of points that represent consecutive line segments, we make a shape as follows: For each segment, we raise two perpendiculars, one on each side of the segment, half the line width. Through these, we find two lines parallel to the segment, one on each side. The points where consecutive lines on the same side of the broken line intersect are the join vertices. For the end points, we use the perpendiculars raised at the end points and half the line width.
Decide on the parameters: points that represent the nodes of the line (i.e. end points of segments that make up the line) and line width.
Potential Special Cases: Two consecutive points are too close; thickness is 0, consecutive segments are too parallel.
Assign vertices: We do it as shown in the image.
Calculate the vertices: This is explained in the paragraph above. If the angle between consecutive segments is almost 180 degrees, we use tops of perpendicular instead of intersection points. The other near-parallel case (when the segments form an angle of almost 0) is not handled. I fix is possible, but would be difficult, and I did not attempt it.
Calculate the number of vertices: 2 * pointCount.
Calculate the triangles: Each segment of the broken line is made of two triangles
Calculate the number of triangles: 2 * (pointCount – 1)
Calculate the UVs. The simplest idea is to use only two UVs: (0.5, 0) for bottom vertices and (0.5, 1) for top vertices. We can also calculate stretch the texture proportionally over the line by using the following UVs: (u_i, 0) for bottom and and (u_i, 1) for top, where
u_i = lengthUpToVertex_i / totalLength


This line is much more complicated to generate. It follows the same idea as the previous mesh builder; we calculate intersections as before. But we do not use this for both top and bottom vertex. If one of them is convex, we replace the corner there with a circular sector, as shown in the picture below.
We break the mesh down into quads and sectors. As you can see, for the non-looping version, there is two quads per segment (this 2(pointCount – 1)), and one sector per point (this includes two sectors at the end points). Notice that the two vertical segments of each quad is either from point to intersection, or from point the top of a perpendicular raised at that point. The sectors also all lie between perpendiculars raised on the points. This gives as all the information we need to calculate the shapes; the trick is to keep track of whether to use the sector at the top or bottom (if at all).
Parameters: points, width, triangleCount (for sectors)
Potential special cases: same as before.
Decide on primitives: Sectors at each end point, a sector at each convex join vertex, and two quads per segment.
Calculate parameters of primitives: Each sector has its center at a input point, and lies between perpendiculars at these points, the start and end angles are the angles of these perpendiculars. The radius of all sectors are width / 2. Note that all sectors lie between perpendiculars on the same side of the line, except the sectors at the end points.
Each quad has two corners made from the input points, and the remaining two points are either points of intersection (the same points of intersection calculated in for the broken line with sharp joins), or the top of a perpendicular. The tricky part is to keep track of which we should use for a particular quad. There are some details for special cases, but the overall test is done by taking the perp-dot product of consecutive segments, and see whether they are positive or negative. This essentially tells us whether the second segment lies to the left or right of the first segment, and therefor whether the angle between them is obtuse or not, and therefor whether the top or bottom join vertex is convex.

Use degrees in the inspector and radians internally. It is hard to visualize the size of radians, and you cannot easily specify special angles such as 90 or 60 degrees. But internally (for math calculations, it makes more sense to stick to radians. My convention is to add inDegrees to variables that represent angles in degrees. I only use inRadians for angles that have both an inDegrees and inRadians versions.
Consider keeping looping and non-looping implementations separate. It is sometimes easier to develop the looping and non-looping versions of a mesh builder separately, keeping them together with appropriate is a brain tax you don’t need to pay. Once you have both working, you may refactor so that shared code is not duplicated, but even then I am not sure it is a good idea. The branching can make the code much harder to understand.
To convert non-looped to looped:
The process is not completely general, you have to analyze it for your specific case.
For more advanced meshes, add additional debug options. Inaddition to showing vertices, you may also want to show:
Do not try to make the code as general as possible (unless you are writing a library for other programmers). Writing robust geometric code is exceptionally hard. If your game is 2D, then make your code only work in 2D. If you know all your points are “nice”, don’t cater for non-nice situations. If you know your code will always generate at least three points for a line, don’t worry about degenerate cases. If you only need loops, don’t implement non-loops.
That said, if you can spot problematic cases:
Do not try to share every vertex that is shareable unless you have to. In our line builder with round joints above we broke the line up into multiple components. This made the code much easier to write. The downside is there are a few redundant vertices, which in theory is somewhat slower. (You will have to work though to see the difference.) Even if you need a super-efficient implementation, it’s always better to start with a correct (potentially unoptimized) version that can be used as a reference implementation for something faster. And a correct version may also be easier to modify into a faster version than trying to build it directly:
Give thoughtful variable names. The mathy nature of geometric code can dupe programmers to give math style names – single letter names – which makes the code more compact, but harder to understand. And it can be tricky to name intermediate calculations. Here is my approach to naming in this type of code. (Note, my code is consumed by our small team. Code meant for a wider audience need to have an even more robust naming strategy.
x, y, z for coordinatesi, j, k for vanilla loop counters (I try to always use vanilla loops, but if I don’t, I will use something more descriptive that reflects the loop counter’s role in the code, such as “triangleOffset”)t for a parameter (similar to t in Lerp and gradient.Evaluate)u and v for texture coordinatesblueSegment and redSegment (where those correspond to the colors in my drawing). While this is a great help during implementation, it may be confusing for anyone who does not have access to the drawing. You have a few options:Test for (almost) zero-area triangles. Test the area of each triangle once it is created, and make sure it is above a threshold. Print a debug message or throw an error with the vertex indices and coordinates of very small triangles. This will help flush out three classes of errors:
The area of a triangle ABC is given by half the magnitude of the cross product of vectors AB and AC.
public static float AreaOfTriangle(Vector3 sideAB, Vector3 sideAC)
{
return 0.5f * Vector3.Cross(sideAB, sideAC).magnitude;
}
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
public enum MeshType{
XYZ,
XY,
XZ
}
[RequireComponent(typeof(MeshFilter))]
public class MeshBuilder: MonoBehaviour
{
private readonly static Color DebugSphereColor = new Color(1, 0.25f, 0);
private const float SmallestValidTriangleArea = 0.01f;
[Header("Debug Options")]
[SerializeField]
private bool drawDebugVertices = false;
[SerializeField]
private bool drawDebugLabels = false;
[SerializeField]
private bool printDebugInfo = false;
[SerializeField]
private float debugSphereRadius = 0.1f;
[SerializeField]
private MeshType meshType = MeshType.XYZ;
//Use property instead to ensure initialization
private MeshFilter meshFilter;
//We keep this as a variable so we can draw debug Info
private List<Vector3> vertices;
private GUIStyle vertexLabelStyle;
[HideInInspector]
[SerializeField]
protected GeometryDebug meshDebug;
protected MeshFilter MeshFilter
{
get
{
if (meshFilter == null)
{
meshFilter = GetComponent<MeshFilter>();
//cannot be null since MeshFilter is required.
}
return meshFilter;
}
}
public void UpdateMesh()
{
if (meshDebug == null)
{
meshDebug = new GeometryDebug();
}
meshDebug.Clear();
DestroyOldMesh();
Preprocess();
var mesh = new Mesh();
vertices = CalculateVertices();
DebugLog("Vertices", vertices.Count);
mesh.SetVertices(vertices);
var triangles = CalculateTriangles();
DebugLog("Triangles", triangles.Count);
mesh.SetTriangles(triangles, 0);
//We set triangles before doing this test so that the number of
//vertex indices and their range can be checked before we execute
//the code below, that will
ValidateTriangleAreas(triangles);
var uvs = CalculateUvs(vertices);
if (uvs == null)
{
//can be null if subclass does not support texturing
DebugLog("Uvs", null);
}
else
{
DebugLog("Uvs", uvs.Count);
mesh.SetUVs(0, uvs);
}
var normals = CalculateNormals();
if (normals == null)
{
//can be null if subclass does override default normals
DebugLog("Normals", null);
mesh.RecalculateNormals();
}
else
{
DebugLog("Normals", normals.Count);
mesh.SetNormals(normals);
}
mesh.RecalculateBounds();
meshFilter.sharedMesh = mesh;
}
private void ValidateTriangleAreas(List<int> triangles)
{
for (int i = 0; i < triangles.Count / 3; i++)
{
int vertexIndexA = triangles[3 * i];
int vertexIndexB = triangles[3 * i + 1];
int vertexIndexC = triangles[3 * i + 2];
var vertexA = vertices[vertexIndexA];
var vertexB = vertices[vertexIndexB];
var vertexC = vertices[vertexIndexC];
float area = MeshBuilderUtils.AreaOfTriangle(vertexB - vertexA, vertexC - vertexA);
if (area < SmallestValidTriangleArea)
{
Debug.LogWarning(
string.Format(
"Triangle is too small. <{0}, {1}, {2}>, <{3}, {4}, {5}>",
vertexIndexA, vertexIndexB, vertexIndexC, vertexA, vertexB, vertexC
));
}
}
}
public void OnDrawGizmos()
{
if (!drawDebugVertices && !drawDebugLabels) return;
if (vertices == null) return; //can happen if no mesh was ever created.
//if(vertexLabelStyle == null)
{
vertexLabelStyle = new GUIStyle();
vertexLabelStyle.normal.textColor = Color.white;
vertexLabelStyle.alignment = TextAnchor.MiddleCenter;
}
if(drawDebugVertices)
{
meshDebug.Radius = debugSphereRadius;
for (int i = 0; i < vertices.Count; i++)
{
var vertex = vertices[i];
var spherePosition = transform.TransformPoint(vertex);
float radius = transform.lossyScale.magnitude * debugSphereRadius;
GeometryDebug.DrawDot(spherePosition, radius, DebugSphereColor, meshType);
}
if (meshDebug != null)
{
meshDebug.Draw(transform, meshType);
}
}
if(drawDebugLabels)
{
for (int i = 0; i < vertices.Count; i++)
{
var vertex = vertices[i];
var spherePosition = transform.TransformPoint(vertex);
float radius = transform.lossyScale.magnitude * debugSphereRadius;
Handles.Label(spherePosition + Vector3.up * radius / 2, i.ToString(), vertexLabelStyle);
}
}
}
virtual protected List<Vector3> CalculateVertices()
{
return MeshBuilderUtils.QuadVertices();
}
virtual protected List<Vector2> CalculateUvs(List<Vector3> vertices)
{
return MeshBuilderUtils.GetStandardUvsXY(vertices, true, true);
}
virtual protected void Preprocess() { }
virtual protected List<int> CalculateTriangles()
{
return new List<int>
{
0, 3, 1,
1, 3, 2
};
}
virtual protected List<Vector3> CalculateNormals()
{
return null;
}
[ContextMenu("Update Mesh")]
protected void UpdateMeshTest()
{
UpdateMesh();
}
private void DestroyOldMesh()
{
if (MeshFilter.sharedMesh != null)
{
if (Application.isPlaying)
{
Destroy(MeshFilter.sharedMesh); //prevents memory leak
}
else
{
DestroyImmediate(MeshFilter.sharedMesh);
}
}
}
protected void DebugLog(string label, object message)
{
if (!printDebugInfo) return;
if (message == null)
{
DebugLog(label, "null");
}
else
{
Debug.Log(label + ": " + message.ToString(), this);
}
}
protected void DebugAddDotXY(Vector3 position, GLColor color)
{
meshDebug.AddDotXY(position, color);
}
public void DebugAddArrow(Vector3 position, Vector3 direction, GLColor color)
{
meshDebug.AddArrow(position, direction, color);
}
private void GetVerticesFromMesh()
{
if (MeshFilter.sharedMesh != null)
{
vertices = MeshFilter.sharedMesh.vertices.ToList();
DebugLog("Vertices", "Refreshed from mesh.");
}
}
private void OnValidate()
{
if(vertices == null)
{
GetVerticesFromMesh();
}
}
}
using System.Collections.Generic;
using UnityEngine;
public class MeshDataMeshBuilder : MeshBuilder
{
private MeshData meshData;
virtual protected MeshData GetMeshData()
{
return new MeshData();
}
protected override void Preprocess()
{
meshData = GetMeshData();
}
sealed protected override List<Vector3> CalculateVertices()
{
return meshData.vertices;
}
sealed protected override List<int> CalculateTriangles()
{
return meshData.triangles;
}
sealed protected override List<Vector3> CalculateNormals()
{
return meshData.normals;
}
sealed protected override List<Vector2> CalculateUvs(List<Vector3> vertices)
{
return meshData.uvs;
}
}
using System.Collections.Generic;
using UnityEngine;
public class SegmentMeshBuilder : MeshBuilder
{
[Header("Mesh Options")]
public Vector3 point0;
public Vector3 point1;
[Min(0)]
public float width;
[Min(0)]
public int trianglesPerRevolution;
private int c0Index;
private int c1Index;
private int p0Index;
private int q0Index;
private int triangleCount;
protected override void Preprocess()
{
//triangles in semicircle
triangleCount = Mathf.CeilToInt(0.5f * trianglesPerRevolution);
c0Index = 0;
p0Index = 1;
c1Index = triangleCount + 2;
q0Index = triangleCount + 3;
}
protected override List<Vector3> CalculateVertices()
{
var vertices = new List<Vector3>();
vertices.AddRange(MeshBuilderUtils.SectorVertices(point0, width / 2, MeshBuilderUtils.Right1, MeshBuilderUtils.Right3, true, triangleCount));
vertices.AddRange(MeshBuilderUtils.SectorVertices(point1, width / 2, MeshBuilderUtils.Right3, MeshBuilderUtils.Right1, true, triangleCount));
return vertices;
}
protected override List<int> CalculateTriangles()
{
var triangles = new List<int>();
triangles.AddRange(MeshBuilderUtils.SectorTriangles(c0Index, triangleCount, true));
triangles.AddRange(MeshBuilderUtils.SectorTriangles(c1Index, triangleCount, true));
triangles.AddRange(MeshBuilderUtils.QuadTriangles(
p0Index, p0Index + triangleCount,
q0Index, q0Index + triangleCount));
return triangles;
}
protected override List<Vector2> CalculateUvs(List<Vector3> vertices)
{
var uvs = new List<Vector2>();
var center = Vector2.one / 2;
uvs.AddRange(MeshBuilderUtils.SectorUvs(center, 0.5f, MeshBuilderUtils.Right1, MeshBuilderUtils.Right3, triangleCount, true));
uvs.AddRange(MeshBuilderUtils.SectorUvs(center, 0.5f, MeshBuilderUtils.Right3, MeshBuilderUtils.Right1, triangleCount, true));
return uvs;
}
}
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Gamelogic.Extensions;
public class LineMeshBuilder : MeshBuilder
{
public List<Vector3> points;
public bool loop;
[Min(0)]
public float width;
override protected List<Vector3> CalculateVertices()
{
float halfWidth = width / 2f;
var topLines = new List<Line>();
var bottomLines = new List<Line>();
int segmentCount = loop ? points.Count : (points.Count - 1);
Debug.Log(segmentCount);
for (int i = 0; i < segmentCount; i++)
{
int j = (i == points.Count - 1) ? 0 : i + 1;
var direction = (points[j] - points[i]).normalized;
meshDebug.AddArrow(points[i], direction, GLColor.Green);
var left = direction.PerpXY();
var top = points[i] + halfWidth * left;
var bottom = points[i] - halfWidth * left;
topLines.Add(new Line { offset = top, direction = direction });
bottomLines.Add(new Line { offset = bottom, direction = direction });
}
var vertices = new List<Vector3>();
for (int i = 0; i < points.Count; i++)
{
int j = (i == 0) ? points.Count - 1 : i - 1;
if ((i == 0) && !loop)
{
vertices.Add2(topLines[i].offset, bottomLines[i].offset);
}
else if ((i == points.Count - 1) && !loop)
{
var direction = topLines[j].direction;
var left = direction.PerpXY();
var top = points[i] + halfWidth * left;
var bottom = points[i] - halfWidth * left;
vertices.Add2(top, bottom);
}
else if (MeshBuilderUtils.IsParallel(topLines[i], topLines[j]))
{
Debug.Log("Parallel");
vertices.Add2(topLines[i].offset, bottomLines[i].offset);
}
else
{
var topIntersection = MeshBuilderUtils.GetIntersection(topLines[i], topLines[j]);
var bottomIntersection = MeshBuilderUtils.GetIntersection(bottomLines[i], bottomLines[j]);
meshDebug.AddDotXY(topIntersection, GLColor.Magenta);
meshDebug.AddDotXY(bottomIntersection, GLColor.Green);
vertices.Add2(topIntersection, bottomIntersection);
}
}
return vertices;
}
override protected List<int> CalculateTriangles()
{
int segmentCount = loop ? points.Count : points.Count - 1;
var triangles = new List<int>();
for (int i = 0; i < segmentCount; i++)
{
int j = (i == points.Count - 1) ? 0 : i + 1;
triangles.AddRange(
MeshBuilderUtils.QuadTriangles(
2 * i + 0,
2 * i + 1,
2 * j + 1,
2 * j + 0
)
);
}
return triangles;
}
override protected List<Vector2> CalculateUvs(List<Vector3> vertices)
{
var lengths = points.Differences((u, v) => (v - u).magnitude, loop);
float totalLength = lengths.Sum();
int segmentCount = loop ? points.Count : points.Count - 1;
var uvs = new List<Vector2>();
uvs.Add2(Vector2.zero, Vector2.up);
float accumulativeLength = 0;
for(int i = 0; i < points.Count - 1; i++)
{
accumulativeLength += lengths[i];
float u = accumulativeLength / totalLength;
uvs.Add2(new Vector2(u, 0), new Vector2(u, 1));
}
return uvs;
}
}
using UnityEngine;
using System.Collections.Generic;
using Gamelogic.Extensions;
public class RoundLineMeshBuilder : MeshDataMeshBuilder
{
[Header("Mesh Options")]
public List<Vector3> points;
public float width;
public bool loop;
//public int triangleCount = 5;
public float trianglesPerRevolution = 12;
protected override MeshData GetMeshData()
{
List<MeshData> meshes = new List<MeshData>();
if (loop)
{
meshes.Add(CalculateQuadsLoop());
meshes.AddRange(CalculateJointFansLoop());
}
else
{
meshes.Add(CalculateQuads());
meshes.Add(CalculateStartCap());
meshes.Add(CalculateEndCap());
meshes.AddRange(CalculateJointFans());
}
return MeshData.Combine(meshes);
}
private MeshData CalculateQuads()
{
return new MeshData
{
vertices = CalculateQuadVertices(),
triangles = CalculateQuadTriangles(),
uvs = CalculateQuadUvs(false)
};
}
private MeshData CalculateQuadsLoop()
{
return new MeshData
{
vertices = CalculateQuadVerticesLoop(),
triangles = CalculateQuadTrianglesLoop(),
uvs = CalculateQuadUvs(true)
};
}
private List<Vector3> CalculateQuadVertices()
{
var vertices = new List<Vector3>();
var direction = (points[1] - points[0]).normalized;
var left = direction.PerpXY();
vertices.Add3(
points[0] + left * width / 2,
points[0],
points[0] - left * width / 2);
for (int i = 0; i < points.Count - 2; i++)
{
CalculateJoin(
points[i], points[i + 1], points[i + 2],
out bool fanAtTop, out Vector3 intersection,
out Vector3 fanStart, out Vector3 fanEnd);
if (fanAtTop)
{
vertices.Add3(fanStart, points[i + 1], intersection);
vertices.Add3(fanEnd, points[i + 1], intersection);
}
else
{
vertices.Add3(intersection, points[i + 1], fanStart);
vertices.Add3(intersection, points[i + 1], fanEnd);
}
}
int last = points.Count - 1;
direction = (points[last] - points[last - 1]).normalized;
left = direction.PerpXY();
vertices.Add3(
points[last] + left * width / 2,
points[last],
points[last] - left * width / 2);
return vertices;
}
private List<Vector3> CalculateQuadVerticesLoop()
{
var vertices = new List<Vector3>();
for (int i = 0; i < points.Count; i++)
{
var point0 = points[i];
var point1 = points[(i + 1) % points.Count];
var point2 = points[(i + 2) % points.Count];
CalculateJoin(
point0, point1, point2,
out bool fanAtTop, out Vector3 intersection,
out Vector3 fanStart, out Vector3 fanEnd);
if (fanAtTop)
{
Debug.Log(fanStart + " " + point1 + " " + intersection);
Debug.Log(fanEnd + " " + point1 + " " + intersection);
vertices.Add3(fanStart, point1, intersection);
vertices.Add3(fanEnd, point1, intersection);
}
else
{
Debug.Log(intersection + " " + point1 + " " + fanStart);
Debug.Log(intersection + " " + point1 + " " + fanEnd);
vertices.Add3(intersection, point1, fanStart);
vertices.Add3(intersection, point1, fanEnd);
}
}
return vertices;
}
private List<int> CalculateQuadTriangles()
{
var triangles = new List<int>();
for (int i = 0; i < points.Count - 1; i++)
{
int p0 = i * 6;
int p1 = i * 6 + 1;
int p2 = i * 6 + 2;
int p3 = i * 6 + 3;
int p4 = i * 6 + 4;
int p5 = i * 6 + 5;
triangles.AddRange(MeshBuilderUtils.QuadTriangles(p1, p4, p3, p0));
triangles.AddRange(MeshBuilderUtils.QuadTriangles(p2, p5, p4, p1));
}
return triangles;
}
private List<int> CalculateQuadTrianglesLoop()
{
var triangles = new List<int>();
int vertexCount = 6 * points.Count;
for (int i = 0; i < points.Count; i++)
{//+ 3 because we do not have the three points for the start cap
int p0 = (i * 6 + 0 + 3) % vertexCount;
int p1 = (i * 6 + 1 + 3) % vertexCount;
int p2 = (i * 6 + 2 + 3) % vertexCount;
int p3 = (i * 6 + 3 + 3) % vertexCount;
int p4 = (i * 6 + 4 + 3) % vertexCount;
int p5 = (i * 6 + 5 + 3) % vertexCount;
triangles.AddRange(MeshBuilderUtils.QuadTriangles(p1, p4, p3, p0));
triangles.AddRange(MeshBuilderUtils.QuadTriangles(p2, p5, p4, p1));
}
return triangles;
}
private MeshData CalculateStartCap()
{
var point0 = points[0];
var point1 = points[1];
var direction = (point1 - point0).normalized;
var left = direction.PerpXY();
var right = -left;
float startAngle = left.Atan2XY();
float endAngle = right.Atan2XY();
int triangleCount = Mathf.CeilToInt(0.5f * trianglesPerRevolution);
var vertices = MeshBuilderUtils.SectorVertices(
point0, width / 2, startAngle, endAngle, true, triangleCount);
var triangles = MeshBuilderUtils.SectorTriangles(0, triangleCount, true);
var center = Vector2.one / 2;
var uvs = MeshBuilderUtils.SectorUvs(center, 0.5f, MeshBuilderUtils.Right1, MeshBuilderUtils.Right3, triangleCount, true);
return new MeshData
{
vertices = vertices,
triangles = triangles,
uvs = uvs
};
}
private MeshData CalculateEndCap()
{
var point0 = points[points.Count - 2];
var point1 = points[points.Count - 1];
var direction = (point1 - point0).normalized;
var left = direction.PerpXY();
var right = -left;
float startAngle = left.Atan2XY();
float endAngle = right.Atan2XY();
int triangleCount = Mathf.CeilToInt(0.5f * trianglesPerRevolution);
var vertices = MeshBuilderUtils.SectorVertices(
point1, width / 2, startAngle, endAngle, false, triangleCount);
var triangles = MeshBuilderUtils.SectorTriangles(0, triangleCount, false);
var center = Vector2.one / 2;
var uvs = MeshBuilderUtils.SectorUvs(center, 0.5f, MeshBuilderUtils.Right3, MeshBuilderUtils.Right1, triangleCount, true);
return new MeshData
{
vertices = vertices,
triangles = triangles,
uvs = uvs
};
}
private List<MeshData> CalculateJointFans()
{
List<MeshData> meshes = new List<MeshData>();
for (int i = 0; i < points.Count - 2; i++)
{
CalculateJoin(
points[i], points[i + 1], points[i + 2],
out bool fanAtTop, out _,
out Vector3 fanStart, out Vector3 fanEnd);
float startAngle = (fanStart - points[i + 1]).Atan2XY();
float endAngle = (fanEnd - points[i + 1]).Atan2XY();
float revolutions = Mathf.Abs(MeshBuilderUtils.GetAngleBetween(startAngle, endAngle, !fanAtTop)) / MeshBuilderUtils.Right4;
int triangleCount = Mathf.CeilToInt(revolutions * trianglesPerRevolution);
var vertices = MeshBuilderUtils.SectorVertices(points[i + 1], width / 2, startAngle, endAngle, !fanAtTop, triangleCount);
var triangles = MeshBuilderUtils.SectorTriangles(0, triangleCount, !fanAtTop);
var center = Vector2.one / 2;
var uvs = MeshBuilderUtils.SectorUvs(center, 0.5f, startAngle, endAngle, triangleCount, !fanAtTop);
meshes.Add(new MeshData { vertices = vertices, triangles = triangles, uvs = uvs });
}
return meshes;
}
private List<MeshData> CalculateJointFansLoop()
{
List<MeshData> meshes = new List<MeshData>();
for (int i = 0; i < points.Count; i++)
{
var point0 = points[i];
var point1 = points[(i + 1) % points.Count];
var point2 = points[(i + 2) % points.Count];
CalculateJoin(
point0, point1, point2,
out bool fanAtTop, out _,
out Vector3 fanStart, out Vector3 fanEnd);
float startAngle = (fanStart - point1).Atan2XY();
float endAngle = (fanEnd - point1).Atan2XY();
float revolutions = Mathf.Abs(MeshBuilderUtils.GetAngleBetween(startAngle, endAngle, !fanAtTop)) / MeshBuilderUtils.Right4;
int triangleCount = Mathf.CeilToInt(revolutions * trianglesPerRevolution);
var vertices = MeshBuilderUtils.SectorVertices(point1, width / 2, startAngle, endAngle, !fanAtTop, triangleCount);
var triangles = MeshBuilderUtils.SectorTriangles(0, triangleCount, !fanAtTop);
var center = Vector2.one / 2;
var uvs = MeshBuilderUtils.SectorUvs(center, 0.5f, startAngle, endAngle, triangleCount, !fanAtTop);
meshes.Add(
new MeshData
{
vertices = vertices,
triangles = triangles,
uvs = uvs
});
}
return meshes;
}
public void CalculateJoin(
Vector3 p0, Vector3 p1, Vector3 p2,
out bool fanAtTop, out Vector3 intersection,
out Vector3 fanStart, out Vector3 fanEnd)
{
var direction0 = (p1 - p0).normalized;
var direction1 = (p2 - p1).normalized;
fanAtTop = PerpDotXY(direction0, direction1) < 0;
var left0 = direction0.PerpXY();
var left1 = direction1.PerpXY();
if (fanAtTop)
{
Line line0 = new Line { offset = p0 - width / 2 * left0, direction = direction0 };
Line line1 = new Line { offset = p1 - width / 2 * left1, direction = direction1 };
intersection = MeshBuilderUtils.GetIntersection(line0, line1);
meshDebug.AddDotXY(intersection, GLColor.Yellow);
fanStart = p1 + left0 * width / 2;
fanEnd = p1 + left1 * width / 2;
}
else
{
Line line0 = new Line { offset = p0 + width / 2 * left0, direction = direction0 };
Line line1 = new Line { offset = p1 + width / 2 * left1, direction = direction1 };
intersection = MeshBuilderUtils.GetIntersection(line0, line1);
meshDebug.AddDotXY(intersection, GLColor.Yellow);
fanStart = p1 - left0 * width / 2;
fanEnd = p1 - left1 * width / 2;
}
}
private float PerpDotXY(Vector3 v1, Vector3 v2)
{
return Vector3.Dot(v1.PerpXY(), v2);
}
private List<Vector2> CalculateQuadUvs(bool loop)
{
var uvs = new List<Vector2>();
int segmentCount = loop ? points.Count : (points.Count - 1);
for(int i = 0; i < segmentCount; i++)
{
uvs.Add3(new Vector2(0.5f, 1), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0));
uvs.Add3(new Vector2(0.5f, 1), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0));
}
return uvs;
}
}
using Gamelogic.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;
public class Line
{
public Vector3 offset;
public Vector3 direction;
public Vector3 Evaluate(float t)
{
return offset + direction * t;
}
override public string ToString()
{
return direction.ToString() + "[t]" + " + " + offset;
}
}
public class MeshData
{
public List<Vector3> vertices;
public List<int> triangles;
public List<Vector2> uvs;
public List<Vector3> normals;
public static MeshData Combine(List<MeshData> meshes)
{
MeshData combinedMesh = new MeshData
{
vertices = new List<Vector3>(),
triangles = new List<int>(),
uvs = new List<Vector2>(),
normals = new List<Vector3>()
};
int vertexIndexOffset = 0;
foreach(var mesh in meshes)
{
combinedMesh.vertices.AddRange(mesh.vertices);
combinedMesh.triangles.AddRange(mesh.triangles.Select(index => index + vertexIndexOffset));
if(mesh.uvs != null)
{
combinedMesh.uvs.AddRange(mesh.uvs);
}
if(mesh.normals != null)
{
combinedMesh.normals.AddRange(mesh.normals);
}
vertexIndexOffset += mesh.vertices.Count;
}
return combinedMesh;
}
}
public static class MeshBuilderUtils
{
private const float ParallelThreshold = 0.01f;
public const float Right1 = Mathf.PI / 2;
public const float Right2 = Mathf.PI;
public const float Right3 = 3 * Mathf.PI / 2;
public const float Right4 = 2 * Mathf.PI;
public static bool IsParallel(Vector3 direction0, Vector3 direction1)
{
return Vector3.Cross(direction0.normalized, direction1.normalized).magnitude < ParallelThreshold;
}
public static bool IsParallel(Line line0, Line line1)
{
return IsParallel(line0.direction, line1.direction);
}
private static Tuple<float, float> SolveLinearEquation(
float a0, float b0, float c0,
float a1, float b1, float c1
)
{
float v = (a1 * c0 - a0 * c1) / (a0 * b1 - a1 * b0);
float t = (-c0 - b0 * v) / a0;
return new Tuple<float, float>(t, v);
}
public static Vector2 Circle2(float angle)
{
return new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
}
public static Vector3 CircleXY(float angle)
{
return new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0);
}
public static Vector3 GetIntersection(Line line0, Line line1)
{
Assert.IsTrue(!IsParallel(line0, line1), "Lines are (almost) parallel.");
var solutions = SolveLinearEquation(
Vector3.Dot(line0.direction, line0.direction),
-Vector3.Dot(line1.direction, line0.direction),
Vector3.Dot(line0.offset - line1.offset, line0.direction),
Vector3.Dot(line0.direction, line1.direction),
-Vector3.Dot(line1.direction, line1.direction),
Vector3.Dot(line0.offset - line1.offset, line1.direction)
);
var intersection = line0.Evaluate(solutions.Item1);
Debug.Log(intersection);
return intersection;
}
public static float GetAngleBetween(float startAngle, float endAngle, bool anticlockwise)
{
if (anticlockwise && endAngle < startAngle)
{
endAngle += Right4;
}
if (!anticlockwise && startAngle < endAngle)
{
startAngle += Right4;
}
float angleDifference = endAngle - startAngle;
return angleDifference;
}
public static float AreaOfTriangle(Vector3 sideAB, Vector3 sideAC)
{
return 0.5f * Vector3.Cross(sideAB, sideAC).magnitude;
}
public static List<Vector3> QuadVertices()
{
return new List<Vector3>
{
new Vector3(-1, 1, 0),
new Vector3(-1, -1, 0),
new Vector3(1, -1, 0),
new Vector3(1, 1, 0),
};
}
public static List<int> QuadTriangles(
int corner0,
int corner1,
int corner2,
int corner3
)
{
return new List<int>
{
corner0,
corner2,
corner1,
corner2,
corner0,
corner3,
};
}
public static List<Vector2> QuadUvs()
{
return new List<Vector2>
{
new Vector2(0, 1),
new Vector2(1, 1),
new Vector2(1, 0),
new Vector2(0, 0)
};
}
public static List<Vector3> SectorVertices(
float angle, int triangleCount)
{
return SectorVertices(Vector3.zero, 1, 0, angle, true, triangleCount);
}
public static List<Vector3> SectorVertices(
Vector3 center,
float radius,
float startAngle,
float endAngle,
bool anticlockwise,
int triangleCount)
{
var vertices = new List<Vector3>();
vertices.Add(center);
startAngle = GLMathf.FloorMod(startAngle, MeshBuilderUtils.Right4);
endAngle = GLMathf.FloorMod(endAngle, MeshBuilderUtils.Right4);
if (anticlockwise && endAngle < startAngle)
{
endAngle += MeshBuilderUtils.Right4;
}
if (!anticlockwise && startAngle < endAngle)
{
startAngle += MeshBuilderUtils.Right4;
}
float angleDifference = endAngle - startAngle;
float triangleAngle = angleDifference / triangleCount;
for (int i = 0; i < triangleCount + 1; i++)
{
float theta = triangleAngle * i + startAngle;
vertices.Add(center + radius * MeshBuilderUtils.CircleXY(theta));
}
return vertices;
}
public static List<int> SectorTriangles(
int start,
int triangleCount,
bool anticlockwise)
{
var triangles = new List<int>();
for (int i = 0; i < triangleCount; i++)
{
if (anticlockwise)
{
triangles.Add3(start, start + i + 2, start + i + 1);
}
else
{
triangles.Add3(start, start + i + 1, start + i + 2);
}
}
return triangles;
}
public static List<Vector2> SectorUvs(
Vector2 center,
float radius,
float startAngle,
float endAngle,
int triangleCount,
bool anticlockwise
)
{
var uvs = new List<Vector2>();
uvs.Add(center);
startAngle = GLMathf.FloorMod(startAngle, MeshBuilderUtils.Right4);
endAngle = GLMathf.FloorMod(endAngle, MeshBuilderUtils.Right4);
if (anticlockwise && endAngle < startAngle)
{
endAngle += MeshBuilderUtils.Right4;
}
if (!anticlockwise && startAngle < endAngle)
{
startAngle += MeshBuilderUtils.Right4;
}
float angleDifference = endAngle - startAngle;
float triangleAngle = angleDifference / triangleCount;
for (int i = 0; i < triangleCount + 1; i++)
{
float theta = triangleAngle * i + startAngle;
uvs.Add(center + radius * MeshBuilderUtils.Circle2(theta));
}
return uvs;
}
//Assumes a set of vertices in the XY plane
public static List<Vector2> GetStandardUvsXY(
List<Vector3> vertices,
bool preserveAspectRatio,
bool mapOriginToCenter
)
{
var boundingBox = GetBoundingBoxXY(vertices, mapOriginToCenter);
var map = GetStandardUvMapXY(boundingBox, preserveAspectRatio, mapOriginToCenter);
return vertices.Select(map).ToList();
}
private static Rect GetBoundingBoxXY(List<Vector3> vertices, bool mapOriginToCenter)
{
var anchor = vertices[0];
var extent = vertices[0];
foreach (var vertex in vertices.Skip(1))
{
if (vertex.x < anchor.x)
{
anchor.x = vertex.x;
}
else if (vertex.x > extent.x)
{
extent.x = vertex.x;
}
if (vertex.y < anchor.y)
{
anchor.y = vertex.y;
}
else if (vertex.y > extent.y)
{
extent.y = vertex.y;
}
}
if (mapOriginToCenter)
{
anchor.x = Mathf.Min(anchor.x, -extent.x);
anchor.y = Mathf.Min(anchor.y, -extent.y);
extent.x = Mathf.Max(extent.x, -anchor.x);
extent.y = Mathf.Max(extent.y, -anchor.y);
}
var size = extent - anchor;
return new Rect(anchor, size);
}
private static Func<Vector3, Vector2> GetStandardUvMapXY(
Rect boundingBox,
bool preserveAspectRatio,
bool mapOriginToCenter)
{
Vector2 anchor = boundingBox.position;
Vector2 size = boundingBox.size;
if (preserveAspectRatio)
{
if (size.x < size.y)
{
size = new Vector3(size.y, size.y, 0);
}
else
{
size = new Vector3(size.x, size.x, 0);
}
}
if (mapOriginToCenter)
{
return v => new Vector2(v.x / size.x + 0.5f, v.y / size.y + 0.5f);
}
else
{
return v => new Vector2((v.x - anchor.x) / size.x, (v.y - anchor.y) / size.y);
}
}
public static int GetTriangleCount(float angle, float trianglesPerRevolution)
{
return Mathf.CeilToInt(angle / (2 * Mathf.PI) * trianglesPerRevolution);
}
}
public static class MeshBuilderExtensions
{
public static List<T> Add2<T>(this List<T> list, T index0, T item1)
{
list.Add(index0);
list.Add(item1);
return list;
}
public static List<T> Add3<T>(this List<T> list, T item0, T item1, T item2)
{
list.Add(item0);
list.Add(item1);
list.Add(item2);
return list;
}
public static List<U> Differences<T, U>(this List<T> list, Func<T, T, U> difference, bool loop)
{
var result = new List<U>();
for(int i = 0; i < list.Count - 1; i++)
{
result.Add(difference(list[i], list[i + 1]));
}
if (loop)
{
result.Add(difference(list[list.Count - 1], list[0]));
}
return result;
}
/*
Gets the vector's angle in the XY plane.
*/
public static float Atan2XY(this Vector3 v)
{
return Mathf.Atan2(v.y, v.x);
}
}
public enum GLColor
{
Red,
Orange,
Yellow,
Green,
Blue,
Purple,
Magenta,
Black,
White
}
[Serializable]
public class GeometryDebug
{
public List<Vector3> dotPositions;
public List<GLColor> dotColors;
public List<Vector3> arrowPositions;
public List<Vector3> arrowDirections;
public List<GLColor> arrowColors;
public List<Color> colorMap = new List<Color>
{
Color255(255, 70, 70),
Color255(255, 150, 0),
Color255(255, 255, 60),
Color255(150, 220, 0),
Color255(25, 174, 255),
Color255(186, 0, 255),
Color255(255, 0, 193),
Color255(0, 0, 0),
Color255(255, 255, 255),
};
private static Color Color255(int r, int g, int b, int a = 255)
{
return new Color(r / 255f, g / 255f, b / 255f, a / 255f);
}
public float Radius
{
get; set;
} = 0.1f;
public GeometryDebug()
{
dotPositions = new List<Vector3>();
dotColors = new List<GLColor>();
arrowPositions = new List<Vector3>();
arrowDirections = new List<Vector3>();
arrowColors = new List<GLColor>();
}
public void Clear()
{
dotPositions.Clear();
dotColors.Clear();
arrowPositions.Clear();
arrowDirections.Clear();
arrowColors.Clear();
}
public void AddDotXY(Vector3 position, GLColor color)
{
dotPositions.Add(position);
dotColors.Add(color);
}
public void AddArrow(Vector3 position, Vector3 direction, GLColor color)
{
arrowPositions.Add(position);
arrowDirections.Add(direction);
arrowColors.Add(color);
}
public void Draw(Transform transform, MeshType meshType = MeshType.XYZ)
{
for(int i = 0; i < dotPositions.Count; i++)
{
var position = transform.TransformPoint(dotPositions[i]);
float radius = transform.lossyScale.magnitude * Radius;
Color color = colorMap[(int)dotColors[i]];
DrawDot(position, radius, color, meshType);
}
for(int i = 0; i < arrowPositions.Count; i++)
{
var position = transform.TransformPoint(arrowPositions[i]);
var direction = transform.TransformDirection(arrowDirections[i]);
float size = transform.lossyScale.magnitude * 0.2f;
Color color = colorMap[(int)arrowColors[i]];
DrawArrow(position, direction, size, color);
}
}
public static void DrawArrow(Vector3 position, Vector3 direction, float size, Color color)
{
Handles.color = color;
Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(direction), size, EventType.Repaint);
}
public static void DrawDot(Vector3 position, float radius, Color color, MeshType meshType)
{
switch (meshType)
{
case MeshType.XYZ:
Gizmos.color = color;
Gizmos.DrawWireSphere(position, radius);
break;
case MeshType.XY:
Handles.color = color;
Handles.DrawSolidDisc(position, Vector3.forward, radius);
break;
case MeshType.XZ:
Handles.color = color;
Handles.DrawSolidDisc(position, Vector3.forward, radius);
break;
}
}
}
]]>