Skip to main content
Складні концепції (Advanced Concepts)

Цей розділ дає кілька прикладів використання складних концепцій в програмуванні. Перевіряйте, адже ми будемо додавати нові приклади з часом.

String Concatenation (Конкатенація рядків)

У JavaScript оператор + дозволяє об'єднувати (конкатенувати) значення рядків. 

Ось простий приклад, щоб побачити, як це працює:

async function startProgram() {
    speak("tar" + "get");  // Об'єднуємо два рядки "tar" та "get"
    await delay(1.0);
    exitProgram();
}

Комбінуючи з даними сенсорів у реальному часі, це дає вам цінну інформацію про вашого робота:

async function startProgram() {
    while (true) {
        speak("Yaw is " + getOrientation().yaw + " degrees.");  // Виводимо кут нахилу в градусах
        await delay(3.0);
    }
}

Запустіть цю програму, а потім обертайте Sphero навколо його вертикальної осі. Порівняйте виведені значення з результатами потокових даних сенсора.

A Gotcha... and a Trick (Неочікувані ситуації і трюк) 

Якщо ви використовуєте команду speak, вам, ймовірно, буде потрібна конкатенація рядків. 

Команда speak приймає лише аргументи типу рядок, тому такий код не працюватиме:

speak(Math.PI);  // Це не працює, бо Math.PI — це число

Але є трюк, щоб це обійти. Коли JavaScript зустрічає математичні операції, такі як + або *, він автоматично перетворює типи даних всіх операндів, щоб виконати операцію. 

Це стосується і оператора конкатенації рядків +

Інакшими словами, якщо ви додаєте рядок і число, JavaScript автоматично перетворює число на рядок, конкатенує два аргументи і отримує результат у вигляді рядка. 

Це працює навіть якщо ви додаєте порожній рядок до числа. Таким чином, код нижче:

var myVal = "" + 7;  // myVal буде рядком "7" 

Ось чому, хоча команда speak(Math.PI) викликає помилку, код speak("" + Math.PI) працює:

speak("" + Math.PI);  // Виводить значення Math.PI як рядок

Ці концепції допомагають вам ефективно працювати з рядками і числовими значеннями в JavaScript, забезпечуючи зручний спосіб для їх об'єднання та подальшого використання у програмах.

Generalized Loops (Узагальнені цикли)

Цикл (loop) повторює певну частину коду, поки не будуть виконані певні умови. Кожен прохід через код називається ітерацією. Кількість ітерацій залежить від умови циклу (loop). 

У JavaScript є два основних типи циклів (loops): while та do...while

Цикл (loop) while працює так:

while (loop condition is true) {  // <-- Ця "відкриваюча фігурна дужка" позначає початок повторюваного коду
    // код для повторення
}  // <-- Ця "закриваюча фігурна дужка" позначає кінець повторюваного коду

Приклад використання циклу для підрахунку до десяти:

var count = 1;  // <-- Оголошуємо змінну для лічильника
while (count <= 10) {  // <-- Перевіряємо умову циклу
    speak("" + count);
    await delay(1.0);
    ++count;  // <-- Оновлюємо змінну лічильника (в даному випадку додаємо 1)
}

Цикл do...while виглядає схоже:

do {
    // код для повторення
} while (умова циклу є true);

Ось той самий приклад, але за допомогою циклу do...while:

var count = 1;  // <-- Оголошуємо змінну для лічильника
do {
    speak("" + count);
    await delay(1.0);
    ++count;  // <-- Оновлюємо змінну лічильника (в даному випадку додаємо 1)
} while (count <= 10);  // <-- Перевіряємо умову циклу

Ви могли б здивуватися, чому в JavaScript є два цикли while і do...while, якщо вони працюють схоже і поводяться однаково. 

Відповідь проста: "Вони не зовсім однакові". 

Зауважте, що цикл while перевіряє свою умову перед виконанням коду, а цикл do...while перевіряє умову після виконання коду. 

Це означає, що якщо умова спочатку буде хибною, цикл while ніколи не виконається, а цикл do...while виконається хоча б один раз. 

Спробуйте змінити початкове значення змінної count на 11 в обох прикладах і подивіться, що станеться. 

