1. Code
  2. Game Development

Erstellen isometrischer Welten: Eine Einführung für Spieleentwickler, Fortsetzung

In diesem Tutorial bauen wir auf der ursprünglichen Einführung in die Erstellung isometrischer Welten auf und lernen die Implementierung von Pickups, Trigger-Kacheln, Level-Swapping, Pfadfindung und -verfolgung, Level-Scrolling, isometrische Höhe und isometrische Projektile kennen.
Scroll to top

German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)

In diesem Tutorial bauen wir auf der ursprünglichen Einführung in die Erstellung isometrischer Welten auf und lernen die Implementierung von Pickups, Trigger-Kacheln, Level-Swapping, Pfadfindung und -verfolgung, Level-Scrolling, isometrische Höhe und isometrische Projektile kennen.


1. Pickups

Pickups sind Gegenstände, die innerhalb des Levels gesammelt werden können, normalerweise indem man einfach darüber geht - zum Beispiel Münzen, Edelsteine, Bargeld und Munition.

Pickup-Daten können auch direkt in unsere Level-Daten aufgenommen werden:

1
[[1,1,1,1,1,1],
2
 [1,0,0,0,0,1],
3
 [1,0,8,0,0,1],
4
 [1,0,0,8,0,1],
5
 [1,0,0,0,0,1],
6
 [1,1,1,1,1,1]]

In diesen Leveldaten verwenden wir 8 zur Bezeichnung eines Pickups (1 und 0 stehen wie zuvor für Wände bzw. begehbare Kacheln).

Es ist wichtig zu verstehen, dass 8 eigentlich zwei Kacheln bezeichnet, nicht nur eine: Es bedeutet, dass wir zuerst eine begehbare Graskachel platzieren und dann einen Pickup darauf platzieren müssen. Das bedeutet, dass sich jeder Pickup immer auf einem Grasplättchen befindet. Wenn es auf einem begehbaren Ziegelstein liegen soll, brauchen wir einen weiteren Ziegel, der mit einer anderen Zahl gekennzeichnet ist, z. B. 9, die für "Pickup auf Ziegelstein" steht.

Typische isometrische Kunst hat mehrere begehbare Kacheln - nehmen wir an, wir haben 30. Der obige Ansatz bedeutet, dass wir, wenn wir N Tonabnehmer haben, zusätzlich zu den 30 Originalkacheln (N * 30) Kacheln benötigen, da jede Kachel eine Version haben muss mit Pickups und einer ohne. Dies ist nicht sehr effizient; Stattdessen sollten wir versuchen, diese Kombinationen dynamisch zu erstellen.

Dazu können wir ein anderes Array allein mit den Pickupdaten verwenden und dieses verwenden, um Aufnahmekacheln über den Ebenenlayoutdaten zu platzieren:

1
// Level layout data

2
[[1,1,1,1,1,1],
3
 [1,0,0,0,0,1],
4
 [1,0,0,0,0,1],
5
 [1,0,0,0,0,1],
6
 [1,0,0,0,0,1],
7
 [1,1,1,1,1,1]]

...Plus:

1
// Pickup layout data

2
[[0,0,0,0,0,0],
3
 [0,0,0,0,0,0],
4
 [0,0,8,0,0,0],
5
 [0,0,0,8,0,0],
6
 [0,0,0,0,0,0],
7
 [0,0,0,0,0,0]]

...ergibt:

pickups in levelpickups in levelpickups in level

Dieser Ansatz stellt sicher, dass wir zusätzlich zu N Pickup-Kacheln nur die 30 Originalkacheln benötigen, da wir jede Kombination erstellen können, indem wir beide Kunstwerke beim Rendern des Levels mischen.

Abholung von Pickups

Das Erkennen von Pickups erfolgt auf die gleiche Weise wie das Erkennen von Kollisionskacheln, jedoch nach dem Bewegen des Charakters.

1
tile coordinate= getTileCoordinates (isoTo2D (Iso point) , tile height);
2
if(isPickup(tile coordinate)) {
3
  pickupItem(tile coordinate);
4
}

In der Funktion isPickup(tile coordinate) prüfen wir, ob der Wert des Pickup-Datenarrays an der angegebenen Koordinate eine Pickup-Kachel ist oder nicht. Die Zahl im Pickup-Array an dieser Kachelkoordinate gibt den Pickup-Typ an.

Wir prüfen auf Kollisionen, bevor wir den Charakter bewegen, prüfen aber danach auf Pickups, denn bei Kollisionen sollte der Charakter den Platz nicht besetzen, wenn er bereits von der Kollisionsplättchen besetzt ist, aber bei Pickups kann sich der Charakter frei darüber bewegen .

Zu beachten ist auch, dass sich die Kollisionsdaten normalerweise nie ändern, aber die Pickupldaten ändern sich, wenn wir einen Artikel abholen. (Dies beinhaltet normalerweise nur das Ändern des Werts im Pickup-Array von beispielsweise 8 auf 0.)

Dies führt zu einem Problem: Was passiert, wenn wir das Level neu starten müssen und damit alle Pickups auf ihre ursprünglichen Positionen zurücksetzen? Wir haben nicht die Informationen dazu, da das Pickup-Array geändert wurde, wenn der Spieler Gegenstände aufhob. Die Lösung besteht darin, während des Spiels ein doppeltes Array für Tonabnehmer zu verwenden und das ursprüngliche Pickup-Array intakt zu halten. Beispielsweise verwenden wir pickupsArray[] und pickupsLive[], klonen das letztere zu Beginn des Levels von dem ersteren und nur während des Spiels pickupsLive[] ändern.

Klicken Sie, um den SWF-Fokus festzulegen, und verwenden Sie dann die Pfeiltasten. Klicken Sie hier für die Vollversion.

Sie sollten beachten, dass wir immer dann nach Pickups suchen, wenn sich der Charakter auf dieser Kachel befindet. Dies kann innerhalb einer Sekunde mehrmals vorkommen (wir prüfen nur, wann sich der Benutzer bewegt, aber wir können innerhalb einer Kachel herumlaufen), aber die obige Logik schlägt nicht fehl. Da wir die Daten des Pickup-Arrays beim ersten Erkennen eines Pickups auf 0 setzen, geben alle nachfolgenden isPickup(tile)-Prüfungen für diese Tile false zurück.


2. Kacheln auslösen

Wie der Name schon sagt, verursachen Trigger-Kacheln etwas, wenn der Spieler darauf tritt oder eine Taste drückt, während er darauf tritt. Sie können den Spieler an einen anderen Ort teleportieren, ein Tor öffnen oder einen Feind hervorbringen, um nur einige Beispiele zu nennen. Pickups sind gewissermaßen nur eine Sonderform von Triggerplättchen: Wenn der Spieler auf ein Plättchen mit einer Münze tritt, verschwindet die Münze und sein Münzzähler erhöht sich.

Schauen wir uns an, wie wir eine Tür implementieren könnten, die den Spieler auf ein anderes Level bringt. Das Plättchen neben der Tür ist ein Triggerplättchen; Wenn der Spieler die Leertaste drückt, geht er zum nächsten Level.

hero at doorhero at doorhero at door

Um die Level zu ändern, müssen wir nur das aktuelle Level-Datenarray mit dem des neuen Levels austauschen und die neue Kachelposition und -richtung für den Heldencharakter festlegen.

Angenommen, es gibt zwei Ebenen mit Türen, die den Durchgang ermöglichen. Da das Bodenplättchen neben der Tür in beiden Levels das Auslöserplättchen ist, können wir dies als neue Position für den Charakter verwenden, wenn er im Level erscheint.

Die Implementierungslogik ist hier dieselbe wie bei Pickups, und wieder verwenden wir ein Array, um Triggerwerte zu speichern. Dies ist ineffizient und Sie sollten zu diesem Zweck andere Datenstrukturen in Betracht ziehen, aber lassen Sie uns dies für das Tutorial einfach halten. Lassen Sie die neuen Level-Arrays wie folgt aussehen (7 bezeichnet eine Tür):

1
// Level 1

2
[[1,1,1,1,1,1],
3
 [1,0,0,0,0,1],
4
 [1,0,0,0,0,7],
5
 [1,0,0,0,0,1],
6
 [1,0,0,0,1,1],
7
 [1,1,1,1,1,1]];
8
9
// Level 2

10
[[1,1,1,1,1,1],
11
 [1,0,0,0,1,1],
12
 [7,0,0,0,0,1],
13
 [1,0,0,0,0,1],
14
 [1,1,0,0,0,1],
15
 [1,1,1,1,1,1]];

Lassen Sie die Levels einige Pickups haben, wie in den folgenden Pickup-Arrays beschrieben:

1
[[0,0,0,0,0,0],
2
 [0,0,0,0,0,0],
3
 [0,0,8,0,0,0],
4
 [0,0,0,8,0,0],
5
 [0,0,0,0,0,0],
6
 [0,0,0,0,0,0]];
