As I've written in previous posts, I'm still not that good with all the math etc. writing these posts is a way for me to learn and hopefully someone else out there finds it useful as well. This time I found a great post that goes into the details of ray-casting to pick objects in 3D space over at Anton Gerdelan's blog: antongerdelan.net/opengl/raycasting.html
I've taken his base solution and made it work with the code-base that we use in this tutorial series. It did not work directly, so a lot of time was spent on what was going wrong.
So, now that credit is where it belongs, lets look at the code.
Code
First we want to calculate the ray from our camera location based on the mouse click on the screen. So first we need to subscribe to mouse events. So in our OnLoad method, we will add the following event handler.
MouseUp += OnMouseUp;
In our event handler we will check that it is the correct button and send the coordinates to the actual object picking method
This is the method that calculates the ray from the camera and into the screen. It takes the mouse coordinates and then backtracks the transformations back to world space. For more details, check antongerdelan.net/opengl/raycasting.html.
private void PickObjectOnScreen(int mouseX, int mouseY)
{
// heavily influenced by: http://antongerdelan.net/opengl/raycasting.html
// viewport coordinate system
// normalized device coordinates
var x = (2f * mouseX) / Width - 1f;
var y = 1f - (2f * mouseY) / Height;
var z = 1f;
var rayNormalizedDeviceCoordinates = new Vector3(x, y, z);
// 4D homogeneous clip coordinates
var rayClip = new Vector4(rayNormalizedDeviceCoordinates.X, rayNormalizedDeviceCoordinates.Y, -1f, 1f);
// 4D eye (camera) coordinates
var rayEye = _projectionMatrix.Inverted() * rayClip;
rayEye = new Vector4(rayEye.X, rayEye.Y, -1f, 0f);
// 4D world coordinates
var rayWorldCoordinates = (_camera.LookAtMatrix.Inverted() * rayEye).Xyz;
rayWorldCoordinates.Normalize();
FindClosestAsteroidHitByRay(rayWorldCoordinates);
}
After that we need to add a method that checks if the game object has been hit by the ray. I added this to the AGameObject class so that all game objects can be picked. Basically we begin by checking if the origin of the ray is inside the sphere that we are checking against. After that we check if the ray is pointing away from the object and lastly we perform Pythagorean method to determine if the ray is within the radius of the sphere that we are looking at. This ray method is something that I've had lying around for a while and used for various purposes.
public double? IntersectsRay(Vector3 rayDirection, Vector3 rayOrigin)
{
var radius = _scale.X;
var difference = Position.Xyz - rayDirection;
var differenceLengthSquared = difference.LengthSquared;
var sphereRadiusSquared = radius * radius;
if (differenceLengthSquared < sphereRadiusSquared)
{
return 0d;
}
var distanceAlongRay = Vector3.Dot(rayDirection, difference);
if (distanceAlongRay < 0)
{
return null;
}
var dist = sphereRadiusSquared + distanceAlongRay * distanceAlongRay - differenceLengthSquared;
var result = (dist < 0) ? null : distanceAlongRay - (double?)Math.Sqrt(dist);
return result;
}
The above method is called from the following that iterates all the objects currently active in the game and then tries to find the object that is closest (if there are many) to the origin of the ray.
private void FindClosestAsteroidHitByRay(Vector3 rayWorldCoordinates)
{
AGameObject bestCandidate = null;
double? bestDistance = null;
foreach (var gameObject in _gameObjects)
{
if (!(gameObject is SelectableSphere) && !(gameObject is Asteroid))
continue;
var candidateDistance = gameObject.IntersectsRay(rayWorldCoordinates, _camera.Position);
if (!candidateDistance.HasValue)
continue;
if (!bestDistance.HasValue)
{
bestDistance = candidateDistance;
bestCandidate = gameObject;
continue;
}
if (candidateDistance < bestDistance)
{
bestDistance = candidateDistance;
bestCandidate = gameObject;
}
}
if (bestCandidate != null)
{
switch (bestCandidate)
{
case Asteroid asteroid:
_clicks += asteroid.Score;
break;
case SelectableSphere sphere:
sphere.ToggleModel();
break;
}
}
}
As you can see above, we have introduced a new GameObject for this part of the tutorial series, the SelectableSphere. It looks like the following:
public class SelectableSphere : AGameObject
{
private ARenderable _original;
private ARenderable _secondaryModel;
public SelectableSphere(ARenderable model, ARenderable secondaryModel, Vector4 position, Vector4 direction, Vector4 rotation)
: base(model, position, direction, rotation, 0)
{
_original = model;
_secondaryModel = secondaryModel;
}
public override void Update(double time, double delta)
{
_rotation.Y = (float) ((time + GameObjectNumber) * 0.5);
var d = new Vector4(_rotation.X, _rotation.Y, 0, 0);
d.Normalize();
_direction = d;
base.Update(time, delta);
}
public void ToggleModel()
{
if (_model == _original)
_model = _secondaryModel;
else
_model = _original;
}
}
Basically an endlessly rotating sphere that will toggle its model when selected.
And lastly we generate these spheres in 2 layers in the OnLoad method in our MainWindow to have stuff on screen that can be selected.
var maxX = 2.5f;
var maxY = 1.5f;
for (float x = -maxX; x < maxX; x += 0.5f)
{
for (float y = -maxY; y < maxY; y += 0.5f)
{
_gameObjects.Add(_gameObjectFactory.CreateSelectableSphere(new Vector4(x, y, -5f, 0)));
}
}
for (float x = -maxX; x < maxX; x += 0.65f)
{
for (float y = -maxY; y < maxY; y += 0.65f)
{
_gameObjects.Add(_gameObjectFactory.CreateSelectableSphereSecondary(new Vector4(x, y, -6f, 0)));
}
}
For the full source code for the financial system discussed in this series, look in the repository.
This post we will look at the actual trade algorithm used. It is a quite naive implementation but works for me. Feel free to extend it for your own needs.
In the Marketplace class, we have an Update method that iterates through all the buy and sell orders available and tries to match them with each other. This simple implementation tries to find an exact match but there is potential to extend it to match multiple sell to one buy, or one sell to multiple sell.
In the demo application this is triggered by a timer.
public void Update()
{
bool updated = false;
lock(_sellOrders)
{
lock (_buyOrders)
{
foreach (var buyName in _buyOrders)
{
List<SellOrder> sellOrders;
if (!_sellOrders.TryGetValue(buyName.Key, out sellOrders))
continue;
// naive
foreach (var buyOrder in buyName.Value)
{
foreach (var sellOrder in sellOrders)
{
if (buyOrder.Quantity == sellOrder.Quantity
&& buyOrder.PricePerItem == sellOrder.PricePerItem)
{
updated = true;
buyOrder.Execute(sellOrder);
}
}
}
}
foreach (var order in _sellOrdersToBeRemoved)
{
_sellOrders[order.SecurityName].Remove(order);
}
foreach (var order in _buyOrdersToBeRemoved)
{
_buyOrders[order.SecurityName].Remove(order);
}
}
}
if (updated)
{
Updated?.Invoke(this, this);
}
}
This is where the money is transferred between the two orders, all actions connected to this BuyOrder are triggered and then finally the sellOrder is executed as well.
When the buy order was added to the Marketplace, it associated a Cancel and Execute action with it to be triggered when the order was successful or canceled.
public void Buy(BuyOrder order)
{
lock (_buyOrders)
{
if (_buyOrders.ContainsKey(order.SecurityName))
_buyOrders[order.SecurityName].Add(order);
else
_buyOrders.Add(order.SecurityName, new List<BuyOrder> { order });
order.AddCancelAction(RemoveBuy);
order.AddExecuteAction(RemoveBuy);
}
}
For both cases, we are just interested in removing the order from the list of available orders. The same goes for the Sell method.
As this is triggered from an iteration of the _buyOrders collection, we need to deffer the removal to after the iteration has completed, hence the addition to a secondary removal list. Just writing this I spotted a bug in the code where the order needs to be flagged as executed so that the same order isn't executed multiple times. I will not fix this in the demo application for now.
Last in the Update method, a call to the Updated event handlers associated with this marketplace object.
if (updated)
{
Updated?.Invoke(this, this);
}
This way a GUI can update itself if there was an update. The Updated event is also fired each time an order is added to the marketplace.
So there, thank you for reading. Hope this helps someone out there : )
I'm quite interested in finance as can be seen on my other blog dreamstateliving, and thus I decided to start working on a small but powerful in game financial system that can be used in my future games.
The goal for this system is to allow for
trading at a marketplace, much like the stock market
buying and selling stuff from vendors
Note that the code provided with this post is intended to be used in games, a lot of shortcuts have been taken. This is not a fool-proof system, more like a base to start building your own in game financial system on.
In this part we will introduce the scenario and go through the Account and TradeAccount classes.
Every player, vendor etc. has their private TradeAccount consisting of a BalanceAccount and a list of Securities (items, shares whatever) owned at his current state.
Each TradeAccount is able to generate a BuyOrder based on a name of the Security to buy, the Quantity of Securities we are shopping for and the PricePerItem we are willing to pay. The BuyOrder also contains a BalanceAccount with the amount of money needed to make the purchase. It is not possible to generate a BuyOrder if the TradeAccounts' BalanceAccount does not hold the amount of money needed to close the transaction.
In a similar fashion, each TradeAccount is able to generate a SellOrder for Securities currently held by that TradeAccount. The SellOrder also consists of the PricePerItem, Quantity and the Security for sale.
Both the SellOrder and the BuyOrder are sent to the Marketplace where the trade should be made. The Marketplace should be possible to instance as many times as needed. For example each vendor would have their own with things for sale, but the global Marketplace would allow for players to trade.
If there is an exact match on Security, Quantity and PricePerItem we will Execute the BuyOrder and transfer the money to the sellers TradeAccount and the securities to the buyers TradeAccount.
This will be our first naive algorithm to match orders.
Lets look at some code.
BalanceAccount
This is just a holder of money. Lets look at the interface.
So first we have an identifier, Id, just so that we can identify this account uniquely. Then we will store the actual current Balance of the account. I chose an unsigned long to store because of three reasons:
I don't really want to handle negative numbers, i.e. if you cannot afford it you can't buy it.
to be able to handle very large sums, I don't really know the applications for this yet, but don't want to limit someone for buying something for a ridiculous amount of money (18,446,744,073,709,551,615 to be exact)
I don't want to handle decimal numbers. Keep it simple, its a game :)
After that we have some functions, CanAfford checks if the account could afford to buy something costing x.
public bool CanAfford(ulong x)
{
return Balance >= x;
}
DepositInto withdraws money from this account and inserts it into the destination account.
As you can see we add an extra lock in the AddBalance function as it is a public one and could be called from outside the DepositInto lock. For the private SubtractBalance method, we have full control and don't need the additional lock overhead.
There is a number of unit tests to verify this implementation in the source code in the repository. It gets quite long and thus not included in the post.
First off, we will expose an event, Updated, that will be triggered whenever the contents of this Trade Account change. This to be able to create event based updates of GUI elements at a later stage.
After that we have the BalanceAccount associated with this TradeAccount, for storage of money that can be used to buy stuff. ValueOfActiveBuyOrders will return the amount of money bound in buy orders created by this TradeAccount.
public ulong ValueOfActiveBuyOrders
{
get
{
ulong sum = 0;
foreach (var item in BuyOrders)
{
sum += item.OrderAccount.Balance;
}
return sum;
}
}
Securities is a dictionary of securities held by this account followed bu the currently active BuyOrders and SellOrders.
Lastly we expose functions to create both Sell and Buy orders.
CreateSellOrder
public SellOrder CreateSellOrder(string name, ulong quantity, ulong pricePerItem)
{
Security security;
if (!Securities.TryGetValue(name, out security))
return null;
var split = security.Split(quantity);
if (split == null)
return null;
var order = new SellOrder(split, pricePerItem);
order.AddCancelAction(CancelSellOrder);
order.AddExecuteAction(ExecuteSellOrder);
lock (SellOrders)
{
SellOrders.Add(order);
}
Updated?.Invoke(this, this);
return order;
}
To createa a SellOrder we first need to check if we actually own any securities with the name provided, if we don't we will stop directly. After that we split our owned security into a new one with the quantity we want to sell. We expect the Split method to return null if the split is invalid (tries to sell more then owned) and we stop there as well. If all is well, we setup the SellOrder, connect both Cancel and Execute handlers to this TradeAccount so that it will know its way home when the mission is accomplished or canceled. Lastly, we add it to the list of active SellOrders and invoke the Updated event.
The ExecuteSellOrder is responsible to transfer money to the selling TradeAccount when a trade is completing. As seen, the SellOrder has already received the money from the buying account so this is just a final put the money where it belongs and cleanup things handler.
The CancelSellOrder is a little more complicated. We are not receiving any money here but we need to put the Securities back where they belong. So find the security we were trying to sell and either merge with the existing one or create a new entry (if it has been cleaned up for any reason). After that some cleanup and update event invoking.
CreateBuyOrder
The CreateBuyOrder basically follows the same pattern as the sell order creator but here we check that we have enough money to make the purchase that we want to.
We do this by creating a new account and trying to deposit the needed amount of money into it. If it fails, we stop there. If all is good, we create the BuyOrder and supply it the just created account and what to look for in the marketplace. Just as for the SellOrder, we connect handlers for both the Execute and Cancel outcomes and add it to the list of active buy orders.
public BuyOrder CreateBuyOrder(string name, ulong quantity, ulong pricePerItem)
{
var orderAccount = new Account(0);
if (!BalanceAccount.DepositInto(orderAccount, quantity*pricePerItem))
return null;
var order = new BuyOrder(name, pricePerItem, quantity, orderAccount);
order.AddCancelAction(CancelBuyOrder);
order.AddExecuteAction(ExecuteBuyOrder);
lock (BuyOrders)
{
BuyOrders.Add(order);
}
Updated?.Invoke(this, this);
return order;
}
The ExecuteBuyOrder looks very similar to the CancelSellOrder above, instead of putting back a canceled sell order, we will put in a newly purchased Security. It needs to find the already owned security and merge with it if it exists or create a new entry in the owned Securites dictionary. After that a cleanup is done.
And surprise, surprise the CancelBuyOrder is similar to the ExecuteSellOrder. Here we put back the pocket-money we received when going to the market and then cleaning up.
So just like with the Account, there are a number of unit tests in the repository that I will not cover here. Actually the repository contains the code for the first working version with a test GUI. We will cover the rest of the code in future posts in this series, but if you are interested you can look at the code already now :)
So there, thank you for reading. Hope this helps someone out there : )
As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful then all the better :)
If you think that the progress is slow, then know that I am a slow learner :P
This part will build upon the game window and shaders from part 13.
Generate font texture
First off, we need to generate a texture for the font that we want to use. For simplicity, all letters that we will need are put on a single line and each letter will get a fixed size rectangle in the texture.
The GenerateCharacters method takes the name of the font that we want to use, the size of the font and outputs the size of a single character in the returned texture. So white on black.
Example output from above algorithm. Original texture was 3432x48 pixels
This texture will be used to calculate the alpha channel of the color that we want to render on screen. I.e. whatever is white on the texture will be rendered as the chose color and the black will be rendered as transparent.
Render Objects
Basically we want to render each character in a quad, so we need to generate a model with 2 triangles that can be reused. So into our RenderObjectFactory we add the following:
public static TexturedVertex[] CreateTexturedCharacter()
{
float h = 1;
float w = RenderText.CharacterWidthNormalized;
float side = 1f / 2f; // half side - and other half
TexturedVertex[] vertices =
{
new TexturedVertex(new Vector4(-side, -side, side, 1.0f), new Vector2(0, h)),
new TexturedVertex(new Vector4(side, -side, side, 1.0f), new Vector2(w, h)),
new TexturedVertex(new Vector4(-side, side, side, 1.0f), new Vector2(0, 0)),
new TexturedVertex(new Vector4(-side, side, side, 1.0f), new Vector2(0, 0)),
new TexturedVertex(new Vector4(side, -side, side, 1.0f), new Vector2(w, h)),
new TexturedVertex(new Vector4(side, side, side, 1.0f), new Vector2(w, 0)),
};
return vertices;
}
It is the front facing quad from the generate cube method already there.
The RenderText.CharacterWidthNormalized is returning the 1/number of characters to get the correct alignment of the x-axis.
We will be needing 2 new render objects to accomplish putting text on the screen. RenderText that handles the whole string to be rendered, and RenderCharacter that handles each individual character in the string.
From the top. Characters string containing all the characters in our font texture in the correct order. Static constructor that initializes the lookup table, mapping each character to its index. Dictionary for faster lookup then doing the index of operation during for each string we want to show. Instance constructor, just decomposes the Color struct to a Vector4, I still don't know why the GL.VertexAttrib4 doesn't support the Color struct out of the box. SetText, allows for changing the contents of this RenderText object. This is a naive implementation that empties all content and then adds new. Optimization could be to try re-use whatever objects that already are in the collection. But for now, this works for me. Render, just sets the color attribute and calls render for all RenderCharacters.
Next, all characters in the string
public class RenderCharacter : AGameObject
{
private float _offset;
public RenderCharacter(ARenderable model, Vector4 position, float charOffset)
: base(model, position, Vector4.Zero, Vector4.Zero, 0)
{
_offset = charOffset;
_scale = new Vector3(0.2f);
}
public void SetChar(float charOffset)
{
_offset = charOffset;
}
public override void Render(ICamera camera)
{
GL.VertexAttrib2(2, new Vector2(_offset, 0));
var t2 = Matrix4.CreateTranslation(
_position.X,
_position.Y,
_position.Z);
var s = Matrix4.CreateScale(_scale);
_modelView = s * t2 * camera.LookAtMatrix;
GL.UniformMatrix4(21, false, ref _modelView);
_model.Render();
}
}
The key component for a character is the x-axis offset in the texture. The Render method just binds the offset attribute to the shader and renders the quad holding the character. At the moment the character is transformed with a Model-View matrix.
The first 2 inputs to our vertex shader are bound from buffers, and we introduce 2 new inputs that we set from the render methods. The texture offset and color.
We also need to send the color forward to the fragment shader so we need an out parameter for that.
The vs_textureOffset is the original texture coordinate plus the new offset to find a character. The original texture coordinate the X-axis was of the width of 1 character and that's why this works. Fragment Shader
#version 450 core
in vec2 vs_textureOffset;
in vec4 vs_color;
uniform sampler2D textureObject;
out vec4 color;
void main(void)
{
vec4 alpha = texture(textureObject, vs_textureOffset);
color = vs_color;
color.a = alpha.r;
}
The vertex shader reads the texture, as it is black and white the red, green and blue channels should have the same values, hence we can look at just one of them and set the alpha channel of our color to get transparency. i.e. the color just sticks to the characters and we cut out everything else.
Game Window
OnLoad
We need to setup some stuff in the OnLoad method of our game window. First out is to setup the new shaders needed to render our text objects.
_textProgram = new ShaderProgram();
_textProgram.AddShader(ShaderType.VertexShader, @"Components\Shaders\1Vert\textVert.c");
_textProgram.AddShader(ShaderType.FragmentShader, @"Components\Shaders\5Frag\textFrag.c");
_textProgram.Link();
Nothing fancy there, next to load our texture and add it to the list of models (for correct disposal)
var textModel = new TexturedRenderObject(RenderObjectFactory.CreateTexturedCharacter(), _textProgram.Id, @"Components\Textures\font singleline.bmp");
models.Add("Quad", textModel);
As we do this in our Asteroid Invaders game, we will be displaying the score. So we need a variable to store this in.
_text = new RenderText(models["Quad"], new Vector4(-0.2f, 0.1f, -0.4f, 1), Color4.Red, "Score");
And lastly, we need to enable transparency. Otherwise the alpha blending wouldn't bite and we would have a black background instead.
OnRenderFrame
The major changes to our OnRenderFrame method consists of updating our score and adding a second render step after all normal game objects for rendering of our transparent objects. This because we need to have whatever we want to show in the transparent areas to be drawn before our text. Otherwise we would get the dark blue background color as a box for all characters.
protected override void OnRenderFrame(FrameEventArgs e)
{
Title = $"{_title}: FPS:{1f / e.Time:0000.0}, obj:{_gameObjects.Count}, score:{_score}";
_text.SetText($"Score: {_score}");
GL.ClearColor(_backColor);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
int lastProgram = -1;
foreach (var obj in _gameObjects)
{
lastProgram = RenderOneElement(obj, lastProgram);
}
// render after all opaque objects to get transparency right
RenderOneElement(_text, lastProgram);
SwapBuffers();
}
private int RenderOneElement(AGameObject obj, int lastProgram)
{
var program = obj.Model.Program;
if (lastProgram != program)
GL.UniformMatrix4(20, false, ref _projectionMatrix);
lastProgram = obj.Model.Program;
obj.Render(_camera);
return lastProgram;
}
Further work
This is not a fully featured way to render text, but enough to get a score on the screen. There's a lot of things that could be done to improve it and here comes a few:
As the solution is based on a bitmap, it scales kind of bad. There exists a lot of material on how to make that smooth and something that I will look at some time in the future. Distance fields is an area that seems promising and that takes advantage of the GPU. More information in the following links if you want to give it a try:
Positioning on the screen. Today the solution uses world coordinates, works for this static camera scene but not so well with a moving camera if you still want to have the text visible to the user.
Text wrapping, multi row text. Better scaling.
The list goes on and I'll probably end up coding some of the stuff on the list whenever I feel a need for it.
This time we will look at how to generate a sphere in code, I got tired of the cubes and wanted a little variation. The decision fell on IcoSpheres, mostly as they seem to be more flexible in the long run.
As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful then all the better :)
If you think that the progress is slow, then know that I am a slow learner :P
This part will build upon the game window and shaders from part 12.
Introduction
So I take no credit for this algorithm to generate the sphere. I found a WPF version over at catch 22, Andreas Kahlers blog. I ported it to work with our OpenTK implementation of vertex buffers and texturing.
Texturing fix for the common artefact of a funny looking stripe that goes from pole to pole is based on a solution described over at sol.gfxil.net.
So, now that credit is where it belongs, lets look at the code.
Code
private struct Face
{
public Vector3 V1;
public Vector3 V2;
public Vector3 V3;
public Face(Vector3 v1, Vector3 v2, Vector3 v3)
{
V1 = v1;
V2 = v2;
V3 = v3;
}
}
First we need a struct where we will store each face of our sphere. Basically just contains 3 vectors for each point in a triangle.
The algorithm from Khalers blog. As noted above, this does not use vertex indexing. Meaning that we have a little memory overhead as every vertex is doubled instead of reused.
Basic algorithm is to generate the initial points manually and then for each iteration split each face into 4 new faces and project them into the unit sphere by normalizing them.
public class IcoSphereFactory
{
private List<Vector3> _points;
private int _index;
private Dictionary<long, int> _middlePointIndexCache;
public TexturedVertex[] Create(int recursionLevel)
{
_middlePointIndexCache = new Dictionary<long, int>();
_points = new List<Vector3>();
_index = 0;
var t = (float)((1.0 + Math.Sqrt(5.0)) / 2.0);
var s = 1;
AddVertex(new Vector3(-s, t, 0));
AddVertex(new Vector3(s, t, 0));
AddVertex(new Vector3(-s, -t, 0));
AddVertex(new Vector3(s, -t, 0));
AddVertex(new Vector3(0, -s, t));
AddVertex(new Vector3(0, s, t));
AddVertex(new Vector3(0, -s, -t));
AddVertex(new Vector3(0, s, -t));
AddVertex(new Vector3(t, 0, -s));
AddVertex(new Vector3(t, 0, s));
AddVertex(new Vector3(-t, 0, -s));
AddVertex(new Vector3(-t, 0, s));
var faces = new List<Face>();
// 5 faces around point 0
faces.Add(new Face(_points[0], _points[11], _points[5]));
faces.Add(new Face(_points[0], _points[5], _points[1]));
faces.Add(new Face(_points[0], _points[1], _points[7]));
faces.Add(new Face(_points[0], _points[7], _points[10]));
faces.Add(new Face(_points[0], _points[10], _points[11]));
// 5 adjacent faces
faces.Add(new Face(_points[1], _points[5], _points[9]));
faces.Add(new Face(_points[5], _points[11], _points[4]));
faces.Add(new Face(_points[11], _points[10], _points[2]));
faces.Add(new Face(_points[10], _points[7], _points[6]));
faces.Add(new Face(_points[7], _points[1], _points[8]));
// 5 faces around point 3
faces.Add(new Face(_points[3], _points[9], _points[4]));
faces.Add(new Face(_points[3], _points[4], _points[2]));
faces.Add(new Face(_points[3], _points[2], _points[6]));
faces.Add(new Face(_points[3], _points[6], _points[8]));
faces.Add(new Face(_points[3], _points[8], _points[9]));
// 5 adjacent faces
faces.Add(new Face(_points[4], _points[9], _points[5]));
faces.Add(new Face(_points[2], _points[4], _points[11]));
faces.Add(new Face(_points[6], _points[2], _points[10]));
faces.Add(new Face(_points[8], _points[6], _points[7]));
faces.Add(new Face(_points[9], _points[8], _points[1]));
// refine triangles
for (int i = 0; i < recursionLevel; i++)
{
var faces2 = new List<Face>();
foreach (var tri in faces)
{
// replace triangle by 4 triangles
int a = GetMiddlePoint(tri.V1, tri.V2);
int b = GetMiddlePoint(tri.V2, tri.V3);
int c = GetMiddlePoint(tri.V3, tri.V1);
faces2.Add(new Face(tri.V1, _points[a], _points[c]));
faces2.Add(new Face(tri.V2, _points[b], _points[a]));
faces2.Add(new Face(tri.V3, _points[c], _points[b]));
faces2.Add(new Face(_points[a], _points[b], _points[c]));
}
faces = faces2;
}
// done, now add triangles to mesh
var vertices = new List<TexturedVertex>();
foreach (var tri in faces)
{
var uv1 = GetSphereCoord(tri.V1);
var uv2 = GetSphereCoord(tri.V2);
var uv3 = GetSphereCoord(tri.V3);
vertices.Add(new TexturedVertex(new Vector4(tri.V1, 1), uv1));
vertices.Add(new TexturedVertex(new Vector4(tri.V2, 1), uv2));
vertices.Add(new TexturedVertex(new Vector4(tri.V3, 1), uv3));
}
return vertices.ToArray();
}
private int AddVertex(Vector3 p)
{
_points.Add(p.Normalized());
return _index++;
}
// return index of point in the middle of p1 and p2
private int GetMiddlePoint(Vector3 point1, Vector3 point2)
{
long i1 = _points.IndexOf(point1);
long i2 = _points.IndexOf(point2);
// first check if we have it already
var firstIsSmaller = i1 < i2;
long smallerIndex = firstIsSmaller ? i1 : i2;
long greaterIndex = firstIsSmaller ? i2 : i1;
long key = (smallerIndex << 32) + greaterIndex;
int ret;
if (_middlePointIndexCache.TryGetValue(key, out ret))
{
return ret;
}
// not in cache, calculate it
var middle = new Vector3(
(point1.X + point2.X) / 2.0f,
(point1.Y + point2.Y) / 2.0f,
(point1.Z + point2.Z) / 2.0f);
// add vertex makes sure point is on unit sphere
int i = AddVertex(middle);
// store it, return index
_middlePointIndexCache.Add(key, i);
return i;
}
}
Get sphere coordinate is my own addition for calculating the texture coordinate for each vertex.
foreach (var tri in faces)
{
var uv1 = GetSphereCoord(tri.V1);
var uv2 = GetSphereCoord(tri.V2);
var uv3 = GetSphereCoord(tri.V3);
FixColorStrip(ref uv1, ref uv2, ref uv3);
vertices.Add(new TexturedVertex(new Vector4(tri.V1, 1), uv1));
vertices.Add(new TexturedVertex(new Vector4(tri.V2, 1), uv2));
vertices.Add(new TexturedVertex(new Vector4(tri.V3, 1), uv3));
}
Now the texture should look OK like this:
Icosphere that shows the texturing glitch fixed
Lastly, change the generation of the initial models in GameWindow class to use these spheres instead of the cubes.
models.Add("Wooden", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\wooden.png", 8));
models.Add("Golden", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\golden.bmp", 8));
models.Add("Asteroid", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\moonmap1k.jpg", 8));
models.Add("Spacecraft", new MipMapGeneratedRenderObject(RenderObjectFactory.CreateTexturedCube6(1, 1, 1), _texturedProgram.Id, @"Components\Textures\spacecraft.png", 8));
models.Add("Gameover", new MipMapGeneratedRenderObject(RenderObjectFactory.CreateTexturedCube6(1, 1, 1), _texturedProgram.Id, @"Components\Textures\gameover.png", 8));
models.Add("Bullet", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\dotted.png", 8));
End result should be as in the following video:
Known issues that still need to be fixed:
Texturing at the poles is still glitchy. I was unable to get the solution described at sol.gfxil.net for it to work.. Yet.
Maybe implement the vertex index solution as soon as I figure it out. As this currently fits my needs that might take a while.
As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful then all the better :)
If you think that the progress is slow, then know that I am a slow learner :P
This part will build upon the game window and shaders from part 11, including homing bullets from this post.
Camera
At this point, it is quite easy to implement a movable camera, it is actually just another level of matrix multiplication that we do when we setup the ModelView matrix for each object. So lets start there by modifying the Render method of the AGameObject class to take a camera as input.
public virtual void Render(ICamera camera)
{
_model.Bind();
var t2 = Matrix4.CreateTranslation(_position.X, _position.Y, _position.Z);
var r1 = Matrix4.CreateRotationX(_rotation.X);
var r2 = Matrix4.CreateRotationY(_rotation.Y);
var r3 = Matrix4.CreateRotationZ(_rotation.Z);
var s = Matrix4.CreateScale(_scale);
_modelView = r1*r2*r3*s*t2*camera.LookAtMatrix;
GL.UniformMatrix4(21, false, ref _modelView);
_model.Render();
}
In this case our cameras should be able to provide a LookAtMatrix and be able to update themselves. So the interface looks like this:
public interface ICamera
{
Matrix4 LookAtMatrix{ get; }
void Update(double time, double delta);
}
The update method is called from the OnFrameUpdate override in the GameWindow and the look at matrix is used for each object rendered in the OnRenderFrame override.
So, lets look at some cameras
Default: Static Camera
This camera is basically what we have out of the box and been using so far. It is located at origin (0, 0, 0) and is pointed towards the negative Z axis. I.e. into the screen (remember right handed coordinate system).
Lets create a camera that implements this camera so that we can change back to it whenever we want to.
public class StaticCamera : ICamera
{
public Matrix4 LookAtMatrix { get; }
public StaticCamera()
{
Vector3 position;
position.X = 0;
position.Y = 0;
position.Z = 0;
LookAtMatrix = Matrix4.LookAt(position, -Vector3.UnitZ, Vector3.UnitY);
}
public StaticCamera(Vector3 position, Vector3 target)
{
LookAtMatrix = Matrix4.LookAt(position, target, Vector3.UnitY);
}
public void Update(double time, double delta)
{}
}
Also added a constructor that makes this camera a little bit more useful, it can initialize to any position and look at any static target. Note that we are using the OpenTK Matrix4 method LookAt to create out camera look at matrix.
First Person Camera
Next camera is the First Person Camera. We send in a AGameObject that the camera should follow and it should give us a feed following the path of the object.
public class FirstPersonCamera : ICamera
{
public Matrix4 LookAtMatrix { get; private set; }
private readonly AGameObject _target;
private readonly Vector3 _offset;
public FirstPersonCamera(AGameObject target)
: this(target, Vector3.Zero)
{}
public FirstPersonCamera(AGameObject target, Vector3 offset)
{
_target = target;
_offset = offset;
}
public void Update(double time, double delta)
{
LookAtMatrix = Matrix4.LookAt(
new Vector3(_target.Position) + _offset,
new Vector3(_target.Position + _target.Direction) + _offset,
Vector3.UnitY);
}
}
Here as well we have an overloaded constructor that takes an offset. Still looking in the direction that the object is moving, but from an offset to the position variable, maybe from the cockpit of an airplane instead of the origin of the model.
Third Person Camera
Our third person camera looks at the object that we are tracking from an offset baside it.
public class ThirdPersonCamera : ICamera
{
public Matrix4 LookAtMatrix { get; private set; }
private readonly AGameObject _target;
private readonly Vector3 _offset;
public ThirdPersonCamera(AGameObject target)
: this(target, Vector3.Zero)
{}
public ThirdPersonCamera(AGameObject target, Vector3 offset)
{
_target = target;
_offset = offset;
}
public void Update(double time, double delta)
{
LookAtMatrix = Matrix4.LookAt(
new Vector3(_target.Position) + (_offset * new Vector3(_target.Direction)),
new Vector3(_target.Position),
Vector3.UnitY);
}
}
Demo in the video of the three basic movable cameras:
This is quite basic, and the amount of waves is set with the 33f, in this case quite rapid. The wave form is based of the location of the bullet in on the Y axis. As it moves upwards this value changes and it is sent to the Sin or Cos functions.
Lastly our new direction is normalized, note that this takes away some velocity as the object now moves on a curved road and the velocity is distributed on more than 1 axis. We could set the Y to 1 to achieve the original upward velocity. Up to the coder :)
Seeker/Homing
A little more advanced. Here we try to get the bullet to seek towards the asteroid that it has locked on to.
First we need to lock the bullet to an Asteroid.
public void SetTarget(Asteroid target)
{
_target = target;
target.LockBullet(this);
}
This in turn calls LockBullet on the Asteroid and we change the model of the asteroid to the bullet model during the time of the lock.
This gets normalized as well.
Lastly, when the bullet object is created, we assign
var bullet = _gameObjectFactory.CreateBullet(_player.Position, _bulletType);
var asteroids = _gameObjects.Where(x => x.GetType() == typeof (Asteroid)).ToList();
bullet.SetTarget((Asteroid) asteroids[bullet.GameObjectNumber%asteroids.Count]);
_gameObjects.Add(bullet);
Here we lock the asteroid directly after creation of the bullet. We just pick one asteroid from the list based on the bullets GameObjectNumber.
This is quite naive homing algorithm, it does not take into account any obstacles on its path and hits them many times instead.
As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful then all the better :)
If you think that the progress is slow, then know that I am a slow learner :P
This part will build upon the game window and shaders from part 10.
Mipmap what?
Mipmapping is an optimization tool that lets OpenGL pick a texture that is closer to the needed size depending on the size of the geometry being rendered. This lets OpenGL sample from an image that has the data needed closer together, i.e. the stride between texels is not very far. Also, this helps preventing scintillation or grid illusions when the object is far away. All this to a small penalty of memory overhead.
An example of a manually created mipmap texture below:
Wood texture with mipmap levels included
Manual mipmap loading
So, lets see how to load a manually created mipmap texture with OpenTK.
Lets first create a new class, basically a copy from the existing TexturedRenderObject, called MipmapManualRenderObject.
public class MipMapManualRenderObject : ARenderable
{
private int _minMipmapLevel = 0;
private int _maxMipmapLevel;
private int _texture;
public MipMapManualRenderObject(TexturedVertex[] vertices, int program, string filename, int maxMipmapLevel)
: base(program, vertices.Length)
Note the addition of the maxMipmapLevel parameter in the constructor. This will help us load the texture.
Now lets modify the LoadTexture method a bit to allow for it to load all the levels and return them to the InitializeTextures method
private List<MipLevel> LoadTexture(string filename)
{
var mipmapLevels = new List<MipLevel>();
using (var bmp = (Bitmap)Image.FromFile(filename))
{
int xOffset = 0;
int width = bmp.Width;
int height = (bmp.Height/3)*2;
var originalHeight = height;
for (int m = 0; m < _maxMipmapLevel; m++)
{
xOffset += m == 0 || m == 1 ? 0 : width*2;
var yOffset = m == 0 ? 0 : originalHeight;
MipLevel mipLevel;
mipLevel.Level = m;
mipLevel.Width = width;
mipLevel.Height = height;
mipLevel.Data = new float[mipLevel.Width * mipLevel.Height * 4];
int index = 0;
ExtractMipmapLevel(yOffset, mipLevel, xOffset, bmp, index);
mipmapLevels.Add(mipLevel);
if (width == 1 || height == 1)
{
_maxMipmapLevel = m;
break;
}
width /= 2;
height /= 2;
}
}
return mipmapLevels;
}
Basically what we do here is to load the image from disk, iterate over it foreach mipmap level and create a MipLevel object that holds the data for that level.
For every iteration, the width and height varibles are halved, for example: 256, 128, 64, 32, 16, 8, 4, 2, 1 would yield in 0 as the original texture followed by 8 mipmaps.
If the width or height goes to 1 before the other, we will continue until both reach 1. If they reach 1 before we reach the max levels provided as input, we just reset the variable to whatever level we are on at the moment. The MipLevel struct looks like following.
public struct MipLevel
{
public int Level;
public int Width;
public int Height;
public float[] Data;
}
For each level the following method will extract the data from the bitmap. The assumed structure is as the example images in this post.
private static void ExtractMipmapLevel(int yOffset, MipLevel mipLevel, int xOffset, Bitmap bmp, int index)
{
var width = xOffset + mipLevel.Width;
var height = yOffset + mipLevel.Height;
for (int y = yOffset; y < height; y++)
{
for (int x = xOffset; x < width; x++)
{
var pixel = bmp.GetPixel(x, y);
mipLevel.Data[index++] = pixel.R/255f;
mipLevel.Data[index++] = pixel.G/255f;
mipLevel.Data[index++] = pixel.B/255f;
mipLevel.Data[index++] = pixel.A/255f;
}
}
}
And the InitializeTexture method would have the following changes:
private void InitTextures(string filename)
{
var data = LoadTexture(filename);
GL.CreateTextures(TextureTarget.Texture2D, 1, out _texture);
GL.BindTexture(TextureTarget.Texture2D, _texture);
GL.TextureStorage2D(
_texture,
_maxMipmapLevel, // levels of mipmapping
SizedInternalFormat.Rgba32f, // format of texture
data.First().Width,
data.First().Height);
for (int m = 0; m < data.Count; m++)
{
var mipLevel = data[m];
GL.TextureSubImage2D(_texture,
m, // this is level m
0, // x offset
0, // y offset
mipLevel.Width,
mipLevel.Height,
PixelFormat.Rgba,
PixelType.Float,
mipLevel.Data);
}
var textureMinFilter = (int)All.LinearMipmapLinear;
GL.TextureParameterI(_texture, All.TextureMinFilter, ref textureMinFilter);
var textureMagFilter = (int)All.Linear;
GL.TextureParameterI(_texture, All.TextureMagFilter, ref textureMagFilter);
// data not needed from here on, OpenGL has the data
}
First off we tell GL.TextureStorage2D that we will have mipmaps by supplying the _maxMipmapLevel variable. We then proceed to iterate over the levels provided by our load method and initialize them with GL.TextureSubImage2D with the level.
At the end, we tell OpenGL that we want to use mipmapping for the minimizing filter by sending in the LinearMipmapLinear constant to the GL.TextureParameterI method
Let OpenGL generate the mipmap
Ok, so maybe we are lazy and don't want to create this by ourselves. Luckily, there is a shortcut. We can tell OpenGL to generate mipmap levels based on the input texture that you load into level 0 with the following command.
GL.GenerateTextureMipmap(_texture);
So, lets create another class called MipmapGeneratedRenderObject as a copy from the original TextureRenderObject and change it to generate the mipmapping by iteself.
We need only change the InitTextures method in this case
private void InitTextures(string filename)
{
int width, height;
var data = LoadTexture(filename, out width, out height);
GL.CreateTextures(TextureTarget.Texture2D, 1, out _texture);
GL.TextureStorage2D(
_texture,
_maxMipmapLevel, // levels of mipmapping
SizedInternalFormat.Rgba32f, // format of texture
width,
height);
GL.BindTexture(TextureTarget.Texture2D, _texture);
GL.TextureSubImage2D(_texture,
0, // this is level 0
0, // x offset
0, // y offset
width,
height,
PixelFormat.Rgba,
PixelType.Float,
data);
GL.GenerateTextureMipmap(_texture);
GL.TextureParameterI(_texture, All.TextureBaseLevel, ref _minMipmapLevel);
GL.TextureParameterI(_texture, All.TextureMaxLevel, ref _maxMipmapLevel);
var textureMinFilter = (int)TextureMinFilter.LinearMipmapLinear;
GL.TextureParameterI(_texture, All.TextureMinFilter, ref textureMinFilter);
var textureMagFilter = (int)TextureMinFilter.Linear;
GL.TextureParameterI(_texture, All.TextureMagFilter, ref textureMagFilter);
// data not needed from here on, OpenGL has the data
}
Call the GL.GenerateTextureMipmap and then set the rendering filters and levels needed to get it working. It may not be the best quality and may differ between different graphics cards, but it does the trick. If you want the best quality, you must use the manual creation above.
Change to the Fragment Shader
For this to work we need to change from texelFetch that took exact texture coordinates and mipmap level to texture that takes a coordinate between 0 to 1 and does the interpolation and level changing automatically.
#version 450 core
in vec2 vs_textureCoordinate;
uniform sampler2D textureObject;
out vec4 color;
void main(void)
{
color = texture(textureObject, vs_textureCoordinate);
}
To send in 1 as width and height instead of the pixel sizes before.
In action
So how does this work in action. Lets initialize 3 objects with the same texture. One without mipmap (to the left) one with auto generated mipmap (right lower) and one with manually created mipmap texture (right upper) and see how it looks like when the object moves farther away and then back close. I added the level numberings as I did not see any difference with this particular texture between the manual and generated one, but it is interesting to see how OpenGL fluidly changes between the levels as needed.
The manually created object has the following texture so that we can see the mipmap level that is used at the moment.
So there, thank you for reading. Hope this helps someone out there : )
All code provided as-is. This is copied from my own code-base, May need some additional programming to work. Use for whatever you want, how you want! If you find this helpful, please leave a comment or share a link, not required but appreciated! :)
In this part of the series we will look at putting everything we've learned so far together to create a simple game, a hybrid of the classics Asteroids and Space Invaders.
The end product will have a functional keyboard based steering of a spacecraft, that is able to shoot bullets and 3 different types of asteroids.
As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful then all the better :)
If you think that the progress is slow, then know that I am a slow learner :P
This part will build upon the game window and shaders from part 9..
End goal
A spaceship that is able to
move around to dodge asteroids,
shoot at asteroids and
receive different score depending on type of asteroid shot down
Objects: AGameObject
This will be the base for our game objects, meaning that we put all generic code that is the same for all game objects into this class.
public abstract class AGameObject
{
public ARenderable Model => _model;
public Vector4 Position => _position;
public Vector3 Scale => _scale;
private static int GameObjectCounter;
protected readonly int _gameObjectNumber;
protected ARenderable _model;
protected Vector4 _position;
protected Vector4 _direction;
protected Vector4 _rotation;
protected float _velocity;
protected Matrix4 _modelView;
protected Vector3 _scale;
public AGameObject(ARenderable model, Vector4 position, Vector4 direction, Vector4 rotation, float velocity)
{
_model = model;
_position = position;
_direction = direction;
_rotation = rotation;
_velocity = velocity;
_scale = new Vector3(1);
_gameObjectNumber = GameObjectCounter++;
}
public void SetScale(Vector3 scale)
{
_scale = scale;
}
public virtual void Update(double time, double delta)
{
_position += _direction*(_velocity*(float) delta);
}
public virtual void Render()
{
_model.Bind();
var t2 = Matrix4.CreateTranslation(_position.X, _position.Y, _position.Z);
var r1 = Matrix4.CreateRotationX(_rotation.X);
var r2 = Matrix4.CreateRotationY(_rotation.Y);
var r3 = Matrix4.CreateRotationZ(_rotation.Z);
var s = Matrix4.CreateScale(_scale);
_modelView = r1*r2*r3*s*t2;
GL.UniformMatrix4(21, false, ref _modelView);
_model.Render();
}
}
Basically we say that each game object should have
a ARenderable object as a model.
a Position, i.e. where it is in the game world
a Direction, where it is pointing
a Velocity, what speed is it moving in the direction
a Rotation, how is it rotated in space
a Scale, how is the original model scaled to fit its purposes
a Number to identify it (mostly used for uniqueness of updates seen later)
Our generic Update method takes both the time (total time since start) and delta time (seconds since last frame). The generic update only calculates a new position for each object based on the direction and velocity variables.
At this point there should be no real surprises in here. The Render method handles the calculation of a new ModelView matrix and sets it in the shader before calling the render on the model (TexturedRenderObject). See that Matrix4.CreateScale is added to the calculation.
Objects: Asteroid
The asteroid class basically just inherits from AGameObject and adds its own override for the Update method.
public override void Update(double time, double delta)
{
_rotation.X = (float)Math.Sin((time + _gameObjectNumber) * 0.3);
_rotation.Y = (float)Math.Cos((time + _gameObjectNumber) * 0.5);
_rotation.Z = (float)Math.Cos((time + _gameObjectNumber) * 0.2);
var d = new Vector4(_rotation.X, _rotation.Y, 0, 0);
d.Normalize();
_direction = d;
base.Update(time, delta);
}
So, what we do is to calculate a new rotation for the object for this frame, we want to asteroids to tumble around the screen. Here we add in the _gameObjectNumber to allow for a little uniquenes in the rotation so that not all asteroids rotate the same.
Then, from the rotation vector, we create a new direction vector based on X and Y dimensions. We want all objects to stay at the same level on the Z axis. Normalize that one and set the direction before calling the base Update method that handles the actual updating of the position.
Objects: Bullet
For the bullet we also override the Update method. But we only set a spin on it, no change in direction. We want to bullet to go straight.
Ok, here we will have a little more code, but here as well we only add to the Update method.
private bool _moveLeft;
private bool _moveRight;
public override void Update(double time, double delta)
{
// if the use wants to move left and we are stopped or already moving left
if (_moveLeft && !(_direction.X > 0 && _velocity > 0))
{
_direction.X = -1;
_velocity += 0.8f * (float)delta;
_moveLeft = false;
}
// if the use wants to move right and we are stopped or already moving right
else if (_moveRight && !(_direction.X < 0 && _velocity > 0))
{
_direction.X = 1;
_velocity += 0.8f * (float)delta;
_moveRight = false;
}
// otherwise decrease speed to a stop. if the use changes direction, this will happen first
else
{
_velocity -= 0.9f * (float)delta;
}
// maximum velocity
if (_velocity > 0.8f)
{
_velocity = 0.8f;
}
// minimum velocity
if (_velocity < 0)
{
_velocity = 0;
_rotation.Y = 0;
}
// to make the spacecraft tilt in the way it is moving, we change the
// rotation of the Y axis based on the current velocity
if (_direction.X < 0 && _velocity > 0)
_rotation.Y = -_velocity;
if (_direction.X > 0 && _velocity > 0)
_rotation.Y = _velocity;
base.Update(time, delta);
}
public void MoveLeft()
{
_moveLeft = true;
}
public void MoveRight()
{
_moveRight = true;
}
Basically we allow the user to move the ship left or right. If the ship is moving in another direction, it will first decelerate to 0 before changing direction. This is done so that the tilt animation looks fluid. We set a minimum and maximum velocity and then calculate the tilt of the ship.
GameWindow
OnLoad
Asteroid texture, take from a picture of the moon over at pexels.com
Programmers art of a spacecraft. Red with Viper stripes:D
In the on load method we need to setup our new game objects and their textures.
var models = new Dictionary<string, ARenderable>();
models.Add("Wooden", new TexturedRenderObject(RenderObjectFactory.CreateTexturedCube(1, 256, 256), _texturedProgram.Id, @"Components\Textures\wooden.png"));
models.Add("Golden", new TexturedRenderObject(RenderObjectFactory.CreateTexturedCube(1, 256, 256), _texturedProgram.Id, @"Components\Textures\golden.bmp"));
models.Add("Asteroid", new TexturedRenderObject(RenderObjectFactory.CreateTexturedCube(1, 256, 256), _texturedProgram.Id, @"Components\Textures\asteroid.bmp"));
models.Add("Spacecraft", new TexturedRenderObject(RenderObjectFactory.CreateTexturedCube6(1, 1536, 256), _texturedProgram.Id, @"Components\Textures\spacecraft.png"));
models.Add("Gameover", new TexturedRenderObject(RenderObjectFactory.CreateTexturedCube6(1, 1536, 256), _texturedProgram.Id, @"Components\Textures\gameover.png"));
models.Add("Bullet", new ColoredRenderObject(RenderObjectFactory.CreateSolidCube(1, Color4.HotPink), _solidProgram.Id));
_gameObjectFactory = new GameObjectFactory(models);
_player = _gameObjectFactory.CreateSpacecraft();
_gameObjects.Add(_player);
_gameObjects.Add(_gameObjectFactory.CreateAsteroid());
_gameObjects.Add(_gameObjectFactory.CreateGoldenAsteroid());
_gameObjects.Add(_gameObjectFactory.CreateWoodenAsteroid());
Instead of storing our renderObjects directly in the GameWindow, we initialize a dictionary of objects that can be reused and send it to a new GameObjectFactory that will handle the creation of game objects. The GameWindow will have a list of game objects instead of render objects.
No big change here, we make sure that our native resources, in this case render programs and render objects (that are bound to openGL) are correctly released.
I take up this part in almost every part of this tutorial series but it can't be said enough times. When working with unmanaged resources it it Your job to dispose of them correctly. For example I had managed to remove the program disposers above and wondered why the game took longer and longer to start until I had to restart the computer. It is because you allocate memory, and if you don't clear it it will still be hogged even though your application has shut down. When I finally figured out what was wrong and reintroduced the disposes, everything works fine again.
This is something that people tend to forget about when working in C#, but it is really important.
OnUpdateFrame, Keyboard handling
Luckily we already have a method for keyboard handling. Currently it only checks for the Escape key to close the game but now we will add support for the following keys as well
A: Move left
D: Move right
Spacebar: fire bullet
private void HandleKeyboard(double dt)
{
var keyState = Keyboard.GetState();
if (keyState.IsKeyDown(Key.Escape))
{
Exit();
}
if (keyState.IsKeyDown(Key.A))
{
_player.MoveLeft();
}
if (keyState.IsKeyDown(Key.D))
{
_player.MoveRight();
}
if (!_gameOver && keyState.IsKeyDown(Key.Space) && _lastKeyboardState.IsKeyUp(Key.Space))
{
_gameObjects.Add(_gameObjectFactory.CreateBullet(_player.Position));
}
_lastKeyboardState = keyState;
}
For the movement it is OK for the user to press and hold the key down, but for the shooting part we want her to press and release space for every bullet. Otherwise we would generate a lot of bullets for every keypress as it will last for multiple frames. So we store a Last Keypress variable and check that the key was up the previous frame and is now down before we add a new bullet.
OnUpdateFrame
Here we will need to call Update for all game objects in play at the moment. Luckily we have them stored in a list that can be iterated over.
protected override void OnUpdateFrame(FrameEventArgs e)
{
_time += e.Time;
var remove = new HashSet<AGameObject>();
var view = new Vector4(0, 0, -2.4f, 0);
int outOfBoundsAsteroids = 0;
foreach (var item in _gameObjects)
{
item.Update(_time, e.Time);
if ((view - item.Position).Length > 2)
{
remove.Add(item);
outOfBoundsAsteroids++;
}
}
foreach (var r in remove)
_gameObjects.Remove(r);
for (int i = 0; i < outOfBoundsAsteroids; i++)
{
_gameObjects.Add(_gameObjectFactory.CreateRandomAsteroid());
}
HandleKeyboard(e.Time);
}
If an asteroid wanders outside of the game screen, we remove it and generate a new one. Note that the removal is done outside of the foreach loop as you should not change a collection being iterated.
OnRenderFrame
Lastly, to render we just iterate over all game objects in play and call their render method.
Note the resetting of the projectionMatrix if the render program changes between objects.
Also note the object counter and score counters in the title row. I have no idea how to write stuff on the screen yet so I went for the easy out and wrote those in the title for now :)
protected override void OnRenderFrame(FrameEventArgs e)
{
Title = $"{_title}: FPS:{1f / e.Time:0000.0}, obj:{_gameObjects.Count}, score:{_score}";
GL.ClearColor(Color.Black); // _backColor);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
int lastProgram = -1;
foreach (var obj in _gameObjects)
{
var program = obj.Model.Program;
if (lastProgram != program)
GL.UniformMatrix4(20, false, ref _projectionMatrix);
lastProgram = obj.Model.Program;
obj.Render();
}
SwapBuffers();
}
GameObjectFactory
Ok, lets look at the game object factory and see if we can get some stuff on the screen and test play a session..
public class GameObjectFactory : IDisposable
{
// set a constant Z distance. We are pretty much doing a 2D game in a 3D space
private const float Z = -2.7f;
private readonly Random _random = new Random();
private readonly Dictionary<string, ARenderable> _models;
public GameObjectFactory(Dictionary<string, ARenderable> models)
{
_models = models;
}
public Spacecraft CreateSpacecraft()
{
var spacecraft = new Spacecraft(_models["Spacecraft"], new Vector4(0, -1f, Z, 0), Vector4.Zero, Vector4.Zero, 0);
// as the spacecraft is just a texture, we scale the z axis to quite thin
spacecraft.SetScale(new Vector3(0.2f, 0.2f, 0.01f));
return spacecraft;
}
// create asteroid and scale, also sets the score given to the player when the asteroid is destroyed.
public Asteroid CreateAsteroid(string model, Vector4 position)
{
var obj = new Asteroid(_models[model], position, Vector4.Zero, Vector4.Zero, 0.1f);
obj.SetScale(new Vector3(0.2f));
switch (model)
{
case "Asteroid":
obj.Score = 1;
break;
case "Wooden":
obj.Score = 10;
break;
case "Golden":
obj.Score = 50;
break;
}
return obj;
}
// low probablity for golden asteroid, bit higher for wooden and usually it is just a rock
public AGameObject CreateRandomAsteroid()
{
var rnd = _random.NextDouble();
var position = GetRandomPosition();
if (rnd < 0.01)
return CreateAsteroid("Golden", position);
if (rnd < 0.2)
return CreateAsteroid("Wooden", position);
return CreateAsteroid("Asteroid", position);
}
// for the bullet we set the velocity from start as well as direction of movement to UnitY, i.e. straight up
// we also send in an initial position based on the spacecraft
public Bullet CreateBullet(Vector4 position)
{
var bullet = new Bullet(_models["Bullet"], position + new Vector4(0, 0.1f, 0, 0), Vector4.UnitY, Vector4.Zero, 0.8f);
bullet.SetScale(new Vector3(0.05f));
return bullet;
}
private Vector4 GetRandomPosition()
{
var position = new Vector4(
((float) _random.NextDouble() - 0.5f) * 1.1f,
((float) _random.NextDouble() - 0.5f) * 1.1f,
Z,
0);
return position;
}
// cleanup models to avoid memory leak
public void Dispose()
{
foreach (var obj in _models)
obj.Value.Dispose();
}
}
So a lot of stuff here. Key parts include the locked in Z axis to a constant value as we are basically doing a top down 2D game.
The spaceship model is just a very thin box with texture on the front facing side.
Bullet velocity is set from the start.
And Disposing of OpenGL resources at the end.
Add some action: basic collision detection
By now you should have noticed that not much is happening when bullets hit the asteroids. So lets add some code to see if objects touch each other on the screen. To accomplish this, we will just add a simple bounding sphere calculation. I.e. if two objects bounding spheres are intersecting, then we have a collision.
First in the GameWindow OnUpdateFrame loop we shall add the following to detect if a bullet or an asteroid is hitting something. Bullets will hit asteroids and if they hit the asteroid should be removed and 2 new created randomly. If the asteroid hits something, it is the spacecraft and the game is over:
if (item.GetType() == typeof (Bullet))
{
var collide = ((Bullet) item).CheckCollision(_gameObjects);
if (collide != null)
{
remove.Add(item);
if (remove.Add(collide))
{
_score += ((Asteroid)collide).Score;
removedAsteroids++;
}
}
}
if (item.GetType() == typeof(Spacecraft))
{
var collide = ((Spacecraft)item).CheckCollision(_gameObjects);
if (collide != null)
{
foreach (var x in _gameObjects)
remove.Add(x);
_gameObjects.Add(_gameObjectFactory.CreateGameOver());
_gameOver = true;
removedAsteroids = 0;
break;
}
}
Then after we have removed whatever items we will remove we add the following to generate new Asteroids.
for (int i = 0; i < removedAsteroids; i++)
{
_gameObjects.Add(_gameObjectFactory.CreateRandomAsteroid());
_gameObjects.Add(_gameObjectFactory.CreateRandomAsteroid());
}
Check collision method for the bullet iterates over all game objects and calculates the distance between itself and all asteroids. If the distance is smaller then the radius of the asteroid, we count it as a hit.
public AGameObject CheckCollision(List<AGameObject> gameObjects)
{
foreach (var x in gameObjects)
{
if(x.GetType() != typeof(Asteroid))
continue;
// naive first object in radius
if ((Position - x.Position).Length < x.Scale.X)
return x;
}
return null;
}
And pretty much the same thing for the spaceship. But here we take both spaceship radius and Asteroid radius into account.
public AGameObject CheckCollision(List<AGameObject> gameObjects)
{
foreach (var x in gameObjects)
{
if (x.GetType() != typeof(Asteroid))
continue;
// naive first object in radius
if ((Position - x.Position).Length < (Scale.X + x.Scale.X))
return x;
}
return null;
}
End results
So, nothing fancy at all in the end. But our first working game mechanics with OpenTK. At least we know that after 10 posts on the topic, it is possible to make a game that is playable :)
All code provided as-is. This is copied from my own code-base, May need some additional programming to work. Use for whatever you want, how you want! If you find this helpful, please leave a comment or share a link, not required but appreciated! :)