По работе мне периодически приходится править и дописывать скрипты для авто-тестов. И так исторически сложилось, что написаны они на Powershell. Но статья будет не об этом.
Обычно Powershell описывается как средство автоматизации для системных администраторов. И естественно, что к нему проявляют мало интереса. Поэтому я хочу рассказать, что его можно использовать не только для скучных рабочих задач.
Ради эксперимента и в качестве разнообразия у меня возникла идея написать небольшую игру с механикой скролшутера. Сначала захотелось ограничиться одной консолью, но потом разум возобладал. Так что для графического движка было решено использовать элементы Windows.Forms:
Add-Type -Assemblyname System.Windows.Forms
function Create-Form ([string]$name, $x, $y, $w, $h){
$win = New-Object System.Windows.Forms.Form
$win.StartPosition = "Manual"
$win.Location = New-Object System.Drawing.Size($x, $y)
$win.Width = $w
$win.Height = $h
$win.Text = $name
$win.Topmost = $True
$win
}
function Create-Label ([string]$name, $x, $y){
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point($x, $y)
$label.Text = $name
$label.AutoSize = $true
$label
}
function Create-Button ([string]$name, $x, $y, $w, $h){
$button = New-Object System.Windows.Forms.Button
$button.Location = New-Object System.Drawing.Point($x, $y)
$button.Size = New-Object System.Drawing.Size($w, $h)
$button.Text = $name
$button.Enabled = $false
$button
}
function Start-Scroll (){
$form = Create-Form "Let's GO!" 200 150 300 400
$start = Create-Label "Press SPACE to run" 90 200
$info = Create-Label "<-- A D --> 'Esc' for exit" 80 340
$ship = Create-Label "/|" 135 400
$form.Controls.Add($start)
$form.Controls.Add($info)
$form.Controls.Add($ship)
$form.ShowDialog()
}
В результате появился “стартовый экран”. Но при этом выполнения скрипта по сути заблокировалось, т.к. после запуска диалогового окна — он ожидает от этого окна ответа и дальше не выполняется. Конечно, можно было бы сделать многопоточный скрипт, но было найдено более простое решение проблемы: добавление таймера.
$timer = New-Object system.windows.forms.timer
$timer.Interval = 100
$timer.add_tick({Check})
$timer.start()
Каждые 100 миллисекунд таймер вызывает функцию Check независимо от того, что выполняется в самом скрипте. Временной интервал выбран на глаз. По моим ощущениям обновление игры происходит достаточна плавно, но при желании можно сделать обновление и чаще.
Как выяснилось в последствии все переменные, указанные в «тике» таймера, сохраняют значение на момент активации таймера и Check каждый раз вызывается с одним и тем же набором данных. Поэтому чтобы функция имела доступ к актуальным данным, вся нужная информация была упакована в объект:
$Data = @{run = $false; hide = $false; pos = 135; shot = 0; spawn = 0; usb = 0; score = 0; fires = @(); enemies = @()}
Чтобы придать функции Start-Scroll законченный вид, осталось добавить хоткеи управления и контроллер звука:
$form.KeyPreview = $True
$form.Add_KeyDown({
if ($_.KeyCode -eq "A") {if ($Data.run -and -not $Data.hide -and $Data.pos -gt 0) {$Data.pos -= 5}}
})
$form.Add_KeyDown({
if ($_.KeyCode -eq "D") {if ($Data.run -and -not $Data.hide -and $Data.pos -lt 265) {$Data.pos += 5}}
})
$form.Add_KeyDown({
if ($_.KeyCode -eq "Escape") {$timer.stop(); $form.Close()}
})
$form.Add_KeyDown({
if ($_.KeyCode -eq "Space") {
if ($Data.run) { Set-Hide }
else { $start.Text = ""; $Data.run = $true }
}
})
$sound = new-Object System.Media.SoundPlayer;
$sound.SoundLocation = "$env:WINDIRMediaWindows Information Bar.wav"
Итого в игре есть флажок $Data.run, который обозначает — запущена ли игра, есть флажок $Data.hide, который выполняет роль паузы, есть набор переменных, где хранятся координаты игрока (pos), количество очков (score ), таймер до выстрела (shot) и таймер до добавления противника (spawn), а также два массива fires и enemies, в которых хранятся соответственно данные по снарядам и противникам.
Управление получилось достаточно простое: A и D для перемещения своего персонажа, Esc — для выхода, а пробел заменяет кнопку “Старт” запуская игру или ставя её на паузу. Чтобы на время паузы все игровые элементы скрывались, используется функция Set-Hide:
function Set-Hide (){
if ($Data.hide) {
$start.Text = ""
$start.Location=New-Object System.Drawing.Point(90, 200)
$Data.enemies | foreach {$_.obj.Visible = $true}
$Data.fires | foreach {$_.obj.Visible = $true}
$info.Visible = $true
$ship.Visible = $true
} else {
$start.Location=New-Object System.Drawing.Point(10, 10)
$Data.enemies | foreach {$_.obj.Visible = $false}
$Data.fires | foreach {$_.obj.Visible = $false}
$info.Visible = $false
$ship.Visible = $false
}
$Data.hide = -not $Data.hide
}
Основная логика игры описана в функции Check:
function Check () {
# Если игра не запущена - ничего не делаем
if (!$Data.run) {return}
# Если пауза - выводим сторонний текст
if ($Data.hide) {
if ($Data.usb -eq 0){
$start.Text = ""
gwmi Win32_USBControllerDevice | %{[wmi]($_.Dependent)} | where {$_.DeviceID -notlike '*ROOT_HUB*'} | Sort Description | foreach { $start.Text += $_.Description +"`n" }
$Data.usb = 500
} else { $Data.usb -= 1 }
return
}
# Обновляем положение игрока
$ship.Location=New-Object System.Drawing.Point($Data.pos, 300)
# Создаем снаряд, если пришло время
if ($Data.shot -eq 0) {
$Data.fires += @{ obj = Create-Label "*" ($Data.pos + 5) 290; x = $Data.pos + 5; y = 290 }
$form.Controls.Add($Data.fires[$Data.fires.Length - 1].obj)
$Data.shot = 4
} else { $Data.shot -= 1 }
# Создаем противника, если пришло время
if ($Data.spawn -eq 0) {
$hp = Get-Random -minimum 4 -maximum 6
$pos = Get-Random -minimum 0 -maximum 200
$Data.enemies += @{ obj = Create-Button "$hp" $pos -22 30 20; x = $pos; y = -22; health = $hp }
$form.Controls.Add($Data.enemies[$Data.enemies.Length - 1].obj)
$Data.spawn = 150 * $Data.enemies.Length
} else { $Data.spawn -= 1 }
# Проверяем снаряды
foreach ($fire in $Data.fires){
# Обновляем положение
$fire.obj.Location = New-Object System.Drawing.Point($fire.x, $fire.y)
$fire.y -= 5
# Проверяем для каждого снаряда/противника - нет ли столкновения
foreach ($enemy in $Data.enemies){
if ($fire.x + 5 -gt $enemy.x -and $fire.x -lt $enemy.x + 25 -and $fire.y -gt $enemy.y -and $fire.y -lt $enemy.y + 20){
$enemy.health -= 1
$enemy.obj.Text = $enemy.health
$fire.y = -20
$sound.Play()
}
}
}
# Если первый в списке снаряд вышел за экран - убираем его
if ($Data.fires[0].y -lt -10) {
$form.Controls.Remove($Data.fires[0].obj)
$Data.fires = $Data.fires[1..($Data.fires.Length - 1)]
}
# Проверяем противников
foreach ($enemy in $Data.enemies){
# Если убит - перезапускаем
if ($enemy.health -gt 0){ $enemy.y += 1 } else {
$Data.score += 1
$enemy.health = Get-Random -minimum 4 -maximum 6
$enemy.x = Get-Random -minimum 1 -maximum 200
$enemy.y = -22
$enemy.obj.Text = $enemy.health
}
# Обновляем положение
$enemy.obj.Location = New-Object System.Drawing.Point($enemy.x, $enemy.y)
# Если приземлился - останавливаем игру
if ($enemy.y -gt 300) {
$Data.run = $false
$start.Text = "Total score: " + $Data.score
}
}
}
Конечно, такая игра не претендует на “Лучшую игру года”. Но она может показать, что Powershell можно использовать не только, чтобы настраивать права доступа и контролировать работу локальной сети.
А ещё, в качестве бонуса, в режиме паузы отображается список подключенных USB-девайсов )
P.S. А те, кому лень собирать код по статье, могут скачать архив со скриптом и bat-ником для запуска.
Автор: Kavaru