7
8
[[0,0,0,0,0,0],
9
 [0,0,0,0,0,0],
10
 [0,0,0,0,0,0],
11
 [0,0,0,0,8,0],
12
 [0,0,8,0,0,0],
13
 [0,0,0,0,0,0]];

Lassen Sie die entsprechenden Trigger-Kachel-Arrays für jede Ebene wie folgt aussehen:

1
[[0,0,0,0,0,0],
2
 [0,0,0,0,0,0],
3
 [0,0,0,0,2,0],
4
 [0,0,0,0,0,0],
5
 [0,0,0,0,0,0],
6
 [0,0,0,0,0,0]];
7
8
[[0,0,0,0,0,0],
9
 [0,0,0,0,0,0],
10
 [0,1,0,0,0,0],
11
 [0,0,0,0,0,0],
12
 [0,0,0,0,0,0],
13
 [0,0,0,0,0,0]];

Die Werte (1 und 2) bezeichnen den Level, der geladen wird, wenn der Spieler die Leertaste drückt.

Hier ist der Code, der ausgeführt wird, wenn der Spieler diese Taste drückt:

1
// Pseudocode

2
tile coordinate = getTileCoordinates(isoTo2D(Iso point), tile height);
3
if (isTrigger(tile coordinate)) {
4
   doRelevantAction(tile coordinate);
5
}

Die Funktion isTrigger() prüft, ob der Triggerdaten-Array-Wert an der angegebenen Koordinate größer als Null ist. Wenn ja, übergibt unser Code diesen Wert an doRelevantAction(), das entscheidet, welche Funktion als nächstes aufgerufen wird. Für unsere Zwecke verwenden wir die einfache Regel, dass es sich um eine Tür handelt, wenn der Wert zwischen 1 und 10 liegt, und daher wird diese Funktion aufgerufen:

1
function swapLevel(level) {
2
    // swap level arrays, pickup arrays, and trigger arrays with new level's data

3
    // move player's starting position to tile next to door that leads to previous level

4
    // set character's direction

5
}

Da der Wert der Kachel im Trigger-Array auch den zu ladenden Level angibt, können wir ihn einfach an swapLevel() übergeben. Dies impliziert wiederum, dass unser Spiel zehn Level hat.

Hier ist eine funktionierende Demo. Versuchen Sie, Gegenstände aufzuheben, indem Sie über sie gehen und die Ebenen wechseln, indem Sie neben Türen stehen und die Leertaste drücken.

Klicken Sie auf den SWF-Fokus, und verwenden Sie dann die Pfeiltasten. Klicken Sie hier für die Vollversion.

Ich habe den Auslöser aktiviert, wenn Space freigegeben wird; Wenn wir nur auf das Drücken der Taste warten, landen wir in einer Schleife, in der wir zwischen den Ebenen wechseln, solange die Taste gedrückt gehalten wird, da der Charakter immer in der neuen Ebene auf einem Triggerplättchen erscheint.

Hier ist der vollständige Code (in AS3):

1
import flash.display.Sprite;
2
import com.csharks.juwalbose.IsoHelper;
3
import flash.display.MovieClip;
4
import flash.geom.Point;
5
import flash.filters.GlowFilter;
6
import flash.events.Event;
7
import com.senocular.utils.KeyObject;
8
import flash.ui.Keyboard;
9
import flash.display.Bitmap;
10
import flash.display.BitmapData;
11
import flash.geom.Matrix;
12
import flash.geom.Rectangle;
13
import flash.events.KeyboardEvent;
14
15
var level1Data=[[1,1,1,1,1,1],
16
[1,0,0,0,0,1],
17
[1,0,0,0,0,7],
18
[1,0,0,0,0,1],
19
[1,0,0,0,1,1],
20
[1,1,1,1,1,1]];
21
22
var pickup1Array=[[0,0,0,0,0,0],
23
[0,0,0,0,0,0],
24
[0,0,8,0,0,0],
25
[0,0,0,8,0,0],
26
[0,0,0,0,0,0],
27
[0,0,0,0,0,0]];
28
29
var level2Data=[[1,1,1,1,1,1],
30
[1,0,0,0,1,1],
31
[7,0,0,0,0,1],
32
[1,0,0,0,0,1],
33
[1,1,0,0,0,1],
34
[1,1,1,1,1,1]];
35
36
var pickup2Array=[[0,0,0,0,0,0],
37
[0,0,0,0,0,0],
38
[0,0,0,0,0,0],
39
[0,0,0,0,8,0],
40
[0,0,8,0,0,0],
41
[0,0,0,0,0,0]];
42
43
var trigger1Array=[[0,0,0,0,0,0],
44
[0,0,0,0,0,0],
45
[0,0,0,0,2,0],
46
[0,0,0,0,0,0],
47
[0,0,0,0,0,0],
48
[0,0,0,0,0,0]];
49
50
var trigger2Array=[[0,0,0,0,0,0],
51
[0,0,0,0,0,0],
52
[0,1,0,0,0,0],
53
[0,0,0,0,0,0],
54
[0,0,0,0,0,0],
55
[0,0,0,0,0,0]];
56
57
58
var levelData:Array=new Array();
59
var pickupArray:Array=new Array();
60
var triggerArray:Array=new Array();
61
var currentLevel:uint=1;
62
var newLevel:uint=1;
63
64
var tileWidth:uint = 50;
65
var borderOffsetY:uint = 70;
66
var borderOffsetX:uint = 275;
67
68
var facing:String = "south";
69
var currentFacing:String = "south";
70
var hero:MovieClip=new herotile();
71
hero.clip.gotoAndStop(facing);
72
var heroPointer:Sprite;
73
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class

74
var heroHalfSize:uint=20;
75
var pickupCount:uint=0;
76
77
//the tiles

78
var grassTile:MovieClip=new TileMc();
79
grassTile.gotoAndStop(1);
80
var wallTile:MovieClip=new TileMc();
81
wallTile.gotoAndStop(2);
82
var pickupTile:MovieClip=new TileMc();
83
pickupTile.gotoAndStop(3);
84
var doorTile:MovieClip=new TileMc();
85
doorTile.gotoAndStop(4);
86
87
//the canvas

88
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
89
addChild(bg);
90
var rect:Rectangle=bg.bitmapData.rect;
91
92
//to handle depth

93
var overlayContainer:Sprite=new Sprite();
94
addChild(overlayContainer);
95
96
//to handle direction movement

97
var dX:Number = 0;
98
var dY:Number = 0;
99
var idle:Boolean = true;
100
var speed:uint = 5;
101
var heroCartPos:Point=new Point();
102
var heroTile:Point=new Point();
103
104
var spaceKeyUp:Boolean=false;
105
//initial hero position

106
var spawnPt:Point=new Point(1,3);
107
108
//add items to start level, add game loop

109
function createLevel()
110
{
111
	if(currentLevel==1){
112
		levelData=level1Data;
113
		pickupArray=pickup1Array;
114
		triggerArray=trigger1Array;
115
	}else{
116
		levelData=level2Data;
117
		pickupArray=pickup2Array;
118
		triggerArray=trigger2Array;
119
	}
120
	levelData[spawnPt.x][spawnPt.y]=2;
121
	
122
	var tileType:uint;
123
	for (var i:uint=0; i<levelData.length; i++)
124
	{
125
		for (var j:uint=0; j<levelData[0].length; j++)
126
		{
127
			tileType = levelData[i][j];
128
			placeTile(tileType,i,j);
129
			if (tileType == 2)
130
			{
131
				//trace(i,j);

132
				levelData[i][j] = 0;
133
			}
134
		}
135
	}
136
	
137
	depthSort();
138
	addEventListener(Event.ENTER_FRAME,loop);
139
}
140
141
//place the tile based on coordinates

142
function placeTile(id:uint,i:uint,j:uint)
143
{
144
var pos:Point=new Point();
145
	if (id == 2)
146
	{
147
		
148
		id = 0;
149
		pos.x = j * tileWidth;
150
		pos.y = i * tileWidth;
151
		pos = IsoHelper.twoDToIso(pos);
152
		hero.x = borderOffsetX + pos.x;
153
		hero.y = borderOffsetY + pos.y;
154
		//overlayContainer.addChild(hero);

155
		heroCartPos.x = j * tileWidth;
156
		heroCartPos.y = i * tileWidth;
157
		heroTile.x=j;
158
		heroTile.y=i;
159
		heroPointer=new herodot();
160
		heroPointer.x=heroCartPos.x;
161
		heroPointer.y=heroCartPos.y;
162
		
163
	}
164
	
165
}
166
167
//the game loop

