Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

02.08.2015, 17:44

Fragen zum Design eines Games mit Gameloop

Guten Abend,

Ich möchte gerne verstehen wie ein kleines, einfaches Spiel, basierend auf einer klassischen Gameloop, funktioniert. Dazu hatte ich die Idee ein kleines Projekt zu erstellen, mehr ein Testprojekt als ein Spiel.


Das Projekt

Für die erste Version ist folgendes geplant:
  • 2D
  • Ein viereckiges Spielfeld
  • mehrere Objekte (Kreise) die sich unendlich mit gleichbleibender Geschwindigkeit bewegen
  • Bei Kollision mit der Bande ist Einfallswinkel gleich Ausgangswinkel
  • Bei Kollision der Kreise sollen diese sich voneinander abstoßen
Ich will es nicht Billard oder Airhocky nennen weil fast alles fehlt, aber die Vorstellung von einem Billiard- oder Airhockeytisch von oben trifft es für das Verständnis schon ganz gut.
Für die Kollision der Kreise habe ich noch keine Vorstellung, für die erste Version reicht aber ein extrem vereinfachtes Modell, beispielsweise könnten sich die Kreise einfach in die entgegengesetzte Richtung abstoßen ohne Geschwindigkeit oder Winkel zu berücksichtigen.


Das Programm

Es soll ein sehr einfaches Programm werden, eher programmiersprachenunabhängig und die Grundprinzipien einer einfachen Spieleschleife darstellen. C# mit WinForms und System.Drawing bietet sich wegen meiner allgemeinen Erfahrungen darin an.
Eine Form mit PictureBox als Spielfeld und einer Klasse für die Objekte ist bereits erstellt. Mittels eines Timers werden die Berechnungen vorgenommen und ausgegeben. Der erste Kreis bewegt sich bereits und damit tauchen auch schon Fragen und Probleme auf für die ich euch gerne um Rat fragen möchte.


Sprites oder geometrische Figuren?

Mal abgesehen von der Programmiersprache und der Engine, welche Basis würde sich anbieten für die Objekte und die Map? Ich könnte die Objekte und Map mittels geometrischen Figuren und Koordinaten darstellen und bei jedem Tick prüfen ob sich die Figuren überschneiden.

Die andere Möglichkeit wären Sprites. Hier gibt es normalerweise umfangreiche Funktionen zur Kollisionserkennung oder Bewegung. Sprites hätten den Vorteil beliebige Figuren erstellen zu können.

Geometrische Figuren würden immer eine geometrische Figur als Umriss benötigen und sei es im Extremfall ein beliebiger, geschlossener Pfad. Dafür bin ich bei der Berechnung von der tatsächlichen Ausgabe unabhängig.


Der kleinste Tick

Es kann kein kontinuierlicher Fluss des Ablaufs stattfinden, man kann nur die Zeit (oder den Ablauf) in kleine Scheiben schneiden und die Berechnung für diese Zeitscheibe (nennen wir es Tick) durchführen. Das ist jetzt eines meiner größten Probleme, der Zusammenhang zwischen den Ticks und dem Ablauf.

Theoretisch müssten in einem Tick alle notwendigen Berechnungen stattfinden um ein sich bewegendes Objekt maximal 1 Pixel zu bewegen. Die Maximale Geschwindigkeit für ein sich bewegendes Objekt beträgt also 1 Pixel pro Tick.
Vielleicht ist diese Annahme bereits falsch – wird das in der Praxis so gemacht ? Es ist doch eher so das schnelle Objekte wie Schüsse mehrere Pixel überspringen?
Falls Objekte innerhalb eines Ticks mehrere Pixel bewegt werden können, wie erkennt man dann Kollisionen bei geometrischen Figuren? Theoretisch könnten beide geometrischen Figuren aus einem Pixel bestehen und sich gegenseitig überspringen wenn sie sich in einem Tick mehrere Pixel weit bewegen.
Eine Möglichkeit wäre dann mit Vektoren zu arbeiten und die Kollisionen der geometrischen Figuren anhand ihrer Bewegungsvektoren zu prüfen. Das würde bei beliebigen geometrischen Figuren sehr aufwendig. Ein weiteres Problem wären kurvige Bewegungen.

Das nächste Problem ist eine Massenkollision innerhalb eines Ticks. Wie löst man so etwas? Mit einem Objekt anfangen und dann nacheinander die anderen berechnen wäre der pragmatische Ansatz – der Wirklichkeit entspricht er nicht.

Entspricht eigentlich die FPS der Anzahl der Ticks? Wenn ein Tick die kleinste Einheit ist in der eine Veränderung stattfinden kann, müsste das zutreffen. Ansonsten würde es keinen Sinn machen wenn ich 60 Mal pro Sekunde das Bild neu zeichne aber nur 10 Mal pro Sekunden Veränderungen vornehme?

Das sind nur ein Teil der Fragen, ich hätte nie gedacht das ein so einfaches, scheinbar offensichtliches Programm so viel Überlegungen und Probleme machen kann. Vielleicht sehe ich aber auch den Wald vor lauten Bäumen nicht oder ich habe nur den falschen Ansatz?

Ich hoffe ich konnte mich verständlich ausdrücken, ansonsten bitte nachfragen.