JavaScript підтримує ще один тип циклу, який заснований на циклі while. Це цикл for, який є коротким записом стандартного циклу while. 

Якщо ви подивитеся на цикл while вище, ви побачите такий шаблон: 

1. Оголошення змінної циклу 
2. Перевірка умови циклу 
  2a) Виконання коду циклу, якщо умова істинна 
3. Оновлення змінної циклу 

Цей шаблон настільки поширений, що багато мов програмування, включаючи JavaScript, надають шаблон для нього:

for (initialize loop variables; test loop condition; update loop variables) {
// код для повторення
}

This is identical to this while loop:

initialize loop variables
while (test loop condition)
{
// код для повторення
update loop variables
}

Повертаючись до нашого прикладу з підрахунком:

for (var count=1; count<=10; ++count) {
    speak("" + count);
    await delay(1.0);
}

Цикли for є потужними — ви можете викликати функції всередині них замість перевірки змінних:

async function startProgram() {
    resetAim();
    // Цикл 'for' без змінних!
    for (setSpeed(50); getSpeed() <= 200; setSpeed(getSpeed() + 50)) {
        await delay(1.0);
    }
    setSpeed(0.0);
    speak("Busted!");
    setMainLed({r: 255, g:0, b:0});
    await delay(1.0);
    exitProgram();
}

Another Kind of for Loop (інший тип циклу for) 

У JavaScript цикл for також може ітерувати через структури з елементами, такі як масиви та об'єкти. 

Розглянемо наступний приклад:

async function startProgram() {
    var key = null;
    var aboutMe = {'my-dog': "Hershey", 'my-other-dog': "Koda", 'my-favorite-computer': "the Commodore 64"};
    for (key in aboutMe) {
        speak(key + " is " + aboutMe[key]);
        await delay(3.0);
    }
    exitProgram();
}

У JavaScript цикл for...in має таку форму:

for (iterator_variable in structure_with_elements) {
    // код для повторення
}

де структура з елементами — це структура даних з елементами всередині, наприклад, масив або об'єкт. 

Змінна-ітератор набирає значення індексів кожного внутрішнього елемента по черзі. 

Отже, в наведеному вище прикладі змінна key буде 'my-dog' під час першого проходу циклу, 'my-other-dog' під час другого проходу, і 'my-favorite-computer' під час третього. Коли програма спробує отримати четвертий ключ, вона не знайде його (оскільки об'єкт aboutMe містить лише 3 елементи), і цикл завершиться.

Ключові слова async та await

JavaScript використовує ключове слово async для позначення коду, який може не виконуватися весь одразу. 

Ключове слово await використовується для паузи виконання коду до того, як асинхронна функція, з якою воно пов'язане, завершиться. 

Ці ключові слова мають важливу роль у програмуванні на JavaScript і особливо при роботі з Sphero. 

Ось основні моменти, на які слід звернути увагу: 

Пункт #1: 
якщо ви оголосили функцію як async, ви повинні викликати її за допомогою ключового слова await. Це стосується також деяких попередньо написаних функцій Sphero. 

Наприклад, якщо ви використовуєте команду roll() у вашій програмі, її потрібно викликати так:

await roll(0, 100);

Якщо не використовувати await, ви отримаєте помилку виконання, оскільки roll є асинхронною функцією. 

Пункт #2: 
якщо ви хочете, щоб функція повернула значення, не робіть її асинхронною функцією. 

Оскільки асинхронні функції можуть зайняти певний час для виконання, вони не можуть просто повернути значення. 

Замість цього вони повертають об'єкт Promise, який є тим, що обіцяє повернути значення в майбутньому, або щось, що може бути помилковим (наприклад, якщо з'єднання з роботом втрачене). 

Є способи дізнатися, чи виконано Promise, але це виходить за рамки цієї статті і не є необхідним для більшості програм Sphero

Пункт #3: 
якщо ваша функція повертає значення, вона не може містити асинхронний код. Це продовження Пункту #2, але варто зазначити, що ви отримаєте помилку під час компіляції, якщо створите функцію без async, яка намагається викликати асинхронні функції у своєму тілі. 

Ось приклад, як це все працює:

// Оголошення звичайної (не асинхронної) функції для форматування
// кута нахилу так, щоб він був більш зрозумілим і ніколи не був негативним.
angleAdjust = function(yaw) {
    while (yaw < 0.0) {
        yaw += 360.0;
    }
    while (yaw > 360.0) {
        yaw -= 360.0;
    }
    return Math.round(yaw * 10.0) / 10.0;
};
async function startProgram() {
    var easyYaw = 0.0;

    while (true) {
        // Викликаємо angleAdjust звичайно (без 'await'), оскільки ця функція
        // гарантовано завершується миттєво.
        easyYaw = angleAdjust(getOrientation().yaw);

        speak("Yaw is " + easyYaw + " degrees.");
        await delay(3.0);
    }
}

У цьому прикладі Функція angleAdjust не є асинхронною, оскільки вона виконується миттєво. Вона може бути викликана без використання await

Функція startProgram є асинхронною і використовує await, щоб зупинити виконання на час затримки.

getCurrentTime (Отримати поточний час)

Sphero має вбудований годинник, який ви можете використовувати у своїх програмах. Ви можете отримати час через функцію getCurrentTime, але є одна особливість. Годинник Sphero відстежує кількість секунд, що минули з 1 січня 1970 року. Отже, реальне число не є дуже корисним. 

На щастя, вам зазвичай не потрібен фактичний час доби. Найімовірніше, вам потрібно знати, скільки часу минуло з того моменту, як ваша програма почала виконуватися. Це легко зробити. 

Приклад getCurrentTime:

async function startProgram() {
  var startTime = getCurrentTime();
  speak('' + (getCurrentTime() - startTime) + ' секунд.');
  await delay(2.0);
  speak('' + (getCurrentTime() - startTime) + ' секунд.');
  await delay(7.0);
  exitProgram();
};
setTimeout та setInterval

Ці функції дозволяють виконувати код після затримки часу. 

Використовуйте setTimeout, щоб викликати функцію після затримки часу. 

Використовуйте setInterval, щоб виконувати функцію повторно, роблячи паузу між викликами. 

Приклад затримки часу setTimeout:

async function delayedCode() {
  setMainLed({r: 0, g: 255, b:0});
  await delay(0.5);
  setMainLed({r:0, g:0, b:0});
  exitProgram();
};
async function startProgram() {
  setTimeout(delayedCode, 2000); // Затримка 2000 мілісекунд.
  while (true) {
    await delay(0.1);
  }
};

Приклад інтервалу setInterval

async function blink() {
  setMainLed({r:255, g:0, b:0});
  await delay(0.167);
  setMainLed({r:0, g:0, b:0});
};
async function startProgram() {
  setInterval(blink, 333); // Затримка 333 мілісекунди.
  while(true) {
    await delay(0.1);
  }
};
Function Pointers (Вказівники на функції)

Змінні в JavaScript можуть містити будь-які об'єкти, включаючи функції. 

Це дозволяє викликати функції непрямо, зберігаючи їх у змінній і "викликаючи" змінну, як якщо б вона була функцією. 

Simple Function Pointer (Приклад вказівника на функцію)

async function saySomething() {
  speak("Something.");
  await delay(1.0);
  exitProgram();
};
async function startProgram() {
  var indirectFn = saySomething;
  await indirectFn();
};

Це може здаватися дивним, але це потужний інструмент. Особливо це дозволяє створювати модель, відому як "автомат станів" — див. нижче.

State Machines (Автомат станів)

state machine (Автомат станів) — або finite state machine (кінцевий автомат станів) — це математична модель, яка представляє систему, що може бути в точно одному з N станів, де 'N' — будь-яке натуральне число. 

Це звучить складно, але основна ідея проста. 

Розглянемо приклад, щоб побачити, як це працює.

Розглянемо конус із морозивом. Він може бути в одному з трьох станів: 

1. Заморожений — все морозиво тверде 
2. Тане — частина морозива тверде, частина рідке 
3. Розтоплений — все морозиво рідке. 

Тепер подумайте про те, як ви будете лизати морозиво в кожному стані. 

Коли конус заморожений, ви отримаєте трохи морозива за раз.
Коли воно тане, ви отримаєте багато морозива. 
Коли воно повністю розтоплене, ви не отримаєте морозива, тому що воно вже на підлозі (і хто хоче лизати підлогу???). 

Тепер уявіть, що ви пишете програму, яка імітує лизання морозива. 