168
function loop(e:Event)
169
{
170
	
171
	if (key.isDown(Keyboard.UP))
172
	{
173
		dY = -1;
174
	}
175
	else if (key.isDown(Keyboard.DOWN))
176
	{
177
		dY = 1;
178
	}
179
	else
180
	{
181
		dY = 0;
182
	}
183
	if (key.isDown(Keyboard.RIGHT))
184
	{
185
		dX = 1;
186
		if (dY == 0)
187
		{
188
			facing = "east";
189
		}
190
		else if (dY==1)
191
		{
192
			facing = "southeast";
193
			dX = dY=0.5;
194
		}
195
		else
196
		{
197
			facing = "northeast";
198
			dX=0.5;
199
			dY=-0.5;
200
		}
201
	}
202
	else if (key.isDown(Keyboard.LEFT))
203
	{
204
		dX = -1;
205
		if (dY == 0)
206
		{
207
			facing = "west";
208
		}
209
		else if (dY==1)
210
		{
211
			facing = "southwest";
212
			dY=0.5;
213
			dX=-0.5;
214
		}
215
		else
216
		{
217
			facing = "northwest";
218
			dX = dY=-0.5;
219
		}
220
	}
221
	else
222
	{
223
		dX = 0;
224
		if (dY == 0)
225
		{
226
			//facing="west";

227
		}
228
		else if (dY==1)
229
		{
230
			facing = "south";
231
		}
232
		else
233
		{
234
			facing = "north";
235
		}
236
	}
237
	if (dY == 0 && dX == 0)
238
	{
239
		hero.clip.gotoAndStop(facing);
240
		idle = true;
241
	}
242
	else if (idle||currentFacing!=facing)
243
	{
244
		idle = false;
245
		currentFacing = facing;
246
		hero.clip.gotoAndPlay(facing);
247
	}
248
	if (! idle && isWalkable())
249
	{
250
		heroCartPos.x +=  speed * dX;
251
		heroCartPos.y +=  speed * dY;
252
		heroPointer.x=heroCartPos.x;
253
		heroPointer.y=heroCartPos.y;
254
255
		var newPos:Point = IsoHelper.twoDToIso(heroCartPos);
256
		//collision check

257
		hero.x = borderOffsetX + newPos.x;
258
		hero.y = borderOffsetY + newPos.y;
259
		heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth);
260
		
261
		if(isPickup(heroTile)){
262
			pickupItem(heroTile);
263
		}
264
		depthSort();
265
		//trace(heroTile);

266
	}
267
	tileTxt.text="Level "+ currentLevel.toString()+" - x: "+heroTile.x +" & y: "+heroTile.y;
268
	pickupTxt.text="Pickups Collected: "+pickupCount.toString();
269
	
270
	if (spaceKeyUp)
271
	{
272
		spaceKeyUp=false;
273
		if(isTrigger(heroTile)){
274
  			doRelevantAction(heroTile);
275
		}
276
	}
277
}
278
function doRelevantAction(tilePt:Point):void{
279
	newLevel=triggerArray[tilePt.y][tilePt.x];
280
	
281
	if(newLevel==1){
282
		spawnPt=getSpawn(trigger1Array,currentLevel);
283
	}else{
284
		spawnPt=getSpawn(trigger2Array,currentLevel);
285
	}
286
	
287
	swapLevel(newLevel);
288
}
289
function getSpawn(ar:Array,index:uint):Point{
290
	var tilePt:Point=new Point();
291
	for (var i:uint=0; i<ar.length; i++)
292
	{
293
		for (var j:uint=0; j<ar[0].length; j++)
294
		{
295
			if(ar[i][j]==index){
296
				tilePt.x=i;
297
				tilePt.y=j;
298
				break;
299
			}
300
		}
301
	}
302
	return tilePt;
303
}
304
function swapLevel(level:uint):void{
305
	removeEventListener(Event.ENTER_FRAME,loop);
306
	currentLevel=level;
307
	//trace("load",level);

308
	createLevel();
309
}
310
function isPickup(tilePt:Point):Boolean{
311
	return(pickupArray[tilePt.y][tilePt.x]==8);
312
}
313
function isTrigger(tilePt:Point):Boolean{
314
	return(triggerArray[tilePt.y][tilePt.x]>0);
315
}
316
function pickupItem(tilePt:Point):void{
317
	pickupCount++;
318
	pickupArray[tilePt.y][tilePt.x]=0;
319
}
320
//check for collision tile

321
function isWalkable():Boolean{
322
	var able:Boolean=true;
323
	var newPos:Point =new Point();
324
	newPos.x=heroCartPos.x +  (speed * dX);
325
	newPos.y=heroCartPos.y +  (speed * dY);
326
	switch (facing){
327
		case "north":
328
			newPos.y-=heroHalfSize;
329
		break;
330
		case "south":
331
			newPos.y+=heroHalfSize;
332
		break;
333
		case "east":
334
			newPos.x+=heroHalfSize;
335
		break;
336
		case "west":
337
			newPos.x-=heroHalfSize;
338
		break;
339
		case "northeast":
340
			newPos.y-=heroHalfSize;
341
			newPos.x+=heroHalfSize;
342
		break;
343
		case "southeast":
344
			newPos.y+=heroHalfSize;
345
			newPos.x+=heroHalfSize;
346
		break;
347
		case "northwest":
348
			newPos.y-=heroHalfSize;
349
			newPos.x-=heroHalfSize;
350
		break;
351
		case "southwest":
352
			newPos.y+=heroHalfSize;
353
			newPos.x-=heroHalfSize;
354
		break;
355
	}
356
	newPos=IsoHelper.getTileCoordinates(newPos,tileWidth);
357
	if(levelData[newPos.y][newPos.x]>=1){
358
		able=false;
359
	}else{
360
		//trace("new",newPos);

361
	}
362
	return able;
363
}
364
365
//sort depth & draw to canvas

366
function depthSort()
367
{
368
	bg.bitmapData.lock();
369
	bg.bitmapData.fillRect(rect,0xffffff);
370
	var tileType:uint;
371
	var mat:Matrix=new Matrix();
372
	var pos:Point=new Point();
373
	for (var i:uint=0; i<levelData.length; i++)
374
	{
375
		for (var j:uint=0; j<levelData[0].length; j++)
376
		{
377
			tileType = levelData[i][j];
378
			//placeTile(tileType,i,j);

379
380
			pos.x = j * tileWidth;
381
			pos.y = i * tileWidth;
382
			pos = IsoHelper.twoDToIso(pos);
383
			mat.tx = borderOffsetX + pos.x;
384
			mat.ty = borderOffsetY + pos.y;
385
			if(tileType==0){
386
				bg.bitmapData.draw(grassTile,mat);
387
			}else if(tileType==1){
388
				bg.bitmapData.draw(wallTile,mat);
389
			}else if(tileType==7){
390
				bg.bitmapData.draw(doorTile,mat);
391
			}
392
			if(pickupArray[i][j]==8){
393
				bg.bitmapData.draw(pickupTile,mat);
394
			}
395
			if(heroTile.x==j&&heroTile.y==i){
396
				mat.tx=hero.x;
397
				mat.ty=hero.y;
398
				bg.bitmapData.draw(hero,mat);
399
			}
400
401
		}
402
	}
403
	bg.bitmapData.unlock();
404
//add character rectangle

405
}
406
function handleKeyUp(e:KeyboardEvent):void{//listen for space key release

407
	if(e.charCode==Keyboard.SPACE){
408
		spaceKeyUp=true;
409
	}
410
}
411
stage.addEventListener(KeyboardEvent.KEY_UP,handleKeyUp);
412
createLevel();

3. Pfadfindung

Pfadfindung und Pfadverfolgung ist ein ziemlich komplizierter Prozess. Es gibt verschiedene Ansätze, die verschiedene Algorithmen verwenden, um den Weg zwischen zwei Punkten zu finden, aber unsere Level-Daten sind ein 2D-Array. Die Dinge sind einfacher als sie es sonst sein könnten - wir haben gut definierte und einzigartige Knoten, die der Spieler besetzen kann, und wir können leicht überprüfen, ob Sie sind begehbar.

Ein detaillierter Überblick über Pfadfindungsalgorithmen würde den Rahmen dieses Artikels sprengen, aber ich werde versuchen, die gebräuchlichste Funktionsweise zu erklären: den kürzesten Pfad-Algorithmus, von dem A* und Dijkstras Algorithmen berühmte Implementierungen sind.

Unser Ziel ist es, Knoten zu finden, die einen Startknoten und einen Endknoten verbinden. Vom Startknoten aus besuchen wir alle acht benachbarten Knoten und markieren sie alle als besucht; Dieser Kernprozess wird für jeden neu besuchten Knoten rekursiv wiederholt. Jeder Thread verfolgt die besuchten Knoten. Beim Springen zu benachbarten Knoten werden bereits besuchte Knoten übersprungen (die Rekursion stoppt); andernfalls wird der Prozess fortgesetzt, bis wir den Endknoten erreichen, wo die Rekursion endet und der vollständige gefolgte Pfad als Knotenarray zurückgegeben wird. Manchmal wird der Endknoten nie erreicht, in diesem Fall schlägt die Pfadfindung fehl. Normalerweise finden wir mehrere Pfade zwischen den beiden Knoten. In diesem Fall nehmen wir den mit der geringsten Anzahl von Knoten.