Sacaldur

Community-Fossil

Beiträge: 2 301

Wohnort: Berlin

Beruf: FIAE

  • Private Nachricht senden

2

03.08.2015, 13:16

Du solltest es vermeiden, mit Pixeln zu rechnen. Grundsätzlich.
Für die Kollisionsabfragen heißt das, dass du mit geometrischen Formen (und Polygonen) rechnen solltest, statt mit Sprites.
Für die Erkennung der Kollisionen gibt es leicht unterschiedliche Vorgehensweisen. Man kann "zwischenschritte" einfügen, statt nur die Position der Frames zu verwenden, man kann mit Hilfe von "Raycasts" die zurückgelegten Strecken prüfen etc. Guck dir einfach mal an, was Physikengines in dieser Hinsicht anwenden, um ein paar Beispiele zu bekommen. Für ganz einfache Spiele reicht es, die Positionen zu aktualisieren, auf Überlappungen zu prüfen und die Objekte entsprechend wieder auseinander zu bewegen.

Engiens gehen i. d. R. so vor, dass die Physik mit einer festen Anzahl an Schritten aktualisiert wird (bspw. 50x die Sekunde), während die Darstellung sich an der Bildwiederholfrequenz orientiert bzw. so oft wie möglich durchgeführt wird. Um keine falschen Werte bei der Darstellung zu verwenden, wird weiterhin eine Interpolation auf die Positionswerte anhand der Geschwindigkeiten etc. durchgeführt. Für den Anfang wäre das aber wohl mehr als notwendig.
Spieleentwickler in Berlin? (Thema in diesem Forum)
---
Es ist ja keine Schande etwas falsch zu machen, als Programmierer tu ich das täglich, [...].

3

04.08.2015, 15:29

Danke für deine Infos Sacaldur, das bringt mich schon ein kleines Stück weiter.

Bei der Schleife könnte man die Zeit für 1 FPS vorausberechnen. Wenn die Berechnung länger dauert wird im nächsten Tick die fehlende Zeitspanne addiert, z.B. 100 FPS, der erste Tick beginnt bei 0 und berechnet jetzt bis 0+10. Tatsächlich benötigt die Berechnung 20ms. Das wird im nächsten Durchlauf ausgeglichen und addiert: [10ms der vorhergehenden Berechnung] + [1 FPS = 10ms] = 20ms.

Eine einfache Schleife dazu könnte so aussehen:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class GameLoop
{
    private const double MINSPAN = 1000 / 60;
    private Random rnd = new Random();
    private double testDistance = 0;

    public void Run()
    {
        DateTime baseTime = DateTime.Now;
        DateTime startTime = baseTime;
        DateTime endTime = baseTime;

        for (int i = 0; i < 20; i++)
        {
            endTime = DateTime.Now.AddMilliseconds(MINSPAN);
            this.calculate(endTime.Subtract(startTime).TotalMilliseconds);
            double remainingTime = endTime.Subtract(DateTime.Now).TotalMilliseconds;
            if (remainingTime > 0)
                Thread.Sleep((int)Math.Ceiling(remainingTime));

            startTime = endTime;
        }

        Debug.WriteLine("time elapsed: " + endTime.Subtract(baseTime).TotalMilliseconds.ToString()
            + "; distance passed: " + this.testDistance.ToString());
    }

    private void calculate(double milliseconds)
    {
        Thread.Sleep(rnd.Next((int)(MINSPAN * 2)));
        this.testDistance += milliseconds;
    }
}


Als Test habe ich einfach zurückgelegte Strecke angenommen die pro 1ms eine Einheit zurücklegt. Die Durchläufe sind mindestens 1 FPS lang, können aber länger sein. Der Funktion calculate() die zu berechnende Zeitspanne
übergeben.

Im Test klappt das mit der Strecke und einer angenommenen gleichbleibenden Geschwindigkeit ganz gut. Jetzt müsste die Berechnung des Punktes folgen. Die erste Idee war einfach über den Vektor und die Zeit die nächste Position zu berechnen.

Leider klappt das dann doch nicht so einfach:
Nehmen wir wieder eine zweidimensionales Spielfeld an. Ein Kreis bewegt sich ... oder einfacher noch ein Punkt, mit gleichbleibender Geschwindigkeit. Bei einer Kollision mit dem Rand wird er im gleichen Winkel abgestoßen. Die Dauer der zu berechnenden Zeit kennen wir, im Normalfall die Dauer von 1 FPS ~ 16ms. Bei dem Modell kann die Berechnungsdauer auch höher sein, theoretisch 1000ms oder mehr. Unser Punkt könnte in der Zeitspanne zwei oder noch mehr Banden berühren, ich kann also nicht nur die Endposition berechnen die der Punkt am Ende einnimmt, ich müsste über den Vektor und die zurückgelegt Strecke in der Zeit die möglichen Kollisionen mit Richtungswechseln berechnen.

Könnte man doch vielleicht eine feste Zeitunterteilung bei der Berechnung annehmen? Z.B. gibt meine Schleife die Berechnung für 100ms vor, tatsächlich würde ich eine Zeitspanne von 10ms annehmen und müsste 10 Positionen berechnen und entsprechend die Kollisionen abgleichen. Aber jetzt beisst sich die Katze in den Schwanz, ich müsste sicherstellen das die 10ms auch in Echtzeit berechnet werden können.

