序
アンドロイドでマルチタッチのプログラムを python でやろうとするとアプリの pydroid3 だと、pygame2 のマルチタッチ機能のついたものでは、そのままできた。しかし、termux を使って python で作ると、x 転送しないと GUI へグラフィックスを表示できないので、なかなか面倒だ。でも、可能だが。(Android OS 中だけですべて完結させるという意味です。ついでに言うと、とても安くて入手しやすい 8 インチ以上のタブレットでクロスプラットフォームなプログラムは作って配布可能かという探索です。)
Lua ならマルチタップのゲームループを使ったプログラム(ゲームとは限りません)はどうなのか?
できそうではある。
というのが本題で、それはとても簡単だった。(pygame2 でマルチタッチのゲームコントローラーをゼロから作りたい場合参考になるか。よく似てるから。)
テトリスのようなブロックのゲームで解説。
テトリス(だと語弊があるので)のようなゲームの作り方は、むずかしいので解説しないですが、ゲームのコーディングの詳細はこちらのチュートリアルにすべてありますので参照してください。
とても丁寧にチュートリアルを作られているので、めちゃくちゃよいです。
https://simplegametutorials.github.io/about/
About these tutorials
Welcome!
Here is how these tutorials work:
First there is a download link to the complete game, and a description of the rules and controls of the game. I recommend playing the game before moving on to the next steps so you can have a good understanding of what the game does.
Then there is a basic overview of the game's data and logic, like how you might think through how you're going to approach programming the game before you begin coding.
Then the code is built up step-by-step. This includes intermediary steps that don't appear in the final code but help us get there. I recommend following along in your own code editor and running the code after ever step.
それ以前に LÖVE2d でハローワールドまでが大変そうという方は、こちら。
たぶん想像よりは道のり短いです。pythonでマルチタッチについて疑問に思ってから、たどり着くまでに一年くらいかかりましたが、ここまではこれた生物はワンクリックでたどり着きます。
LÖVE2d について、ものすごく要約すると、main.lua、conf.lua、他に必要があれば用意したサウンドエフェクツやプラグインのようなプログラムのファイルをまとめて zip 圧縮したものを LÖVE2d で開いたらプログラムが動くということ。
そういうわけなので LÖVE2d が対応しているならば、そのプラットフォームでは同じ Lua のプログラムコードが動作するということです。
ここでの本題は、上記のチュートリアルのテトリスのようなゲーム block はキーボードの入力を待ち受けているので、「画面タッチにも対応させます」ということになります。
Jigoku さんの作った LÖVE2d 用の Paddy というマルチタップ用のコントローラーを使います。
この記事では、main.lua
,paddy.lua
,conf.lua
の 3 つのファイルだけでチュートリアルのBlockというゲームを画面タッチコントロールにします。
この 3 つを ZIP でアーカイブして、LÖVE2d で開くとゲームとして遊べます。
つまり、3つファイルの内容をコピペして zip 圧縮してして、block.love というファイル名にして LÖVE2d インストールして block.love クリックしてみて内容確認して、から読んだ方がいいです。
Android OS の場合は、アプリストアか、LÖVE2d のホームページから LÖVE2d をダウンロードしてインストールします。
! 10インチの画面サイズを想定しています。
main.lua
function love.load()
require('paddy') -- [[ここでpaddyを読み込みます]]
press = false
time = 0
timeA = 0
-- music = love.audio.newSource("All_is_full_of_love1.mp3", "stream") --[[BGMがほしければ、なにかいい感じに曲を作って読み込むといい。musescoreとかで譜面で書いてmp3でダウンロードするなど。]]
-- music:play()
love.graphics.setBackgroundColor(.43,.43,.43)
pieceStructures = {
{
{
{' ', ' ', ' ', ' '},
{'i', 'i', 'i', 'i'},
{' ', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 'i', ' ', ' '},
{' ', 'i', ' ', ' '},
{' ', 'i', ' ', ' '},
{' ', 'i', ' ', ' '},
},
},
{
{
{' ', ' ', ' ', ' '},
{' ', 'o', 'o', ' '},
{' ', 'o', 'o', ' '},
{' ', ' ', ' ', ' '},
},
},
{
{
{' ', ' ', ' ', ' '},
{'j', 'j', 'j', ' '},
{' ', ' ', 'j', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 'j', ' ', ' '},
{' ', 'j', ' ', ' '},
{'j', 'j', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{'j', ' ', ' ', ' '},
{'j', 'j', 'j', ' '},
{' ', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 'j', 'j', ' '},
{' ', 'j', ' ', ' '},
{' ', 'j', ' ', ' '},
{' ', ' ', ' ', ' '},
},
},
{
{
{' ', ' ', ' ', ' '},
{'l', 'l', 'l', ' '},
{'l', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 'l', ' ', ' '},
{' ', 'l', ' ', ' '},
{' ', 'l', 'l', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', ' ', 'l', ' '},
{'l', 'l', 'l', ' '},
{' ', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{'l', 'l', ' ', ' '},
{' ', 'l', ' ', ' '},
{' ', 'l', ' ', ' '},
{' ', ' ', ' ', ' '},
},
},
{
{
{' ', ' ', ' ', ' '},
{'t', 't', 't', ' '},
{' ', 't', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 't', ' ', ' '},
{' ', 't', 't', ' '},
{' ', 't', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 't', ' ', ' '},
{'t', 't', 't', ' '},
{' ', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 't', ' ', ' '},
{'t', 't', ' ', ' '},
{' ', 't', ' ', ' '},
{' ', ' ', ' ', ' '},
},
},
{
{
{' ', ' ', ' ', ' '},
{' ', 's', 's', ' '},
{'s', 's', ' ', ' '},
{' ', ' ', ' ', ' '},
},
{
{'s', ' ', ' ', ' '},
{'s', 's', ' ', ' '},
{' ', 's', ' ', ' '},
{' ', ' ', ' ', ' '},
},
},
{
{
{' ', ' ', ' ', ' '},
{'z', 'z', ' ', ' '},
{' ', 'z', 'z', ' '},
{' ', ' ', ' ', ' '},
},
{
{' ', 'z', ' ', ' '},
{'z', 'z', ' ', ' '},
{'z', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
},
},
}
gridXCount = 10
gridYCount = 18
pieceXCount = 4
pieceYCount = 4
timerLimit = 0.5
function canPieceMove(testX, testY, testRotation)
for y = 1, pieceYCount do
for x = 1, pieceXCount do
local testBlockX = testX + x
local testBlockY = testY + y
if pieceStructures[pieceType][testRotation][y][x] ~= ' ' and (
testBlockX < 1
or testBlockX > gridXCount
or testBlockY > gridYCount
or inert[testBlockY][testBlockX] ~= ' '
) then
return false
end
end
end
return true
end
function newSequence()
sequence = {}
for pieceTypeIndex = 1, #pieceStructures do
local position = love.math.random(#sequence + 1)
table.insert(
sequence,
position,
pieceTypeIndex
)
end
end
function newPiece()
pieceX = 3
pieceY = 0
pieceRotation = 1
pieceType = table.remove(sequence)
if #sequence == 0 then
newSequence()
end
end
function reset()
inert = {}
for y = 1, gridYCount do
inert[y] = {}
for x = 1, gridXCount do
inert[y][x] = ' '
end
end
newSequence()
newPiece()
timer = 0
end
reset()
end
function love.update(dt)
if not music:isPlaying( ) then
love.audio.play( music )
end
timer = timer + dt
timeA = timeA + dt
paddy.update(dt)
if press ~= true then
if paddy.isDown("b") then
time = timeA
local testRotation = pieceRotation + 1
if testRotation > #pieceStructures[pieceType] then
testRotation = 1
end
if canPieceMove(pieceX, pieceY, testRotation) then
pieceRotation = testRotation
press = true
end
elseif paddy.isDown("x") then
time = timeA
local testRotation = pieceRotation - 1
if testRotation < 1 then
testRotation = #pieceStructures[pieceType]
end
if canPieceMove(pieceX, pieceY, testRotation) then
pieceRotation = testRotation
press = true
end
elseif paddy.isDown("left") then
time = timeA
local testX = pieceX - 1
if canPieceMove(testX, pieceY, pieceRotation) then
pieceX = testX
press = true
end
elseif paddy.isDown("right") then
time = timeA
local testX = pieceX + 1
if canPieceMove(testX, pieceY, pieceRotation) then
pieceX = testX
press = true
end
elseif paddy.isDown("down") or paddy.isDown("a") then
time = timeA
press = true
while canPieceMove(pieceX, pieceY + 1, pieceRotation) do
pieceY = pieceY + 1
timer = timerLimit
end
end
else
if time + 0.2 < timeA then
press = false
end
end
if timer >= timerLimit then
timer = 0
local testY = pieceY + 1
if canPieceMove(pieceX, testY, pieceRotation) then
pieceY = testY
else
-- Add piece to inert
for y = 1, pieceYCount do
for x = 1, pieceXCount do
local block =
pieceStructures[pieceType][pieceRotation][y][x]
if block ~= ' ' then
inert[pieceY + y][pieceX + x] = block
end
end
end
-- Find complete rows
for y = 1, gridYCount do
local complete = true
for x = 1, gridXCount do
if inert[y][x] == ' ' then
complete = false
break
end
end
if complete then
for removeY = y, 2, -1 do
for removeX = 1, gridXCount do
inert[removeY][removeX] = inert[removeY - 1][removeX]
end
end
for removeX = 1, gridXCount do
inert[1][removeX] = ' '
end
end
end
newPiece()
if not canPieceMove(pieceX, pieceY, pieceRotation) then
reset()
end
end
end
end
function love.keypressed(key)
if key == 'x' then
local testRotation = pieceRotation + 1
if testRotation > #pieceStructures[pieceType] then
testRotation = 1
end
if canPieceMove(pieceX, pieceY, testRotation) then
pieceRotation = testRotation
end
elseif key == 'z' then
local testRotation = pieceRotation - 1
if testRotation < 1 then
testRotation = #pieceStructures[pieceType]
end
if canPieceMove(pieceX, pieceY, testRotation) then
pieceRotation = testRotation
end
elseif key == 'left' then
local testX = pieceX - 1
if canPieceMove(testX, pieceY, pieceRotation) then
pieceX = testX
end
elseif key == 'right' then
local testX = pieceX + 1
if canPieceMove(testX, pieceY, pieceRotation) then
pieceX = testX
end
elseif key == 'c' then
while canPieceMove(pieceX, pieceY + 1, pieceRotation) do
pieceY = pieceY + 1
timer = timerLimit
end
end
end
function love.draw()
local function drawBlock(block, x, y)
local colors = {
[' '] = {.57, .57, .57},
-- i = {.47, .76, .94},
-- j = {.93, .91, .42},
-- l = {.49, .85, .76},
-- o = {.92, .69, .47},
-- s = {.83, .54, .93},
-- t = {.97, .58, .77},
-- z = {.66, .83, .46},
i = {.99, .99, .99},
j = {.99, .99, .99},
l = {.99, .99, .99},
o = {.99, .99, .99},
s = {.99, .99, .99},
t = {.99, .99, .99},
z = {.99, .99, .99},
preview = {.75, .75, .75},
}
local color = colors[block]
love.graphics.setColor(color)
local blockSize = 46
local blockDrawSize = blockSize - 1
love.graphics.rectangle(
'fill',
(x - 1) * blockSize,
(y - 1) * blockSize,
blockDrawSize,
blockDrawSize
)
end
local offsetX = 4
local offsetY = 1
for y = 1, gridYCount do
for x = 1, gridXCount do
drawBlock(inert[y][x], x + offsetX, y + offsetY)
end
end
for y = 1, pieceYCount do
for x = 1, pieceXCount do
local block = pieceStructures[pieceType][pieceRotation][y][x]
if block ~= ' ' then
drawBlock(block, x + pieceX + offsetX, y + pieceY + offsetY)
end
end
end
for y = 1, pieceYCount do
for x = 1, pieceXCount do
local block = pieceStructures[sequence[#sequence]][1][y][x]
if block ~= ' ' then
drawBlock('preview', x + 7, y + 20 )
end
end
end
paddy.draw()
end
Paddy の方のコードを調整してコントローラーの部分を画面のいい位置にしないといけない。
paddy
--[[
paddy - an onscreen controller display for touch enabled devices
* Copyright (C) 2017 Ricky K. Thomson
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* u should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
--]]
paddy = {}
paddy.debug = true
-- The size of the buttons which can be pressed.
paddy.buttonw = 80
paddy.buttonh = 80
-- This lists any buttons which are currently being pressed
paddy.touched = {}
-- Create a dpad widget
paddy.dpad = {}
-- The properties of the canvas to draw
paddy.dpad.w = paddy.buttonw*3
paddy.dpad.h = paddy.buttonh*3
paddy.dpad.x = 10
paddy.dpad.y = love.graphics.getHeight()-20-paddy.dpad.h + 450
paddy.dpad.canvas = love.graphics.newCanvas(paddy.dpad.w,paddy.dpad.h)
-- These just make things look prettier
paddy.dpad.opacity = 200
paddy.dpad.padding = 5
-- Setup the names for the buttons, and their position on the canvas
paddy.dpad.buttons = {
{ name="up", x=paddy.buttonw, y=0 },
{ name="left", x=0, y=paddy.buttonh },
{ name="right",x=paddy.buttonw*2, y=paddy.buttonh },
{ name="down", x=paddy.buttonw, y=paddy.buttonh*2 },
}
-- Create a buttons widget
paddy.buttons = {}
-- The properties of the canvas to draw
paddy.buttons.w = paddy.buttonw*3
paddy.buttons.h = paddy.buttonh*3
--paddy.buttons.x = love.graphics.getWidth()-20-paddy.buttons.w
paddy.buttons.x = 555
paddy.buttons.y = love.graphics.getHeight()-20-paddy.buttons.h + 450
paddy.buttons.canvas = love.graphics.newCanvas(paddy.buttons.w,paddy.buttons.h)
-- These just make things look prettier
paddy.buttons.opacity = 200
paddy.buttons.padding = 5
-- Setup the names for the buttons, and their position on the canvas
paddy.buttons.buttons = {
{ name="y", x=paddy.buttonw, y=0 },
{ name="x", x=0, y=paddy.buttonh },
{ name="b", x=paddy.buttonw*2, y=paddy.buttonh },
{ name="a", x=paddy.buttonw, y=paddy.buttonh*2 },
}
-- Stores any widgets containing interactive buttons
paddy.widgets = { paddy.dpad, paddy.buttons }
function paddy.draw()
-- Draw the control pad
for _,widget in ipairs(paddy.widgets) do
love.graphics.setColor(0.607,0.607,0.607,0.196)
love.graphics.circle("fill", widget.x+widget.w/2,widget.y+widget.h/2,widget.w/2)
love.graphics.setCanvas(widget.canvas)
love.graphics.clear()
love.graphics.setColor(0.607,0.607,0.607,1)
for _,button in ipairs(widget.buttons) do
if button.isDown then
love.graphics.setColor(0.607,0.607,0.607,1)
love.graphics.rectangle("fill",
button.x+widget.padding,
button.y+widget.padding,
paddy.buttonw-widget.padding*2,
paddy.buttonh-widget.padding*2,
10
)
else
love.graphics.setColor(0.607,0.607,0.607,0.784)
love.graphics.rectangle("line",
button.x+widget.padding,
button.y+widget.padding,
paddy.buttonw-widget.padding*2,
paddy.buttonh-widget.padding*2,
10
)
end
-- Temporary code until button naming can be improved
if paddy.debug then
love.graphics.setColor(1,1,1,1)
local font = love.graphics.newFont(10)
love.graphics.setFont(font)
local str = button.name
love.graphics.printf(
button.name,
button.x+paddy.buttonw/2,
button.y+paddy.buttonh/2,
font:getWidth(str),
"center"
)
end
end
love.graphics.setCanvas()
love.graphics.setColor(1,1,1,widget.opacity)
love.graphics.draw(widget.canvas, widget.x, widget.y)
end
-- debug related
if paddy.debug then
for _,id in ipairs(paddy.touched) do
local x,y = love.touch.getPosition(id)
love.graphics.circle("fill",x,y,20)
end
end
end
function paddy.isDown(key)
-- Check for any buttons which are currently being pressed
for _,widget in ipairs(paddy.widgets) do
for _,button in ipairs(widget.buttons) do
if button.isDown and button.name == key then return true end
end
end
end
function paddy.update(dt)
-- Decide which buttons are being pressed based on a
-- simple collision, then change the state of the button
paddy.touched = love.touch.getTouches()
for _,widget in ipairs(paddy.widgets) do
for _,button in ipairs(widget.buttons) do
button.isDown = false
for _,id in ipairs(paddy.touched) do
local tx,ty = love.touch.getPosition(id)
if tx >= widget.x+button.x
and tx <= widget.x+button.x+paddy.buttonw
and ty >= widget.y+button.y
and ty <= widget.y+button.y+paddy.buttonh then
button.isDown = true
end
end
end
end
end
return paddy
オリジナルの paddy.lua を書き換えたのはボタンの表示位置に関する部分。だけだったと記憶していますが、deff で確認していません。オリジナルの場合は右の方に表示させたいボタンが消えますから、画面の幅にあわせた位置にボタンがくるようにしたいとポジションに関する箇所のコードを書き換えました。
それと paddy.lua は update でボタンの入力状況を判断するコードなので、press (ボタンを押した) と release (ボタンを離した) の判断をなんとかしないといけないので、main.lua の方で timer を使ってなんとかしました。それは timeA
が現れる箇所になります。他にも方法はあるとおもいます。
paddy.dpad.x = 10
paddy.dpad.y = love.graphics.getHeight()-20-paddy.dpad.h + 450
paddy.buttons.x = 555
paddy.dpad.y = love.graphics.getHeight()-20-paddy.dpad.h + 450
conf.lua
function love.conf(t)
t.window.width = 40 * 14
t.window.height = 40 * 25
end
スーパーコライダーでゲームのサウンドエフェクトを作れれば楽しそう。
https://supercollider.github.io/