Es gibt viele Standardlösungen für die Pfadfindung basierend auf 2D-Arrays. Daher überspringen wir die Neuerfindung dieses Rads. Lassen Sie uns diese AS3-Lösung verwenden (ich empfehle Ihnen, diese großartige erklärende Demo zu lesen)).

Die Lösung gibt ein Array mit Punkten zurück, die den Pfad vom Startpunkt zum Endpunkt bilden:

1
path = PathFinder.go(start x, start y, end x, end y, levelData);

Pfad folgend

Sobald wir den Pfad als Node-Array haben, müssen wir das Zeichen dazu bringen, ihm zu folgen.

Angenommen, wir möchten, dass der Charakter zu einer Kachel geht, auf die wir klicken. Wir müssen zuerst nach einem Pfad zwischen dem Knoten suchen, den das Zeichen derzeit belegt, und dem Knoten, auf den wir geklickt haben. Wenn ein erfolgreicher Pfad gefunden wird, müssen wir das Zeichen auf den ersten Knoten im Knotenarray verschieben, indem wir als Ziel festlegen. Sobald wir den Zielknoten erreicht haben, prüfen wir, wo sich noch weitere Knoten im Knotenarray befinden und setzen, wenn ja, den nächsten Knoten als Ziel - und so weiter, bis wir den letzten Knoten erreichen.

Wir werden auch die Richtung des Spielers basierend auf dem aktuellen Knoten und dem neuen Zielknoten jedes Mal ändern, wenn wir einen Knoten erreichen. Zwischen den Knoten gehen wir einfach in die gewünschte Richtung, bis wir den Zielknoten erreichen. Dies ist eine sehr einfache KI.

Sehen Sie sich dieses Arbeitsbeispiel an:

Klicken Sie, um das Zeichen zu verschieben. Klicken Sie hier für die Vollversion.

Hier ist die vollständige Quelle:

1
import flash.display.Sprite;
2
import com.csharks.juwalbose.IsoHelper;
3
import flash.display.MovieClip;
4
import flash.geom.Point;
5
import flash.filters.GlowFilter;
6
import flash.events.Event;
7
import com.senocular.utils.KeyObject;
8
import flash.ui.Keyboard;
9
import flash.display.Bitmap;
10
import flash.display.BitmapData;
11
import flash.geom.Matrix;
12
import flash.geom.Rectangle;
13
import flash.events.MouseEvent;
14
15
import libs.PathFinder.*;
16
17
var levelData=[[1,1,1,1,1,1,1,1],
18
[1,0,0,0,0,2,0,1],
19
[1,0,0,1,0,0,0,1],
20
[1,1,0,0,0,0,0,1],
21
[1,0,0,0,0,1,1,1],
22
[1,0,0,1,0,0,0,1],
23
[1,0,0,1,0,0,0,1],
24
[1,1,1,1,1,1,1,1]];
25
26
var tileWidth:uint = 50;
27
var borderOffsetY:uint = 70;
28
var borderOffsetX:uint = 275;
29
30
var facing:String = "south";
31
var currentFacing:String = "south";
32
var hero:MovieClip=new herotile();
33
hero.clip.gotoAndStop(facing);
34
var heroPointer:Sprite;
35
var heroHalfSize:uint=20;
36
37
//the tiles

38
var grassTile:MovieClip=new TileMc();
39
grassTile.gotoAndStop(1);
40
var wallTile:MovieClip=new TileMc();
41
wallTile.gotoAndStop(2);
42
43
//the canvas

44
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
45
addChild(bg);
46
var rect:Rectangle=bg.bitmapData.rect;
47
48
//to handle depth

49
var overlayContainer:Sprite=new Sprite();
50
addChild(overlayContainer);
51
52
//to handle direction movement

53
var dX:Number = 0;
54
var dY:Number = 0;
55
var idle:Boolean = true;
56
var speed:uint = 5;
57
var heroCartPos:Point=new Point();
58
var heroTile:Point=new Point();
59
60
var path:Array=new Array();
61
var destination:Point=new Point();
62
var stepsTillTurn:uint=5;
63
var stepsTaken:uint;
64
65
var glowFilter:GlowFilter=new GlowFilter(0x00ffff);
66
67
//add items to start level, add game loop

68
function createLevel()
69
{
70
	var tileType:uint;
71
	for (var i:uint=0; i<levelData.length; i++)
72
	{
73
		for (var j:uint=0; j<levelData[0].length; j++)
74
		{
75
			tileType = levelData[i][j];
76
			placeTile(tileType,i,j);
77
			if (tileType == 2)
78
			{
79
				levelData[i][j] = 0;
80
			}
81
		}
82
	}
83
	overlayContainer.addChild(heroPointer);
84
	overlayContainer.alpha=0.9;
85
	overlayContainer.scaleX=overlayContainer.scaleY=0.3;
86
	overlayContainer.y=320;
87
	overlayContainer.x=10;
88
	depthSort();
89
	
90
	addEventListener(Event.ENTER_FRAME,loop);
91
}
92
93
//place the tile based on coordinates

94
function placeTile(id:uint,i:uint,j:uint)
95
{
96
var pos:Point=new Point();
97
	if (id == 2)
98
	{
99
		
100
		id = 0;
101
		pos.x = j * tileWidth +tileWidth/2;
102
		pos.y = i * tileWidth +tileWidth/2;
103
		pos = IsoHelper.twoDToIso(pos);
104
		hero.x = borderOffsetX + pos.x;
105
		hero.y = borderOffsetY + pos.y;
106
		//overlayContainer.addChild(hero);

107
		heroCartPos.x = j * tileWidth +tileWidth/2;
108
		heroCartPos.y = i * tileWidth +tileWidth/2;
109
		
110
		heroTile.x=j;
111
		heroTile.y=i;
112
		heroPointer=new herodot();
113
		heroPointer.x=heroCartPos.x;
114
		heroPointer.y=heroCartPos.y;
115
		
116
	}
117
	var tile:MovieClip=new cartTile();
118
	tile.gotoAndStop(id+1);
119
	tile.x = j * tileWidth;
120
	tile.y = i * tileWidth;
121
	tile.name="tile_"+j.toString()+"_"+i.toString();
122
	overlayContainer.addChild(tile);
123
}
124
125
//the game loop

126
function loop(e:Event)
127
{
128
	aiWalk();
129
	
130
	if (dY == 0 && dX == 0)
131
	{
132
		hero.clip.gotoAndStop(facing);
133
		idle = true;
134
	}
135
	else if (idle||currentFacing!=facing)
136
	{
137
		//trace(idle,"facing ",currentFacing,facing);

138
		idle = false;
139
		
140
		currentFacing = facing;
141
		hero.clip.gotoAndPlay(facing);
142
	}
143
	if (! idle )
144
	{
145
		heroCartPos.x +=  speed * dX;
146
		heroCartPos.y +=  speed * dY;
147
		heroPointer.x=heroCartPos.x;
148
		heroPointer.y=heroCartPos.y;
149
150
		var newPos:Point = IsoHelper.twoDToIso(heroCartPos);
151
		//collision check

152
		hero.x = borderOffsetX + newPos.x;
153
		hero.y = borderOffsetY + newPos.y;
154
		heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth);
155
		depthSort();
156
		//trace(heroTile);

157
	}
158
	tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y;
