Get into GameDev:
cheat-codes for gamedev interviews
━ Practice Test ━
Introduction
Practice Test (Mock-Interview)
Imagine you are entering an interview for a specific game company that is on your list of places to apply. Prepare any notes you think you may need to reference, including your resume. Then go through each of these questions as if they were a real interview. Try to answer the question without any external help.
Hints
If you are struggling on how to start, consider revealing one of the hints I have provided. Once you have an answer you are confident with, begin revealing the hints, one-by-one. If you reveal a hint that was not consistent with your answer, it's possible you still had a perfectly acceptable answer! But I would suggest you revisit your answer and consider if you could take a different approach in the direction the hint is guiding you towards.
Hints
If you are struggling on how to start, consider revealing one of the hints I have provided. Once you have an answer you are confident with, begin revealing the hints, one-by-one. If you reveal a hint that was not consistent with your answer, it's possible you still had a perfectly acceptable answer! But I would suggest you revisit your answer and consider if you could take a different approach in the direction the hint is guiding you towards.
Practice Question: Have you played our recent game? (Remember that this interviewer is from a specific game company of your choosing) What are your thoughts on the core game loop? How could it be improved?
Games can often be explained in terms of arcs and loops. Think about how your suggested change affects arcs and loops present in the existing design.
Answer:
We cover how to answer questions like these in the behavioral questions chapter. Make sure to focus on the positives. Certainly avoid say their game is bad. Even if you do think part of the game is bad, it would be more strategic to design your answer around an additive change that supplements the game part you feel is lacking. It's likely the person you are talking to made the game, including the bad parts.
Example Answer:
- (In this case I am role-playing as a gameplay engineering candidate applying to Naughty Dog sometime following the release of their game The Last of Us, Part II). I really enjoyed how the game narratively explored the perils of war and how violence can lead to a viscous cycle of revenge. My favorite aspect of this was when I began to feel that story come through the gameplay.
- For example, later on in the game I recall a scene where I, as Ellie, had to fight some people that I had made friends with as Abby; that was challenging, emotionally as a player. I wish the game provided me with more opportunity to explore taking a more pacifist route when I began to feel that way. For example, and this was actually and pain-point I specifically remember from The Last of Us Part I as well, I would sometimes be in an encounter where I felt it would make the most sense for Ellie to thematically just run away... yet the encounter required I kill all enemies in the vicinity before I could progress past the encounter. Once I was stuck in an arena and I was trying to creep around, avoiding the enemies, but I could not find a way out. Then I eventually fought the enemies to later discover that the exit I was trying to use would only prompt me with an escape action once I had killed all the people.
- So if I were to think through improving that experience, I would have allowed the player to use the exit before all enemies were killed. I can understand that sometimes for story-reasons this may not be possible. In those cases, I would still want to provide the player with some sort of indicator that they did indeed find the exit... but that it is not currently available because enemies are nearby.
Example Answer Summary:
I complement their game. I think through a core objective of their game: "exploring the 'perils of war' theme through gameplay". Then I explain a moment when the game did not reach its full potential in pursuit of that objective. Finally I explore potential solutions, including an analysis of their shortcomings.
We cover how to answer questions like these in the behavioral questions chapter. Make sure to focus on the positives. Certainly avoid say their game is bad. Even if you do think part of the game is bad, it would be more strategic to design your answer around an additive change that supplements the game part you feel is lacking. It's likely the person you are talking to made the game, including the bad parts.
Example Answer:
- (In this case I am role-playing as a gameplay engineering candidate applying to Naughty Dog sometime following the release of their game The Last of Us, Part II). I really enjoyed how the game narratively explored the perils of war and how violence can lead to a viscous cycle of revenge. My favorite aspect of this was when I began to feel that story come through the gameplay.
- For example, later on in the game I recall a scene where I, as Ellie, had to fight some people that I had made friends with as Abby; that was challenging, emotionally as a player. I wish the game provided me with more opportunity to explore taking a more pacifist route when I began to feel that way. For example, and this was actually and pain-point I specifically remember from The Last of Us Part I as well, I would sometimes be in an encounter where I felt it would make the most sense for Ellie to thematically just run away... yet the encounter required I kill all enemies in the vicinity before I could progress past the encounter. Once I was stuck in an arena and I was trying to creep around, avoiding the enemies, but I could not find a way out. Then I eventually fought the enemies to later discover that the exit I was trying to use would only prompt me with an escape action once I had killed all the people.
- So if I were to think through improving that experience, I would have allowed the player to use the exit before all enemies were killed. I can understand that sometimes for story-reasons this may not be possible. In those cases, I would still want to provide the player with some sort of indicator that they did indeed find the exit... but that it is not currently available because enemies are nearby.
Example Answer Summary:
I complement their game. I think through a core objective of their game: "exploring the 'perils of war' theme through gameplay". Then I explain a moment when the game did not reach its full potential in pursuit of that objective. Finally I explore potential solutions, including an analysis of their shortcomings.
For the next few questions I will walk you through an engineering analysis of a bug and some solution approaches. In many of these problems there may be more than one correct solution. Think through your options and explain trade-offs in your approach.
Practice Question: You are working on a new AI type called the monkey… and you notice that levels which include the monkey are running slower than others. More specifically, the game is running at a lower frame rate during these levels. How would you approach debugging this AI?
Think through you the tools that you would likely have at your disposal. What suspicious do you have with respect to the source of this slowdown? How would you be able to confirm or reject those hypothetical causes?
Answer: We cover how to answer questions like these in the behavioral questions chapter. Good answers should convey a familiarity and confidence with modern debugging tools. Good candidates will already have suspicions about what could be the issue and they will explain how they can use their tools to narrow down the potential causes of the problem. They might ask insightful clarifying questions such as "does the issue seem to be affected by the number of monkeys in a level?"
Example Answer: I'd first want to confirm that it is indeed the monkey AI that is causing the slowdown. Using debug tools I would quickly play the level without the monkey and observe if frame rate restored to a normal number. I'd like to see if there is anything the monkey is doing in particular that might stand out as particularly expensive. I'd open the game's frame debugger to see if it is allocating a large amount or very frequent request of memory. I'd enable any debug logging to see if an error is being reported. I'd also be curious why this bug is being reported now, was this always the case for the monkey AI or could something have changed recently?
Example Answer Summary: I propose a number of potential causes and then suggest ways to test and reject those hypotheses. I mention and convey a familiarity with specific debugging tools like frame debuggers, memory allocation visualizers, and debug logging.
Practice Question: Thanks to your analysis, you gain further clarity on the situation. The AI seems to slow down the level, specifically when they are targeting an enemy for a special banana attack. During the targeting process of the banana attack, the monkey needs to first ensure that the target is not next to any walls, and this is an expensive computation. So let’s say you have about five monkeys and every frame each of them are going through the banana attack targeting process. What are some approaches you might take towards optimizing this scenario?
A serious mistake has been made with respect to handling the AI's computation, can you identify the issue?
*Every* monkey AI is performing the targeting process *every* frame, why is that wrong? How could it be fixed?
Example Answer:
Candidates should readily identify the glaring mistake in this AI design. The monkey's are processing their think logic on every frame update. Frame updates correspond to the frequency at which the screen visuals should be updated, an AI's think should always be divorced from this update loop. In other words, even if the game runs the graphics update loop at 60 FPS, the monkey does not need to think sixty frames every second. We should find a more appropriate update interval, for example it may be perfectly sufficient for the monkey to only check if the player can be attacked every two seconds. In addition to changing the AI think frequency, we can round-robin the AI so that they are not all thinking on the same frame. We can also cache the result so that this calculation is not repeated unless the target moves positions (or if the walls somehow move). If multiple monkeys are all just checking the player's position maybe they could share this data and only one calculation result may be needed for all of the monkeys to share. Candidates may also identify the five monkeys as a potential issue, we can use systems like AI attack tokens to stagger the monkey's attacks. Cooldowns as well can be used to prevent repeat calculations. We can also spread aka amortize the calculation itself over several frames if we can somehow break the calculation into sub parts.
Example Answer Summary:
- Divorce the AI think loop from the game's graphical update loop - Introduce caching, cooldowns, and amortization to spread out the calculations - Introduce tokens, round-robin, and shared data (blackboarding) to reduce the calculation frequency
Candidates should readily identify the glaring mistake in this AI design. The monkey's are processing their think logic on every frame update. Frame updates correspond to the frequency at which the screen visuals should be updated, an AI's think should always be divorced from this update loop. In other words, even if the game runs the graphics update loop at 60 FPS, the monkey does not need to think sixty frames every second. We should find a more appropriate update interval, for example it may be perfectly sufficient for the monkey to only check if the player can be attacked every two seconds. In addition to changing the AI think frequency, we can round-robin the AI so that they are not all thinking on the same frame. We can also cache the result so that this calculation is not repeated unless the target moves positions (or if the walls somehow move). If multiple monkeys are all just checking the player's position maybe they could share this data and only one calculation result may be needed for all of the monkeys to share. Candidates may also identify the five monkeys as a potential issue, we can use systems like AI attack tokens to stagger the monkey's attacks. Cooldowns as well can be used to prevent repeat calculations. We can also spread aka amortize the calculation itself over several frames if we can somehow break the calculation into sub parts.
Example Answer Summary:
- Divorce the AI think loop from the game's graphical update loop - Introduce caching, cooldowns, and amortization to spread out the calculations - Introduce tokens, round-robin, and shared data (blackboarding) to reduce the calculation frequency
Practice Question: Thanks to your fixes, the banana attack targeting calculation is now sufficiently fast. However, that has turned your attention to another issue. The banana projectile has some very expensive graphics and display logic that slow down the game once they are spawned. In fact, the game can only handle maybe a dozen of them before it begins to lag, regardless of if they are on-screen. You've consulted with the graphics team and they have confirmed that effect is the one we need to have in the game and that the shader itself cannot be optimized. At a high-level what are some approaches you might take to optimizing this problem?
Something important is not happening if the graphics are still incurring a lot of expense despite being off-screen.
How can you reduce the cumulative expense of several bananas' graphics without compromising their individual visual fidelity?
There is a clear problem if the graphics are incurring a large expense despite not being shown. When graphics are off-screen or occluded, they should be culled. Candidates should reference both frustum and occlusion culling which are likely relevant to this issue. If the shader itself is optimized, the candidate should also be thinking about the number of these bananas that we show. Candidates can explore trade-off like preventing banana attacks if several bananas are already on-screen. Candidates may be drawn to pooling solutions if they think instantiation is the issue behind the problem, that's a good concern but the problem statement did not communicate clues in that direction. Candidates may also discuss tradeoffs such as level-of-detail scaling which may be applied to bananas if they are far enough away to not incur a compromised visual fidelity.
Example Answer:
If the graphics are very expensive despite not being shown, then something has gone wrong with the game's culling procedure. We should ensure that bananas which are not visible, due to obstructions, are occlusion culled. Likewise, bananas beyond the player's field-of-view should be frustum culled. If some bananas are visible but very far we may be able to optimize their display without sacrificing fidelity with approaches such as billboarding or LOD scaling.
Example Answer Summary:
- Occlusion and Frustum culling to decrease expense when not visible
- LOD scaling and billboarding when visible but far away
Example Answer:
If the graphics are very expensive despite not being shown, then something has gone wrong with the game's culling procedure. We should ensure that bananas which are not visible, due to obstructions, are occlusion culled. Likewise, bananas beyond the player's field-of-view should be frustum culled. If some bananas are visible but very far we may be able to optimize their display without sacrificing fidelity with approaches such as billboarding or LOD scaling.
Example Answer Summary:
- Occlusion and Frustum culling to decrease expense when not visible
- LOD scaling and billboarding when visible but far away
Practice Question: You notice that once the projectiles have spawned the game is performant. But at the exact time of each spawn, the game hits a serious lag spike. What is the problem occurring here and how can we fix it?
Spawning objects can require an expensive runtime allocation. How can we anticipate the spawn to prevent the mid-game lag spike?
Answer:
The expensive spawn is probably causing some memory allocation at runtime. Usually with videogames you want to avoid this my pre-loading any allocations you will require throughout the course of the program and then doling out those objects over time, returning them when expired. This paradigm is called pooling.
Example Answer:
I would work with designers to determine the maximum number of bananas we want to support during gameplay. I would then implement a pool that spawns that many bananas during the load of the level and only activates them when needed, returning expired bananas to the pool. If the designers can not tolerate a max bananas count, then we can still pre-load our expected number of bananas and dynamically allocate any bananas that exceed our current pool size as a compromise.
Follow up question: What are some of the benefits and drawbacks to using an array vs a linked-list for storage of your pool's elements?
Answer: An array is a set amount of contiguous memory, so if we need to grow it's size that reallocation can be expensive. But in the case of most pool designs, where we set a max and honor it, a resize should never happen. The benefits the array provides is in the cache coherency of contiguous memory; a linked-list could store it's data anywhere which could make traversing the object list of the pool expensive.
The expensive spawn is probably causing some memory allocation at runtime. Usually with videogames you want to avoid this my pre-loading any allocations you will require throughout the course of the program and then doling out those objects over time, returning them when expired. This paradigm is called pooling.
Example Answer:
I would work with designers to determine the maximum number of bananas we want to support during gameplay. I would then implement a pool that spawns that many bananas during the load of the level and only activates them when needed, returning expired bananas to the pool. If the designers can not tolerate a max bananas count, then we can still pre-load our expected number of bananas and dynamically allocate any bananas that exceed our current pool size as a compromise.
Follow up question: What are some of the benefits and drawbacks to using an array vs a linked-list for storage of your pool's elements?
Answer: An array is a set amount of contiguous memory, so if we need to grow it's size that reallocation can be expensive. But in the case of most pool designs, where we set a max and honor it, a resize should never happen. The benefits the array provides is in the cache coherency of contiguous memory; a linked-list could store it's data anywhere which could make traversing the object list of the pool expensive.
Practice Question: You create a basic pooling system that uses a global banana array to speed banana spawning for the monkey AI. When testing with a monkey in your test map it works great! But soon after committing your code a crash is reported. It seems that when more than one monkey uses this pool, the game sometimes crashes. The more monkeys, the more likely the game is to crash. What's going on here and how can we fix it?
If every monkey is on a separate thread, what could happen when they try to edit a shared resource?
Answer:
If two distinct AI are sharing the global banana pool and crashing then there must be some sort of collision happening during a write. We don't know exactly how these AI are implemented but we should probably suspect something a problem like two AI receiving a reference to the same banana and there being a write collision. If the pool does not already have multithreaded support, there are a few approaches we could use to add that support.
- We could add a simple lock, a mutex for the entire pool would stall requests for a banana until the previous request has been processed. This would cause contention, aka when one thread is waiting for another, which is less than ideal.
- Another approach is to use atomic functions such as a lockless list (that uses CAS) to remove the banana from the pool, or an atomic integer on the banana to specify which monkey controls it (if any).
- I think in this circumstance, our best approach would be to convert the banana spawn into a request to the pool. Then the pool can maintain a list of queued requests and handle them all in bulk at a specific point in the frame. Then the monkey can be somehow notified when the bananas they requested have been spawned. This is a nice design because it does not rely on the banana being available on the same frame as the request so you have time to process an expensive allocation if you want to grow the pool.
Example Answer:
I suspect the crash is due to write contention and I would fix it by adding multithreading support to the banana pool. For example, I can store bananas in a lockless list that removes elements using CAS.
If two distinct AI are sharing the global banana pool and crashing then there must be some sort of collision happening during a write. We don't know exactly how these AI are implemented but we should probably suspect something a problem like two AI receiving a reference to the same banana and there being a write collision. If the pool does not already have multithreaded support, there are a few approaches we could use to add that support.
- We could add a simple lock, a mutex for the entire pool would stall requests for a banana until the previous request has been processed. This would cause contention, aka when one thread is waiting for another, which is less than ideal.
- Another approach is to use atomic functions such as a lockless list (that uses CAS) to remove the banana from the pool, or an atomic integer on the banana to specify which monkey controls it (if any).
- I think in this circumstance, our best approach would be to convert the banana spawn into a request to the pool. Then the pool can maintain a list of queued requests and handle them all in bulk at a specific point in the frame. Then the monkey can be somehow notified when the bananas they requested have been spawned. This is a nice design because it does not rely on the banana being available on the same frame as the request so you have time to process an expensive allocation if you want to grow the pool.
Example Answer:
I suspect the crash is due to write contention and I would fix it by adding multithreading support to the banana pool. For example, I can store bananas in a lockless list that removes elements using CAS.
Practice Question: You implement a linked list storage system for the banana pool with multithread access supported with CAS. That seems to be working well, but the game --very rarely-- is leaking memory. You designed it so that when a banana needs to be removed (aka "deleted") from the pool, the list is traversed until we reach a node that we want (in this case, just the first banana we find) then we CAS the pointer that is pointing to our desired banana to instead point whatever node previous followed our banana. What's wrong with our design here? How can we fix it?
Consider two hypothetical threads, #1 and #2 that are accessing a linked list of three elements: START -> A -> C -> END
Thread #1 finds A and decides to remove it from the list. It notes that C follows A so it prepares to complete a CAS that will make START point to C instead of at A. Before it executes the CAS, thread #2 inserts a new node, B, in between A and C. This results in... START -> A -> B -> C -> END. Now thread #1 continues with its work to remove A and it completed the work since the CAS will be approved since START still points at A. The final result (with A now removed) is START--\__B_/->C, now I've probably done a shit job visualizing that but basically the pointer from START goes around B to point to C, nothing is pointing at B. B is leaked! How can we avoid this?
https://i.gyazo.com/de0b9e62e24dfcba5fd4d21a7ca03bc7.png
https://en.wikipedia.org/wiki/Non-blocking_linked_list
Thread #1 finds A and decides to remove it from the list. It notes that C follows A so it prepares to complete a CAS that will make START point to C instead of at A. Before it executes the CAS, thread #2 inserts a new node, B, in between A and C. This results in... START -> A -> B -> C -> END. Now thread #1 continues with its work to remove A and it completed the work since the CAS will be approved since START still points at A. The final result (with A now removed) is START--\__B_/->C, now I've probably done a shit job visualizing that but basically the pointer from START goes around B to point to C, nothing is pointing at B. B is leaked! How can we avoid this?
https://i.gyazo.com/de0b9e62e24dfcba5fd4d21a7ca03bc7.png
https://en.wikipedia.org/wiki/Non-blocking_linked_list
This is the classic ABA problem that can occur with CAS deletion. It should be easy to recognize that a single CAS is never going to be enough on its own to implement a safe deletion. We need to augment the CAS to prevent this problem. There are many ways to fix this. A simple approach is for the deleting thread to make two passes through the list. First to just mark the node as deleted, and second to remove the node that has been marked as deleted. You could add some sort of simple atomic markup on the node to identify its state/ownership. There are other operations available on certain hardware such as LL/SC (linked-load/store-conditional) which will fail if memory has been touched prior to the store operation.
Practice Question: Awesome work with the monkey, we fixed all the issues and the banana attack is complete. For your final question we will ask you to implement a function that tests basic vector math skills. The function should validate if a player's character can hug the monkey. We will use this function to show a highlight around the monkey and prompt the player with a button press icon to convey that a hug may be initiated. You are tasked with completing the function below and implementing any helper methods required. You may assume that a character's Transform object provides easy access to a character's normalized forward direction, position, scale, and Euler rotation angles. Following your implementation, state any edge cases that your implementation can be extended to address.
// A hug is valid if...
// The player is within kMinDist of the monkey
// It would take no more than a 90 degree turn for the player to directly face the monkey
float kMinDist = 10;
float kMinDegrees = 180;
bool CanHug( Transform player, Transform monkey){
}
// A hug is valid if...
// The player is within kMinDist of the monkey
// It would take no more than a 90 degree turn for the player to directly face the monkey
float kMinDist = 10;
float kMinDegrees = 180;
bool CanHug( Transform player, Transform monkey){
}
To check if the monkey is within kMinDist range of the player, we can check the squared distance between their positions against the value kMinDist * kMinDist (we compare the squared distance to prevent the expensive sqrt function). To check if the monkey is within the player's FOV (field of view) there is a basic vector operation that can take in two vectors to determine the angle between them. Can you recall which vector operation that is?
The dot product is the best choice here to determine the angle between two vectors. Those vectors would be the player's current look direction (which we can get from their transform) and the direction from the player to the monkey.
//—----------- SOLUTION —-----------------
Bool IsInFOV( Vec3 forward, Vec3 lineOfSight ){
float scaler = ( forward.mag * lineOfSight .mag );
float cosAngle = Dot( forward, lineOfSight ) / scalar;
float angle = acos( cosAngle );
return angle < Deg2Rad( kMinDegrees );
}
Bool CanHug( Transform player, Transform monkey ){
Vec3 playerTomonkey = monkey.pos - player.pos;
if( playerTomonkey.magSqr > kMinDist * kMinDist )
return false;
if( !IsInFOV( player.forward, playerTomonkey )
return false;
Return true;
}
Reflection: There are many edge cases the candidate should identify as they solve this problem such as... checking if a wall or other obstacle is between the player and enemy, checking if the player is above/below the enemy, checking if the player or enemy is in a non-huggable state (such as they are already being hugged... but maybe it can become a group hug!).
Congratulations on completing the practice test! These questions cover some of the basic knowledge that a junior to mid-level engineer might be tested on. We cover how to approach these problems, and MANY more, within this book. Please consider purchasing the book here or sharing this resource with a friend. I wish you luck on the exam! Additionally I am available to provide private tutoring if you would like, sign up here.
Random Questions
Below are a few more question prompts to help you review and think through what topics you would like to study further.
- Describe the role of space partitioning in video games and some approaches to implementing it.
- What is the significance of the magnitude of the cross product’s result?
- What are some edge cases we need to watch out for when using the cross product?
- What’s the equation of a plane? What do those variables represent?
- What is the magnitude of the vector that results from projecting a vector onto a normalized vector?
- What is the magnitude of the vector that results from projection a normalized vector onto a normalized vector?
- How would you obtain a point on a plane?
- How would you clear the fifth bit of an integer?
- How would you write a class with pure virtual methods?
- How would you write a lockless insert and delete? What about a lockless push and pop?
- Explain the benefits and drawbacks of using a deque vs an array.
- Explain the benefits and drawbacks of using an event system versus polling.
- With reference to the vTable, explain the costs of using virtual functions.
- What’s an orthonormal matrix?
- If I take the cross product of two normalized vectors, is the resulting vector normalized?
- Why might a bug only occur sometimes?
- Why might a bug only occur in optimized builds?
- Explain when you should use pointers versus references.
- When should you implement a destructor? What about a virtual destructor?
- Given a pointer, P, what is represented by &P, *P, and P?
- Including its determinant, describe the unique attributes of a rotation matrix.