絞り込み

最新の投稿

プログラミング Polygon
プログラミング

岡大の新工学部HPのヘッダーを無理やり修正する

岡山大学工学部の新ホームページには、表示上の欠陥があります。 情報・電気・数理データサイエンス系とかいう欲張りで長すぎる名前が、「令和3年度〜新生工学部Webサイト」と被ってしまっています。 この現象は、画面の横幅が412px を下回った時に起こります。画像で記述されたロゴがどちらも透過画像であるため、文字が読みづらくなります。 本来は、HPを作成した岡山大学自然系研究科等事務部総務課に連絡すべきですし、今からしようと思いますが、成績が悪くて連絡しづらい夏季休業期間中ということで無理やり修正します。 修正する上での気を付けたことJavaScriptは使わない見た目の問題を後付けスクリプトでいじることはできるだけ避けたい行為です。 疑似要素を活用する既存の要素のプロパティ変更には限界があります。 例えば今回の場合、ロゴがpng画像なので、文字列を改行させるわけにはいきません。 そこで、before/after擬似要素を追加して、「令和3年度〜新生工学部Webサイト」を無理やり書き込みます。 Stylusを入れようStylusのダウンロード Google Chrome拡張の「Stylus」を入れます。Firefoxにも対応していますが、リンクは割愛します。 CSS:root {     --sublogo_width: 35vw;     --header_height: 37px; } #header .inr #logo {     position: relative; } #header .inr #logo a {     z-index: 4; } /* 本来のサブロゴは消す */ #header .inr .sp_subLOGO {     display: none; } h1#logo::after {     position: absolute;     top: 0;     display: none;     height: var(--header_height);     content: '令和3年度~新生工学部Webサイト';     align-items: center;     justify-content: flex-end;     right: 60px;     width: var(--sublogo_width);     font-size: 1rem;     text-align: right;     font-family: '游明朝','Yu Mincho',YuMincho,'Hiragino Mincho Pro',serif;     z-index: 3; } @media screen and (min-width: 310px) and (max-width: 1000px) {     h1#logo::after {         display: flex;     } } sublogo_width の35vw は完全に適当に決めているので、ベターな値があるかもしれません。 結果 疑似要素に「令和3年度〜新生工学部Webサイト」を書いているため、その横幅に応じて改行がされます。 スマホの横幅だといい感じに改行されて、メインロゴと被りません。なお、あまりに狭い場合はメディアクエリでサブロゴを隠します。 align-items: center; のおかげでいい感じに中央寄せできています。数年前にはできなかった選択肢。ありがとうflexbox!

> 内容を見る

プログラミング Polygon
プログラミング

コミットメッセージとバージョンアップとタグ付けと更新履歴を自動化しよう

コミットメッセージの構文を統一し、レポジトリのCHANGELOG.mdの更新とタグ付けを自動化します。 参考https://itnext.io/how-to-automate-versioning-and-publication-of-an-npm-package-233e8757a526https://numb86-tech.hatenablog.com/entry/2019/07/12/212053https://github.com/conventional-changelog/commitlint この記事でやることコミットメッセージの記法を統一更新履歴とタグの自動化 前提知識Conventional Commitshttps://www.conventionalcommits.org/ja/v1.0.0/ Conventional Commits の仕様はコミットメッセージのための軽量の規約です。 明示的なコミット履歴を作成するための簡単なルールを提供します。この規則に従うことで自動化ツールの導入を簡単にします。 コミットメッセージで機能追加・修正・破壊的変更などを説明することで、この規約は SemVer と協調動作します。 SemVerは流石に知ってたんですが、私はこれを知らずに思考停止でコミットメッセージ書いてたんですよ。いけませんね。 VSCodeにConventional Commitsの拡張機能を入れるhttps://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits ▲これを入れます。 レポジトリパネルに丸マークが出ます。 commitlintのhookを用意する次に、コミットメッセージを制限します。 yarn add -D husky @commitlint/{config-conventional,cli} echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.jsWindowsでは @commitlint/config-conventional @commitlint/cli という風に分けて書きます。 commitlint.config.js がルートに作成されているか確認してください。 yarn husky install npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'上記コマンドでhookを用意します。 package.json{ (...略) "config": { "commitizen": { "path": "cz-conventional-changelog" } }, "husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } } }package.json を編集。これでcommit-msg のhookが準備できました。 この状態で適当なコミットをすると、「Conventional Commitsの記法になってない」という理由で怒られます。 はじめてのConventional Commit 変更をステージしてから、さっきの丸マークを押します。 あとは画面の指示に従って選択肢を選びます。まずは「タイプ」。 次に「スコープ」。コミットが関係する領域です。一回作れば使いまわせます。 その後「絵文字」「概要」「詳細」「関係するissue」の順番で書いていきます。 Conventional Commitの記法に従って、以下のメッセージが生成されます。<タイプ>(スコープ):(絵文字) 概要 詳細 関係するissueissueを閉じたいなら、最後(手順6/6)にclose #1 と書けばいいんです。 standard-versionで更新履歴とタグを自動化yarn add -D standard-versionめっちゃ便利な神パッケージです。CHANGELOG.md を自動で書いてくれます。 package.json{ (略) "scripts": { "release": "standard-version", "push": "git push --follow-tags origin main" (略) }, (略) }releaseコマンドで作業をトリガーします。 初回リリースyarn run release --first-release初回は--first-release オプションをつけます。 yarn run release # yarn run v1.22.11 # $ standard-version # ✔ bumping version in package.json from 0.1.1 to 0.1.2 # ✔ outputting changes to CHANGELOG.md # ✔ committing package.json and CHANGELOG.md # ✔ tagging release v0.1.2 # ℹ Run `git push --follow-tags origin main` to publish # ✨  Done in 4.70sこれでバージョンが上がってタグが付き、更新履歴が自動で更新されます。 CHANGELOG.md# Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. ### [0.1.3](https://github.com/sasigume/project-21600/compare/v0.1.2...v0.1.3) (2021-09-16) ### [0.1.2](https://github.com/sasigume/project-21600/compare/v0.1.1...v0.1.2) (2021-09-16) ### Bug Fixes * **lint:** :ambulance: commitlintの余計な打ち間違いを削除 ([8aa522f](https://github.com/sasigume/project-21600/commit/8aa522f1333f2fd2df60c8d2e285b8e7b8d60e2a)), closes [#1](https://github.com/sasigume/project-21600/issues/1)自動生成された結果です。改行が無駄に多い気がしますね。 バージョン指定なおyarn run release --release-as x.x.x でバージョン指定もできます。 最後にyarn run push でタグ付きでプッシュしましょう。