159
}
160
function aiWalk():void{
161
	if(path.length==0){//path has ended

162
		dX=dY=0;
163
		return;
164
	}
165
	if(heroTile.equals(destination)){//reached current destination, set new, change direction

166
		//wait till we are few steps into the tile before we turn

167
		stepsTaken++;
168
		if(stepsTaken<stepsTillTurn){
169
			return;
170
		}
171
		//place the hero at tile middle before turn

172
		var pos:Point=new Point();
173
		pos.x = heroTile.x * tileWidth +tileWidth/2;
174
		pos.y = heroTile.y * tileWidth +tileWidth/2;
175
		pos = IsoHelper.twoDToIso(pos);
176
		hero.x = borderOffsetX + pos.x;
177
		hero.y = borderOffsetY + pos.y;
178
		heroCartPos.x = heroTile.x * tileWidth +tileWidth/2;
179
		heroCartPos.y = heroTile.y * tileWidth +tileWidth/2;
180
		heroPointer.x=heroCartPos.x;
181
		heroPointer.y=heroCartPos.y;
182
		depthSort();
183
		
184
		//new point, turn, find dX,dY

185
		stepsTaken=0;
186
		destination=path.pop();
187
		if(heroTile.x<destination.x){
188
			dX = 1;
189
		}else if(heroTile.x>destination.x){
190
			dX = -1;
191
		}else {
192
			dX=0;
193
		}
194
		if(heroTile.y<destination.y){
195
			dY = 1;
196
		}else if(heroTile.y>destination.y){
197
			dY = -1;
198
		}else {
199
			dY=0;
200
		}
201
		if(heroTile.x==destination.x){//top or bottom

202
			dX=0;
203
		}else if(heroTile.y==destination.y){//left or right

204
			dY=0;
205
		}
206
		
207
		if (dX == 1)
208
	{
209
		
210
		if (dY == 0)
211
		{
212
			facing = "east";
213
		}
214
		else if (dY==1)
215
		{
216
			facing = "southeast";
217
			dX = dY=0.5;
218
		}
219
		else
220
		{
221
			facing = "northeast";
222
			dX=0.5;
223
			dY=-0.5;
224
		}
225
	}
226
	else if (dX==-1)
227
	{
228
		
229
		if (dY == 0)
230
		{
231
			facing = "west";
232
		}
233
		else if (dY==1)
234
		{
235
			facing = "southwest";
236
			dY=0.5;
237
			dX=-0.5;
238
		}
239
		else
240
		{
241
			facing = "northwest";
242
			dX = dY=-0.5;
243
		}
244
	}
245
	else
246
	{
247
		
248
		if (dY == 0)
249
		{
250
			facing=currentFacing;
251
		}
252
		else if (dY==1)
253
		{
254
			facing = "south";
255
		}
256
		else
257
		{
258
			facing = "north";
259
		}
260
	}
261
	}
262
	
263
	
264
	
265
}
266
//sort depth & draw to canvas

267
function depthSort()
268
{
269
	bg.bitmapData.lock();
270
	bg.bitmapData.fillRect(rect,0xffffff);
271
	var tileType:uint;
272
	var mat:Matrix=new Matrix();
273
	var pos:Point=new Point();
274
	for (var i:uint=0; i<levelData.length; i++)
275
	{
276
		for (var j:uint=0; j<levelData[0].length; j++)
277
		{
278
			tileType = levelData[i][j];
279
			//placeTile(tileType,i,j);

280
281
			pos.x = j * tileWidth;
282
			pos.y = i * tileWidth;
283
			pos = IsoHelper.twoDToIso(pos);
284
			mat.tx = borderOffsetX + pos.x;
285
			mat.ty = borderOffsetY + pos.y;
286
			if(tileType==0){
287
				bg.bitmapData.draw(grassTile,mat);
288
			}else{
289
				bg.bitmapData.draw(wallTile,mat);
290
			}
291
			if(heroTile.x==j&&heroTile.y==i){
292
				mat.tx=hero.x;
293
				mat.ty=hero.y;
294
				bg.bitmapData.draw(hero,mat);
295
			}
296
297
		}
298
	}
299
	bg.bitmapData.unlock();
300
//add character rectangle

301
}
302
function handleMouseClick(e:MouseEvent):void{
303
	path.splice(0,path.length);
304
	var clickPt:Point=new Point();
305
	clickPt.x=e.stageX-borderOffsetX;
306
	clickPt.y=e.stageY-borderOffsetY;
307
	clickPt=IsoHelper.isoTo2D(clickPt);
308
	clickPt.x-=tileWidth/2;
309
	clickPt.y+=tileWidth/2;
310
	clickPt=IsoHelper.getTileCoordinates(clickPt,tileWidth);
311
	if(clickPt.x<0||clickPt.y<0||clickPt.x>levelData.length-1||clickPt.x>levelData[0].length-1){
312
		//trace("invalid");

313
		//we have clicked outside

314
		return;
315
	}
316
	if(levelData[clickPt.y][clickPt.x]==1){
317
		//trace("wall");

318
		//we clicked on a wall

319
		return;
320
	}
321
	trace("find ",heroTile, clickPt);
322
	destination=heroTile;
323
	path= PathFinder.go(heroTile.x, heroTile.y, clickPt.x, clickPt.y, levelData);
324
	path.reverse();
325
	path.push(clickPt);
326
	path.reverse();
327
	paintPath();
328
}
329
function paintPath():void{//show hihglighted path in minimap

330
	var sp:Sprite;
331
	for (var i:uint=0; i<levelData.length; i++)
332
	{
333
		for (var j:uint=0; j<levelData[0].length; j++)
334
		{
335
			sp=overlayContainer.getChildByName("tile_"+j.toString()+"_"+i.toString()) as Sprite;
336
			sp.filters=[];
337
		}
338
	}
339
	for(i=0;i<path.length;i++){
340
		sp=overlayContainer.getChildByName("tile_"+(path[i].x).toString()+"_"+(path[i].y).toString()) as Sprite;
341
		sp.filters=[glowFilter];
342
		overlayContainer.setChildIndex(sp,overlayContainer.numChildren-1);
343
	}
344
	overlayContainer.setChildIndex(heroPointer,overlayContainer.numChildren-1);
345
}
346
stage.addEventListener(MouseEvent.CLICK, handleMouseClick);
347
createLevel();

Sie haben vielleicht bemerkt, dass ich die Kollisionsprüfungslogik entfernt habe; es wird nicht mehr benötigt, da wir unseren Charakter nicht manuell mit der Tastatur bewegen können. Wir müssen jedoch gültige Klickpunkte herausfiltern, indem wir feststellen, ob wir innerhalb des begehbaren Bereichs geklickt haben und nicht in einer Wandfliese oder einer anderen nicht begehbaren Fliese.

Ein weiterer interessanter Punkt für die Codierung der KI: Wir möchten nicht, dass sich der Charakter zum nächsten Plättchen im Node-Array umdreht, sobald er im aktuellen angekommen ist, da ein solcher sofortiger Zug dazu führt, dass unser Charakter an den Grenzen von Fliesen. Stattdessen sollten wir warten, bis sich der Charakter einige Schritte innerhalb der Kachel befindet, bevor wir nach dem nächsten Ziel suchen. Es ist auch besser, den Helden kurz vor dem Drehen manuell in die Mitte des aktuellen Plättchens zu platzieren, damit sich alles perfekt anfühlt.

Wenn Sie sich die obige Demo ansehen, werden Sie möglicherweise feststellen, dass unsere Zeichenlogik unterbrochen wird, wenn sich der Held diagonal in die Nähe einer Wandfliese bewegt. Dies ist ein Extremfall, bei dem unser Held für einen Rahmen in der Wandfliese zu sein scheint. Dies geschieht, weil wir die Kollisionsprüfung deaktiviert haben. Eine Problemumgehung besteht darin, einen Pfadfindungsalgorithmus zu verwenden, der die diagonalen Lösungen ignoriert. (Fast alle Pfadfindungsalgorithmen haben Optionen zum Aktivieren oder Deaktivieren von Diagonallauflösungen.)


4. Projektile

Ein Projektil ist etwas, das sich mit einer bestimmten Geschwindigkeit in eine bestimmte Richtung bewegt, wie eine Kugel, ein Zauberspruch, eine Kugel usw.

Alles an dem Projektil ist identisch mit dem Heldencharakter, abgesehen von der Höhe: Anstatt über den Boden zu rollen, schweben Projektile oft in einer bestimmten Höhe darüber. Eine Kugel bewegt sich über die Taillenhöhe des Charakters, und möglicherweise muss sogar ein Ball herumspringen.

Interessant ist, dass die isometrische Höhe der Höhe in einer 2D-Seitenansicht entspricht. Es sind keine komplizierten Konvertierungen erforderlich. Wenn sich ein Ball in kartesischen Koordinaten 10 Pixel über dem Boden befindet, befindet er sich in isometrischen Koordinaten 10 Pixel über dem Boden. (In unserem Fall ist die relevante Achse die y-Achse.)

Versuchen wir, einen Ball zu implementieren, der in unserem ummauerten Grasland herumhüpft. Wir ignorieren Dämpfungseffekte (und lassen das Hüpfen endlos weiterlaufen) und für einen Hauch von Realismus fügen wir dem Ball einen Schatten hinzu. Wir bewegen den Schatten genauso wie wir den Heldencharakter (d. h. ohne einen Höhenwert zu verwenden), aber für den Ball müssen wir den Höhenwert zum isometrischen Y-Wert addieren. Der Höhenwert ändert sich von Frame zu Frame je nach Schwerkraft, und sobald der Ball den Boden berührt, drehen wir die aktuelle Geschwindigkeit entlang der y-Achse.

Bevor wir uns mit dem Bouncen in einem isometrischen System befassen, werden wir sehen, wie wir es in einem kartesischen 2D-System implementieren können. Lassen Sie uns die Höhe des Balls durch eine Variable zValue darstellen. Stellen Sie sich vor, der Ball ist zunächst zehn Pixel hoch, also zValue = 10. Wir verwenden zwei weitere Variablen: incrementValue, das bei 0 beginnt, und die gravity, die den Wert 1 hat.

