Однажды, возвращаясь из командировки, мне необходимо было как-то скрасить своё время. Гостиничный Wi-Fi само собой был в недосягаемости, а телефон в роуминге. Так Я познакомился с TouchDevelop, который давно был установлен на телефон, но в котором не было времени основательно покопаться. Несколько часов пролетели под написанием калькулятора, умеющего конвертировать величины.
Что такое TouchDevelop
Некоторую ясность может внести следущее видео:
Но если быть чуть чётче, то TouchDevelop — это приложение для Windows Phone от Microsoft Research. Оно позволяет писать скрипты, при этом основной упор сделан на удобное взаимодействие с разработчиком. В TouchDevelop присутствует ООП, есть стандартная библиотека классов для взаимодействия с телефоном (ввод/вывод данных, проигрывание музыки, видео, работа с фотокамерой, акселерометром, работа с графикой и т.д.), а также своё коммьюнити с облаком, в котором мы делимся своими результатами, смотрим что делают другие и учимся у них и которое мы можем использовать для реализации рейтинга игроков (в случае, если мы делаем игрушку) и всё это opensource (да, форки тоже можно делать). Если вы заинтересовались, то прошу под кат, где будет разобран пример разработки тетриса.
Пишем скрипт
Те, у кого нет Windows Phone, могут приблизительно понять как происходит разработка по следующему видео:
Для тех же у кого Windows Phone есть, нужно учесть, что статья приобретает некий интерактивный характер (т.к. на невзломанном телефоне нельзя делать скриншоты). Итак, для начала необходимо скачать приложение на Маркетплейсе: скачать TouchDevelop. После этого необходимо залогиниться и нажать плюсик внизу и дать название своему скрипту. TouchDevelop позволяет использовать классы, события, глобальные переменные, ресурсы и библиотеки (ссылки на другие скрипты). Сам тетрис будет выглядеть классическим образом:
Итак, для начала определим главную функцию. Нам нужно будет определить формы фигур, поле, на котором мы играем, начальную инициализацию игры и отрисовку самих фигур, для этого в методе main напишем:
action main() {
code→readShapeDefiniton;
code→initGame;
code→drawLines;
code→drawShape();
}
Сами фигуры мы опишем через JSON:
action readShapeDefiniton() {
$s := "{"Shapes": [n{"Name": "I-Shape",n"Tiles": [n{"x":0, "y":0},n{"x":0, "y":-1},n{"x":0, "y":1},n{"x":0, "y":2}]n},{"Name": "L-Shape",n"Tiles": [n{"x":0, "y":0},n{"x":0, "y":-1},n{"x":0, "y":1},n{"x":1, "y":1}]n},{"Name": "Z-Shape",n"Tiles": [n{"x":0, "y":0},n{"x":-1, "y":0},n{"x":0, "y":1},n{"x":1, "y":1}]n},n{"Name": "T-Shape",n"Tiles": [n{"x":0, "y":0},n{"x":-1, "y":0},n{"x":1, "y":0},n{"x":0, "y":1}]},n{"Name": "J-Shape",n"Tiles": [n{"x":0, "y":0},n{"x":0, "y":-1},n{"x":0, "y":1},n{"x":-1, "y":1}]},n{"Name": "S-Shape",n"Tiles": [n{"x":0, "y":0},n{"x":1, "y":0},n{"x":0, "y":1},n{"x":-1, "y":1}]},n{"Name": "O-Shape",n"Tiles": [n{"x":-1, "y":0},n{"x":0, "y":0},n{"x":0, "y":1},n{"x":-1, "y":1}]}n]}";
data→ShapeDefinition := web→json($s);
meta private;
}
Инициализация будет выглядеть следующим образом:
action initGame() {
data→GameOver := false;
data→BoardHeight := 768;
data→TileSize := 34;
data→GameBoardHeight := 20 * data→TileSize;
data→GameBoardWidth := data→TileSize * 10;
data→GameBoardY := data→BoardHeight - data→GameBoardHeight;
data→Board := media→create_full_board;
data→CurrentShape := data→Board→create_sprite_set;
data→ShapeStack := data→Board→create_sprite_set;
data→NextShape := data→Board→create_sprite_set;
data→Board→create_boundary(0);
$<hh user=bottom> := data→Board→create_rectangle(data→GameBoardWidth, 10);
$<hh user=bottom>→set_pos(data→GameBoardWidth / 2, data→BoardHeight + 5);
data→RowChecker := data→Board→create_rectangle(data→GameBoardWidth, data→TileSize - 2);
data→RowChecker→set_pos(data→GameBoardWidth / 2, data→GameBoardY - data→TileSize / 2);
data→RowChecker→hide;
data→ShapeStack→add($<hh user=bottom>);
data→Board→post_to_wall;
data→lnterval := 1000;
data→ScoreSprite := data→Board→create_text(100, 20, 40, "0");
data→ScoreSprite→set_pos(60, 20);
data→PreviewX := data→GameBoardWidth + data→TileSize / 2 * 5;
data→PreviewY := data→GameBoardY + data→TileSize / 2 * 5;
data→FullRowCount := 0;
data→ShapeCount := 0;
data→Level := 0;
data→Drop := false;
data→Score := 0;
code→preview;
meta private;
}
var Board : Board {
}
Делаем отрисовку фигуры и задаём глобальную переменную, обозначающую текущую фигуру:
action drawShape() {
foreach sprite in data→NextShape where true do {
data→CurrentShape→add_from(data→NextShape, $sprite);
$sprite→hide;
}
code→move(data→TileSize * - 7, - data→TileSize * 4, true);
code→setShapePos;
code→preview;
data→Board→update_on_wall;
meta private;
}
var CurrentShape : Sprite_Set {
}
Что ж помимо того, чтобы фигура отрисовывалась, необходимо ей управлять: перемещать влево, вправо, ускорять перемещение вниз, а также вращать:
action rotateShape() {
... ;
code→rotateShapeBy(90);
if code→collisionCheck then {
code→rotateShapeBy( - 90);
}
else {
$left := data→ShapeLeft;
if $left < 0 then {
code→moveShape($left * - 1, 0);
if code→collisionCheck then {
code→moveShape($left, 0);
code→rotateShapeBy( - 90);
}
}
$right := data→ShapeRight - data→GameBoardWidth;
if $right > 0 then {
code→moveShape($right * - 1, 0);
if code→collisionCheck then {
code→moveShape($right, 0);
code→rotateShapeBy( - 90);
}
}
}
meta private;
}
var ShapeDefinition : Json_Object {
}
action moveShape(xd: Number, yd: Number) returns b1: Boolean {
$b1 := code→move($xd, $yd, false);
meta private;
}
var ShapeStack : Sprite_Set {
}
Добавим событие вызова цикла игры:
event gameloop() {
code→gameLoop;
meta private;
}
И опишем его:
action gameLoop() {
$interval := data→lnterval;
if data→Drop then {
$interval := 80;
}
if data→LastMove→add_milliseconds($interval)→less(time→now) then {
$collided := code→moveShape(0, data→TileSize);
if $collided then {
data→Drop := false;
code→putShapeOnStack;
data→ShapeCount := data→ShapeCount + 1;
code→checkRows;
data→Score := data→FullRowCount * 100 + data→ShapeCount * 10;
if math→floor(data→Score / 1000) > data→Level then {
data→lnterval := data→lnterval * 0.8;
data→Level := data→Level + 1;
}
data→ScoreSprite→set_text(data→Score ∥ "");
if data→GameOver then {
$sprite := data→Board→create_text(460, 260, 80, "Game Over");
... ;
$sprite→set_pos(240, 400);
data→Board→update_on_wall;
time→sleep(2);
bazaar→post_leaderboard_score(data→Score);
bazaar→post_leaderboard_to_wall;
}
else {
code→drawShape;
}
}
else {
code;
}
data→LastMove := time→now;
data→Board→update_on_wall;
}
meta private;
}
Проверка коллизий и добавление фигур на поле:
action collisionCheck() returns collision: Boolean {
$collision := false;
foreach sprite in data→CurrentShape where $collision→equals(false) do {
$sprite_set := $sprite→overlap_with(data→ShapeStack);
$collision := ($sprite_set→count > 0);
}
... ;
meta private;
}
action putShapeOnStack() {
... ;
foreach sprite1 in data→CurrentShape where true do {
data→ShapeStack→add_from(data→CurrentShape, $sprite1);
}
meta private;
}
Проверяем, не достигнута ли верхняя граница поля (в случае, если да, то игра проиграна):
action checkRows() {
$y := data→RowChecker→y;
if data→RowChecker→overlap_with(data→ShapeStack)→count > 0 then {
data→GameOver := true;
}
else {
while $y < data→BoardHeight do {
data→RowChecker→set_y($y);
$overlap := data→RowChecker→overlap_with(data→ShapeStack);
$max := 10;
if($overlap→count = $max) then {
code→blink($overlap, 5);
foreach sprite in $overlap where true do {
$sprite→hide;
data→ShapeStack→remove($sprite);
}
foreach sprite1 in data→ShapeStack where true do {
if $sprite1→y < $y then {
$sprite1→move(0, data→TileSize);
}
}
data→FullRowCount := data→FullRowCount + 1;
}
$y := $y + data→TileSize;
}
}
data→RowChecker→set_y(data→GameBoardY - data→TileSize / 2);
... ;
meta private;
}
Перемещаем фигуру, а также вращаем по тапу:
var RowChecker : Sprite {
}
var ShapeLeft : Number {
}
var ShapeRight : Number {
}
action setShapePos() {
data→ShapeLeft := data→GameBoardWidth;
data→ShapeRight := 0;
foreach sprite in data→CurrentShape where true do {
$w := data→TileSize / 2;
$left := $sprite→x - $w;
$right := $sprite→x + $w;
if $left < data→ShapeLeft then {
data→ShapeLeft := $left;
}
if $right > data→ShapeRight then {
data→ShapeRight := $right;
}
}
meta private;
}
var LastMove : DateTime {
}
event swipe_boardu003A_Board(x: Number, y: Number, delta_x: Number, delta_y: Number) {
code→swipeShape($delta_x, $delta_y);
meta private;
}
var lnterval : Number {
}
event tap_boardu003A_Board(x: Number, y: Number) {
code→rotateShape;
meta private;
}
action drawLines() {
$w := data→TileSize;
$pic := media→create_picture(data→Board→width, data→Board→height);
$y := data→GameBoardY;
for 0 ≤ i < 11 do {
$pic→draw_line($i * $w, $y, $i * $w, data→BoardHeight, colors→dark_gray, 1);
}
for 0 ≤ i1 < 21 do {
$pic→draw_line(0, $i1 * $w + $y, data→GameBoardWidth, $i1 * $w + $y, colors→dark_gray, 1);
}
data→Board→set_background_picture($pic);
meta private;
}
var BoardHeight : Number {
}
var GameBoardHeight : Number {
}
var GameBoardWidth : Number {
}
var TileSize : Number {
}
var GameBoardY : Number {
}
var ScoreSprite : Sprite {
}
action rotateShapeBy(degree: Number) {
$r := $degree * math→u03C0 / 180;
$centerSprite := data→CurrentShape→at(0);
$yo := $centerSprite→y;
$xo := $centerSprite→x;
foreach sprite in data→CurrentShape where $sprite→color→equals(colors→purple)→equals(false) do {
$sprite→set_x($sprite→x - $xo);
$sprite→set_y($sprite→y - $yo);
$x1 := ($sprite→x * math→cos($r)) + ($sprite→y * math→sin($r) * - 1);
$y1 := ($sprite→x * math→sin($r)) + ($sprite→y * math→cos($r));
$sprite→set_x($x1 + $xo);
$sprite→set_y($y1 + $yo);
}
code→setShapePos;
meta private;
}
event tap_sprite_in_ShapeStack(sprite: Sprite, index_in_set: Number, x: Number, y: Number) {
code→rotateShape;
meta private;
}
event tap_sprite_in_CurrentShape(sprite: Sprite, index_in_set: Number, x: Number, y: Number) {
code→rotateShape;
meta private;
}
action swipeShape(delta_x: Number, delta_y: Number) {
if $delta_y > 100 then {
data→Drop := true;
}
else {
if $delta_x > 100 and data→ShapeRight < data→GameBoardWidth - data→TileSize then {
code→moveShape(data→TileSize * 2, 0);
}
else {
if $delta_x > 15 and data→ShapeRight < data→GameBoardWidth then {
code→moveShape(data→TileSize, 0);
}
}
if $delta_x < - 100 and data→ShapeLeft > 0 + data→TileSize then {
code→moveShape(data→TileSize * - 2, 0);
}
else {
if $delta_x < - 15 and data→ShapeLeft > 0 then {
code→moveShape( - data→TileSize, 0);
}
}
}
data→Board→evolve;
data→Board→update_on_wall;
meta private;
}
event swipe_sprite_in_CurrentShape(sprite: Sprite, index_in_set: Number, x: Number, y: Number, delta_x: Number, delta_y: Number) {
code→swipeShape($delta_x, $delta_y);
meta private;
}
event swipe_sprite_in_ShapeStack(sprite: Sprite, index_in_set: Number, x: Number, y: Number, delta_x: Number, delta_y: Number) {
code→swipeShape($delta_x, $delta_y);
meta private;
}
Что ж, осталось, совсем чуть-чуть. Добавляем превью для следующей фигуры:
action preview() {
code→loadShape(data→PreviewX, data→PreviewY, math→rand(7));
meta private;
}
action loadShape(x: Number, y: Number, shape: Number) {
$c := colors→blue;
if $shape = 1 then {
$c := colors→red;
}
if $shape = 2 then {
$c := colors→green;
}
if $shape = 3 then {
$c := colors→yellow;
}
if $shape = 4 then {
$c := colors→cyan;
}
if $shape = 5 then {
$c := colors→orange;
}
if $shape = 6 then {
$c := colors→purple;
}
$halfSize := data→TileSize / 2;
foreach json in data→ShapeDefinition→field("Shapes")→at($shape)→field("Tiles") where true do {
$sprite := data→Board→create_rectangle(data→TileSize, data→TileSize);
$sprite→set_pos($x, $y);
$sprite→move($json→number("x") * data→TileSize, $json→number("y") * data→TileSize);
$sprite→set_color($c);
$sprite→show;
data→NextShape→add($sprite);
}
meta private;
}
var PreviewX : Number {
}
var PreviewY : Number {
}
var NextShape : Sprite_Set {
}
var GameOver : Boolean {
}
И добавляем задержку и мигание при составлении фигур в одну линию:
action sleep(milliseconds: Number) {
$dt := time→now→add_milliseconds($milliseconds);
while $dt→greater_or_equal(time→now) do {
skip;
}
meta private;
}
action blink(sprite_set1: Sprite_Set, times: Number) {
for 0 ≤ i < $times do {
foreach sprite in $sprite_set1 where true do {
if $sprite→is_visible then {
$sprite→hide;
}
else {
$sprite→show;
}
}
data→Board→update_on_wall;
code→sleep(200);
}
meta private;
}
action move(xd: Number, yd: Number, start: Boolean) returns b1: Boolean {
foreach sprite in data→CurrentShape where true do {
$sprite→move($xd, $yd);
if $sprite→is_visible→equals(false) and $sprite→y > data→GameBoardY then {
$sprite→show;
}
}
$b1 := code→collisionCheck();
if $b1 then {
if $start then {
$xd := 0;
$yd := data→TileSize;
}
foreach sprite1 in data→CurrentShape where true do {
$sprite1→move( - $xd, - $yd);
}
}
code→setShapePos;
meta private;
}
var FullRowCount : Number {
}
var ShapeCount : Number {
}
var Score : Number {
}
var Level : Number {
}
var Drop : Boolean {
}
Вуаля, готово. Результат будет таким:
Полезные ссылки:
- TouchDevelop на Microsoft Research
- Коммьюнити TouchDevelop
- Обучающие материалы
- TouchDevelop на маркетплэйсе
Автор: III