Один із підходів може виглядати так:

var STATES = {FROZEN:0, MELTING:1, MELTED:2};
var coneState = ???; // Встановіть це на один з STATES.FROZEN, тощо
async function lick() {
  switch(coneState) {
    case STATES.FROZEN:
      speak("You got a little ice cream.");
      break;
    case STATES.MELTING:
      speak("You got a lot of ice cream!");
      break;

    case STATES.MELTED:
      speak("You didn't get any ice cream.");
      break;

    default:
      speak("This isn't an ice cream cone!");
      break;
  }
  await delay(2.0);
};

Легко зрозуміти, але уявіть, як виглядатиме функція lick, якщо б у конуса було 10 або 100 станів. 

Це стане великим і неорганізованим, особливо якщо кожен випадок вимагатиме більше коду, ніж просто один виклик speak

Автомати станів дозволяють зробити цей код значно чистішим і простішим для підтримки. 

Ось як виглядатиме така реалізація:

var stateFrozen = {
  lick: async function() {
    speak("You got a little ice cream.");
    await delay(2.0);
  };
};
var stateMelting = {
  lick: async function() {
    speak("You got a lot of ice cream!");
    await delay(2.0);
  };
};
var stateMelted = {
  lick: async function() {
    speak("You didn't get any ice cream.");
    await delay(2.0);
  };
};
var coneState = ???; // Один з stateFrozen, stateMelting, stateMelted
async function lick() {
  await coneState.lick();
};

Тепер, як це допомагає програмувати для Sphero? Ми можемо використовувати автомати станів для надання Sphero багатокрокових поведінок. 

Розглянемо приклад, коли ми хочемо, щоб Sphero виконувала послідовність дій: спочатку відлік ("Ready-set-go!"), потім поїхала вперед і повернулася назад. 

Цю програму легко реалізувати за допомогою автомата станів. 

Ready-Set-Go (Приклад на старт, увага, руш!)

var currentState = null;
async function stateReady() {
  setStabilization(false);
  setMainLed({r:255, g:0, b:0});
  speak("Ready!");
  await delay(1.0);
  currentState = stateSet;
};
async function stateSet() {
  setMainLed({r:255, g:255, b:0});
  speak("Set!");
  await delay(1.0);
  currentState = stateGo;
};
async function stateGo() {
  setStabilization(true);
  resetAim();
  setMainLed({r:0, g:255, b:0});
  speak("Go!");
  await roll(0, 100);
  await delay(1.0);
  setMainLed({r:0, g: 0, b:0});
  await delay(2.0);
  currentState = statePause;
};
async function statePause() {
  stopRoll(180);
  await delay(1.0);
  currentState = stateReturn;
};
async function stateReturn() {
  await roll(180, 100);
  await delay(3.0);
  stopRoll(180);
  exitProgram();
};
async function startProgram() {
  currentState = stateReady;
  while(true) {
    await currentState();
  }
};

Цей приклад демонструє, як автомат станів може допомогти створити багатокрокові поведінки для робота Sphero.

Приклади складних концепцій

Цей розділ дає кілька прикладів використання складних концепцій в програмуванні. 

Перевіряйте, адже ми будемо додавати нові приклади з часом.

Basic State Machine (Основний автомат станів)

Основний автомат станів є основною оболонкою, яку можна використовувати як відправну точку для ваших програм. 

Він надає основну структуру автомата станів та допоміжні функції для інструкцій та ініціалізації. 

Це служить основою для прикладів нижче.

var YIELD_DELAY = 0.033;
var TIPPED_ANGLE = 45.0;

// ГЛОБАЛЬНІ ПЕРЕМІННІ ============================================================
var currentState = null;

// ГОЛОВНИЙ ЦИКЛ ================================================================
async function startProgram() {
  await sayInstructions();
  await initialize();

  while (!tipped()) {
    if (currentState) {
      await currentState();
    }

    await delay(YIELD_DELAY);
  }

  exitProgram();
};

// ДОПОМОЖНІ ФУНКЦІЇ ============================================================
tipped = function () {
  return Math.abs(getOrientation().roll) > TIPPED_ANGLE;
};

async function sayInstructions() {
  speak("Tip your robot to the left or right to end the program.");
  await delay(3.0);
};