In jedem Frame addieren wir incrementValue zu zValue und subtrahieren gravity von IncrementValue. Wenn zValue 0 erreicht, bedeutet dies, dass der Ball den Boden erreicht hat; An dieser Stelle kehren wir das Vorzeichen von IncrementValue um, indem wir es mit -1 multiplizieren und in eine positive Zahl verwandeln. Dies bedeutet, dass sich der Ball ab dem nächsten Frame nach oben bewegt und somit abprallt.

So sieht das im Code aus:

1
zValue=10;
2
gravity=1;
3
incrementValue=0;
4
5
gameLoop(){
6
    incrementValue-=gravity;
7
    zValue-=incrementValue;
8
    if(zValue<=0){
9
        zValue=0;
10
        incrementValue*=-1;
11
    }
12
}

Wir werden tatsächlich eine leicht modifizierte Version davon verwenden:

1
zValue=10;
2
gravity=1;
3
incrementValue=0;
4
incrementReset=-12
5
6
gameLoop(){
7
    incrementValue-=gravity;
8
    zValue-=incrementValue;
9
    if(zValue<=0){
10
        zValue=0;
11
        incrementValue=incrementReset;
12
    }
13
}

Dies entfernt den Dämpfungseffekt und lässt den Ball für immer hüpfen.

Wenden wir dies auf unseren Ball an, erhalten wir die folgende Demo:

Klicken Sie auf den SWF-Fokus, und verwenden Sie dann die Pfeiltasten. Klicken Sie hier für die Vollversion.

Hier ist der vollständige AS3-Code:

1
import flash.display.Sprite;
2
import com.csharks.juwalbose.IsoHelper;
3
import flash.display.MovieClip;
4
import flash.geom.Point;
5
import flash.events.Event;
6
import flash.display.Bitmap;
7
import flash.display.BitmapData;
8
import flash.geom.Matrix;
9
import flash.geom.Rectangle;
10
import com.senocular.utils.KeyObject;
11
12
var levelData=[[1,1,1,1,1,1],
13
[1,0,0,0,0,1],
14
[1,0,0,0,0,1],
15
[1,0,0,2,0,1],
16
[1,0,0,0,0,1],
17
[1,1,1,1,1,1]];
18
19
var tileWidth:uint = 50;
20
var borderOffsetY:uint = 70;
21
var borderOffsetX:uint = 275;
22
23
var key:KeyObject = new KeyObject(stage);
24
25
var facing:String = "south";
26
var currentFacing:String = "south";
27
var heroHalfSize:uint = 20;
28
29
//the tiles

30
var grassTile:MovieClip=new TileMc();
31
grassTile.gotoAndStop(1);
32
var wallTile:MovieClip=new TileMc();
33
wallTile.gotoAndStop(2);
34
var pickupTile:MovieClip=new TileMc();
35
pickupTile.gotoAndStop(3);
36
var ball:Sprite=new Ball();
37
var shadow_S:Sprite=new Shadow();
38
39
//the canvas

40
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
41
addChild(bg);
42
var rect:Rectangle = bg.bitmapData.rect;
43
44
//to handle depth

45
var heroPointer:Sprite;
46
var overlayContainer:Sprite=new Sprite();
47
addChild(overlayContainer);
48
49
//to handle direction movement

50
var dX:Number = 0;
51
var dY:Number = 0;
52
var speed:uint = 5;
53
var ballCartPos:Point=new Point();
54
var ballTile:Point=new Point();
55
56
var zValue:int = 50;
57
var gravity:int = -1;
58
var incrementValue:Number = 0;
59
60
//add items to start level, add game loop

61
function createLevel()
62
{
63
	var tileType:uint;
64
	for (var i:uint=0; i<levelData.length; i++)
65
	{
66
		for (var j:uint=0; j<levelData[0].length; j++)
67
		{
68
			tileType = levelData[i][j];
69
			placeTile(tileType,i,j);
70
			if (tileType == 2)
71
			{
72
				levelData[i][j] = 0;
73
			}
74
		}
75
	}
76
	overlayContainer.addChild(heroPointer);
77
	overlayContainer.alpha=0.5;
78
	overlayContainer.scaleX=overlayContainer.scaleY=0.5;
79
	overlayContainer.y=290;
80
	overlayContainer.x=10;
81
82
	depthSort();
83
	addEventListener(Event.ENTER_FRAME,loop);
84
}
85
86
//place the tile based on coordinates

87
function placeTile(id:uint,i:uint,j:uint)
88
{
89
	var pos:Point=new Point();
90
	if (id == 2)
91
	{
92
93
		id = 0;
94
		pos.x = j * tileWidth;
95
		pos.y = i * tileWidth;
96
		pos = IsoHelper.twoDToIso(pos);
97
		
98
		ball.x = borderOffsetX + pos.x;
99
		ball.y = borderOffsetY + pos.y;
100
		
101
		ballCartPos.x = j * tileWidth;
102
		ballCartPos.y = i * tileWidth;
103
		ballTile.x = j;
104
		ballTile.y = i;
105
		
106
		heroPointer=new herodot();
107
		heroPointer.x=ballCartPos.x;
108
		heroPointer.y=ballCartPos.y;
109
		
110
	}
111
	var tile:MovieClip=new cartTile();
112
	tile.gotoAndStop(id+1);
113
	tile.x = j * tileWidth;
114
	tile.y = i * tileWidth;
115
	overlayContainer.addChild(tile);
116
117
}
118
119
//the game loop

120
function loop(e:Event)
121
{
122
	incrementValue -=  gravity;
123
	zValue -=  incrementValue;
124
	if (zValue <= 0)
125
	{
126
		zValue = 0;
127
		incrementValue = -12;
128
	}
129
130
131
	if (key.isDown(Keyboard.UP))
132
	{
133
		dY = -1;
134
	}
135
	else if (key.isDown(Keyboard.DOWN))
136
	{
137
		dY = 1;
138
	}
139
	else
140
	{
141
		dY = 0;
142
	}
143
	if (key.isDown(Keyboard.RIGHT))
144
	{
145
		dX = 1;
146
		if (dY == 0)
147
		{
148
			facing = "east";
149
		}
150
		else if (dY==1)
151
		{
152
			facing = "southeast";
153
			dX = dY = 0.5;
154
		}
155
		else
156
		{
157
			facing = "northeast";
158
			dX = 0.5;
159
			dY = -0.5;
160
		}
161
	}
162
	else if (key.isDown(Keyboard.LEFT))
163
	{
164
		dX = -1;
165
		if (dY == 0)
166
		{
167
			facing = "west";
168
		}
169
		else if (dY==1)
170
		{
171
			facing = "southwest";
172
			dY = 0.5;
173
			dX = -0.5;
174
		}
175
		else
176
		{
177
			facing = "northwest";
178
			dX = dY = -0.5;
179
		}
180
	}
181
	else
182
	{
183
		dX = 0;
184
		if (dY == 0)
185
		{
186
			//facing="west";

187
		}
188
		else if (dY==1)
189
		{
190
			facing = "south";
191
		}
192
		else
193
		{
194
			facing = "north";
195
		}
196
	}
197
198
199
	if (isWalkable())
200
	{
201
		ballCartPos.x +=  speed * dX;
202
		ballCartPos.y +=  speed * dY;
203
		
204
		heroPointer.x=ballCartPos.x;
205
		heroPointer.y=ballCartPos.y;
206
207
208
		var newPos:Point = IsoHelper.twoDToIso(ballCartPos);
209
		//collision check

210
		ball.x = borderOffsetX + newPos.x;
211
		ball.y = borderOffsetY + newPos.y;
212
		ballTile = IsoHelper.getTileCoordinates(ballCartPos,tileWidth);
213
214
	}
215
	depthSort();
216
217
}
218
219
//check for collision tile

220
function isWalkable():Boolean
221
{
222
	var able:Boolean = true;
223
	var newPos:Point =new Point();
224
	newPos.x=ballCartPos.x +  (speed * dX);
225
	newPos.y=ballCartPos.y +  (speed * dY);
226
	switch (facing)
227
	{
228
		case "north" :
229
			newPos.y -=  heroHalfSize;
230
			break;
231
		case "south" :
232
			newPos.y +=  heroHalfSize;
233
			break;
234
		case "east" :
235
			newPos.x +=  heroHalfSize;
236
			break;
237
		case "west" :
238
			newPos.x -=  heroHalfSize;
239
			break;
240
		case "northeast" :
241
			newPos.y -=  heroHalfSize;
242
			newPos.x +=  heroHalfSize;
243
			break;
244
		case "southeast" :
245
			newPos.y +=  heroHalfSize;
246
			newPos.x +=  heroHalfSize;
247
			break;
248
		case "northwest" :
249
			newPos.y -=  heroHalfSize;
250
			newPos.x -=  heroHalfSize;
251
			break;
252
		case "southwest" :
253
			newPos.y +=  heroHalfSize;
254
			newPos.x -=  heroHalfSize;
255
			break;
256
	}
257
	newPos = IsoHelper.getTileCoordinates(newPos,tileWidth);
258
	if (levelData[newPos.y][newPos.x] == 1)
259
	{
260
		able = false;
261
	}
262
	else
263
	{
264
		//trace("new",newPos);

265
	}
266
	return able;
267
}
268
269
//sort depth & draw to canvas