> 内容を見る

プログラミング Polygon
プログラミング

Jetpack Composeで、Mutable系統をStateに登録したらあかん

タイトルで書いてあることを悟るまでに数日潰したので、怒鳴り散らしながらメモとして残します。 Jetpack ComposeとはAndroid開発において新たな時代の到来を感じさせるUI Toolkitです。 Androidは十数年前に登場して以降、様々なアプリが開発されてきています。 その影響もあり、Android開発を行うときは十数年前のクソみたいな古い風習が残っています。 例えばUIを作成するときです。 Androidでは命令型UIを採用しており、XMLと呼ばれる言語で書いていく必要がありました。 以下のような奴です<?xml version="1.0" encoding="utf-8"?> <androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/LinearLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:background="@android:color/white" android:elevation="4dp" android:orientation="horizontal"> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/title" android:layout_weight="1" android:gravity="center" android:layout_gravity="center_vertical" android:textSize="30sp" android:layout_height="wrap_content" android:layout_width="0dp" android:clickable="true" android:text="@string/title" android:focusable="true" /> <androidx.appcompat.widget.AppCompatImageButton android:id="@+id/delete_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_delete_title" android:contentDescription="@string/delete" /> </androidx.appcompat.widget.LinearLayoutCompat> こんなコードから以下のようなUIが出力されます こんなのコードだけ見ても何を表しているのか、慣れていない人にとってはさっぱりわかりません。慣れててもよくわかりません。 これだけならいいのですが、ここからデータを表示するとなると、KotlinやJavaでコードを書く必要があります。 その書き方がかなりめんどくさいわけです。例えば、以下のような感じになります。 val binding = ChecklistTitleBinding.inflate(inflater, parent, false) val title: TextView = binding.title //要素の文字の部分の設定 title.apply { text = "Hello World" textSize = getTextSize(context).toFloat() //タイトル名をクリックした際の処理 setOnClickListener { clickTitleOnListener(title.text.toString()) } } この通り、部品一つずつ指定して書いていく必要があります。 これで、部品の名前とかはXMLで書いているのでそっちを見ないといけないし、指定するのを忘れていたらバグにつながります。 そこでJetpack Composeの出番です。 これは最近はやっている宣言的UIを採用しており、パッと見たらわかる感じになっています。 例えば、以下のように書くと、先ほど紹介したコードと同じことができます。 @Composable fun LazyCard(text: String = "Hello World"){ Card( modifier = Modifier .padding(10.dp) .fillMaxWidth() ) { Row( modifier = Modifier.padding(12.dp) ) { Text( text = text, modifier = Modifier.weight(1f) ) Icon( painter = painterResource(R.drawable.ic_baseline_delete_24), contentDescription = null ) } } } 以下のような感じですね これだと、コードを見ただけで、先ほどよりは断然分かりやすいと思います。 どのような見た目をしているかだったり、どこにデータが流れていくかであったりが分かりやすいので非常に使いやすいです。 このように、Jetpack Composeは、Androidの新時代を感じさせるUI Toolkitになっています。 本題やっと本題です。 Kotlinには複数のデータを扱うために、標準ライブラリの中にいくつかの型(class)が存在します 例えば配列を扱うためのList型、複数のPair型(keyとvalueを持った型)を扱うためのMap型などがそれにあたります。 これらの型は、一度値を作成したら値を追加、削除することができません しかし、これらを継承した型である、MutableList型、MutableMap型等、Mutable系統であれば可能ですval list = listOf("は", "ひ", "ふ") //コンパイルエラー list.add("へ") val mutableList = mutableListOf("は", "ひ", "ふ") //これはOK mutableList.add("へ") 結構扱いやすく、私もよく利用している型です。 そのため、Jetpack Composeにおいても何気なく使ったことが運の尽きでした。 ある日、こんな感じの、+ボタンを押したらカードが追加されていくアプリを作成しました。 ソースコードは以下の通りです。@Composable fun List(){ //副作用 var list by remember { mutableStateOf((0..3).toMutableList()) } //UI部分 LazyColumn(Modifier.fillMaxSize()){ item{ //カード表示部分 for(i in list){ LazyCard(text = i.toString()) } } item { //ボタン AddButton( //ボタンを押したときの処理 onClick = { val tempList = list tempList.add(4) list = tempList } ) } } } (副作用ってなんやねんて話ですが、話し出すと記事が一つできるような内容ですので省きますが、簡単に言うと、「関数外のところに影響を及ぼす部分」のことです。) このremember{}の部分が何をしているかというと、画面に表示するためのMutableListを保存しています。 rememberのところでState型を保有しておき、それを返してくれます。 これを変数に入れておき、値が変更されると、UIを更新してくれる優れものです。 今回の場合ですと、Listの中身が追加されれば、表示されるリストの数も追加されて以下のようになるはずです。 そして、画面内の+ボタンを押すと、Listの中身が追加されるようになっています。//ボタン AddButton( //ボタンを押したときの処理 onClick = { //別の変数に代入 val tempList = list //変数を追加   tempList.add(4) //listに反映 list = tempList } )つまり、このボタンを押せば増えてくれるはずです。 しかし、実際には増えてくれません。 この挙動のせいで数日間頭を抱える事態に陥ってしまいました 原因結論から書くと、rememberで渡された値が参照渡しになっていることでした。 私の意図した動きは以下の通りです//副作用の部分 var list by remember { mutableStateOf((0..3).toMutableList()) } AddButton( onClick = { //別の変数に、実際の値のコピーを代入 val tempList = list //tempListのリストに、変数を追加   tempList.add(4) //本体のlistに反映(ここで差分を検知してUIの再描画が走る) list = tempList } ) しかし、実際にはこう動いていました。//副作用の部分 var list by remember { mutableStateOf((0..3).toMutableList()) } AddButton( onClick = { //別の変数に、listの参照先を代入 val tempList = list //tempListのリストに、変数を追加(ここで、listの方にも変数が追加される。この追加方法ではUIの再描画が走らない)   tempList.add(4) //本体のlistとtempListの両方に値が追加されたせいで、差分が一切ないため、UIの再描画が走らない list = tempList } ) いろいろと調べてると、どうやらKotlin(というよりJava)では、クラス型は参照渡しを採用しているらしいです。 http://syrinx.q.t.u-tokyo.ac.jp/tori/java/basic/entity_ref.html Mutable系統もクラス型なので参照渡しということですね。 解決策値を新しく作って、ぶち込めば解決です。//副作用の部分 var list by remember { mutableStateOf((0..3).toList()) } AddButton( onClick = { list = list + listOf(list.size) } ) listに、新しくList型を作り直して、それをぶち込んでいます。 ここで重要なのが、Mutable型の特徴であるadd()関数では差分を検知してくれないということです。 ですので、Mutable型を使うことに対するメリットがないばかりか、意図せず使ってしまい、バグを誘発する危険性があります。 だからこそ、Mutable型をStateの部分(rememberのところ)では避けて、普通の型を使うべきでしょう。 以上 しかし、こんな記事俺以外に誰が見るんだろうか・・・