async function initialize() {
  // Додайте код налаштування тут
};

// СТАНИ =====================================================================
// Тут знаходяться функції для ваших станів.
Rainbow Shaker (Робот-радуга)

Робот-радуга циклічно змінює кольори райдуги під час того, як ви трясете робот. 

Використовується автомат станів і подія onCollision для відображення правильного кольору без використання умовних операторів if чи switch.

STOP_ANGLE = 45.0;
YIELD_DELAY = 0.03;

// ГЛОБАЛЬНІ ПЕРЕМІННІ ============================================================
var currentState = null; // вказує на код для поточного стану

// ГОЛОВНИЙ ЦИКЛ ================================================================
async function startProgram() {
  await giveInstructions();
  initialize();
  while (Math.abs(getOrientation().roll) < STOP_ANGLE) {
    await delay(YIELD_DELAY);
  }
  exitProgram();
};

// ДОПОМОЖНІ ФУНКЦІЇ ============================================================
async function giveInstructions() {
  speak("Shake your robot to see a rainbow.");
  await delay(2.0);
};

initialize = function () {
  registerEvent(EventType.onCollision, doShakeState);
  setMainLed({
    r: 0,
    g: 0,
    b: 0
  });
  currentState = setToRed;
};

async function doShakeState() {
  await currentState();
};

// КОЛЬОРОВІ СТАНИ ============================================================
async function setToRed() {
  setMainLed({
    r: 255,
    g: 0,
    b: 0
  });
  currentState = setToOrange;
};

async function setToOrange() {
  setMainLed({
    r: 255,
    g: 128,
    b: 0
  });
  currentState = setToYellow;
};

async function setToYellow() {
  setMainLed({
    r: 255,
    g: 255,
    b: 0
  });
  currentState = setToGreen;
};

async function setToGreen() {
  setMainLed({
    r: 0,
    g: 255,
    b: 0
  });
  currentState = setToBlue;
};

async function setToBlue() {
  setMainLed({
    r: 0,
    g: 0,
    b: 255
  });
  currentState = setToIndigo;
};

async function setToIndigo() {
  setMainLed({
    r: 16,
    g: 0,
    b: 64
  });
  currentState = setToViolet;
};

async function setToViolet() {
  setMainLed({
    r: 64,
    g: 0,
    b: 64
  });
  currentState = setToRed;
  speak('All done! Shake again to start over.');
  await delay(2.0);
};
Spinner (Вертушка)

Вертушка перетворює вашого робота на "фліппер" (підкидну іграшку), використовуючи автомат станів, 

getGyroscope і getOrientation, щоб вибрати елемент з масиву залежно від кута нахилу робота.

// КОНСТАНТИ ===================================================================
var YIELD_DELAY = 0.033;
var TIPPED_ANGLE = 45.0;
var SPIN_START_THRESH = 20.0;
var SPIN_STOP_THRESH = 5.0;
var YAW_BIAS = 45.0;
var DIRECTIONS = ['ONE', 'TWO', 'THREE', 'FOUR'];

// ПЕРЕМІННІ ===================================================================
var currentState = null;
var direction = null;

// ГОЛОВНИЙ ЦИКЛ ================================================================
async function startProgram() {
  initialize();
  await giveDirections();
  while (!tipped()) {
    if (currentState) {
      await currentState();
    }
    await delay(YIELD_DELAY);
  }
  exitProgram();
};

// ДОПОМОЖНІ ФУНКЦІЇ ============================================================
async function giveDirections() {
  speak('Spin me.');
  await delay(1.0);
};

tipped = function () {
  return Math.abs(getOrientation().roll) > TIPPED_ANGLE;
};

initialize = function () {
  setStabilization(false);
  resetAim();
  currentState = waitingForSpin;
};

boundAngle = function (angle) {
  while (angle > 360.0) {
    angle -= 360.0;
  }
  while (angle < 0.0) {
    angle += 360.0;
  }
  return angle;
};

valueFromHeading = function () {
  var yaw = getOrientation().yaw + YAW_BIAS;
  var index = 0;
  yaw = boundAngle(yaw);
  index = Math.floor(yaw / 360.0 * DIRECTIONS.length);
  direction = DIRECTIONS[index];
};