Wie könnte denn der weitere Ansatz aussehen? Meine Kenntnisse in Physik sind mangelhaft, vielleicht könnte mir jemand mit einem einfachen Beispiel weiterhelfen?

4

08.08.2015, 03:16

Der Ansatz mit den variablen Frames hat funktioniert aber das Ergebnis war enttäuschend. Eine variable FPS scheint nicht praktikabel zu sein. Das einzige Beispiel um den Schnittpunkt zweier Strecken zu berechnen hab ich bei Stackoverflow gefunden http://stackoverflow.com/a/14143738/292237. Damit kann man zuerst die Strecke bis zum Schnittpunkt berechnen, den Vektor ändern und anschließend die restliche Strecke berechnen. Bereits mit wenigen Objekten ist es sehr langsam und die Rundungsdifferenzen schlagen trotzdem durch.
Also zurück zu den einfacheren Möglichkeiten. In dem neuen Beispiel wird ein fester Vektor von 1:1 benutzt und pro Frame um 1 Pixel verschoben. Bei Kollision mit der Bande wird im gleichen Winkel abgestoßen. Das funktioniert zwar sauber kann aber nicht die Lösung sein.
Wie kann man Strecken mit variablen Richtungen berechnen, z.B einen Vector von 1:3? Ich hoffe das Beispiel macht es deutlicher:

Das kleine Beispiel basiert auf Punkten, die Kreise sind nur Fake. Bei der Bande werden die Punkte abgestoßen, wenn zwei Punkte kollidieren werden beide gelöscht. Die Buttons einfach mehrfach drücken.

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
namespace ThingsSimulation
{
    public partial class Form1 : Form
    {
        private System.Windows.Forms.Button button1 = new System.Windows.Forms.Button();
        private System.Windows.Forms.Button button2 = new System.Windows.Forms.Button();
        private System.Windows.Forms.Button button3 = new System.Windows.Forms.Button();
        private System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
        private List<Thing> things;
        private int r = 5;
        private SolidBrush brush = new SolidBrush(Color.Magenta);
        public Form1()
        {
            this.button1.Location = new System.Drawing.Point(10, 10);
            this.button1.Size = new System.Drawing.Size(60, 26);
            this.button1.Text = "test1";
            this.button1.Click += new System.EventHandler(this.button1_Click);
            this.Controls.Add(this.button1);
            this.button2.Location = new System.Drawing.Point(80, 10);
            this.button2.Size = new System.Drawing.Size(60, 26);
            this.button2.Text = "test2";
            this.button2.Click += new System.EventHandler(this.button2_Click);
            this.Controls.Add(this.button2);
            this.button3.Location = new System.Drawing.Point(150, 10);
            this.button3.Size = new System.Drawing.Size(60, 26);
            this.button3.Text = "reset";
            this.button3.Click += new System.EventHandler(this.button3_Click);
            this.Controls.Add(this.button3);
            this.timer.Interval = 3;
            this.timer.Tick += new System.EventHandler(this.timer_Tick);
            this.timer.Enabled = true;
            this.Size = new Size(800, 500);
            this.DoubleBuffered = true;
            this.things = new List<Thing>();
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            foreach (Thing thing in this.things)
                e.Graphics.FillEllipse(new SolidBrush(thing.Color), thing.X - r, thing.Y - r, r * 2, r * 2);

            e.Graphics.DrawString("Objects: " + things.Count.ToString(), this.Font, Brushes.Black, 10, 40);
        }
        private void timer_Tick(object sender, EventArgs e)
        {
            foreach (Thing t in things)
            {
                t.X += t.VX;
                if (t.X == r || t.X == this.ClientSize.Width - r)
                    t.VX *= -1;

                t.Y += t.VY;
                if (t.Y == r || t.Y == this.ClientSize.Height - r)
                    t.VY *= -1;
            }

            for (int i = 0; i < this.things.Count; i++)
            {
                Thing ti = this.things[i];
                for (int j = i + 1; j < this.things.Count; j++)
                {
                    Thing tj = this.things[j];
                    if (ti.X == tj.X && ti.Y == tj.Y)
                        ti.Abandoned = tj.Abandoned = true;
                }
            }

            List<Thing> thingsTemp = new List<Thing>();
            foreach (Thing t in this.things)
                if (!t.Abandoned)
                    thingsTemp.Add(t);

            this.things = thingsTemp;

            this.Invalidate();
            Application.DoEvents();
        }
        private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 10; i < 310; i += 10)
                this.things.Add(new Thing(i, 310 - i, 1, 1, Color.Blue));

            for (int i = 10; i < 310; i += 10)
                this.things.Add(new Thing(this.ClientSize.Width - i - r, 310 - i - r, -1, 1, Color.Red));
        }
        private void button2_Click(object sender, EventArgs e)
        {
            for (int i = 10; i < 310; i += 10)
                this.things.Add(new Thing(i, 310 - i, 1, 1, Color.Magenta));

            for (int i = 10; i < 310; i += 10)
                this.things.Add(new Thing(this.ClientSize.Width - i, 310 - i, -1, 1, Color.CadetBlue));
        }
        private void button3_Click(object sender, EventArgs e)
        {
            this.things = new List<Thing>();
        }
    }

    public class Thing
    {
        public int X;
        public int Y;
        public int VX;
        public int VY;
        public bool Abandoned = false;
        public Color Color;

        public Thing(int x, int y, int vx, int vy, Color color)
        {
            this.X = x;
            this.Y = y;
            this.VX = vx;
            this.VY = vy;
            this.Color = color;
        }
    }
}