> 内容を見る

プログラミング Polygon
プログラミング

Twitterのタイムライン上の画像だけを表示したい!

こんにちは.小石こはです. 突然ですが,皆さんTwitterアカウントはお持ちでしょうか? 私は趣味垢として見る専のアカウントを持っています. 先日,とある推しが誕生日を迎えまして,TLに絵師様の誕生日祝いのイラストがたくさん流れてきました. それを見ているうちにtwitterのタイムライン上の画像をギャラリーのように表示させたいなと思い,今回の記事の内容に至りました. 今回作成したもの以下の画像のように,タイムライン上の最新の画像を何件か取得して,ローカルwebサイト上に表示させるものを作成しました. ↓Twitterのタイムラインの様子 ↓今回作成した,ローカルwebサイト上にタイムライン上の画像を表示させたもの Twitter APIの利用申請,tweepyの導入 Twitterからデータを取得したいので,Twitter APIの利用申請をします. Twitter API→ https://developer.twitter.com/en/docs/twitter-api 次に,Twitter APIを便利に使うためのPythonのライブラリであるtweepyを以下のコマンドで導入します. (今回はPython3.9.5を使用します)pip install tweepy 今回は,ローカルwebサイト上でTwitterのタイムライン上の画像をギャラリーのように表示させることを最終目標とします.自分自身以外には公開しないので,フロント実装が面倒になり,Streamlit(フロントエンドアプリケーションを作成できるPythonのフレームワーク)を使用することにしました. 作成したプログラム作成したプログラムのディレクトリ構造は以下の通りです. main.pyの中身は以下の通りです. import streamlit as st from PIL import Image import streamlit.components.v1 as components from requests_oauthlib import OAuth1Session import tweepy import requests import json with open('twitter.json') as f: twitter_keys = json.load(f) def main():      # twitter.jsonからKEYを取得     CONSUMER_KEY = twitter_keys['consumer_key'] CONSUMER_SECRET = twitter_keys['consumer_secret'] ACCESS_TOKEN_KEY = twitter_keys['access_token'] ACCESS_TOKEN_SECLET = twitter_keys['access_token_secret'] auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) auth.set_access_token(ACCESS_TOKEN_KEY, ACCESS_TOKEN_SECLET) api = tweepy.API(auth, wait_on_rate_limit=True, wait_on_rate_limit_notify=True) public_tweets = api.home_timeline() # タイムラインの情報を取得 img_urls = []  # タイムラインの情報から,画像のURLを取得 for tweet in public_tweets: if 'media' in tweet.entities: for media in tweet.extended_entities['media']: media_url = media['media_url'] img_urls.append(media_url) # 取得した画像のURLをjpgに変換,imagesフォルダに保存 for i in range(0,len(img_urls)): url = img_urls[i] file_name = f"./images/{i}.jpg" response = requests.get(url) img = response.content with open(file_name, "wb") as f: f.write(img) # ローカルwebサイト上に画像を表示させる for i in range(0,len(img_urls)): img = Image.open(f'./images/{i}.jpg') st.image(img, use_column_width=True) if __name__ == "__main__": main() プログラムの実行は,以下のコマンドをたたきます. すると,ローカルサーバーが立ち上がります.streamlit run main.py これで完成です. Twitterを開かなくても,streamlit run main.pyとたたけばタイムライン上の画像を閲覧できるようになりました. 大きなサイズで表示されるので,眼福で,とてもほくほくしました…. これからも自分用で使っていこうと思います! 以上です.ありがとうございました.

> 内容を見る

プログラミング Polygon
プログラミング

Nuxt.js+Netlifyを使って初心者でもできるwebサイト構築