// СТАНИ =======================================================================
async function waitingForSpin() {
  if (Math.abs(getGyroscope().pitch) > SPIN_START_THRESH) {
    currentState = updateWhileSpinning;
    valueFromHeading();
  }
};

async function updateWhileSpinning() {
  valueFromHeading();
  if (Math.abs(getGyroscope().pitch) < SPIN_STOP_THRESH) {
    speak(direction);
    await delay(1.0);
    currentState = waitingForSpin;
  }
};
Map (Карта)

Цей приклад використовує двовимірні масиви і автомат станів для створення світу, який можна досліджувати, нахиляючи робота вліво-вправо або вперед-назад.

var YIELD_DELAY = 0.033;
var TIPPED_ANGLE = 35.0;
var RESET_THRESH = 15.0;
var currentState = null;
var map = [
  ["ON THE CLIFFS", "ON THE CLIFFS", "ON THE BEACH", "ON THE BEACH", "NEAR THE JETTY"],
  ["IN A CREVICE", "ON A RIDGE", "IN THE HILLS", "IN A GLADE", "BY A ROCKY OUTCROPPING"],
  ["ON A RIDGE", "IN THE HILLS", "IN A CLEARING", "ON THE PLAINS", "ON THE PLAINS"],
  ["IN A CANYON", "IN THE HILLS", "ON A GRASSY KNOLL", "IN THE FOREST", "IN THE FOREST"],
  ["IN A CANYON", "ON THE PLAINS", "IN THE FIELDS", "IN THE FOREST", "IN THE DARK FOREST"],
];
var location = { row: 0, col: 0 };
var locDesc = null;
// ГОЛОВНИЙ ЦИКЛ ================================================================
async function startProgram() {
  await giveInstructions();
  await initialize();
  while (true) {
    if (currentState) {
      await currentState();
    }
    await delay(YIELD_DELAY);
  }
  exitProgram();
}
// ДОПОМОЖНІ ФУНКЦІЇ ============================================================
tipped = function () {
  return Math.abs(getOrientation().roll) > TIPPED_ANGLE;
};
async function giveInstructions() {
  speak('Change pitch and roll to navigate the map.');
  await delay(3.0);
};
async function initialize() {
  location.row = Math.floor(map.length / 2);
  location.col = Math.floor(map[0].length / 2);
  currentState = updateLocation;
  registerEvent(EventType.onCollision, onShake);
  await sayWhere();
};
async function sayWhere() {
  speak('You are ' + map[location.row][location.col]);
  await delay(2.0);
};
async function sayNoGo() {
  speak("You can't go that way.");
  await delay(2.0);
};
async function onShake() {
  exitProgram();
};
async function changePosition(rowMod, colMod) {
  var oldCol = location.col;
  var oldRow = location.row;
  location.col += colMod;
  location.col = Math.min(location.col, map[0].length - 1);
  location.col = Math.max(0, location.col);
  location.row += rowMod;
  location.row = Math.min(location.row, map.length - 1);
  location.row = Math.max(0, location.row);
  if (oldCol === location.col && oldRow === location.row) {
    setMainLed({ r: 255, g: 0, b: 0 });
    await sayNoGo();
  } else {
    setMainLed({ r: 0, g: 255, b: 0 });
    await sayWhere();
  }
  currentState = waitForReset;
};
// СТАНИ =======================================================================
async function updateLocation() {
  var orient = getOrientation();
  if (orient.roll > TIPPED_ANGLE) {
    await changePosition(0, 1);
  } else if (orient.roll < -TIPPED_ANGLE) {
    await changePosition(0, -1);
  } else if (orient.pitch > TIPPED_ANGLE) {
    await changePosition(1, 0);
  } else if (orient.pitch < -TIPPED_ANGLE) {
    await changePosition(-1, 0);
  }
};
async function waitForReset() {
  var roll = Math.abs(getOrientation().roll);
  var pitch = Math.abs(getOrientation().pitch);
  if (roll < RESET_THRESH && pitch < RESET_THRESH) {
    currentState = updateLocation;
    setMainLed({ r: 0, g: 0, b: 0 });
  }
};

Ці приклади демонструють, як автомат станів і інші концепції можуть бути використані для створення інтерактивних і цікавих програм для робота Sphero.