Hi all,
I’m writing a program that has numerous moving objects on screen and my frame rate starts to take a serious hit at around 100 - 110 objects. I’ve seen programs with thousands running easily so clearly I’m writing inefficient logic.
The program uses the twitter4j library to connect to the twitter API. I am searching for all of the words from a user supplied string. When one of these words is found an object is created and animates a wave across the screen and then reverses direction while a “drip” (don’t have a better name for it) grows from the end that hit the edge of the screen.
When It reaches up over 100 objects(waves) on screen the frame rate starts to drop quite considerably, the lowest I’ve seen it is around 20fps which is disastrous for live performance.
I think the error is either in the draw loop with the removal of the dead waves or in the wave class itself as I’ve noticed that when there are a lot of “drips” is when the frame rate takes the biggest hit.
Am I on the right track for where the issue is? Is there a more efficient way to do what i’m trying to do? (Probably)
Here’s a video link of it in action showing the fluctuating frame rate:
I do understand that with the frame rate it is a requested frame rate, but when it hits 30fps it is noticeably slower.
This is as close as I can get to an MVCE, it does work, but I’ve had to take the API credentials out for security reasons.
Main Tab
import java.util.*;
import twitter4j.util.*;
import java.util.List;
import twitter4j.*;
import twitter4j.management.*;
import twitter4j.api.*;
import twitter4j.conf.*;
import twitter4j.json.*;
import twitter4j.auth.*;
TwitterStream twitterStream;
// quotes from: https://www.thoughtco.com/famous-short-quote-2833143
String uInput = "There are only two tragedies in life: one is not getting what one wants, and the other is getting it.";
ArrayList<String> words;
ArrayList<Wave> waves;
int vertSpace;
int ySpacing;
char waveDirection;
boolean twitterInitialised = false;
void setup() {
size(600, 1000);
words = new ArrayList<String>();
waves = new ArrayList<Wave>();
userDefined = split(uInput.toLowerCase(), " ");
vertSpace = userDefined.length;
ySpacing = height/(vertSpace+1);
openTwitterStream();
}
void draw() {
background(12);
if (int(random(10)) < 5) waveDirection = '-';
else waveDirection = '+';
for (int i = 0; i < words.size(); i++) {
if (words.get(i) != null) {
for (int n = 0; n < userDefined.length; n++) {
if (words.get(i).equals(userDefined[n])) {
waves.add(new Wave(ySpacing*(n+1), userDefined[n], waveDirection, int(random(200, 400))));
}
}
}
}
for (Wave w : waves) w.draw();
words.clear(); //clear the words array
for (Iterator<Wave> w = waves.iterator(); w.hasNext(); ) {
if (w.next().isDead()) {
w.remove();
}
}
fill(-1);
textSize(12);
text("Num Waves: " + waves.size(), 15, height-35);
text("FPS: " + nf(int(frameRate), 0, 0), 125, height-35);
noFill();
}
void keyPressed() {
if (key == ESC) {
if (twitterInitialised) {
twitterInitialised = false;
twitterStream.cleanUp(); //closes down the stream
}
exit();
}
}
Twitter Class
String[] userDefined;
//GetMentions men = new GetMentions();
// Stream it
void openTwitterStream() {
// OAuth stuff
ConfigurationBuilder cb = new ConfigurationBuilder();
ConfigurationBuilder b = new ConfigurationBuilder();
//stream api OAuth
cb.setOAuthConsumerKey("---");
cb.setOAuthConsumerSecret("---");
cb.setOAuthAccessToken("---");
cb.setOAuthAccessTokenSecret("---");
b.setOAuthConsumerKey("---");
b.setOAuthConsumerSecret("---");
b.setOAuthAccessToken("---");
b.setOAuthAccessTokenSecret("---");
//the stream object
twitterStream = new TwitterStreamFactory(cb.build()).getInstance();
// filter is used to pass querys to the stream
// see twitter4j's java doc
FilterQuery filtered = new FilterQuery();
// if you enter keywords here it will filter, otherwise it will sample
String[] keywords = new String[userDefined.length];
for (int i = 0; i < userDefined.length; i++) {
keywords[i] = userDefined[i];
}
//track is like "search"... well kind of
// for a better explanation go
// dev.twitter.com/streaming/overview/request-parameters"
filtered.track(keywords);
// the StatusListener interface is where the magic happens
// code there will be executed upon tweets arriving
// so we want to attach one to our stream
twitterStream.addListener(listener);
if (keywords.length == 0) {
// sample() method internally creates a thread which manipulates TwitterStream
// and calls these adequate listener methods continuously.
// ref //dev.twitter.com/streaming/reference/get/statuses/sample
// "Returns a small random sample of all public statuses"
twitterStream.sample();
} else {
twitterStream.filter(filtered);
}
println("connected");
twitterInitialised = true;
}
// Implementing StatusListener interface
// the methods are pretty much self explantory
// they will be called according to the messages arrived
// onStatus is probably what you are lookikng for...
StatusListener listener = new StatusListener() {
//@Override
public void onStatus(Status status) {
//println("@" + status.getUser().getScreenName() + " - " + status.getText());
String s = status.getText().toLowerCase();
String[] tweetWords = s.split(" ");
for (int i = 0; i < tweetWords.length-1; i++) words.add(tweetWords[i]);
//println("tweetWords: " + tweetWords.length);
//tweetWords = null;
}
//@Override
public void onDeletionNotice(StatusDeletionNotice statusDeletionNotice) {
println("Got a status deletion notice id:" + statusDeletionNotice.getStatusId());
}
//@Override
public void onTrackLimitationNotice(int numberOfLimitedStatuses) {
//println("Got track limitation notice:" + numberOfLimitedStatuses);
}
//@Override
public void onScrubGeo(long userId, long upToStatusId) {
println("Got scrub_geo event userId:" + userId + " upToStatusId:" + upToStatusId);
}
//@Override
public void onStallWarning(StallWarning warning) {
println("Got stall warning:" + warning);
}
//@Override
public void onException(Exception ex) {
ex.printStackTrace();
}
};
Wave Class
class Wave {
PVector startPos, endPos;
String word;
float strokeWidth;
int yStored;
float bezierRadius, bezierY;
boolean randStartEndPos, dead;
char waveDirection;
float opacityIter, strokeIter;
int dripAmt, opacity, growSpd, decaySpd;
Wave(int yPos, String word, char waveDirection, int dripAmt) {
yStored = yPos;
this.word = word;
bezierY = yPos;
startPos = new PVector(-10, yPos);
endPos = new PVector(-10, yPos);
strokeWidth = 1;
randStartEndPos = false;
dead = false;
this.waveDirection = waveDirection;
this.dripAmt = height/dripAmt;
opacity = 100;
growSpd = width/(int(random(5, 250)));
decaySpd = width/int(random(100, 150));
opacityIter = 0.05;
strokeIter = 0.25;
}
void draw() {
stroke(-1, opacity);
strokeWeight(strokeWidth);
noFill();
bezier(startPos.x, startPos.y, startPos.x+bezierRadius, bezierY, endPos.x-bezierRadius, bezierY, endPos.x, endPos.y);
noStroke();
fill(210, 55, 41);
animate();
if (randStartEndPos) {
stroke(-1, opacity);
strokeWeight(strokeWidth);
line(0, startPos.y, startPos.x, startPos.y);
}
}
void animate() {
if (endPos.x < width+5 && !randStartEndPos) {
endPos.x+=growSpd;
} //
else if (endPos.x > width && startPos.x > -11) {
if (!randStartEndPos) {
startPos.x = random(endPos.x-20, endPos.x-10);
endPos.x = width+5;
bezierRadius = (endPos.x-startPos.x)/2;
randStartEndPos = true;
}
strokeWidth+=strokeIter;
opacity-=opacityIter;
startPos.x-=decaySpd;
if (waveDirection == '+') bezierY += dripAmt;
else bezierY -= dripAmt;
bezierRadius = (endPos.x-startPos.x)/2;
}
if (startPos.x < -10) dead = true;
}
boolean isDead() {
return dead;
}
}