こんにちは.初めまして.小石こはです. #ショート記事週間3日目(のっかりました) 今回はNuxt.jsとNetlifyを使用して自身のポートフォリオwebサイトを作りました. この記事では,Node.jsのインストールから実際にサイトを公開するまでの流れを紹介します. Node.jsをインストールしよう!まずはNuxt.jsを使うために,Node.jsをインストールしていきましょう! Node.jsのインストールはこちら→ https://nodejs.org/ja/download/ Node.jsをインストールできたら,プロンプトで以下のコマンドを使って,Node.jsが正常にインストールでき,使えることができるか確認しましょう.node.exe --version npm -version以上の2つのコマンドを実行し,現在のバージョンが表示されればOKです! Nuxt.jsでプロジェクトを始めよう!npmコマンドが正常に使えることを確認したら,プロジェクトを作成したいディレクトリに移動し,以下のコマンドでNuxt.jsのプロジェクトを作ります.npx create-nuxt-app (プロジェクト名)すると以下のようにいろいろと聞かれるので,適当に答えます.create-nuxt-app v3.7.1 ✨  Generating Nuxt.js project in (プロジェクト名) ? Project name: (プロジェクト名) ? Programming language: JavaScript ? Package manager: Npm ? UI framework: None ? Nuxt.js modules: Axios - Promise based HTTP client, Progressive Web App (PWA) ? Linting tools: ? Testing framework: None ? Rendering mode: Universal (SSR / SSG) ? Deployment target: Server (Node.js hosting) ? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript), Depend abot (For auto-updating dependencies, GitHub only) ? What is your GitHub username? (GitHubのusername) ? Version control system: Git 🎉  Successfully created project (プロジェクト名)   To get started:         cd (プロジェクト名)         npm run dev   To build & start for production:         cd (プロジェクト名)         npm run build         npm run start Webサイトのデザインを考えるWebサイトのデザインを考えていきましょう. 以下のような案がでました. が,暗すぎると指摘され,自分でもそう思ったので, 最終的に以下のデザインに決定しました. (このサイトのデザインを少し参考にしました.) もろもろの準備次はIDE(統合開発環境)でプロジェクトディレクトリを開きましょう.今回はWebStormを使用していきます. WebStormのインストールはこちら→ https://www.jetbrains.com/ja-jp/webstorm/ Nuxt.jsで作ったプロジェクトのディレクトリ構造は以下のようになっています.(今回の例ではプロジェクト名をkohaにしています) まずはnuxt.config.jsを編集していきます. 以下のような記述を追加してください.export default{ head:{ link:[ {rel:"stylesheet", href:"https://fonts.googleapis.com/css2?family=Geo&display=swap"} ], }, target:'static', } Google Fontを使用しています. Google Fontはこちら→ https://fonts.google.com/ また,画像を使用したいので,プロジェクト名のディレクトリの下にassetsというディレクトリをつくり,その中に画像を入れていきます. 次に,componentsのディレクトリの中にSidebar.vueというファイルを作ります. Vueファイルの中身をつくっていくでは,Sidebar.vueの中身を書いていきましょう. Sidebar.vueの中身は以下の通りです.(ロゴはこのサイトを使用しました.)<template> <div class="back"> <div class="menu"> <div class="toTitle"> <img src="~/assets/logoN.png" alt="ロゴ"/> </div> <ul> <li>PROFILE</li> <li>WORKS</li> </ul> </div> </div> </template> <script> export default { data: function () { return { name: 'Sidebar', } }, } </script> <style scoped> .back { position: relative; background-color: #f5f5f5; } .menu { position: absolute; background-color: #2e8b57; width: 230px; height: 640px; left: 65px; } .toTitle { width: 200px; height: 20px; } ul { list-style: none; padding-top: 170px; padding-right: 68px; text-align: right; } li { padding: 15px; font-size: 25px; font-family: 'Geo', sans-serif; color: #f5f5f5; } li:hover { letter-spacing: 5px; opacity: 0.8; } </style> また,pagesディレクトリの中にあるindex.vueの中身は以下の通りです.<template> <div class="back"> <Sidebar/> <div class="image"></div> </div> </template> <script> export default { data: function () { return { title: 'index', } }, } </script> <style> .back { position: relative; background-color: #f5f5f5; } .image { position: absolute; width: 1174px; height: 783px; top: 80px; left: 245px; background-image: url('~/assets/my.png'); } </style>WebStormのターミナルで以下のコマンドを実行して,ローカルホストに接続してうまくいっているか確認してみましょう.npm run devこのあといろいろと調整しますが,とりあえずこれで完成です! では,ビルドをするために以下のコマンドを実行しましょう.(このコマンドでdistディレクトリがつくられます)npm run generate次に以下のコマンドを実行し,ローカルサーバーが立ち上がるので,うまくできたか確認しましょう.npm run start GitHubのリポジトリにあげる次はGitHubのリポジトリにあげるために,WebStorm上でCommitしましょう. Commitできたら,GitHubのWebサイト上で新しいリポジトリをつくります. 新たなリポジトリを作ったときに表示されるGitHubのガイドに沿って,WebStormのターミナルで以下のコマンドを実行します.git remote add origin https://github.com/(GitHubのaccount name)/(リポジトリ名).git git branch -M main git push -u origin main Netlifyにデプロイするいよいよ終盤です. Netlifyにデプロイしていきましょう. Netlifyはこちら→ https://www.netlify.com/ Netlifyのダッシュボードで,「New site from Git」というボタンをクリックします. Continuous DeploymentでGitHubを選択します. 先ほど作ったリポジトリを選択し,各項目は以下のように設定し,「Deploy site」のボタンを押します. 以下の画像のように,「Published」と表示されれば成功です! 緑色のURLで全世界にWebサイトが公開されています. 以上です.簡単にWebサイトが構築できるので,みんな気軽に作ってみよう! ありがとうございました.

> 内容を見る

プログラミング Polygon
プログラミング

VIVACE(音ゲー)待望のアプリ化!