270
function depthSort()
271
{
272
	bg.bitmapData.lock();
273
	bg.bitmapData.fillRect(rect,0xffffff);
274
	var tileType:uint;
275
	var mat:Matrix=new Matrix();
276
	var pos:Point=new Point();
277
	for (var i:uint=0; i<levelData.length; i++)
278
	{
279
		for (var j:uint=0; j<levelData[0].length; j++)
280
		{
281
			tileType = levelData[i][j];
282
			//placeTile(tileType,i,j);

283
284
			pos.x = j * tileWidth;
285
			pos.y = i * tileWidth;
286
			pos = IsoHelper.twoDToIso(pos);
287
			mat.tx = borderOffsetX + pos.x;
288
			mat.ty = borderOffsetY + pos.y;
289
			if (tileType == 0)
290
			{
291
				bg.bitmapData.draw(grassTile,mat);
292
			}
293
			else
294
			{
295
				bg.bitmapData.draw(wallTile,mat);
296
			}
297
298
			if (ballTile.x == j && ballTile.y == i)
299
			{
300
				mat.tx = ball.x;//+ tileWidth;

301
				mat.ty = ball.y;// +tileWidth/2;

302
				bg.bitmapData.draw(shadow_S,mat);
303
				mat.ty = mat.ty - zValue - heroHalfSize;
304
				bg.bitmapData.draw(ball,mat);
305
			}
306
307
		}
308
	}
309
	bg.bitmapData.unlock();
310
311
}
312
createLevel();

Verstehen Sie, dass die Rolle, die der Schatten spielt, eine sehr wichtige ist, die zum Realismus dieser Illusion beiträgt. Im obigen Beispiel habe ich die halbe Ballhöhe zur Y-Position des Balls addiert, damit er in Bezug auf den Schatten an der richtigen Position springt.

Beachten Sie außerdem, dass wir jetzt die beiden Bildschirmkoordinaten (x und y) verwenden, um drei Dimensionen in isometrischen Koordinaten darzustellen. Die y-Achse in Bildschirmkoordinaten ist auch die z-Achse in isometrischen Koordinaten. Das kann verwirrend sein!


5. Isometrisches Scrollen

Wenn der Ebenenbereich viel größer als der sichtbare Bildschirmbereich ist, müssen wir ihn scrollen lassen.

Der sichtbare Bildschirmbereich kann als kleineres Rechteck innerhalb des größeren Rechtecks des gesamten Levelbereichs betrachtet werden. Scrollen ist im Wesentlichen nur das Verschieben des inneren Rechtecks in das größere:

Scrolling StartScrolling StartScrolling Start

Normalerweise bleibt die Position des Spielers in Bezug auf das Bildschirmrechteck, üblicherweise in der Bildschirmmitte, gleich, wenn ein solches Scrollen stattfindet. Um das Scrollen zu implementieren, müssen wir lediglich den Eckpunkt des inneren Rechtecks verfolgen:

Scrolling NextScrolling NextScrolling Next

Dieser Eckpunkt, der in kartesischen Koordinaten angegeben ist (im Bild können wir nur die isometrischen Werte zeigen), fällt in eine Kachel in den Höhendaten. Zum Scrollen inkrementieren wir die x- und y-Position des Eckpunktes in kartesischen Koordinaten. Jetzt können wir diesen Punkt in isometrische Koordinaten umwandeln und damit den Bildschirm zeichnen.

Die neu konvertierten Werte im isometrischen Raum müssen auch die Ecke unseres Bildschirms sein, was bedeutet, dass sie die neuen sind (0, 0). Beim Analysieren und Zeichnen der Ebenendaten ziehen wir diesen Wert von der isometrischen Position jeder Kachel ab und zeichnen ihn nur, wenn die neue Position der Kachel innerhalb des Bildschirms liegt. Wir können dies in Schritten wie folgt ausdrücken:

  • Aktualisieren Sie die x- und y-Koordinaten des kartesischen Eckpunkts.
  • Wandeln Sie dies in einen isometrischen Raum um.
  • Subtrahieren Sie diesen Wert von der isometrischen Zeichenposition jeder Kachel.
  • Zeichnen Sie die Kachel nur, wenn die neue isometrische Zeichenposition innerhalb des Bildschirms liegt.

Sehen Sie sich dieses Beispiel an (verwenden Sie die Pfeile zum Scrollen):

Klicken Sie, um den SWF-Fokus festzulegen, und verwenden Sie dann die Pfeiltasten. Klicken Sie hier für die Vollversion.

Hier ist der vollständige AS3-Quellcode:

1
import flash.display.Sprite;
2
import com.csharks.juwalbose.IsoHelper;
3
import flash.display.MovieClip;
4
import flash.geom.Point;
5
import flash.filters.GlowFilter;
6
import flash.events.Event;
7
import com.senocular.utils.KeyObject;
8
import flash.ui.Keyboard;
9
import flash.display.Bitmap;
10
import flash.display.BitmapData;
11
import flash.geom.Matrix;
12
import flash.geom.Rectangle;
13
14
var levelData=[[1,1,1,1,1,1,1,1,1,1,1,1],
15
[1,0,0,0,0,0,0,0,0,1,0,1],
16
[1,0,0,0,0,0,0,0,0,1,0,1],
17
[1,0,0,1,0,0,0,0,0,0,0,1],
18
[1,0,0,1,2,0,0,0,0,0,0,1],
19
[1,0,0,1,0,0,0,0,0,0,0,1],
20
[1,0,0,0,0,0,0,0,0,1,0,1],
21
[1,0,0,0,0,0,0,0,0,0,0,1],
22
[1,0,0,0,1,1,1,0,0,0,0,1],
23
[1,0,0,0,0,0,0,0,0,0,0,1],
24
[1,0,0,0,0,0,0,0,1,1,0,1],
25
[1,1,0,0,0,0,0,0,0,0,0,1],
26
[1,1,1,1,1,1,1,1,1,1,1,1]];
27
28
var tileWidth:uint = 50;
29
var borderOffsetY:uint = 70;
30
var borderOffsetX:uint = 275;
31
32
var facing:String = "south";
33
var currentFacing:String = "south";
34
var hero:MovieClip=new herotile();
35
hero.clip.gotoAndStop(facing);
36
var heroPointer:Sprite;
37
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class

38
var heroHalfSize:uint=20;
39
40
//the tiles

41
var grassTile:MovieClip=new TileMc();
42
grassTile.gotoAndStop(1);
43
var wallTile:MovieClip=new TileMc();
44
wallTile.gotoAndStop(2);
45
46
//the canvas

47
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
48
addChild(bg);
49
var rect:Rectangle=bg.bitmapData.rect;
50
51
//to handle depth

52
var overlayContainer:Sprite=new Sprite();
53
addChild(overlayContainer);
54
55
//to handle direction movement

56
var dX:Number = 0;
57
var dY:Number = 0;
58
var idle:Boolean = true;
59
var speed:uint = 6;
60
var heroCartPos:Point=new Point();
61
var heroTile:Point=new Point();
62
63
var cornerPoint:Point=new Point();
64
65
//add items to start level, add game loop

66
function createLevel()
67
{
68
	var tileType:uint;
69
	for (var i:uint=0; i<levelData.length; i++)
70
	{
71
		for (var j:uint=0; j<levelData[0].length; j++)
72
		{
73
			tileType = levelData[i][j];
74
			placeTile(tileType,i,j);
75
			if (tileType == 2)
76
			{
77
				levelData[i][j] = 0;
78
			}
79
		}
80
	}
81
	overlayContainer.addChild(heroPointer);
82
	overlayContainer.alpha=0.5;
83
	overlayContainer.scaleX=overlayContainer.scaleY=0.2;
84
	overlayContainer.y=310;
85
	overlayContainer.x=10;
86
	depthSort();
87
	addEventListener(Event.ENTER_FRAME,loop);
88
}
89
90
//place the tile based on coordinates

