ActionScript 3
Podstawy tworzenia gier
Szukaj…
Wprowadzenie
izometryczna animacja postaci + ruch
Concepts koncepcje zastosowane w tym przykładzie:
Klasa | Stosowanie |
---|---|
URLRequest + Loader + Event | Ładowanie mapy atlasu (duszka) ze ścieżki zewnętrznej. |
BitmapData + Sprite + beginBitmapFill + Matrix + stageWidth & stageHeight | rysowanie załadowanych zasobów do bitmapdata, za pomocą bitmap kafelkowych, rysowanie z transformacją. |
MovieClip + scrollRect + Bitmap + Rectangle | tworzenie i klikanie klipu filmowego używając Bitmapy jako osi czasu. |
KeyboardEvent | wykrywanie danych wejściowych użytkownika |
Event.EXIT_FRAME | implementacja funkcji pętli gry |
② Zasoby: (brak pozwolenia na wykorzystanie tych zasobów do celów komercyjnych)
③ Kod i komentarze:
Uwaga: FPS 15 Używany w tym samouczku, jest zalecany, ale jeśli potrzebujesz więcej, musisz samodzielnie zmodyfikować część kodu.
najpierw musimy pobrać nasze zasoby z zewnętrznych adresów URL.
const src_grass_tile_url:String = "https://i.stack.imgur.com/sjJFS.png";
const src_character_atlas_url:String = "https://i.stack.imgur.com/B7ztZ.png";
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, setGround);
loader.load(new URLRequest(src_grass_tile_url));
setGround zostanie caled, gdy src_grass_tile_url
zostanie załadowany i będzie gotowy do użycia. w trakcie wdrażania setGround, aby zdobyć zasoby i narysować je jako tło gry
function setGround(e:Event):void {
/* drawing ground */
/* loader is a displayObject, so we can simply draw it into the bitmap data*/
/* create an instance of Bitmapdata with same width and height as our window*/
/* (also set transparent to false because grass image, does not contains any transparent pixel) */
var grass_bmd:BitmapData = new BitmapData(loader.width, loader.height, false, 0x0);
/* time to draw */
grass_bmd.draw(loader); // drawing loader into the bitmapData
/* now we have to draw a tiled version of grass_bmd inside a displayObject Sprite to displaying
BitmapData on stage */
var grass_sprite:Sprite = new Sprite();
// for drawing a bitmap inside sprite, we must use <beginBitmapFill> with graphic property of the sprite
// then draw a full size rectangle with that Fill-Data
// there is a repeat mode argument with true default value so we dont set it true again.
// use a matrix for scalling grass Image during draw to be more cute!
var mx:Matrix = new Matrix();
mx.scale(2, 2);
grass_sprite.graphics.beginBitmapFill(grass_bmd, mx);
grass_sprite.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
// now add sprite to displayobjectcontainer to be displayed
stage.addChild(grass_sprite);
// well done, ground is ready, now we must initialize our character
// first, load its data, i just re-use my loader for loading new image, but with another complete handler (setCharacter)
// so remove existing handler, then add new one
loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, setGround);
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, setCharacter);
loader.load(new URLRequest(src_character_atlas_url));
}
kod jest cholernie skomentowany, po tym jak skończyliśmy z gruntem, nadszedł czas na implementację charakteru. postać zawiera także zasób, który musi być ładowany w ten sam sposób. więc na końcu setGround
kierujemy się do setCharacter
który jest kolejnym kompletnym wywołaniem zwrotnym.
function setCharacter(e:Event):void {
// let assuming that what is really character!
// a set of images inside a single image!
// that images are frames of our character (also provides movement for different directions)
// first load this
var character_bmd:BitmapData = new BitmapData(loader.width, loader.height, true, 0x0); // note character is transparent
character_bmd.draw(loader);
// take a look at sprite sheet, how many frames you see?
// 42 frames, so we can get width of a single frame
const frame_width:uint = character_bmd.width / 42; // 41 pixels
// as i show you above, to displaying a BitmapData, we have to draw it using a DisplayObject,
// another way is creating a Bitmap and setting its bitmapdata
var character_bmp:Bitmap = new Bitmap(character_bmd);
// but its not enough yet, a movieClip is necessary to cliping and animating this bitmap (as a child of itself)
var character_mc:MovieClip = new MovieClip();
character_mc.addChild(character_bmp);
character_bmp.name = "sprite_sheet"; // setting a name to character_bmp, for future accessing
character_mc.scrollRect = new Rectangle(0, 0, frame_width, character_bmd.height); // cliping movieclip, to dusplaying only one frame
character_mc.name = "character"; // setting a name to character_mc, for future accessing
stage.addChild(character_mc); // adding it to stage.
// well done, we have a character, but its static yet! 2 steps remaining. 1 controlling 2 animating
// at first setting a control handler for moving character in 8 directions.
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, keyUp);
}
teraz postać jest gotowa do kontrolowania. jest dobrze wyświetlany i gotowy do sterowania. więc dołączone są zdarzenia klawiatury i nasłuchiwanie klawiszy strzałek w następujący sposób:
// we storing key stats inside <keys> Object
var keys:Object = {u:false, d:false, l:false, r:false};
function keyDown(e:KeyboardEvent):void {
switch (e.keyCode) {
case 38: //up
keys.u = true;
break;
case 40: //down
keys.d = true;
break;
case 37: //left
keys.l = true;
break;
case 39: //right
keys.r = true;
break;
}
}
function keyUp(e:KeyboardEvent):void {
switch (e.keyCode) {
case 38: //up
keys.u = false;
break;
case 40: //down
keys.d = false;
break;
case 37: //left
keys.l = false;
break;
case 39: //right
keys.r = false;
break;
}
}
keys:Object
przechowuje zmienną logiczną 4 dla każdego klawisza strzałki, proces przenoszenia musi odbywać się wewnątrz funkcji aktualizacji (Pętli) w grze, więc musimy przekazać mu statystyki klawiatury. pozwala na implementację funkcji Loop .
// initialize game Loop function for updating game objects
addEventListener(Event.EXIT_FRAME, loop);
// speed of character movement
const speed:Number = 5;
// this function will be called on each frame, with same rate as your project fps
function loop(e:Event):void {
if (keys.u) stage.getChildByName("character").y -= speed;
else if (keys.d) stage.getChildByName("character").y += speed;
if (keys.l) stage.getChildByName("character").x -= speed;
else if (keys.r) stage.getChildByName("character").x += speed;
}
prędkość jest stałą pomocniczą, określa prędkość charakteru. powyższy kod przedstawia prosty ruch w 8 kierunkach z niskim priorytetem: w Up > Down
Left > Right
. więc jeśli strzałka w górę i w dół zostanie naciśnięta w tym samym czasie, postać porusza się tylko w górę (nie zamrażając).
dobra robota!!! pozostał tylko jeden krok, animacja, najważniejsza część tego samouczka
czym tak naprawdę jest animacja? zestaw klatek kluczowych zawierający co najmniej jedną ramkę
pozwala na tworzenie naszego obiektu klatek kluczowych, który zawiera nazwy klatek kluczowych
a także niektóre dane dotyczące początkowej i końcowej ramki tej klatki kluczowej
Uwaga : w grach izometrycznych każda klatka kluczowa zawiera 8 kierunków (można ją zmniejszyć do 5 za pomocą przerzucania)
var keyframs:Object = {
idle: {up:[0,0], up_right:[1,1], right:[2,2], down_right:[3,3], down:[4,4]}, // [2,2] means start frame is 2 and end frame is 2
run: {up:[5,10], up_right:[11,16], right:[17,22], down_right:[23,28], down:[29,34]}
};
powinniśmy zignorować pozostałe klatki, ten przykład zapewnia tylko bezczynność i uruchomienie animacji
na przykład klatka początkowa bezczynnej animacji z kierunkiem w prawo to: <keyframs.idle.right [0]>
teraz pozwala na implementację funkcji Animatora
var current_frame:uint;
function animate(keyframe:Array):void {
// how it works
// just called with a keyframe with direction (each frame),
// if keyframe is what is already playing, its just moved to next frame and got updated (or begning frame for loop)
// other wise, just moved to begining frame of new keyframe
if (current_frame >= keyframe[0] && current_frame <= keyframe[1]) { // check if in bound
current_frame++;
if (current_frame > keyframe[1]) // play back if reached
current_frame = keyframe[0];
} else {
current_frame = keyframe[0]; // start new keyframe from begining
}
// moving Bitmap inside character MovieClip
var character:MovieClip = stage.getChildByName("character") as MovieClip;
var sprite_sheet:Bitmap = character.getChildByName("sprite_sheet") as Bitmap;
sprite_sheet.x = -1 * current_frame * character.width;
}
czytaj komentarze do powyższej funkcji, jednak głównym zadaniem tej funkcji jest przenoszenie Bitmap
sprite_sheet wewnątrz znaku MovieClip
.
wiemy, że każdą aktualizację należy wykonać wewnątrz funkcji Loop, więc wywołamy tę funkcję z Loop z powiązanymi klatkami kluczowymi. to jest zaktualizowana funkcja Loop:
// speed of character movement
const speed:Number = 8;
var last_keyStat:Object = {u:false, d:false, l:false, r:false}; // used to getting a backup of previous keyboard stat for detecting correct idle direction
// this function will be called on each frame, with same rate as your project fps
function loop(e:Event):void {
if (keys.u) stage.getChildByName("character").y -= speed;
else if (keys.d) stage.getChildByName("character").y += speed;
if (keys.l) stage.getChildByName("character").x -= speed;
else if (keys.r) stage.getChildByName("character").x += speed;
// animation detection
if (keys.u && keys.l) { animate(keyframs.run.up_right); flip(true); }
else if (keys.u && keys.r) { animate(keyframs.run.up_right); flip(false); }
else if (keys.d && keys.l) { animate(keyframs.run.down_right); flip(true); }
else if (keys.d && keys.r) { animate(keyframs.run.down_right); flip(false); }
else if (keys.u) { animate(keyframs.run.up); flip(false); }
else if (keys.d) { animate(keyframs.run.down); flip(false); }
else if (keys.l) { animate(keyframs.run.right); flip(true); }
else if (keys.r) { animate(keyframs.run.right); flip(false); }
else {
// if character dont move, so play idle animation
// what is the best practice to detecting idle direction?
// my suggestion is to sotring previous keyboard stats to determining which idle direction is correct
// do any better thing if possible (absolutely is possible)
// i just simply copy it from above, and replaced (keys) with (last_keyStat) and (run) with (idle)
if (last_keyStat.u && last_keyStat.l) { animate(keyframs.idle.up_right); flip(true); }
else if (last_keyStat.u && last_keyStat.r) { animate(keyframs.idle.up_right); flip(false); }
else if (last_keyStat.d && last_keyStat.l) { animate(keyframs.idle.down_right); flip(true); }
else if (last_keyStat.d && last_keyStat.r) { animate(keyframs.idle.down_right); flip(false); }
else if (last_keyStat.u) { animate(keyframs.idle.up); flip(false); }
else if (last_keyStat.d) { animate(keyframs.idle.down); flip(false); }
else if (last_keyStat.l) { animate(keyframs.idle.right); flip(true); }
else if (last_keyStat.r) { animate(keyframs.idle.right); flip(false); }
}
// update last_keyStat backup
last_keyStat.u = keys.u;
last_keyStat.d = keys.d;
last_keyStat.l = keys.l;
last_keyStat.r = keys.r;
}
czytając komentarze, po prostu wykrywamy prawdziwą klatkę kluczową za pomocą statystyk klawiatury. następnie zrób to samo w celu wykrycia bezczynności animacji. w przypadku bezczynnych animacji nie musimy używać klawiszy do wykrywania, w którym kierunku jest włączony znak, więc zmienna pomocnika simle może być przydatna do przechowywania poprzedniego stanu klawiatury (last_keyStat).
pojawiła się także nowa funkcja flip
która jest kolejną funkcją pomocniczą używaną do symulacji brakujących animacji (left + up_left + down_left) również ta funkcja wprowadza kilka poprawek, które zostały skomentowane poniżej:
// usage of flip function is because of Movieclip registration point, its a fix
// as the registration point of MovieClip is not placed in center, when flipping animation (for non existing directions inside spritesheet)
// character location changes with an unwanted value equal its width, so we have to prevent this and push it back or forward during flip
function flip(left:Boolean):void {
var character:MovieClip = stage.getChildByName("character") as MovieClip;
if (left) {
if (character.scaleX != -1) {
character.scaleX = -1;
character.x += character.width; // comment this line to see what happen without this fix
}
} else {
if (character.scaleX != 1) {
character.scaleX = 1;
character.x -= character.width; // comment this line to see what happen without this fix
}
}
}
nasza praca tutaj się kończy. Specjalne podziękowania dla redaktorów, dzięki którym ten samouczek jest bardziej niezrozumiały. także Oto prezentacja tego samouczka na żywo oraz zewnętrzny link pełnego kodu.