これは1年前のお話...学祭に向けて、昨年 しげ と ふぉ と3人で、VIVACEという音ゲーを制作 しました。 当日の朝4時まで Discord をつないでコーディングをし、学祭ギリギリでビルドして、エラーがないことも確認できました。 Discord の通話を切った後、悲劇は起こります 私: 「じゃぁもう4時半だし、PlayStore に APK ファイルをアップロードして、寝るか〜〜」 カチッ カチッ (ドラッグの音) Google Play Store: 「 エラー: APK ファイルのサイズを 100 MB まで小さくするか、APK 拡張ファイルを使用する必要があります。」 私: 「 !!!!!!!!!」 そうです、この音ゲーアプリ、楽曲ファイルが原因で軽く100MBを超えちゃってるんです。 さすがに眠くて、Google Play Store でのリリースは諦めました。(当日は APK 配布) 今回は追加ダウンロード機能を実装して、アプリストアへのリリースまでやります!大体の音ゲーって、アプリサイズ縮小のために、起動後にコンテンツのダウンロードを行いますよね。 今回はそれを、AssetBundle と用意している API サーバを用いて実装していきます。 AssetBundle とは、アプリのビルド時に行っている処理を事前に行っておく事により、実行時に外部からアセットがロードできるようにしたものです。 これを Web サーバなどに置いておくことで、ダウンロードしてリソースを使用できるようになります。 AssetBundle の作成今回は AssetBundleBrowser を使用します。これはプレリリース版ですので、使用は自己責任でお願いします。 AssetBundleBrowser (GitHub) https://github.com/Unity-Technologies/AssetBundles-Browser 使い方は簡単、このツールに AssetBundle にまとめたいものをドラッグします。 今回だと、音源とプレビュー用の音源、各難易度の譜面とアートワークをセットし、 Build タブに移動します。 AssetBundle は各プラットフォームごとに分かれています。 今回のリリースは、iOS と Android に絞ってますので、この2つを作成します。 配信するためのサーバを用意します本来は用意してあるバックエンドサーバから、AssetBundle を DL できるようにするといいのですが、Google Cloud Platform を使用しているため、あまりディスク容量を増やしたくありません。 ということで、Google Drive のいつもの作戦を使います! 本来 Google Drive で共有リンクを生成すると、 https://drive.google.com/file/d/ hogehoge / view?usp=sharing というリンクになるのですが、以下のようにリンクを変更すると、ファイルをダウンロードさせるヘッダーに変更できます。 https://drive.google.com/uc?id= hogehoge &usp=sharing 毎回リンクを手動で変更させるのは面倒なので、データベースには共有リンクで登録しておき、アプリ内でリンクを変換させるようにしておきます。 music.asset_bundle_ios .Replace("https://drive.google.com/file/d/", "https://drive.google.com/uc?id=") .Replace("/view?usp=sharing", "&usp=sharing"); また、曲選択画面に表示される曲一覧は、DB と同期できるようにしてあります。 急遽任意の曲を非表示にするときや、楽曲を追加した時でもリアルタイム更新できるようになりました。 (セキュリティのため、通信には JWT での認証が必要です) アプリ内画面では、APIのデータを取得しこのように表示されます。 運用・メンテナンスしていく上では、こういうリモートでの更新機能も大切ですね。 Unity 内部のコード調べてみると、UnityWebRequest には AssetBundle をダウンロードする専用の UnityWebRequestAssetBundle が用意されているようです。 また、一度DLした楽曲はキャッシュしておきたいので、第2引数に CachedAssetBundle を入れて呼び出すことで、2回目以降の起動でダウンロードを省略できます。 キャッシュされる条件ですが、CachedAssetBundle 以外にも version(uint) や hash(Hash128) を第2引数に入れてロードすることで、同じくキャッシュが行われるそうです。(非推奨になりつつありますが、WWW.LoadFromCacheOrDownload を使ってキャッシュする場合は version の型は int に変わるそうなので注意) ちなみに非推奨という点では、SendWebRequest の後のリクエスト処理も、request.isNetworkError ではなく、request.result の値を、UnityWebRequest.Result.Success・UnityWebRequest.Result.ConnectionError・UnityWebRequest.Result.ProtocolError... といった用意されたクラスの enum の値で識別するのを推奨するようになりましたね。見やすくてとても気に入っています。(コード詳細↓) private IEnumerator DownloadAssetBundle(MusicList music, string downloadUrl) {   var c = new CachedAssetBundle   {     name = music.name,     hash = Hash128.Parse(music.name + music.version)   };   using var request = UnityWebRequestAssetBundle.GetAssetBundle(downloadUrl, c);   yield return request.SendWebRequest();   switch (request.result)   {     case UnityWebRequest.Result.Success:       /* 略 */       break;     case UnityWebRequest.Result.ConnectionError:    case UnityWebRequest.Result.ProtocolError:     case UnityWebRequest.Result.DataProcessingError:       /* 略 */       break;     case UnityWebRequest.Result.InProgress:       break;     default: throw new ArgumentOutOfRangeException();   } } CachedAssetBundle を使用することで、URL から抽出されるファイル名と指定したバージョン番号を意識しなくて良くなり(そもそも Google Drive のリンクなのでできないですが...)、保存場所の衝突など AssetBundle でよく発生する問題をハッシュ値で回避することができます。 あとは対応するプラットフォーム毎にダウンロードURLを切り替え、リソース使用部分を全て AssetBundle から読み込むように設定し直します。 今回のプロジェクトの場合、バックエンドのスコア登録機能も、データベースの楽曲テーブルと同期させる必要がありました。 無事ストアリリースできました!! ストアリリース後、1日で60人の方が遊んでくれて、正直GCPの料金が上がらないか心配 とても嬉しい です。 ということで、是非こちらから遊んでみてくださいね ♪ 今秋、楽曲追加も予定しています。 AppStore: https://bit.ly/vivace-app-ios GooglePlayStore: https://bit.ly/vivace-app-android

> 内容を見る

NoImage Polygon
プログラミング

Git hookを書こう!