5

10.08.2015, 18:37

Anbei die letzte Version des Testprogramms. Ich will es nicht Spiel nennen aber ein wenig rumballern kann man. Vor allem interessant was heute mit einem schnellen Rechner und einfachsten Grafikfunktionen machbar ist. Der Code ist keinesfalls optimiert ich hoffe das ist kein Problem, deswegen will ich es auch nicht auf der Projektseite vorstellen. Es löst leider das Problem mit den variablen Zeitintervallen nicht und basiert auf festen Zeitintervallen. Aber vielleicht enthält es doch für dein einen oder anderen etwas Brauchbares.

Steuerung und Menü wird beim Start erkärt, viel Spaß!

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
namespace ThingsSimulation
{
    public class Form1 : Form
    {
        private string help =
            "\t[ESC]\tquit\t" + Environment.NewLine +
            "\t[SPACE]\trestart\t" + Environment.NewLine +
            Environment.NewLine +
            "\t[F1]\tpause\\help\t" + Environment.NewLine +
            "\t[F2]\tshow things life\t" + Environment.NewLine +
            "\t[F3]\tshow bullet life\t" + Environment.NewLine +
            "\t[F4]\tuse random colors\t" + Environment.NewLine +
            Environment.NewLine +
            "\t[F5]\t" + GameMode.Single.ToString() + "\t" + Environment.NewLine +
            "\t[F6]\t" + GameMode.Lines.ToString() + "\t" + Environment.NewLine +
            "\t[F7]\t" + GameMode.Random.ToString() + "\t" + Environment.NewLine +
            "\t[F8]\t" + GameMode.Overdose.ToString() + "\t" + Environment.NewLine +
            Environment.NewLine +
            "\t[LEFT]\tmove left\t" + Environment.NewLine +
            "\tor [S]" + Environment.NewLine + Environment.NewLine +
            "\t[RIGHT]\tmove right\t" + Environment.NewLine +
            "\tor [F]" + Environment.NewLine + Environment.NewLine +
            "\t[UP]\tshoot (" + BULLET_LIFE + " energie)\t" + Environment.NewLine +
            "\tor [E]" + Environment.NewLine + Environment.NewLine +
            "\t[DOWN]\tshield (" + SHIELD_LIFE + " energie)\t" + Environment.NewLine +
            "\tor [D]";

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern short GetKeyState(int keyCode);

        private const double FPS = 1000 / 60;
        private const int BULLET_SPEED = 6;
        private const int BULLET_R = 5;
        private const int BULLET_LIFE = 5;
        private const int BULLET_TICK_CD = 3;
        private const int Bullet_COLOR_RANGE = 60;
        private Color bulletCollor { get { return Color.FromArgb(130, 130, 0); } }

        private const int SHIELD_SPEED = 3;
        private const int SHIELD_R = 45;
        private const int SHIELD_LIFE = 100;
        private const int SHIELD_TICK_CD = 60;
        private const int SHIELD_START_DELAY = 60;

        private const int PLAYER_SPEED = 6;
        private const int PLAYER_R = 15;
        private const int PLAYER_LIFE = 1000;
        private const int PLAYER_ENERGIE = 1000;
        private const int PLAYER_ENERGIE_TICK_CD = 1;
        private const int PLAYER_COLOR_RANGE = 200;
        private Color playerColor { get { return Color.FromArgb(0, 50, 255); } }

        private const int THING_R = 10;
        private const int THING_LIFE = 10;
        private Color thingColor { get { return Color.FromArgb(0, 140, 0); } }
        private const int THING_COLOR_RANGE = 80;

        private bool quit = false;
        private double infoFPS = 0;
        private int score = 0;
        private bool thingInfo = false;
        private bool bulletInfo = false;
        private bool thingsRndColor = false;
        private Random rnd;

        private ulong tickCount = 0;
        private ulong lastBulletTick = 0;
        private ulong lastShieldTick = 0;
        private ulong lastEnergieTick = 0;

        private Player player;
        private List<Thing> things;
        private List<Bullet> bullets;

        private enum GameMode { Single, Lines, Random, Overdose };
        private GameMode gameMode = GameMode.Single;

