Api's Adventure on tulevaa Infernon peliohjelmointia käsittelevää opastani varten tekemäni esimerkki.
Peli on Infernon Limbo toteutus MBasic:in mukana tulevasta esimerkki pelistä: Pitman.
Pelissä ohjataan Kiinanpalatsikoira Apia ja yritetään kerätä kaikki pelikentällä olevat luut.
Peli on vielä hiukan keskeneräinen, mutta jo nyt mukava ja haastava pelattava.
Pelipaketti datatiedostoineen: http://www.tip9ug.jp/who/jalih/game.zip
# # Just another crappy game from jalski's game factory # # It's still unfinished... # You can do what you like with it. # implement ApiAdventure; include "sys.m"; sys : Sys; include "draw.m"; draw : Draw; Context, Display, Point, Rect, Image, Screen, Font: import draw; include "tk.m"; tk: Tk; Toplevel : import tk; include "tkclient.m"; tkclient : Tkclient; include "keyboard.m"; Up, Down, Right, Left: import Keyboard; include "bufio.m"; bufio : Bufio; Iobuf : import bufio; ApiAdventure: module { init: fn(ctxt: ref Context, argv: list of string); }; EMPTY, WALL, LADDER, WALL2 : con iota; ROCK, BONE : con iota; TILEX : con 49; TILEY : con 49; ROWS : con 8; COLUMNS : con 11; LEFT : con Point(-1, 0); RIGHT : con Point(1, 0); UP : con Point(0, -1); DOWN : con Point(0, 1); AUDIO_MAGIC : con "rate"; AUDIO_DATA : con "/dev/audio"; AUDIO_CTL : con "/dev/audioctl"; AUDIO_BUF_SIZE : con Sys->ATOMICIO; NULL_DEVICE : con "/dev/null"; #SOUND1 : con "./sounds/chord.iaf"; audio_data : string; FONTPATH : con "/fonts/lucida/unicode.18.font"; MSG : con "LEVEL COMPLETE"; font : ref Font; display : ref Display; buffer : ref Image; t: ref Toplevel; tiles := array [4] of ref Image; player_img, player_mask : ref Image; rock_img, rock_mask : ref Image; bone_img, bone_mask : ref Image; # Pelaaja "luokka" Player : adt { pos : Point; img : ref Image; new : fn(x : int, y : int, img : ref Image) : ref Player; draw : fn(player : self ref Player); if_can_fall : fn(player : self ref Player); }; # Objekti "luokka" pelin objekteja varten (luut ja kivet). Object : adt { id : int; pos : Point; img : ref Image; mask : ref Image; new : fn(id : int, x : int, y : int, img : ref Image, mask : ref Image) : ref Object; }; # Objektilista "luokka" Objects : adt { objects : array of ref Object; new : fn() : ref Objects; add : fn(objects : self ref Objects, id : int, x : int, y : int, img : ref Image, mask : ref Image); del : fn(objects : self ref Objects, index : int); sort : fn(objects : self ref Objects); draw : fn(objects : self ref Objects); search_pos : fn(objects : self ref Objects, pos : Point) : (int , ref Object); }; # Pelikenttä "luokka" Level : adt { bones : int; map : array of array of int; objects : ref Objects; player : ref Player; new : fn() : ref Level; draw : fn(l : self ref Level); copy : fn(l : self ref Level) : ref Level; can_objects_fall : fn(l : self ref Level); }; level : ref Level; # Käsiteltävä kenttä. levels : list of ref Level; # kenttälista. back_list : list of ref Level; # Lista kenttien taaksepäin selaamista varten. currentl : int; # Nykyisen kentän numero. # Käyttöliittymän määrittelyt gameui_cfg := array[] of { "frame .f", "button .f.left -bitmap small_color_left.bit -bd 0 -command {send cmdch prevl}", "button .f.right -bitmap small_color_right.bit -bd 0 -command {send cmdch nextl}", "button .f.reset -bitmap sadsmiley.bit -bd 0 -command {send cmdch reset}", "label .f.l -text {level: }", "label .f.level", "pack .f.left .f.right -side left", "pack .f.l -side left", "pack .f.level -side left", "pack .f.reset -side right", "pack .f -fill x", }; # Pääohjelma. Ohjelman suoritus alkaa tästä. init(ctxt: ref Context, nil: list of string) { sys = load Sys Sys->PATH; draw = load Draw Draw->PATH; tk = load Tk Tk->PATH; tkclient = load Tkclient Tkclient->PATH; bufio = load Bufio Bufio->PATH; sys->pctl(Sys->NEWPGRP, nil); tkclient->init(); if(ctxt == nil) ctxt = tkclient->makedrawcontext(); display = ctxt.display; font = Font.open(display, FONTPATH); menubut : chan of string; (t, menubut) = tkclient->toplevel(ctxt, "", "Api's Adventure (with doggy style)", 0); cmdch := chan of string; tk->namechan(t, cmdch, "cmdch"); for (i := 0; i < len gameui_cfg; i++) cmd(t, gameui_cfg[i]); center(t); tkclient->onscreen(t, "exact"); tkclient->startinput(t, "ptr"::"kbd"::nil); #setup_audio(); load_images(); load_levels("./levels/levels.txt"); currentl = 1; # Ekasta kentästä aloitellaan. cmd(t, ".f.level configure -text " + string currentl); back_list = hd levels :: back_list; level = (hd levels).copy(); cmd(t, "panel .p -bd 3 -relief flat"); cmd(t, "pack .p -fill both -expand 1"); tk->putimage(t, ".p", buffer, nil); level.draw(); level.player.draw(); level.objects.draw(); cmd(t, ".p dirty; update"); # Tk-ohjelman viestien käsittely silmukka for(;;) alt { s := <-t.ctxt.kbd => processkbd(s); # Käsitellään näppäimen painallus omassa aliohjelmassa. s := <-t.ctxt.ptr => tk->pointer(t, *s); s := <-t.ctxt.ctl or s = <-t.wreq or s = <-menubut => tkclient->wmctl(t, s); c := <- cmdch => case c { "nextl" => # Nuoli oikealle nappulaa painettu if (tl levels != nil) { back_list = hd levels :: back_list; levels = tl levels; level = (hd levels).copy(); currentl ++; cmd(t, ".f.level configure -text " + string currentl); level.draw(); level.player.draw(); level.objects.draw(); #cmd(t, "focus .p"); cmd(t, ".p dirty; update"); } "prevl" => # Nuoli vasemmalle nappulaa painettu. if (tl back_list != nil) { levels = hd back_list :: levels; back_list = tl back_list; level = (hd levels).copy(); currentl --; cmd(t, ".f.level configure -text " + string currentl); level.draw(); level.player.draw(); level.objects.draw(); #cmd(t, "focus .p"); cmd(t, ".p dirty; update"); } "reset" => # Surunaama nappulaa painettu level = (hd levels).copy(); level.draw(); level.player.draw(); level.objects.draw(); #cmd(t, "focus .p"); cmd(t, ".p dirty; update"); } } } # Täällä käsitellään painallukset näppäimistöltä. processkbd(kbd: int) { case kbd { Up => npos := level.player.pos.add(UP); if (npos.y >= ROWS || npos.y < 0) break; (nil, found) := level.objects.search_pos(npos); if ((level.map[level.player.pos.x][level.player.pos.y] == LADDER && level.map[npos.x][npos.y] == LADDER) || (level.map[level.player.pos.x][level.player.pos.y] == LADDER && (level.map[npos.x][npos.y] == EMPTY && found == nil))) { draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); level.player.pos = npos; level.player.draw(); } Down => npos := level.player.pos.add(DOWN); if (npos.y >= ROWS || npos.y < 0) break; (nil, obj) := level.objects.search_pos(npos); if (obj != nil) break; if (level.map[npos.x][npos.y] == LADDER || (level.map[level.player.pos.x][level.player.pos.y] == LADDER && level.map[npos.x][npos.y] == EMPTY)) { draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); level.player.pos = npos; level.player.if_can_fall(); level.player.draw(); level.can_objects_fall(); level.objects.draw(); } Left => npos := level.player.pos.add(LEFT); if (npos.x >= COLUMNS || npos.x < 0) break; if (level.map[npos.x][npos.y] != WALL) { (index, obj) := level.objects.search_pos(npos); if (obj != nil) { case obj.id { ROCK => nrpos := obj.pos.add(LEFT); if (nrpos.x < COLUMNS && nrpos.x >= 0) { (nil, tobj) := level.objects.search_pos(nrpos); if (tobj == nil) if (level.map[nrpos.x][nrpos.y] == EMPTY) { draw_tile(level.map[obj.pos.x][obj.pos.y], obj.pos); obj.pos = nrpos; draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); level.player.pos = npos; if (level.map[level.player.pos.x][level.player.pos.y] != LADDER) level.player.if_can_fall(); level.objects.sort(); level.can_objects_fall(); level.objects.draw(); level.player.draw(); } } BONE => level.objects.del(index); level.bones --; draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); draw_tile(level.map[npos.x][npos.y], npos); level.player.pos = npos; if (level.map[level.player.pos.x][level.player.pos.y] != LADDER) level.player.if_can_fall(); level.objects.sort(); level.can_objects_fall(); level.objects.draw(); level.player.draw(); } } else { if (level.map[npos.x][npos.y] == WALL2) { level.map[npos.x][npos.y] = EMPTY; draw_tile(level.map[npos.x][npos.y], npos); } draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); level.player.pos = npos; if (level.map[level.player.pos.x][level.player.pos.y] != LADDER) level.player.if_can_fall(); level.objects.sort(); level.can_objects_fall(); level.objects.draw(); level.player.draw(); } } Right => npos := level.player.pos.add(RIGHT); if (npos.x >= COLUMNS || npos.x < 0) break; if (level.map[npos.x][npos.y] != WALL) { (index, obj) := level.objects.search_pos(npos); if (obj != nil) { case obj.id { ROCK => nrpos := obj.pos.add(RIGHT); if (nrpos.x < COLUMNS && nrpos.x >= 0) { (nil, tobj) := level.objects.search_pos(nrpos); if (tobj == nil) if (level.map[nrpos.x][nrpos.y] == EMPTY) { draw_tile(level.map[obj.pos.x][obj.pos.y], obj.pos); obj.pos = nrpos; draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); level.player.pos = npos; if (level.map[level.player.pos.x][level.player.pos.y] != LADDER) level.player.if_can_fall(); level.objects.sort(); level.can_objects_fall(); level.objects.draw(); level.player.draw(); } } BONE => level.objects.del(index); level.bones --; draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); draw_tile(level.map[npos.x][npos.y], npos); level.player.pos = npos; if (level.map[level.player.pos.x][level.player.pos.y] != LADDER) level.player.if_can_fall(); level.objects.sort(); level.can_objects_fall(); level.objects.draw(); level.player.draw(); } } else { if (level.map[npos.x][npos.y] == WALL2) { level.map[npos.x][npos.y] = EMPTY; draw_tile(level.map[npos.x][npos.y], npos); } draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos); level.player.pos = npos; if (level.map[level.player.pos.x][level.player.pos.y] != LADDER) level.player.if_can_fall(); level.objects.sort(); level.can_objects_fall(); level.objects.draw(); level.player.draw(); } } * => return; } if (level.bones <= 0) { red := display.color(Draw->Red); textw := font.width(MSG); texth := font.height; buffer.text(Point(COLUMNS * TILEX / 2 - (textw / 2), ROWS * TILEY / 2 - texth), red, Point(0,0), font, MSG); cmd(t, ".p dirty; update"); #play_audio_file(SOUND1); sys->sleep(500); if (tl levels != nil) { back_list = hd levels :: back_list; levels = tl levels; level = (hd levels).copy(); currentl ++; cmd(t, ".f.level configure -text " + string currentl); level.draw(); level.player.draw(); level.objects.draw(); cmd(t, ".p dirty; update"); } else exit; } cmd(t, ".p dirty; update"); } setup_audio() { df := sys->open(AUDIO_DATA, Sys->OWRITE); if (df == nil) audio_data = NULL_DEVICE; else audio_data = AUDIO_DATA; } play_audio_file(f: string) { buff := array[AUDIO_BUF_SIZE] of byte; inf := sys->open(f, Sys->OREAD); if (inf == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: could not open %s: %r\n", f); return; } n := sys->read(inf, buff, AUDIO_BUF_SIZE); if (n < 0) { sys->fprint(sys->fildes(2), "Api's Adventure: could not read %s: %r\n", f); return; } if (n < 10 || string buff[0:4] != AUDIO_MAGIC) { sys->fprint(sys->fildes(2), "Api's Adventure: %s: not an audio file\n", f); return; } i := 0; for (;;) { if (i == n) { sys->fprint(sys->fildes(2), "Api's Adventure: %s: bad header\n", f); return; } if (buff[i] == byte '\n') { i++; if (i == n) { sys->fprint(sys->fildes(2), "Api's Adventure: %s: bad header\n", f); return; } if (buff[i] == byte '\n') { i++; if ((i % 4) != 0) { sys->fprint(sys->fildes(2), "Api's Adventure: %s: unpadded header\n", f); return; } break; } } else i++; } cf := sys->open(AUDIO_CTL, Sys->OWRITE); if (cf != nil) { if (sys->write(cf, buff, i - 1) < 0) { sys->fprint(sys->fildes(2), "Api's Adventure: could not write %s: %r\n", AUDIO_CTL); exit; } } df := sys->open(audio_data, Sys->OWRITE); if (df == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: could not open %s: %r\n", audio_data); return; } if (n > i && sys->write(df, buff[i:n], n - i) < 0) { sys->fprint(sys->fildes(2), "Api's Adventure: could not write %s: %r\n", audio_data); return; } if (sys->stream(inf, df, Sys->ATOMICIO) < 0) { sys->fprint(sys->fildes(2), "Api's Adventure: could not stream %s: %r\n", audio_data); return; } } Player.new(x : int, y : int, img : ref Image) : ref Player { player := ref Player(Point(x,y), img); return player; } Player.if_can_fall(player : self ref Player) { pos := player.pos; if (level.map[pos.x][pos.y] != EMPTY) return; for(i := pos.y + 1; i < ROWS; i ++) { (nil, found) := level.objects.search_pos(Point(pos.x, i)); if (level.map[pos.x][i] != EMPTY || found != nil) break; pos.y = i; } if (!player.pos.eq(pos)) { draw_tile(level.map[player.pos.x][player.pos.y], player.pos); player.pos = pos; } } Object.new(id : int, x : int, y : int, img : ref Image, mask : ref Image) : ref Object { object := ref Object(id, Point(x, y), img, mask); return object; } Objects.new() : ref Objects { objects := ref Objects; return objects; } Objects.add(objects : self ref Objects, id : int, x : int, y : int, img : ref Image, mask : ref Image ) { i := len objects.objects; nobjects := array [i + 1] of ref Object; (nobjects[0:], nobjects[i], nobjects[i + 1:]) = (objects.objects[0:i], Object.new(id, x, y, img, mask), objects.objects[i:]); objects.objects = nobjects; } Objects.del(objects : self ref Objects, index : int) { if (len objects.objects >= 1) { objects.objects[index:] = objects.objects[index + 1:]; objects.objects = objects.objects[:len objects.objects - 1]; } } Objects.search_pos(objects : self ref Objects, pos : Point) : (int, ref Object) { count := len objects.objects; if (count < 1) { return (0, nil); } for(i := 0; i < count; i ++) { if (objects.objects[i].pos.eq(pos)) { return (i, objects.objects[i]); } } return (0, nil); } Objects.draw(objects : self ref Objects) { count := len objects.objects; for(i := 0; i < count; i ++) { draw_sprite(objects.objects[i].img, objects.objects[i].mask, objects.objects[i].pos); } } Objects.sort(objects : self ref Objects) { count := len objects.objects; for(i := 1; i < count ; i ++) { v := objects.objects[i]; j := i; while( ( j > 0 ) && ( objects.objects[j - 1].pos.y < v.pos.y )) { objects.objects[j] = objects.objects[j - 1]; j --; } objects.objects[j] = v; } } load_images() { tiles[0] = display.open("./data/empty.bit"); if (tiles[0] == nil) { sys->fprint(sys->fildes(2), "Apis's Adventure: failed to allocate image\n"); exit; } tiles[1] = display.open("./data/wall.bit"); if (tiles[1] == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } tiles[2] = display.open("./data/ladder.bit"); if (tiles[2] == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } tiles[3] = display.open("./data/wall2.bit"); if (tiles[3] == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } rock_img = display.open("./data/rock.bit"); if (rock_img == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } rock_mask = display.open("./data/rock_mask.bit"); if (rock_mask == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } player_img = display.open("./data/api.bit"); if (player_img == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } player_mask = display.open("./data/api_mask.bit"); if (player_mask == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } bone_img = display.open("./data/bone.bit"); if (bone_img == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } bone_mask = display.open("./data/bone_mask.bit"); if (bone_mask == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } r := Rect((0, 0), (COLUMNS * TILEX, ROWS * TILEY)); buffer = display.newimage(r, t.image.chans, 0, Draw->Black); if (buffer == nil) { sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n"); exit; } } draw_tile(tile : int, p : Point) { p.x = p.x * tiles[tile].r.dx(); p.y = p.y * tiles[tile].r.dy(); buffer.draw(tiles[tile].r.addpt(p), tiles[tile], nil, Point(0, 0) ); } Level.draw(l : self ref Level) { for(row := 0; row < ROWS; row ++) for(column := 0; column < COLUMNS; column ++) { p := Point( column * TILEX, row * TILEY ); buffer.draw(tiles[l.map[column][row] ].r.addpt(p), tiles[l.map[column][row] ], nil, Point(0, 0) ); } } Level.new() : ref Level { l := ref Level; return l; } Level.copy(l: self ref Level): ref Level { c := ref *l; c.map = array[COLUMNS] of { * => array[ROWS] of int }; for(i := 0; i < COLUMNS; i++) c.map[i][0:] = l.map[i]; c.objects = Objects.new(); for(i = 0; i < len l.objects.objects ; i++) c.objects.add(l.objects.objects[i].id, l.objects.objects[i].pos.x, l.objects.objects[i].pos.y, l.objects.objects[i].img, l.objects.objects[i].mask); c.player = ref *l.player; return c; } Level.can_objects_fall(l : self ref Level) { count := len l.objects.objects; for(j := 0; j < count; j ++) { pos := l.objects.objects[j].pos; for(i := pos.y + 1; i < ROWS; i ++) { if (l.map[pos.x][i] != EMPTY || Point(pos.x, i).eq(l.player.pos)) break; (nil, found) := l.objects.search_pos(Point(pos.x, i)); if (found != nil) break; pos.y = i; } if (!l.objects.objects[j].pos.eq(pos)) { draw_tile(l.map[l.objects.objects[j].pos.x][l.objects.objects[j].pos.y], l.objects.objects[j].pos); l.objects.objects[j].pos = pos; } } } Player.draw(player : self ref Player) { draw_sprite(player.img, player_mask, player.pos); } draw_sprite(image : ref Image, mask : ref Image, p : Point) { p.x = p.x * image.r.dx(); p.y = p.y * image.r.dy(); buffer.draw(image.r.addpt(p), image, mask, Point(0, 0) ); } load_levels(file: string) { x := 0; y := 0; max := Point(0, 0); buf := bufio->open(file, Bufio->OREAD); if (buf == nil) { sys->fprint(sys->fildes(2), "Api's Adventure level parser: cannot load %s: %r", file); exit; } bones := 0; map := array [COLUMNS] of { * => array [ROWS] of { * => EMPTY } }; player := Player.new(0, 0, player_img); objects := Objects.new(); line := 1; while((c := buf.getc()) >= 0) { case c { ';' => while((c = buf.getc()) != '\n' && c != Bufio->EOF) ; line ++; '\n' => max.y = ++ y; if(x != COLUMNS) { sys->fprint(sys->fildes(2), "Api's Adventure level parser: error on line: %d. Level map must have %d columns\n", line, COLUMNS); exit; } line ++; x = 0; if((c = buf.getc()) == '\n' || c == Bufio->EOF) { if (max.y != ROWS) { sys->fprint(sys->fildes(2), "Api's Adventure level parser: error on line: %d. Level map must have %d rows\n", line, ROWS); exit; } lev := Level.new(); lev.bones = bones; lev.map = map; lev.player = player; lev.objects = objects; levels = lev :: levels; bones = 0; map = array[COLUMNS] of { * => array[ROWS] of { * => EMPTY } }; player = Player.new(0, 0, player_img); objects = Objects.new(); max = Point(0, 0); y = 0; line ++; } else buf.ungetc(); '0' => map[x][y] = EMPTY; x ++; '1' => map[x][y] = WALL; x ++; '2' => map[x][y] = LADDER; x ++; '3' => map[x][y] = WALL2; x ++; '@' => player.pos = Point(x, y); x ++; 'o' => objects.add(ROCK, x, y, rock_img, rock_mask); objects.sort(); x ++; 'b' => objects.add(BONE, x, y, bone_img, bone_mask); objects.sort(); bones ++; x ++; * => sys->fprint(sys->fildes(2), "Api's Adventure level parser: impossible character: '%c' for level in line: %d\n", c, line); exit; } if (x > max.x) max.x = x; if (x >= COLUMNS || y >= ROWS) { if((c = buf.getc()) != '\n') { sys->fprint(sys->fildes(2), "Api's Adventure level parser: error on line: %d. Level map must be: %dx%d\n", line, COLUMNS, ROWS); exit; } buf.ungetc(); } } } cmd(win: ref Tk->Toplevel, s: string): string { r := tk->cmd(win, s); if (len r > 0 && r[0] == '!') { sys->print("error executing '%s': %s\n", s, r[1:]); } return r; } # Keskittää peli-ikkunan. center(t: ref Tk->Toplevel) { org: Point; ir := tk->rect(t, ".", Tk->Border|Tk->Required); org.x = t.screenr.dx() / 2 - COLUMNS * TILEX / 2; org.y = (t.screenr.dy() / 2) - (ir.dy() + (ROWS * TILEY) / 2); if (org.y < 0) org.y = 0; tk->cmd(t, ". configure -x " + string org.x + " -y " + string org.y); }
Aihe on jo aika vanha, joten et voi enää vastata siihen.