91
function placeTile(id:uint,i:uint,j:uint)
92
{
93
var pos:Point=new Point();
94
	if (id == 2)
95
	{
96
		
97
		id = 0;
98
		pos.x = j * tileWidth;
99
		pos.y = i * tileWidth;
100
		pos = IsoHelper.twoDToIso(pos);
101
		hero.x = borderOffsetX + pos.x;
102
		hero.y = borderOffsetY + pos.y;
103
		//overlayContainer.addChild(hero);

104
		heroCartPos.x = j * tileWidth;
105
		heroCartPos.y = i * tileWidth;
106
		heroTile.x=j;
107
		heroTile.y=i;
108
		heroPointer=new herodot();
109
		heroPointer.x=heroCartPos.x;
110
		heroPointer.y=heroCartPos.y;
111
		
112
	}
113
	var tile:MovieClip=new cartTile();
114
	tile.gotoAndStop(id+1);
115
	tile.x = j * tileWidth;
116
	tile.y = i * tileWidth;
117
	overlayContainer.addChild(tile);
118
}
119
120
//the game loop

121
function loop(e:Event)
122
{
123
	if (key.isDown(Keyboard.UP))
124
	{
125
		dY = -1;
126
	}
127
	else if (key.isDown(Keyboard.DOWN))
128
	{
129
		dY = 1;
130
	}
131
	else
132
	{
133
		dY = 0;
134
	}
135
	if (key.isDown(Keyboard.RIGHT))
136
	{
137
		dX = 1;
138
		if (dY == 0)
139
		{
140
			facing = "east";
141
		}
142
		else if (dY==1)
143
		{
144
			facing = "southeast";
145
			dX = dY=0.5;
146
		}
147
		else
148
		{
149
			facing = "northeast";
150
			dX=0.5;
151
			dY=-0.5;
152
		}
153
	}
154
	else if (key.isDown(Keyboard.LEFT))
155
	{
156
		dX = -1;
157
		if (dY == 0)
158
		{
159
			facing = "west";
160
		}
161
		else if (dY==1)
162
		{
163
			facing = "southwest";
164
			dY=0.5;
165
			dX=-0.5;
166
		}
167
		else
168
		{
169
			facing = "northwest";
170
			dX = dY=-0.5;
171
		}
172
	}
173
	else
174
	{
175
		dX = 0;
176
		if (dY == 0)
177
		{
178
			//facing="west";

179
		}
180
		else if (dY==1)
181
		{
182
			facing = "south";
183
		}
184
		else
185
		{
186
			facing = "north";
187
		}
188
	}
189
	if (dY == 0 && dX == 0)
190
	{
191
		hero.clip.gotoAndStop(facing);
192
		idle = true;
193
	}
194
	else if (idle||currentFacing!=facing)
195
	{
196
		idle = false;
197
		currentFacing = facing;
198
		hero.clip.gotoAndPlay(facing);
199
	}
200
	if (! idle && isWalkable())
201
	{
202
		heroCartPos.x +=  speed * dX;
203
		heroCartPos.y +=  speed * dY;
204
		
205
		cornerPoint.x -=  speed * dX;
206
		cornerPoint.y -=  speed * dY;
207
		
208
		heroPointer.x=heroCartPos.x;
209
		heroPointer.y=heroCartPos.y;
210
211
		var newPos:Point = IsoHelper.twoDToIso(heroCartPos);
212
		heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth);
213
		depthSort();
214
		//trace(heroTile);

215
	}
216
	tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y;
217
}
218
219
//check for collision tile

220
function isWalkable():Boolean{
221
	var able:Boolean=true;
222
	var newPos:Point =new Point();
223
	newPos.x=heroCartPos.x +  (speed * dX);
224
	newPos.y=heroCartPos.y +  (speed * dY);
225
	switch (facing){
226
		case "north":
227
			newPos.y-=heroHalfSize;
228
		break;
229
		case "south":
230
			newPos.y+=heroHalfSize;
231
		break;
232
		case "east":
233
			newPos.x+=heroHalfSize;
234
		break;
235
		case "west":
236
			newPos.x-=heroHalfSize;
237
		break;
238
		case "northeast":
239
			newPos.y-=heroHalfSize;
240
			newPos.x+=heroHalfSize;
241
		break;
242
		case "southeast":
243
			newPos.y+=heroHalfSize;
244
			newPos.x+=heroHalfSize;
245
		break;
246
		case "northwest":
247
			newPos.y-=heroHalfSize;
248
			newPos.x-=heroHalfSize;
249
		break;
250
		case "southwest":
251
			newPos.y+=heroHalfSize;
252
			newPos.x-=heroHalfSize;
253
		break;
254
	}
255
	newPos=IsoHelper.getTileCoordinates(newPos,tileWidth);
256
	if(levelData[newPos.y][newPos.x]==1){
257
		able=false;
258
	}else{
259
		//trace("new",newPos);

260
	}
261
	return able;
262
}
263
264
//sort depth & draw to canvas

265
function depthSort()
266
{
267
	bg.bitmapData.lock();
268
	bg.bitmapData.fillRect(rect,0xffffff);
269
	var tileType:uint;
270
	var mat:Matrix=new Matrix();
271
	var pos:Point=new Point();
272
	for (var i:uint=0; i<levelData.length; i++)
273
	{
274
		for (var j:uint=0; j<levelData[0].length; j++)
275
		{
276
			tileType = levelData[i][j];
277
			
278
			//pos.x = j * tileWidth;

279
			//pos.y = i * tileWidth;

280
			
281
			pos.x = j * tileWidth+cornerPoint.x;
282
			pos.y = i * tileWidth+cornerPoint.y;
283
			
284
			pos = IsoHelper.twoDToIso(pos);
285
			mat.tx = borderOffsetX + pos.x;
286
			mat.ty = borderOffsetY + pos.y;
287
			if(tileType==0){
288
				bg.bitmapData.draw(grassTile,mat);
289
			}else{
290
				bg.bitmapData.draw(wallTile,mat);
291
			}
292
			if(heroTile.x==j&&heroTile.y==i){
293
				mat.tx=hero.x;
294
				mat.ty=hero.y;
295
				bg.bitmapData.draw(hero,mat);
296
			}
297
298
		}
299
	}
300
	bg.bitmapData.unlock();
301
//add character rectangle

302
}
303
createLevel();

Bitte beachten Sie, dass der Eckpunkt in die entgegengesetzte Richtung zur Positionsaktualisierung des Helden erhöht wird, während er sich bewegt. Dies stellt sicher, dass der Held in Bezug auf den Bildschirm dort bleibt, wo er ist:

1
heroCartPos.x += speed * dX;
2
heroCartPos.y += speed * dY;
3
cornerPoint.x -= speed * dX;
4
cornerPoint.y -= speed * dY;

Die Zeichenlogik ändert sich nur in zwei Zeilen, in denen wir die kartesischen Koordinaten jeder Kachel bestimmen. Wir übergeben einfach den Eckpunkt an den ursprünglichen Punkt, der tatsächlich die obigen Punkte 1, 2 und 3 kombiniert:

1
pos.x = j * tileWidth+cornerPoint.x;
2
pos.y = i * tileWidth+cornerPoint.y;

Ein paar Anmerkungen:

  • Während des Scrollens müssen wir möglicherweise zusätzliche Kacheln an den Bildschirmrändern zeichnen, oder wir sehen, dass Kacheln verschwinden und an den Bildschirmextremen erscheinen.
  • Wenn Sie Kacheln haben, die mehr als einen Platz einnehmen, müssen Sie mehr Kacheln an den Rändern ziehen. Wenn beispielsweise die größte Kachel im gesamten Set X mal Y misst, müssen Sie X weitere Kacheln nach links und rechts und Y weitere Kacheln nach oben und unten ziehen. Dadurch wird sichergestellt, dass die Ecken der größeren Kachel beim Scrollen in oder aus dem Bildschirm weiterhin sichtbar sind.
  • Wir müssen immer noch sicherstellen, dass wir keine leeren Bereiche auf dem Bildschirm haben, während wir in der Nähe der Grenzen des Levels zeichnen.
  • Der Level sollte nur so lange scrollen, bis die extremste Kachel am entsprechenden Bildschirmextrem gezeichnet wird - danach sollte sich der Charakter im Bildschirmraum weiter bewegen, ohne dass der Level scrollt. Dazu müssen wir alle vier Ecken des inneren Bildschirmrechtecks verfolgen und die Scroll- und Spielerbewegungslogik entsprechend drosseln. Sind Sie bereit für die Herausforderung, dies selbst umzusetzen?

Abschluss

Diese Serie richtet sich insbesondere an Einsteiger, die versuchen, isometrische Spielwelten zu erkunden. Viele der erläuterten Konzepte haben alternative Ansätze, die etwas komplizierter sind, und ich habe absichtlich die einfachsten ausgewählt. Sie erfüllen möglicherweise nicht alle Szenarien, denen Sie begegnen können, aber die gewonnenen Erkenntnisse können verwendet werden, um auf diesen Konzepten aufzubauen, um viel kompliziertere Lösungen zu erstellen.