        public Form1()
        {
            this.StartPosition = FormStartPosition.CenterScreen;
            this.Text = "Things Simulation";
            this.BackColor = Color.Black;
            this.Shown += new System.EventHandler(this.Form1_Shown);
            this.Size = new Size((int)(Screen.PrimaryScreen.Bounds.Width * 0.4), (int)(Screen.PrimaryScreen.Bounds.Height * 0.4));
            this.DoubleBuffered = true;
            this.Font = new System.Drawing.Font("Consolas", 10F);
            this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
            this.Resize += new System.EventHandler(this.Form1_Resize);
        }
        private void InitializeComponent()
        {
            this.SuspendLayout();
            // 
            // Form1
            // 
            this.Name = "Form1";
            this.ResumeLayout(false);
        }
        private void Form1_Shown(object sender, EventArgs e)
        {
            this.restart();
            this.showHelp();
            this.gameLoop();
        }
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            this.quit = true;
        }
        private void Form1_Resize(object sender, EventArgs e)
        {
            this.restart();
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if (this.WindowState == FormWindowState.Minimized)
                return;

            // things
            foreach (Thing t in this.things)
                t.Paint(e.Graphics);

            // player
            this.player.Paint(e.Graphics);

            // bullets
            foreach (Bullet b in this.bullets)
                b.Paint(e.Graphics);

            // things info
            if (this.thingInfo)
                foreach (Thing t in this.things)
                    t.PaintInfo(e.Graphics);

            // player info
            if (this.thingInfo)
                this.player.PaintInfo(e.Graphics);

            // bullet info
            if (this.bulletInfo)
                foreach (Bullet b in this.bullets) b.PaintInfo(e.Graphics);

            // game info
            e.Graphics.DrawString(
                "    fps: " + this.infoFPS + "ms" + Environment.NewLine +
                " things: " + things.Count.ToString() + Environment.NewLine +
                "bullets: " + bullets.Count.ToString() + Environment.NewLine +
                "   life: " + player.Life.ToString() + Environment.NewLine +
                "energie: " + player.Energie.ToString() + Environment.NewLine +
                "  score: " + score.ToString(), this.Font, Brushes.LightGreen, 4, 4);
        }
        private void restart()
        {
            this.rnd = new Random();
            this.score = 0;
            this.tickCount = this.lastShieldTick = this.lastBulletTick = 0;
            this.player = new Player((int)(this.ClientSize.Width / 2), (int)(this.ClientSize.Height - PLAYER_R),
                PLAYER_SPEED, 0, PLAYER_R, PLAYER_LIFE, PLAYER_ENERGIE, this.playerColor, PLAYER_COLOR_RANGE);
            this.things = new List<Thing>();
            this.bullets = new List<Bullet>();

            if (this.gameMode == GameMode.Lines) this.gameModeLines();
            else if (this.gameMode == GameMode.Random) this.gameModeRandom();
            else if (this.gameMode == GameMode.Overdose) this.gameModeOverdose();
            else this.gameModeSolo();
        }
        private void gameModeSolo()
        {
            this.things.Add(new Thing(200, 200, 1, -1, 35, 35, this.getThingColor(), THING_COLOR_RANGE));
        }
        private void gameModeLines()
        {
            int a = 30; int j = 20;
            int w = this.ClientSize.Width;
            for (int i = 0; i < j; i++)
            {
                this.things.Add(new Thing(a + i * 8, a + i * 8, 1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(a + i * 10, a + i * 5, 2, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(a + i * 8, (j * 8 + a) - i * 8, 1, -1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(a + i * 10, (j * 8 + a) - i * 5, 2, -1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));

                this.things.Add(new Thing(w - a - i * 8, a + i * 8, -1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(w - a - i * 10, a + i * 5, -2, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(w - a - i * 8, (j * 8 + a) - i * 8, -1, -1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(w - a - i * 10, (j * 8 + a) - i * 5, -2, -1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
            }

            int b = 30; a = 40; j = 10;
            for (int i = 0; i < j; i++)
            {
                this.things.Add(new Thing(a + i * 8, (j * 8) + b - i * 8, -1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(a + i * 8, (j * 8) + b + i * 8, -1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(a + (j * 8) + i * 8 - 8, 8 + b + i * 8, -1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing((j * 8) + a + i * 8 - 8, (j * 8 * 2) + b - 8 - i * 8, -1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));

                this.things.Add(new Thing(w - a - j * 8 - i * 8, 8 + b + i * 8, 1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(w - a - j * 8 - i * 8, j * 8 * 2 + b - 8 - i * 8, 1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(w - a - i * 8 - 8, j * 8 + b - i * 8, 1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
                this.things.Add(new Thing(w - a - i * 8 - 8, j * 8 + b + i * 8, 1, 1, THING_R, THING_LIFE, this.getThingColor(), THING_COLOR_RANGE));
            }
        }
        private void gameModeRandom()
        {
            int x1 = 100; int y1 = 100;
            int x2 = this.ClientSize.Width - 100; int y2 = this.ClientSize.Height - 200;
            int w = x2 - x1; int h = y2 - y1;

            for (int i = 0; i < 30; i++)
            {
                int r = rnd.Next(10, 60); int x = rnd.Next(0, 2) == 1 ? -1 : 1; int y = rnd.Next(0, 2) == 1 ? -1 : 1;
                this.things.Add(new Thing(x1 + rnd.Next(w), y1 + rnd.Next(h), x * rnd.Next(1, 5),
                    y * rnd.Next(1, 3), r, r, this.getThingColor(), THING_COLOR_RANGE));
            }
        }
        private void gameModeOverdose()
        {
            int x1 = 100; int y1 = 100;
            int x2 = this.ClientSize.Width - 100; int y2 = this.ClientSize.Height - 200;
            int w = x2 - x1; int h = y2 - y1;

            for (int i = 0; i < 1000; i++)
            {
                int r = rnd.Next(10, 30); int x = rnd.Next(0, 2) == 1 ? -1 : 1; int y = rnd.Next(0, 2) == 1 ? -1 : 1;
                this.things.Add(new Thing(x1 + rnd.Next(w), y1 + rnd.Next(h), x * rnd.Next(0, 3),
                    y * rnd.Next(0, 3), r, r, this.getThingColor(), 100));
            }
        }
        private void gameLoop()
        {
            DateTime startTime = DateTime.Now;
            DateTime endTime = startTime;

            while (!this.quit)
            {
                endTime = DateTime.Now.AddMilliseconds(FPS);
                this.infoFPS = endTime.Subtract(startTime).TotalMilliseconds;
                this.calculate();
                this.Invalidate();
                Application.DoEvents();
                double remainingTime = endTime.Subtract(DateTime.Now).TotalMilliseconds;
                if (remainingTime > 0) Thread.Sleep((int)Math.Ceiling(remainingTime));
                this.tickCount++;
                startTime = endTime;
            }
            this.Close();
        }
        private void calculate()
        {
            // input

            if ((GetKeyState(27) & 0x8000) == 0x8000) { this.quit = true; return; } // esc
            if ((GetKeyState(32) & 0x8000) == 0x8000) { Thread.Sleep(200); this.restart(); return; }   // space

            if ((GetKeyState(112) & 0x8000) == 0x8000) { this.showHelp(); } // F1
            if ((GetKeyState(113) & 0x8000) == 0x8000) { Thread.Sleep(200); this.thingInfo = !this.thingInfo; }     // F2
            if ((GetKeyState(114) & 0x8000) == 0x8000) { Thread.Sleep(200); this.bulletInfo = !this.bulletInfo; }   // F3
            if ((GetKeyState(115) & 0x8000) == 0x8000) { Thread.Sleep(200); this.thingsRndColor = !this.thingsRndColor; this.restart(); return; } // F4

            if ((GetKeyState(116) & 0x8000) == 0x8000) { Thread.Sleep(200); this.gameMode = GameMode.Single; this.restart(); }      // F5
            if ((GetKeyState(117) & 0x8000) == 0x8000) { Thread.Sleep(200); this.gameMode = GameMode.Lines; this.restart(); }       // F6
            if ((GetKeyState(118) & 0x8000) == 0x8000) { Thread.Sleep(200); this.gameMode = GameMode.Random; this.restart(); }      // F7
            if ((GetKeyState(119) & 0x8000) == 0x8000) { Thread.Sleep(200); this.gameMode = GameMode.Overdose; this.restart(); }    // F8

            bool keyS = (GetKeyState(83) & 0x8000) == 0x8000;
            bool keyE = (GetKeyState(69) & 0x8000) == 0x8000;
            bool keyF = (GetKeyState(70) & 0x8000) == 0x8000;
            bool keyD = (GetKeyState(68) & 0x8000) == 0x8000;
            bool keyLeft = (GetKeyState(37) & 0x8000) == 0x8000;
            bool keyUp = (GetKeyState(38) & 0x8000) == 0x8000;
            bool keyRight = (GetKeyState(39) & 0x8000) == 0x8000;
            bool keyDown = (GetKeyState(40) & 0x8000) == 0x8000;

            // energie

            if (this.tickCount - this.lastEnergieTick > PLAYER_ENERGIE_TICK_CD)
                if (this.player.Energie < PLAYER_ENERGIE)
                {
                    this.player.Energie++;
                    this.lastEnergieTick = this.tickCount;
                }

            // move player

            if (keyS || keyLeft) { player.VX = -Math.Abs(player.VX); player.X += player.VX; }
            if (keyF || keyRight) { player.VX = Math.Abs(player.VX); player.X += player.VX; }

            if (player.X <= player.R) { player.X = player.R; player.VX = Math.Abs(player.VX); }
            if (player.X >= this.ClientSize.Width - player.R) { player.X = this.ClientSize.Width - player.R; player.VX = -Math.Abs(player.VX); }

            // move bullets

            foreach (Bullet b in this.bullets)
                if (b is Shield)
                {
                    if (this.tickCount - ((Shield)b).StartTick > SHIELD_START_DELAY)
                        b.Y += b.VY;
                }
                else
                    b.Y += b.VY;

            // add bullet

            if (keyE || keyUp)
                if (this.player.Energie > BULLET_LIFE)
                    if (this.tickCount - this.lastBulletTick > BULLET_TICK_CD)
                    {
                        bullets.Add(new Bullet(player.X, player.Y1 - BULLET_R, 0, -BULLET_SPEED, BULLET_R, BULLET_LIFE, bulletCollor, Bullet_COLOR_RANGE));
                        this.lastBulletTick = this.tickCount;
                        this.player.Energie -= BULLET_LIFE;
                    }

            // add shield

            if (keyD || keyDown)
                if (this.player.Energie > SHIELD_LIFE)
                    if (this.tickCount - this.lastShieldTick > SHIELD_TICK_CD)
                    {
                        bullets.Add(new Shield(player.X, player.Y, 0, -SHIELD_SPEED, SHIELD_R, SHIELD_LIFE, bulletCollor, Bullet_COLOR_RANGE, this.tickCount));
                        this.lastShieldTick = this.tickCount;
                        this.player.Energie -= SHIELD_LIFE;
                    }

            // move things

            foreach (Thing t in things)
            {
                t.X += t.VX;
                if (t.X <= t.R)
                {
                    t.VX *= -1;
                    t.X = t.R;
                }

                if (t.X >= this.ClientSize.Width - t.R)
                {
                    t.VX *= -1;
                    t.X = this.ClientSize.Width - t.R;
                }

                t.Y += t.VY;
                if (t.Y <= t.R)
                {
                    t.VY *= -1;
                    t.Y = t.R;
                }

                if (t.Y >= this.ClientSize.Height - t.R)
                {
                    t.VY *= -1;
                    t.Y = this.ClientSize.Height - t.R;
                }
            }

            // collision thing <-> player

            foreach (Thing t in things)
                if (t.Life > 0)
                    if (this.player.IntersectRectangle(t))
                    { player.Life -= t.Life; t.Life = 0; }

            // collision bullet <-> top of map

            foreach (Bullet b in this.bullets)
                if (b.Life > 0)
                    if (b.Y + b.R <= b.R)
                        b.Life = 0;

            // collision thing  <-> bullet

            foreach (Thing t in things)
                if (t.Life > 0)
                    foreach (Bullet b in this.bullets)
                        if (b.Life > 0)
                            if (b.IntersectRectangle(t))
                            {
                                b.Life--;
                                t.Life--;
                                score++;
                            }

            // remove things

            List<Thing> thingsTemp = new List<Thing>();
            foreach (Thing t in this.things)
                if (t.Life > 0)
                    thingsTemp.Add(t);

            this.things = thingsTemp;

            // remove bullets

            List<Bullet> bulletsTemp = new List<Bullet>();
            foreach (Bullet b in this.bullets)
                if (b.Life > 0)
                    bulletsTemp.Add(b);

            // player dead / finisch

            if (this.player.Life < 1 || this.things.Count == 0)
            {
                this.Invalidate();

                string s = string.Empty;
                if (this.player.Life < 1)
                {
                    s = "\tyou are dead.\t" + Environment.NewLine + Environment.NewLine;
                    this.player.Life = PLAYER_LIFE;
                }

                s += "\tplayed ticks: " + this.tickCount.ToString() + '\n' + Environment.NewLine;
                s += "\tscore: " + this.score.ToString() + '\n';

                MessageBox.Show(s, "Things Simulation", MessageBoxButtons.OK);
                if (this.things.Count == 0)
                    restart();
            }

            this.bullets = bulletsTemp;
        }
        private void showHelp()
        {
            MessageBox.Show(this.help, "help", MessageBoxButtons.OK);
            Thread.Sleep(200);
        }
        private Color getThingColor()
        {
            if (this.thingsRndColor)
                return Color.FromArgb(rnd.Next(50, 200), rnd.Next(50, 200), rnd.Next(50, 200));

            return thingColor;
        }
    }
    public class Thing
    {
        protected const int COLOR_OFFSET = 20;
        private const int MIN_R = 4;
        public int X;
        public int Y;
        public int VX;
        public int VY;
        public int R;
        protected int life;
        protected double lifeOnePercent;
        protected double radiusOnePercent;
        protected Color color;
        protected byte colorRange;
        protected Font font = new System.Drawing.Font("Consolas", 9F);

        public Thing(int x, int y, int vx, int vy, int r, int life, Color color, byte colorRange)
        {
            this.X = x;
            this.Y = y;
            this.VX = vx;
            this.VY = vy;
            this.R = r;
            this.radiusOnePercent = (r - MIN_R) * 0.01;
            this.lifeOnePercent = life * 0.01;
            this.Life = life;
            this.color = color;
            this.colorRange = colorRange;
        }
        public int Life
        {
            get { return this.life; }
            set
            {
                int i = (int)((value / this.lifeOnePercent) * this.radiusOnePercent) + MIN_R;

                if (i < MIN_R)
                    this.R = MIN_R;
                else
                    this.R = i;

                this.life = value;
            }
        }
        public int X1 { get { return X - R; } }
        public int Y1 { get { return Y - R; } }
        public int X2 { get { return X + R; } }
        public int Y2 { get { return Y + R; } }
        public int W { get { return R + R; } }
        public int H { get { return R + R; } }
        public bool IntersectRectangle(Thing t)
        {
            // http://www.back-side.net/codingrects.html

            int xl = X1;
            int yo = Y1;
            int xr = t.X1 + t.W;
            int yu = t.Y1 + t.H;

            if (t.X1 < X1)
                xl = t.X1;
            if (t.Y1 < Y1)
                yo = t.Y1;
            if (X1 + W > t.X1 + t.W)
                xr = X1 + W;
            if (Y1 + H > t.Y1 + t.H)
                yu = Y1 + H;

            return (W + t.W > xr - xl & H + t.H > yu - yo);
        }
        public virtual void Paint(Graphics g)
        {
            g.FillEllipse(new SolidBrush(calculateColor(0)), X1, Y1, W, H);
            g.DrawEllipse(new Pen(calculateColor(COLOR_OFFSET)), X1, Y1, W, H);
        }
        public virtual void PaintInfo(Graphics g)
        {
            g.DrawString(this.Life.ToString(), this.font, Brushes.White, X, Y);
        }
        protected virtual Color calculateColor(int offset)
        {
            byte factor = (byte)(this.colorRange - ((this.Life / this.lifeOnePercent) * this.colorRange * 0.01) + offset);
            int i = this.color.R + factor;
            int r = i > byte.MaxValue ? byte.MaxValue : (byte)i;
            i = this.color.G + factor;
            int g = i > byte.MaxValue ? byte.MaxValue : (byte)i;
            i = this.color.B + factor;
            int b = i > byte.MaxValue ? byte.MaxValue : (byte)i;
            return Color.FromArgb(r, g, b);
        }
    }
    public class Bullet : Thing
    {
        public Bullet(int x, int y, int vx, int vy, int r, int life, Color color, byte colorRange) : base(x, y, vx, vy, r, life, color, colorRange) { }
        public virtual void Paint(Graphics g)
        {
            g.DrawEllipse(new Pen(calculateColor(0)), X1, Y1, W, H);
        }
    }
    public class Shield : Bullet
    {
        public ulong StartTick = 0;
        public Shield(int x, int y, int vx, int vy, int r, int life, Color color, byte colorRange, ulong startTick)
            : base(x, y, vx, vy, r, life, color, colorRange)
        {
            this.StartTick = startTick;
        }
    }
    public class Player : Thing
    {
        private int maxR;
        public int Energie;
        public Player(int x, int y, int vx, int vy, int r, int life, int energie, Color color, byte colorRange)
            : base(x, y, vx, vy, r, life, color, colorRange)
        {
            this.maxR = r;
            this.Energie = energie;
        }
        public override void Paint(Graphics g)
        {
            g.FillEllipse(new SolidBrush(calculateColor(0)), X - this.maxR, Y - this.maxR, this.maxR * 2, this.maxR * 2);
            g.DrawEllipse(new Pen(calculateColor(COLOR_OFFSET)), X - this.maxR, Y - this.maxR, this.maxR * 2, this.maxR * 2);
        }
    }
}

6

14.08.2015, 09:47

Ist UpdateWorld bei dir das Zeichnen oder die Berechnung der Spielwelt?

Mir gings es um variable Dauer bei der Berechnung - im Idealfall mit einer genauen Zeitdauer. Die Berechnungsroutine bekommte eine Zeitspanne übergeben. Wenn sich z.B. ein Objekt auf der X-Achse bewegt mit 1 Pixel pro Millisekunde würde folgendes möglich sein:
- Zeitspanne 10ms --> das Objekt wird 10 Pixel auf der X-Achse bewegt.
- Zeitspanne 12ms --> das Object wird 12 Pixel auf der X-Achse bewegt.

Das funktioniert noch soweit wenn ich einfach die zurückgelegte Strecke innerhalb der Zeitspanne berechne. Aber angenommen das Objekt stößt auf ein Hindernis und prallt dort ab? Nehmen wir 30ms Zeitspanne an, würde das Objekt 30 Pixel zurücklegen. Es trifft aber nach 3 Pixel auf das Hindernis. Ich müsste berechnen ob eine Kollision stattfindet, dann den genauen Schnittpunkt, dann die noch fehlende Strecke. Das ist bei kleinen Zeitspannen sicher kein Problem, bei größeren können mehrere Kollision in der Zeitspanne auftreten. Kommen Kollision sich bewegender Objekte hinzu funktioniert der Ansatz gar nicht mehr, im negativsten Fall "verpassen" sich zwei Objekte die eigentlich kollidieren müssten.

Nach dem Wikiartikel hier in dem Forum ist es wohl unmöglich variable Zeitspannen zu berechnen. Es wird immer mit einer festen Berechnungsdauer gearbeitet. Innerhalb eines Schleifendurchlaufs können die Berechnungen mehrmals aufgerufen werden. Das große Problem bleibt aber immer noch wenn der Berechnungsschritt länger als ein Schleifendurchlauf dauert. Hier gibt es wohl Methoden um das zu erkennen und die Mindestdauer der Schleifendurchläufe anzupassen.

Eigentlich möchte ich eine Gameloop die echtzeitfähig ist und den Anspruch einer Physik-Engine genügen würde - ob man dann wirklich physikalische Gesetze simuliert oder nur simple Bewegungen oder Kollision ist egal. Bis jetzt habe ich dazu noch nichts gefunden (die beste Information war immer noch das Wiki hier), vielleicht ist die Vorstellung auch Blödsinn und in der Praxis nicht praktikabel.

TGGC

1x Rätselkönig

Beiträge: 1 799

Beruf: Software Entwickler

  • Private Nachricht senden

7

15.08.2015, 01:09

Wo soll denn das Problem Sein? Wenn du nach 3 von 30 Millisekunden eine Kollision feststellst, dann wird die einfach berechnet und dann nach der Kollision ein weiterer Schritt mit 27 ms berechnet. Gibts da auch wieder eine Kollision wird eben weitergereichnet bis in der verbleibenden Zeit keine Kollision mehr erkannt wird.

Werbeanzeige