En Game Engine er som ordet siger en motor, der driver et spil.
I denne tekst vil jeg prøve at opbygge en simpel Game Engine i C# under Visual Studio. Samtidig vil jeg prøve at beskrive de elementer der kan indgå i en Game Engine.
Foruden en game engine omfatter et spil også en model, som indeholder en repræsentation af de forskellige elementer, der indgår i spillet som f.eks. spillebordet (Board), brikkerne, spillerne osv.
Herudover er der som regel brug for en brugergrænseflade, hvor anvisninger og statusmeddelelser kan gives til spilleren.
Spillet virker i praksis på den måde, at Game engine sørger for flowet: Henter input fra tastatur og mus, opdaterer skærmbilledet, tildeler point og opdater modellen. Ofte indgår der også et element af kunstig intelligens (AI) i spillet, hvis computeren skal agere modspiller. Heri indgår forskellige strategier og overvejelser om, hvordan man kan vinde et spil efter de gældende regler.
Kommende udfordringer og udvidelser
Dette er en simpel Game Engine baseret på de principper, der er opbygget igennem de foregående eksempler. Se f.eks. DirectX Eksempel 1, hvor der redegøres for opbygningen af en meget simpel Game Engine.
Denne engine består af en Main funktion, hvori de forskellige elementer initialiseres, og hvor et loop sættes i gang, som skal fortsætte i resten af programmets køretid:
static void Main() { using (Engine engine = new Engine()) { engine.InitializeGraphics(); engine.Show(); // While the form is still valid, render and process messages while (engine.Created) { engine.UpdateModel(); engine.Render(); Application.DoEvents(); } } }
Klassens øvrige metoder går igen fra de foregående eksempler:
Metode | Vigtigste indhold |
Constructor | Opsætning af skærmbilledets dimensioner og caption |
InitializeGraphics | Opsætning af device Opsætning af view matrix - kameraets placering og synsretning Kald af RestoreDevice Initiering af de involverede modellers objekter |
RestoreDevice | Opsætning af parametre til håndtering af dybdebuffer
og lys Opsætning af lyskilder Opsætning af projektionsmatrix |
Render | Blankstilling af Backbuffer med ønskede baggrundsfarve Tegning af modellens grafik og tekst Kald af render metoder for hvert af modellens objekter |
UpdateModel | Opdatering af scenen |
Keydown | Indlæsning af tastetryk fra tastaturet |
Mousedown | Indlæsning af knaptryk fra tastaturet |
Herunder ses koden for en standard engine - der er indsat nogle opsætninger for de enkelte parametre; men hvis man genbruger denne engine skal disse parametre naturligvis ændres, så de passer til den aktuelle applikation:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace DirectXEksempel05 { public partial class Engine : Form { // Global variables for this project private Device device = null; // Rendering device private Matrix view; // Positions the camera in the scene private Matrix projection; // Controls scene perspective public Engine() { InitializeComponent(); // Set the initial size of the form this.ClientSize = new System.Drawing.Size(600, 600); // And its caption this.Text = "DirectX Eksempel 5A - Game Engine"; } private void UpdateModel() { // Her tilføjes opdateringer af modellen } private void Render() { //Clear the backbuffer to a blue color device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, System.Drawing.Color.DarkBlue, 1.0f, 0); device.BeginScene(); //Begin the scene // Her indsættes flere metodekald device.EndScene(); //End the scene device.Present(); } private void RestoreDevice() { device.RenderState.ZBufferEnable = true; // Enable z-buffering device.RenderState.ZBufferWriteEnable = true; device.RenderState.Lighting = true; // Enable lighting device.RenderState.Ambient = Color.Gray; // Set the ambient color // Configure a directional light device.Lights[0].Type = LightType.Directional; device.Lights[0].Direction = new Vector3(10, 3, 0); device.Lights[0].Diffuse = Color.White; device.Lights[0].Enabled = true; // projection transform float aspectRatio = this.Width / this.Height; projection = Matrix.PerspectiveFovLH((float)(Math.PI / 4.0f), 1.0f, 1.0f, 100.0f); } public void InitializeGraphics() { // Device til DirectX Rendering PresentParameters presentParams = new PresentParameters(); presentParams.Windowed = true; presentParams.SwapEffect = SwapEffect.Discard; presentParams.AutoDepthStencilFormat = DepthFormat.D16; presentParams.EnableAutoDepthStencil = true; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams); // Initial values for the view and world transforms view = Matrix.LookAtLH(new Vector3(0, 0, -10), new Vector3(0, 0, 0), new Vector3(0, 1, 0)); device.RenderState.Ambient = Color.White; device.RenderState.CullMode = Cull.None; this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.Keydown); this.MouseDown += new MouseEventHandler(this.Mousedown); RestoreDevice(); } private void Keydown(Object sender, System.Windows.Forms.KeyEventArgs e) { if (e.KeyCode == Keys.Escape) { Application.Exit(); } } private void Mousedown(object sender, System.Windows.Forms.MouseEventArgs e) { // Update the mouse path with the mouse information Point mouseDownLocation = new Point(e.X, e.Y); string eventString = null; switch (e.Button) { case MouseButtons.Left: eventString = "L"; break; case MouseButtons.Right: eventString = "R"; break; case MouseButtons.Middle: eventString = "M"; break; case MouseButtons.XButton1: eventString = "X1"; break; case MouseButtons.XButton2: eventString = "X2"; break; case MouseButtons.None: default: break; } MessageBox.Show(eventString + " " + e.X + " " + e.Y); } static void Main() { using (Engine engine = new Engine()) { engine.InitializeGraphics(); engine.Show(); // While the form is still valid, render and process messages while (engine.Created) { engine.Render(); engine.UpdateModel(); Application.DoEvents(); } } } } }
Koden kan hentes her.
I denne udvidelse af programmet er formålet at vise, hvad den udviklede Game Engine kan bruges til. Der skal i den forbindelse opbygges en model, som skal spille sammen med vores Game Engine. I den første kodestump ser vi, hvordan modellen erklæres og associeres til vores Game Engine. Der er tilføjet nogle metoder og properties til Engine klassen, fordi oplysninger fra Engine klassen skal bruges af modellens metoder - mere herom senere.
// Global variables for this project private Device device = null; // Rendering device private Matrix view; // Positions the camera in the scene private Matrix projection; // Controls scene perspective private Model model; public Matrix View { set { view = value; } get { return view; } } public Matrix Projection { set { projection = value; } get { return projection; } } public Engine() { // Set the initial size of the form this.ClientSize = new System.Drawing.Size(600, 600); // And its caption this.Text = "DirectX Eksempel 5B - Game Engine og Model"; }
public Device GetDevice() { return device; }
Metoden UpdateModel (i den tidligere version af vores Game Engine kaldt UpdateScene) skal bruges, når modellen skal opdateres - metoden kaldes fra event handleren MouseDown. Som det ses, sker der ikke andet, end at vi kalder en tilsvarende metode i model objektet. I realiteten kunne vi godt kalde modellens UpdateModel metode direkte fra MouseDown; men jeg har medtaget dette stoppested for at vise princippet.
private void UpdateModel() { // Her tilføjes opdateringer af modellen // Kaldes fra MouseDown model.UpdateModel(); }
I MouseDown metoden gemmes den aktuelle position for musen i modellens MouseLocation property. Denne position skal bruges til beregning af, hvilket af modellens objekter, der bliver peget på. I andre situationer kunne det være input fra tastaturet, som skulle bruges til manipulation med modellens objekter. Linien this.UpdateModel() kunne lige så godt have været model.UpdateModel(). Udover MouseLocation gemmes også MouseButton i modellen.
private void Mousedown(object sender, System.Windows.Forms.MouseEventArgs e) { model.MouseLocation = new Point(e.X, e.Y); switch (e.Button) { case MouseButtons.Left: model.MouseButton = 'L'; break; case MouseButtons.Right: model.MouseButton = 'R'; break; case MouseButtons.Middle: model.MouseButton = 'M'; break; case MouseButtons.None: default: model.MouseButton = ' '; break; } this.UpdateModel(); }
Den følgende kodestump er fra metoden InitializeGraphics, hvor View matricen bliver defineret. Kameraet stilles i positionen (6, 6, -25) og der fokuseres på punktet (6, 6, 0). Årsagen hertil er, at modellens objekter bliver placeret omkring dette fokuseringspunkt. I slutningen ses oprettelsen af model objektet, som får engine objektet med som parameter - de to objekter, model og engine, er herefter associerede og kan kalde metoder og properties hos hinanden.
this.View = Matrix.LookAtLH(new Vector3(6, 6, -25), new Vector3(6, 6, 0), new Vector3(0, 1, 0)); device.RenderState.Ambient = Color.White; device.RenderState.CullMode = Cull.None; this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.Keydown); this.MouseDown += new MouseEventHandler(this.Mousedown); model = new Model(this); RestoreDevice();
Render metoden indeholder nogle indledende klargøringer til tegning af objekterne: opsætning af view- og projektionsmatricerne. Derefter kaldes modellens Render metode.
device.BeginScene(); //Begin the scene device.Transform.View = this.View; device.Transform.Projection = this.Projection; model.Render(); device.EndScene(); //End the scene
Modellen skal indeholde nogle objekter kaldet Bricks - disse objekter placeres forskellige steder i rummet, hvorefter vi vil prøve at udpege de enkelte objekter med musen. Opbygningen af modellen følger nedenstående klassediagram:
Model objektet indeholder en samling af Brick objekter (i dette første eksempel 3 objekter). Modellen kan aktiveres fra Engine objektet på 2 måder:
Brick klassen arver fra ModelComponent klassen - denne struktur er etableret, fordi jeg regner med, at senere modeller kan drage nytte af en lignende struktur. ModelComponent klassen indeholder attributter og properties, som vil være fælles for mange klasser, der indgår i en model af denne type.
Herunder følger koden til ModelComponent klassen:
using System; using System.Collections.Generic; using System.Text; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D;
namespace DirectXEksempel05 { class ModelComponent { private Vector3 position; private Matrix world; private float pitch; private float yaw; private float roll; protected ModelComponent() { position = new Vector3(0, 0, 0); world = Matrix.Identity; pitch = 0.0F; yaw = 0.0F; roll = 0.0F; } protected ModelComponent(Vector3 aPosition) { position = aPosition; world = Matrix.Identity; pitch = 0.0F; yaw = 0.0F; roll = 0.0F; }
// Properties public Vector3 Position { get { return position; } set { position = value; } }
public Matrix World { get { return world; } set { world = value; } }
public float Pitch { get { return pitch; } set { pitch = value; } }
public float Yaw { get { return yaw; } set { yaw = value; } }
public float Roll { get { return roll; } set { roll = value; } } } }
Herunder følger koden til Brick klassen:
namespace DirectXEksempel05 { class Brick : ModelComponent { private Mesh mesh; private Color color; public Brick(Device device, Vector3 aPosition, Color aColor) : base(aPosition) { mesh = Mesh.Box(device, 1f, 1f, 1f); color = aColor; } public void Render(Device device) { Material meshMaterial = new Material(); meshMaterial.AmbientColor = new ColorValue(color.R, color.G, color.B); meshMaterial.DiffuseColor = new ColorValue(color.R, color.G, color.B); device.Transform.World = Matrix.RotationYawPitchRoll(Yaw, Pitch, Roll) * Matrix.Translation(Position); device.Material = meshMaterial; device.SetTexture(0, null); mesh.DrawSubset(0); } } }
Brick klassen indeholder ikke meget andet end Render metoden - i andre situationer, hvor objekterne f.eks. skal animeres rundt i rummet, kan en UpdateModel metode også komme på tale. Render metoden er opbygget efter samme principper, som vi har set i tidligere eksempler - jeg har medtaget YawPitchRoll rotationen ved opbygningen af World matricen, selvom den ikke bidrager med noget i denne sammenhæng, hvor objekterne ikke skal flytte sig, når de først er placeret.
Model klassen er den klasse, der i dette tilfælde er forudset til at skulle indeholde styringen af spillet - i første omgang er der kun indbygget kode, som kan afgøre, om musen peger på et af de tre Brick objekter - senere skal det udvides med logik, der kan afgøre, om der er lovlige træk, og om spilleren eventuelt har vundet. Herunder følger koden til Model klassen:
namespace DirectXEksempel05 { class Model { private Engine engine; // Associeret Engine private Brick b1; private Brick b2; private Brick b3; private Point mouseLocation = new Point(0, 0); private char mouseButton; public Point MouseLocation { set { mouseLocation = value; } get { return mouseLocation; } } public char MouseButton { set { mouseButton = value; } get { return mouseButton; } } public Model(Engine anEngine) { engine = anEngine; MouseButton = ' '; b1 = new Brick(engine.GetDevice(), new Vector3(0, 0, 0), Color.Aquamarine); b2 = new Brick(engine.GetDevice(), new Vector3(5, 3, 0), Color.Yellow); b3 = new Brick(engine.GetDevice(), new Vector3(9, 8, 0), Color.Red); } public void Render() { b1.Render(engine.GetDevice()); b2.Render(engine.GetDevice()); b3.Render(engine.GetDevice()); } public void UpdateModel() { // Omregn musens placering til world koordinater Vector3 nearVector = new Vector3(MouseLocation.X, MouseLocation.Y, 0); Vector3 farVector = new Vector3(MouseLocation.X, MouseLocation.Y, 1); Vector3 lower; Vector3 upper; // Create ray. nearVector.Unproject(engine.GetDevice().Viewport, engine.Projection, engine.View, Matrix.Translation(b1.Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Unproject(engine.GetDevice().Viewport, engine.Projection, engine.View, Matrix.Translation(b1.Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Subtract(nearVector); // Perform intersection test for the bounding box and ray. lower = b1.Position - new Vector3(0.5f, 0.5f, 0.5f); upper = b1.Position + new Vector3(0.5f, 0.5f, 0.5f); if (Geometry.BoxBoundProbe(lower, upper, nearVector, farVector)) { // Er boksen fundet? MessageBox.Show("Brick 1"); } // Omregn musens placering til world koordinater nearVector = new Vector3(MouseLocation.X, MouseLocation.Y, 0); farVector = new Vector3(MouseLocation.X, MouseLocation.Y, 1); // Create ray. nearVector.Unproject(engine.GetDevice().Viewport, engine.GetDevice().Transform.Projection, engine.GetDevice().Transform.View, Matrix.Translation(b2.Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Unproject(engine.GetDevice().Viewport, engine.GetDevice().Transform.Projection, engine.GetDevice().Transform.View, Matrix.Translation(b2.Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Subtract(nearVector); // Perform intersection test for the bounding box and ray. lower = upper = b2.Position; lower.Subtract(new Vector3(0.5f, 0.5f, 0.5f)); upper.Add(new Vector3(0.5f, 0.5f, 0.5f)); if (Geometry.BoxBoundProbe(lower, upper, nearVector, farVector)) { // Er boksen fundet? MessageBox.Show("Brick 2"); } // Omregn musens placering til world koordinater nearVector = new Vector3(MouseLocation.X, MouseLocation.Y, 0); farVector = new Vector3(MouseLocation.X, MouseLocation.Y, 1); // Create ray. nearVector.Unproject(engine.GetDevice().Viewport, engine.GetDevice().Transform.Projection, engine.GetDevice().Transform.View, Matrix.Translation(b3.Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Unproject(engine.GetDevice().Viewport, engine.GetDevice().Transform.Projection, engine.GetDevice().Transform.View, Matrix.Translation(b3.Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Subtract(nearVector); // Perform intersection test for the bounding box and ray. lower = b3.Position - new Vector3(0.5f, 0.5f, 0.5f); upper = b3.Position + new Vector3(0.5f, 0.5f, 0.5f); if (Geometry.BoxBoundProbe(lower, upper, nearVector, farVector)) { // Er boksen fundet? MessageBox.Show("Brick 3"); } } } }
Den mest interessante kode findes i metoden UpdateModel, som bliver kaldt, når brugeren har trykket på en af musens knapper - hvilken knap undersøges ikke her; men muligheden foreligger, da en kode for den aktiverede knap er tilgængelig i MouseButton propertien.
Her følger en gennemgang af en af de tre næsten ens sektioner i UpdateModel - hver sektion håndterer én bestemt Brick:
Hermed er en vigtig forudsætning opfyldt for at kunne lave forskellige spil, hvor man flytter brikker rundt på et bræt: at man kan udpege en brik eller et felt på brættet.
Snapshot fra en afprøvning af programmet - der er lige blevet trykket på den gule knap:
Koden til dette eksempel kan hentes her.
I denne udvidelse vil jeg ændre modellen, således at der defineres et antal brikker med tilfældige farver anbragt i et to-dimensionelt array. Der skal laves nogle få ændringer i Model klassens kode. Først skal der erklæres flere variabler:
private Engine engine; // Associeret Engine private Brick[,] bricks; private Point mouseLocation = new Point(0, 0); private char mouseButton; private int x; // Tabellens dimensioner private int y;
Variablerne x og y skal indeholde tabellens dimensioner - værdierne tildeles i Model klassens constructor, som ses herunder. Det vigtigste er oprettelsen af det to-dimensionelle array af Brick objekter. Hvert objekt tildeles en af 6 farver, som vælges tilfældigt.
public Model(int antalX, int antalY, Engine anEngine) { engine = anEngine; MouseButton = ' '; x = antalX; y = antalY; Color color = new Color(); // Tabel med Bricks i tilfældige farver bricks = new Brick[x, y]; Random rnd = new Random(); for (int i = 0; i < x; i++) for (int j = 0; j < y; j++) { int c = rnd.Next(6); switch (c) { case 0: color = Color.Red; break; case 1: color = Color.Blue; break; case 2: color = Color.Yellow; break; case 3: color = Color.Green; break; case 4: color = Color.Cyan; break; case 5: color = Color.DarkOrange; break; } bricks[i, j] = new Brick(engine.GetDevice(), new Vector3(i + 1, j + 1, 0), color); } }
Render metoden er ændret, så den gennemløber alle Brick objekter i det to-dimesionelle array:
public void Render() { for (int i = 0; i < x; i++) for (int j = 0; j < y; j++) { bricks[i, j].Render(engine.GetDevice()); } }
UpdateModel metoden er tilsvarende ændret, således at alle Brick objekter i det to-dimensionelle array gennemløbes for at finde ud af, om musen peger på en af dem:
public void UpdateModel() { Vector3 nearVector; Vector3 farVector; Vector3 lower; Vector3 upper; for (int i = 0; i < x; i++) for (int j = 0; j < y; j++) { nearVector = new Vector3(MouseLocation.X, MouseLocation.Y, 0); farVector = new Vector3(MouseLocation.X, MouseLocation.Y, 1); // Create ray. nearVector.Unproject(engine.GetDevice().Viewport, engine.GetDevice().Transform.Projection, engine.GetDevice().Transform.View, Matrix.Translation(bricks[i, j].Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Unproject(engine.GetDevice().Viewport, engine.GetDevice().Transform.Projection, engine.GetDevice().Transform.View, Matrix.Translation(bricks[i, j].Position) * Matrix.Scaling(0.5f, 0.5f, 0.5f)); farVector.Subtract(nearVector); // Perform intersection test for the bounding box and ray. lower = bricks[i, j].Position - new Vector3(0.5f, 0.5f, 0.5f); upper = bricks[i, j].Position + new Vector3(0.5f, 0.5f, 0.5f); if (Geometry.BoxBoundProbe(lower, upper, nearVector, farVector)) { // Er boksen fundet? MessageBox.Show("X: " + i + " Y: " + j); break; } } }
Også denne version af projektet er tilgængelig for download. Herunder ses et billede fra en afprøvning af programmet.
Spilleregler tilføjes - denne udfordring tages op i et andet kapitel, som tilføjes senere.