#ショート記事週間(そんなものはない) 皆さん、開発にGitは使ってますか?使ってない人、使いましょう。使っている人も、hookという機能を使ったことがありますか?今日はGit hookの紹介をします。 hookとはGit hookというのは、各種gitコマンドを実行する前後のタイミングにおいて実行されるシェルスクリプトのことです。 リポジトリの .git/hooks/ に特定の名前のシェルスクリプトを置くことで実行されます。これを利用すれば、例えばcommitする直前に署名のメールアドレスを確認したりできるようになります。 例:commitする直前にメールアドレスを確認する(pre-commit)#!/bin/sh # hookはデフォルトでは標準入力を受け取れないのでターミナルから入力をリダイレクトする exec < /dev/tty echo "The author of this commit is to be $(git config user.name) <$(git config user.email)>." # readで入力待ちにする echo -n 'Press any key to continue...' read 小ネタこの記事の内容はこれぐらいのカスカスな記事なんですが、書いたhookが「自分のスイッチを切る装置」並に無駄(ここでは労力に対しての作用が少ないという意味)だったので紹介します。 具体的には、ファイル名末尾(拡張子)が .pdf .log .aux のいずれかがstagedであればそのままcommitするかどうかを尋ね、そのままcommitしない場合それを取り除いて(git restore --staged FILE)commitするというスクリプトです。 #!/bin/bash exec < /dev/tty SECRET=$(git diff --name-only --staged | grep -e '.pdf$' -e '.log$' -e '.aux$') if [ $? -eq 0 ]; then   echo 'Following files which should not be public are to be committed.'   echo -e "\e[31m${SECRET}\e[m"   echo -n 'Are you sure to commit them? [y/N]: '   read YN   case $YN in     [Yy]* ) exit 0 ;;     * ) ;;   esac   echo -n 'Do you want to omit them and commit? [Y/n]: '   read YN   case $YN in     [Nn]* ) echo 'Commit aborted.'       exit 1 ;;     * ) ;;   esac   git restore --staged $SECRET fi でもこれ、.gitignore に *.pdf$ *.log$ *.aux$ を指定すれば終いなんですよね……case文の勉強になったしまあいいか……

> 内容を見る

プログラミング Polygon
プログラミング

Minecraftでプレイヤーの参加をツイートするbotの作り方

こんにちはさしぐめです。初投稿の今回はマインクラフトとTypeScriptを使ったbotについて書こうと思います。 Twitterアプリを申請する Developer PortalでTwitterアプリの申請をしましょう。この時、botとして使いたいアカウントで申請するのがベストです。 間違えてメインアカで申請しちゃった場合は、OAuth1.0aを使ってサブアカのトークンを発行しましょう。(手順は割愛) また、アプリの権限はRead & Write にします(DM権限は使いません)。 APIを用意する Webhookの受け皿にはCloud Functions for Firebase (実質GCP)を使います。 Herokuでもいいんですが、個人的に ログが見やすいサーバーレスかつ応答が早いFirestore等と連携できる外出先で確認できるアプリがある という理由で選びました。 https://firebase.google.com/pricing/ なお、従量制プランを選択する必要があり、処理時間と回数が膨大になると請求される可能性もありますのでご注意ください。 ステップ1: 認証情報を用意 Firebaseプロジェクトを用意して、開発環境を用意します。ここの手順は日々変わるので割愛します。 また、Cloud Functions for Firebaseでは.env ではなくCLIで一つづつ環境変数を設定していきます。 後述するWebhookプラグインはリクエストのヘッダーをカスタマイズできません。仕方なくbodyでパスフレーズを送って認証します。 まずはオレオレパスフレーズを作ります。OAuthでもなんでもないただのパスワードなので、個人用途以外で使わないでください。$ cd functions/ # 32桁のオレオレパスフレーズを生成 $ openssl rand -base64 32 # configに登録 $ firebase functions:config:set minecraft.auth="(オレオレパスフレーズ)" もっといい生成方法があったら教えてください>< アプリの申請が通ると以下のキーが手に入ります。これらを全部コンフィグに登録します。コンシューマキーコンシューマシークレットユーザのトークンユーザのシークレット $ firebase functions:config:set twitter.key="(コンシューマキー)" $ firebase functions:config:set twitter.secret="(コンシューマシークレット)" $ firebase functions:config:set minecraft.auth=(ユーザのトークン)" $ firebase functions:config:set minecraft.auth="(ユーザのシークレット)" # エミュレータ用ファイルを生成 $ firebase functions:config:get > .runtimeconfig.json {   "twitter": {     "user_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",     "user_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",     "key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",     "secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"   }, "minecraft": {     "auth": "(オレオレパスフレーズ)"   } }.runtimeconfig.json がこんな感じになります。これがローカルでテストする時の環境変数代わりになります。 functions/models/AdminConfig.d.tsexport interface AdminConfig {   twitter: {     key: string;     secret: string;     user_token: string;     user_secret: string;   };   minecraft: {     auth: string;   }; }TypeScriptの場合、こんな感じで型を作っておくと自動補完が楽です。 ステップ3: APIのエンドポイントを用意 さあいよいよAPIを作ります。 $ npm i twitter-api-clienttwitter-api-client というライブラリを使います。 FirebaseなのでFirebaseが用意しているfunctions.https.onRequest を使いますが、別にexpressでもいいです。 functions/src/twitter/post.tsimport * as functions from 'firebase-functions'; import { AdminConfig } from '../models/adminConfig'; const config = functions.config() as AdminConfig; import { TwitterClient } from 'twitter-api-client'; const client = new TwitterClient({ apiKey: config.twitter.key, apiSecret: config.twitter.secret, accessToken: config.twitter.user_token, accessTokenSecret: config.twitter.user_secret, }); const post = functions.region('asia-northeast1').https.onRequest(async (request, response) => { if (request.method !== 'POST') { response.status(405).json({ message: `POSTメソッドを使ってください`, }); return; } const body: { // マイクラサーバーのplugins/SimpleWebhooks/config.ymlで内容は定義してある tweet?: string; // コンフィグで定義 auth?: string; // テスト用 test?: boolean; } = request.body;   functions.logger.debug(`Received body: ${JSON.stringify(body)}`); if (Object.keys(body).length == 0) { response.status(400).json({ message: `bodyはJSONオブジェクトにしてください`, }); return; } if (body.auth !== config.minecraft.auth) { response.status(401).json({ message: `認証できませんでした`, }); return; } let tweet = body.tweet; if (!tweet) { response.status(422).json({ message: `ツイート本文を指定してください`, }); return; } // testがtrueならツイートはしない if (body.test) { response.status(200).json({ message: `テスト成功`,       ...body,     }); return; } // 以下、Twitter APIと通信する try { await client.tweets .statusesUpdate({ status: tweet, }) .then(async (apiResponse) => { // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update         // レスポンスにcreated_atがあればツイート成功 if (apiResponse.created_at) { response.status(200).json({ message: `ツイート成功`, ...apiResponse }); functions.logger.info(`Successfully tweeted: ${tweet}`); } else { response.status(500).json( apiResponse ?? { message: `Twitter APIからの応答がありませんでした`, } ); } }) .catch((e) => { response.status(500).json({ message: `Twitter API呼び出し中のエラー: ${JSON.stringify(e)}`, }); }); return; } catch (e) { response.status(500).json({ message: `その他のエラー: ${JSON.stringify(e)}`, }); } return; }); export default post; ステップ3: フォルダにまとめる expressならこんなことしなくて良いですが、Firebaseの書き方だとこうやってエンドポイントを整理します。 functions/src/index.tsconst twitter = require('./twitter'); exports.twitter = twitter; functions/src/twitter/index.tsexport { default as post } from './post'; こうすることで、エンドポイントが /twitter-post になります。 ステップ4: テストとデプロイ $ npm run serve Firebaseのエミュレータが立ち上がり、 http://localhost:5001/(プロジェクト名)/asia-northeast1/twitter-post でテスト可能になります。 curl --location --request POST 'http://localhost:5001/(プロジェクト名)/asia-northeast1/twitter-post' \ --header 'Content-Type: application/json' \ --data-raw '{     "tweet": "テストツイート",     "auth": "(オレオレパスフレーズ)",     "test": true }' こんな感じのリクエストを送ればテストできます。 $ npm run deploy デプロイすると https://asia-northeast1-(プロジェクト名).cloudfunctions.net/twitter-post にAPIが公開されます。 マイクラサーバー側の設定 あとはマイクラサーバーからWebhookを送るだけです。 Webhookプラグインを入れる https://github.com/Akenland/SimpleWebhooks SimpleWebhooks プラグインを使います。PaperMCの1.16.5で動作を確認しました。 プラグインの設定 $ vi plugins/SimpleWebhooks/config.yml config.yml webhooks:   join:     twitter-join:       url: https://asia-northeast1-(プロジェクト名).cloudfunctions.net/twitter-post       json:         tweet: "{PLAYER_DISPLAYNAME}が{SERVER_MOTD}に参加しました!"         auth: "(オレオレパスフレーズ)" こんな感じで、URLと送信するJSONを設定してください。YAMLマジで苦手。 豆知識: Cyberduckなら、サーバー上のファイルをローカルのように編集できます。宗教的こだわりがない場合は例えばVSCodeで開いて編集しましょう。 動作確認 YAMLを保存したら、マイクラサーバー側でwebhookをリロードします。 > webhooks reload サーバー再起動の必要はなく、このコマンドを実行するだけです。 プレイヤーが参加すると、Firebaseの管理画面にログが出るはずです。 .sasigumeが自由にサバイバル!に参加しました!#mc_asobinon 参加方法はこちら→ https://t.co/N1cU2977TQ— アソビノン - 誰でも編集できる攻略サイト (@asobinon) July 23, 2021 うちのサーバーではこんな感じでツイートしています。 --- 以上、マイクラサーバーと連携するbotの作り方でした。

> 内容を見る

プログラミング Polygon
プログラミング

Ktorを知る ~Pipeline編~

こんにちは、オキリョウと申します! 前回、Ktorの特徴について解説させていただきました。(https://oucrc.net/articles/ia_sv-bbmz) そこで今回はKtorで重要な概念であるPipelineについて解説していきたいと思います。 PipelineとはPipelineとは、Ktorにおいて処理を順序良く処理していくためのものです。 ライフサイクルを定義するためのもの、みたいな感じに考えていただけてたらと思います。 前回、Ktorには以下の特徴があるといいました。拡張性が高く、柔軟非同期処理に強いこれらはPipelineを導入することで実現しています。 Ktor内では、継承可能なクラスとして定義されており、そこらじゅうで使われています。Ktor内のクラスの継承元をたどっていくと一枚嚙んでいた・・・ということも珍しくありません。 それではより詳しく見ていきましょう。 Pipelineを図で表すと以下のようになります。 この画像の場合、オレンジ色の向きへと処理が流れていきます。 つまりフェーズ(Phase)1->フェーズ2->フェーズ3->フェーズ4の順番に処理が流れていくということです。 各フェーズ同士は依存していません。しかし、共有する必要のある値ももちろん存在します。 それらはPipelineが所有しているPipelineContextに保存しておき、そこから値を取り出す仕組みになっています。 また、フェーズは好きなところに付け足すことも可能です。 このように、左から右へと、それぞれ独立したフェーズを処理していく流れがPipelineです。 Pipelineの使用例では、Pipelineがどのように使われているのか見てみましょう。 例えば、サーバーサイドアプリケーションの中でも必須である、「リクエストを受けとってリスポンスを返す」という部分はPipelineで実装されています。(ApplicationCallPipelineという名前のクラスです) このオレンジ色の矢印がApplicationCallPipelineです。 こんな感じに差し込まれています。 リクエストを受けとって、そこから各フェーズを実行してリスポンスを作成、返すという感じです。 ではより具体的に見ていきましょう。 このような感じになっています。 左から順番にSetup -> Monitoring -> Features -> Call -> Fallbackの順番に処理が流れています。これら青い部分がフェーズですね。 これらのフェーズの役割は以下の通りです Setup: callやattributesを用意する Monitoring: callを追跡するための部分で、ログやコードの評価を行う、エラーハンドリングなどに使われる Feature: 大体のFeatureが入るところ Call: callを完全に作りきるために利用するところ Fallback: ハンドリングされていないcallを何らかの形で処理する部分 補足call:ApplicationCallの事。リスポンスであったりattributesであったりを管理している attributes:DIコンテナ(多分、詳しく知りたい人は公式をチェック) Feature:Ktorの追加機能の事。後述 それぞれのフェーズを順に通していく中で徐々にリスポンスを作成しています。 この流れがPipelineです。 ちなみにRouting等もPipelineを使用しています。 ではPipelineについてある程度説明したところで拡張性が高く、柔軟非同期処理に強いを実現している方法について説明していきたいと思います。 拡張性が高く柔軟Pipelineはそれぞれのフェーズを定められた順に実行していくことは先ほどまで書いた通りです。 また、これらのフェーズは互いに依存しておらず、好きなところに差し込むことができるということも説明したと思います。 必要なフェーズを差し込んだり、要らないフェーズを抜くことができるということは、必要な処理だけを記述することができるということです。 それに加えて、実はフェーズの間に処理を入れることも可能です。(intercept) 先ほどちらっと登場したFeatureというのは、このような処理を練りこんだライブラリといえます。 そのようにすることで、本来複雑になるはずのライブラリの作成、導入が非常にシンプルにできるわけです。 せっかくですので、リスポンスのヘッダーに現在時刻を付ける、というFeatureを定義、導入したコードを掲載します。 まずは現在時刻を付けるFeatureの定義からpackage com.example import io.ktor.application.* import io.ktor.response.* import io.ktor.util.* import io.ktor.util.pipeline.* import java.time.LocalDateTime class SendTimeHeader(configuration: Configuration) { private val name = configuration.headerName //設定 //この場合、ヘッダーのkeyの部分を設定できるようにする class Configuration{ var headerName: String = "Send-Time" } //差し込まれる処理 private fun intercept(context: PipelineContext<Unit, ApplicationCall>){ //設定された値と、現在時刻をheaderにセット context.call.response.header(name, LocalDateTime.now()) } //アプリケーション実行時に行われる処理 companion object Feature : ApplicationFeature<ApplicationCallPipeline, Configuration, SendTimeHeader>{ //Attributeを設定 override val key: AttributeKey<SendTimeHeader> = AttributeKey("SendTimeHeader") //install時の処理 override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): SendTimeHeader { val configuration = Configuration().apply(configure) //インスタンスの作成 val feature = SendTimeHeader(configuration) //ApplicationCallPipelineのCallというフェーズに以下の処理を入れる pipeline.intercept(ApplicationCallPipeline.Call){ //先ほど定義した、interceptの処理 feature.intercept(this) } return feature } } } こんな感じです。 非常に見づらくなってしまい申し訳ないですが、雰囲気は理解できたかと思います。 実装の流れはそのうち別の記事で説明すると思います。 ちなみに今回の場合は、ApplicationCallPipeline↓ のCallのところにこちらの処理が入ります。 それでは利用するときのコードも掲載します package com.example import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* import io.ktor.util.* import io.ktor.util.pipeline.* import kotlinx.coroutines.launch fun main(args: Array<String>) = io.ktor.server.netty.EngineMain.main(args) fun Application.main(){ //先ほどのFeatureの導入 install(SendTimeHeader){ headerName = "Time" } routing { //ホームルートにアクセスしたときに「Hello World」を返す get("/"){ call.respondText("Hello World") } } } これだけですね。 installのところでFeatureを指定(必要ならば設定も行う)するだけで、すべてのルートにて、現在時刻が追加で刻まれたヘッダーが返却されます。 一見複雑に見えるかもしれませんが、普段は利用するだけ、つまり下側だけですので非常にシンプルです。 上側の方も、やっていることはそこまで複雑ではなく、他のフレームワークで同じことをすることを考えたらはるかに簡単だと思います。 他にも認証機能やログを取る機能もこんな感じで簡単に導入できます。 このように、簡単にいろいろなライブラリを導入したり、作成したりすることができます。 ですので、自分が好きなライブラリで固めることも可能ですし、かなり規模の大きいアプリケーションを作成することも全然可能です。 これもPipelineだからできることです。 非同期処理に強い前回、KtorではCoroutineというものを使い倒しており、そのために非同期処理に強いという話を書きました。 Pipelineもこの流れを強く受けています。というのも、PipelineContextはCoroutine Scopeを継承しており、Coroutine Scopeとして扱うことができるためです。 例えばrouting { //ホームルートにアクセスしたときに「Hello World」を返す get("/"){ launch{ TODO("Coroutineを起動") } } } という感じで、さらっとCoroutine builderを呼び出すことができます。 get()関数は引数としてPipelineContextを取るからですね。 このように、わざわざCoroutine Scopeを定義する必要もないため、気軽に非同期処理を書くことができます。 また、非同期処理が強い理由としてProceed関数およびProceedWith関数の存在もあります。 ややこしい説明になりますが、この関数を実行したインターセプター(フェーズの間に入れる処理群の事)の処理は後回しにされます。 なんの役に立つのかと思うかもしれませんが、意外と使えます。 例えばかなり重たい処理があったとします。その時はPipelineと違うCoroutineを立ち上げて、その後Proceed関数を実行します。 本来だとCoroutineを立ち上げた場合は、そのCoroutineの処理が終わるまで待機しないといけないため、Coroutineの処理が長ければ長いほどレスポンスが遅くなります。 しかし、後回しにすることで、かなり重たい処理とほかの処理を並列に実行することができます。 重たい処理の完了を待つことなく別の処理を進めることができるため、早く処理を終わらすことができるというわけです。 このようにPipelineを用いることで、非同期処理を簡単に、しかし効果的に書き上げることができるのです。 他にもPipelineではCoroutineを生かして処理の効率化を図っていますが、ここでは省略しています。 まとめいかがだったでしょうか?ちょっと説明が難しくなってしまい、申し訳ございません。 ただ、このフレームワークはかなり面白いので、実際に組んでみる価値は大いにあります。 また、公式がより分かりやすく説明してくれています(英語ですが) ぜひそちらの方にも目を通してみてください! あと、ここまでの説明は公式の解説と、コードを読んでみた結果から書いています。 何かしら間違っていたら申し訳ございません。

> 内容を見る